Files
sailing-analytics/java
Axel Uhl 9092744d4a bug6241 aftermath: dispatch regatta raceAdded catch-up outside computeIfAbsent to fix replication initial-load deadlock
The runWhenDoneLoading rewrite in 15dbbc03a1 (companion bug6241
listener-leak fix) made runWhenDoneLoading register a race-removal
listener on the race's regatta via TrackedRegatta.addRaceListener. When
a replica performs its initial load from a master
(RacingEventServiceImpl.initiallyFillFromInternal), it registers a
synchronous RaceAdditionListener on each deserialized regatta. Those
regattas already contain their races, so addRaceListener's catch-up loop
fires raceAdded for every existing race -- and it did so from INSIDE the
ConcurrentHashMap.computeIfAbsent mapping function, because the
SynchronousRunnableExecutor runs the dispatch inline. raceAdded then
called runWhenDoneLoading, which re-entered addRaceListener and nested a
second computeIfAbsent on the same raceListeners map while the outer one
still held the key's reservation node. ConcurrentHashMap forbids the
mapping function from modifying the map, so the nested update spun/blocked
forever on the reservation node -- a single-threaded self-deadlock, not a
lock-ordering deadlock.

This affected production: a replica would hang indefinitely during
initial load from a non-empty master (both automatic replica startup via
the replication Activator and operator-initiated replication). It also
wedged the CI build, which runs the replication initial-load tests;
TrackedRaceWithSensorFixStoreContentsReplicationTest hung until the 6h
GitHub Actions cap cancelled the job. The normal getOrCreateTrackedRegatta
path was unaffected because the listener there is registered on a
freshly-created empty regatta (zero-race catch-up, no re-entry).

Fix: TrackedRegattaImpl.addRaceListener now captures the catch-up work in
a holder inside the mapping function and runs it AFTER computeIfAbsent
returns, with the entry fully installed and no reservation node held. The
holder is populated only when a new entry was actually created, preserving
the duplicate-registration collapse, and the catch-up dispatch still runs
under the trackedRacesLock read lock, so ordering relative to enqueEvent
is unchanged. A nested addRaceListener for a different listener key now
proceeds normally.

TrackedRaceWithSensorFixStoreContentsReplicationTest now passes in ~7s.

Assisted-By: Claude (Anthropic claude-4.8-opus)
2026-10-07 14:07:58 +02:00
..
…