fix: make the TripleStore and TrustStateRegistry singletons thread-safe - #227
Merged
Merged
Conversation
TripleStore.get() created its instance lazily without synchronization, and the HTTP server accepts requests before the loader thread starts, so several threads make the first call at once and each could construct its own instance. The constructor creates the `empty` repo, which first creates `admin`. RDF4J checks whether a repository exists and writes its config under separate locks, so concurrent create requests for a new repo are all answered with 204 (5 of 5 in most rounds against 6.1.0), and every extra instance ran initNewRepo on `admin`. On a fresh store that left several npa:hasRepoInitId values in `admin`: 2, 5 and 3 on the three fleet instances, one everywhere else. The ID is never read, so this was harmless in itself, but the same race on a counted repo would duplicate its npa:hasNanopubCount and checksum, and every extra instance leaked its HTTP client and idle-connection thread on every start. get() now uses a volatile field with double-checked locking; later calls still read without locking. Construction goes through a private factory field so a test can slow it down and check that 16 concurrent first calls construct exactly one instance (the test fails on the old code). TrustStateRegistry.get() had the same pattern (a lost hash, costing one extra trust-state update) and is now synchronized; it is called rarely. The existing duplicate init IDs are left in place, since nothing reads them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a startup race condition, found while investigating why
npa:thisRepoin theadminrepo carries severalnpa:hasRepoInitIdvalues: 2 on query.knowledgepixels.com, 5 on query.petapico.org and 3 on query.nanodash.net. Every other repo has exactly one.Cause
TripleStore.get()created its instance lazily without synchronization. Port 9393 starts accepting requests (MainVerticle.java:615) before the loader thread starts (line 628), so several threads make the first call at once, and each could construct its ownTripleStore.emptyrepo, which first createsadmin.PUT /repositories/<id>) checks whetherconfig.ttlexists and writes it under separate locks. Concurrent create requests for a new repo are therefore all answered with 204. Tested againsteclipse/rdf4j-workbench:6.1.0-tomcat, with five rounds of 5 concurrent PUTs, the number of 204s was 5, 5, 3, 4 and 5.initNewRepo(admin), which writes another init ID.On an existing store,
adminalready exists and gets a 409, so this only happens when a store is first built. Consistent with that, the counts didn't change when all three instances restarted on 2026-09-24.Impact
hasRepoInitId.npa:hasNanopubCountandnpa:hasNanopubChecksum, breaking the full-vs-meta checksum comparison. It hasn't happened on the current stores: all other repos have one init ID, and the checksums match fleet-wide.Fix
TripleStore.get(): avolatilefield with double-checked locking onTripleStore.class. Later calls still read the field without locking. Construction goes through a privateinstanceFactoryfield, so a test can substitute a slow factory.TrustStateRegistry.get(): had the same pattern (the worst outcome is a lost cached hash, costing one extra trust-state update). It is nowsynchronized; it is called rarely.Testing
TripleStoreTest.concurrentFirstGetConstructsSingleInstance. 16 threads make the firstget()call at once against a factory that takes 50 ms; the test asserts that exactly one instance is constructed and returned to all of them.get(): the threads received different instances).🤖 Generated with Claude Code