Repository navigation
Store a data file's future before its check can read it - #71
Draft
jwrosewell wants to merge 3 commits into
Draft
jwrosewell wants to merge 3 commits into
jwrosewell wants to merge 3 commits into
Conversation
registerDataFile schedules the first check and only then stores the future it gets back, so a check with a short delay can start in between. If the check reads the future in that gap it finds none, clears nothing, and its own re-registration then finds the stored future and schedules no next check. DataUpdateService_UpdateFromFile_FileUpdated failed this way once in a full build on macOS x64 with Java 11. The two new tests force that order on every run, one for a check that applies an update and one for a check that applies none, which is the path the retry after a 429 response takes. Registration is held after scheduling until the check has read the future or is blocked on a lock that registration holds, and the check's first read waits until registration has stored the future. Every wait is bounded, so a design that deadlocks fails rather than hangs. Each failure message lists the order in which the two threads read and wrote the future. For #70
Every read and write of a data file's future now happens while holding one lock in the data update service. registerDataFile holds it from scheduling the check to storing the future, and the check takes it before it cancels, clears or replaces the future, so a check that starts at once waits until registration has stored its future. Unregistering takes it for the same reason. The lock is the service's own rather than the data file's update lock, because the file system watcher holds the update lock while it refreshes the engine and while it waits for the new file to open. Taking that lock for the future would make unregistering wait for a refresh. Only scheduling, cancelling and storing happen while holding the new lock, so it is never held while waiting for the configuration lock or an update lock. For #70
The lock's comment now says it covers the service's own reads and writes of the future, the test data file's comment says it records the check's reads, and the second test's comment says which checks share its path. No code changes. For #70
This was referenced Nov 8, 2026
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.
For #70
Problem
registerDataFileschedules the first update check and only then stores the future thatschedulereturns, so a check with a short delay can start in between. If the check reads the future in that gap,updatedFileAvailablefinds none and clears nothing. Registration then stores the future, and the check's own re-registration at line 245 finds it and schedules no next check. A check that finds no update goes the same way through the reschedule at lines 227 to 240. Either way the stored future belongs to a check that has already run, and no further check ever runs.DataUpdateService_UpdateFromFile_FileUpdatedfailed this way in the full build of #68 onMacOS_X64_Java_11(run 42455459082, attempt 1, job 128653447065).Proof that this order loses the next check
Two new tests in
DataUpdateServiceTestsforce the losing order on every run, with no reliance on timing:DataUpdateService_UpdateFromFile_FileUpdated_CheckBeforeFutureStored, for a check that applies an updateDataUpdateService_UpdateFromFile_FileNotUpdated_CheckBeforeFutureStored, for a check that finds none, which is also the path the retry after a 429 response takesThe mocked
schedulestarts the check at once and then holds registration until the check has read the future or is blocked on a lock that registration holds (configureScheduleRaceandwaitForCheck). A data file subclass,RaceDataFile, holds the check after its first read until registration has stored the future, and records each read and write. Every wait is bounded at 10 seconds, so a design that deadlocks fails rather than hangs.Against the unchanged service on rc, both tests fail for all four parameter sets on Ubuntu and on macOS x64, and the other 92 tests pass (run 42468489378, Ubuntu job, macOS x64 job). The events are the three steps the issue describes, in that order.
The existing tests before the fix
DataUpdateServiceTestsas it is on rc, run 50 times onmacos-15-intelwith three busy loops per core, failed in 2 of the 50 runs (macOS x64 job). Both failures were inDataUpdateService_UpdateFromFile_FileUpdated, with the sameTooFewActualInvocationsonfutureFactory.scheduleas the full build of #68.What changed
DataUpdateServiceDefaultgains one lock,futureLock, held for every read and write the service makes of a data file's future:registerDataFileholds it from the check that there is no future, through scheduling, to storing the futureupdatedFileAvailableholds it to cancel and clear the future, and releases it before refreshing the engineunregisterDataFileholds it to cancel and clear the futureA check that starts at once now waits at its first read of the future until registration has stored it. Nothing else changes. The same futures are scheduled with the same delays and cancelled in the same places, and the events fire in the same order.
The lock is the service's own rather than the data file's update lock, because the file system watcher holds the update lock in
dataFileUpdatedfor the whole engine refresh, and for its loop that waits for the new file to open, which has no limit. Taking the update lock for the future would make unregistering, and so closing an engine, wait for a refresh.Deadlocks checked
futureLockis always the innermost lock. OnlygetFuture,setFuture,cancel,futureFactory.schedule,getIntervaland debug logging run while it is held, and none of them takesconfigLockor an update lock or calls back into the service.futureLock, throughupdatedFileAvailable.unregisterDataFiletakesconfigLockand thenfutureLock.registerDataFilereleasesfutureLockbefore it takesconfigLock. No path takes any two of these locks in the opposite order.checkForUpdatereleasesfutureLockbefore it callsregisterDataFile, the engine'srefreshDataor the update complete callbacks.checkForUpdatesynchronously fromregisterDataFilewhile holding no lock, and theregisterDataFilecall that the check ends with takesfutureLockfor itself.cancel(true)on the check's own future interrupts the check's thread without waiting, as it did before.FutureFactoryDefaultthe scheduled checks and the watcher's poll share one thread, so they never contend for the lock.FutureFactorywhoseschedulewaited for the scheduled task to finish would now deadlock, because that task needsfutureLock.FutureFactoryDefaultreturns at once, and it is the only implementation in this repository.I read every exit path of the four changed functions. Each lock is a
synchronizedblock, so it is released on every return and exception. ARejectedExecutionExceptionfromscheduleafter the factory is closed is still caught and logged inregisterDataFile, and still propagates from the reschedule, as before.After the fix
All of these are in run 42468849600, at ce1a533. The head commit, 9424d53, changes only the wording of three new comments.
DataUpdateServiceTests50 times onmacos-15-intelwith three busy loops per core (job).The whole
pipeline.enginesmodule and the modules that use the data update service, built with the native libraries and tested with the Enterprise data files on Java 11 (Ubuntu, Windows, macOS x64). Each cell is tests run, failed, skipped.DataUpdateServiceTestsran 100 tests with no failures on each of the three.Full pull request build
common-ci's
nightly-pull-request.yml, run as a dry run on this pull request at 9424d53, passed in all 15 configurations fromci/options.json, being Ubuntu, Ubuntu ARM, Windows, macOS and macOS x64 on Java 8 to 21 (run 42473320389).DataUpdateServiceTestsran 100 tests with no failures in each, for example onMacOS_X64_Java_11(job).What remains
AspectEngineDataFileDefault.close()still cancels the future directly, without the lock, afterunregisterDataFile. A check that is already running when its data file is closed can still schedule another check afterwards, as before. Neither is changed here.registerDataFilepasses milliseconds toapplyIntervalRandomisation, which adds a random number of seconds as if they were milliseconds. The random spread there is at most 600 milliseconds by default rather than 10 minutes. Not changed here.RegisterDataFilecreates the timer, which starts counting at once (TimerFactory), and only then stores it indataFile.Timer. The callback clears the timer throughClearTimerand re-registers at lines 859 to 866, or reschedules throughdataFile.Timer.Changeat lines 893 to 900, with no lock around any of it. That has not been reproduced in .NET and is not changed here.