Conversation
TimeoutWatchdog is declared SHARED, but all of its per-event state lived in one member (tls_, a name left over from the earlier thread_local version). In a multi-schedule job the sPreProcessEvent callback runs on every schedule concurrently, and each call rewrites that member, including a move-assignment of the same std::stop_source. That is a data race in any multi-schedule job that loads Services.Reco or Services.SimAndReco, for example Mu2eG4/fcl/g4test_03MT.fcl with five schedules, even with every budget at 0. With a budget enabled, one schedule starting a new event also replaced the deadline and stop token of a module still running on another schedule. The state now lives in an art::PerScheduleContainer sized at construction, and every entry point takes the art::ScheduleID it acts on. The pre-event callback passes ScheduleContext::id(). ModuleGuard takes the ID once and stores it, and the two consumers, TZClusterFinder and AgnosticHelixFinder, pass their legacy-module scheduleID(). The startModule comment claimed each module starts with a fresh stop token. Stop requests are in fact sticky for the rest of the event, so the comment now says that; the existing FIXME is unchanged. The debugLevel > 1 event print now names the schedule, and art::Utilities is listed as a direct dependency of the library. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DZynGvjEZXCiW6Ag6y5vG
|
@FNALbuild run build test |
|
❌ Those tests have already run or are running for 1dc8bab (build) |
michaelmackenzie
left a comment
There was a problem hiding this comment.
This looks good to me, thanks! I think it's worth noting that we do not intend to run with multiple threads Online, but it's probably best to be prepared for it in case it ever happens.
|
☀️ The build tests passed at 1dc8bab.
N.B. These results were obtained from a build of this Pull Request at 1dc8bab after being merged into the base branch at 329c100. For more information, please check the job page here. |
|
What does "pass the MT test" mean - what test? What is the project and goal? |
Originally the code failed the CI test that used g4MT since I set it to be a LEGACY service without multi-threading support. I had to update to SHARED to pass that CI test. |
|
Why does the g4MT test use the timeout service? How does this test manage the other services, which should all be legacy? It has been years since we discussed MT, so I may have forgot something, but my recollection is that nothing is validated to run MT except Mu2eG4, and that runs MT internally, using geant mechanisms, not in art. |
|
@rlcee The service is loaded by |
Intent
TimeoutWatchdogis declaredSHARED, but all of its per-event state lived in a single member,State tls_. The name is left over from thethread_localversion in #1777; that storage was removed when the service moved fromLEGACYtoSHAREDto pass the MT test. In a job with more than one schedule, thesPreProcessEventcallback runs on every schedule at once. Each call rewrites that one member, including a move-assignment of the samestd::stop_source. That is a data race, and it happens in any multi-schedule job that loadsServices.RecoorServices.SimAndReco, even with every budget at 0.Mu2eG4/fcl/g4test_03MT.fcl(5 schedules,Services.SimAndReco) is one such job. Once a budget is enabled, it gets worse: one schedule starting a new event would replace the deadline and stop token of a module still running on another schedule.Change
art::PerScheduleContainer<State>. It is sized withexpand_to_num_schedules()in the constructor and never resized, so each schedule reads and writes only its own element and no lock is needed.startEvent,startModule,endModule,check,stopToken,eventDeadline,moduleDeadline) takes theart::ScheduleIDit acts on, and access goes through the bounds-checkedat().ScheduleContext::id().ModuleGuardtakes the ID once at construction and stores it, so code callingguard.check()/guard.stopToken()does not change.TZClusterFinderandAgnosticHelixFinder, now pass their legacy-modulescheduleID()toModuleGuard. A GitHub code search finds no other user of the service in Offline, Production or mu2e-trig-config.art::Utilitiesis added to the library's link list, since the library now uses it directly.startModulecomment said each module starts with a fresh stop token. The code does the opposite: a stop request is sticky for the rest of the event. The comment now says so, and the existingFIXMEis unchanged.debugLevel > 1event print now includes the schedule number.Single-schedule jobs behave exactly as before.
Validation
TimeoutWatchdog.cc,TimeoutWatchdog_service.cc,TZClusterFinder_module.ccandAgnosticHelixFinder_module.ccwith the buildtest flags (-std=c++20 -Wall -Werror -pedantic -O3 ..., envsetp106). Linked the library and service plugin with-Wl,--no-undefined.EmptyEvent, 200 events,@table::Services.Reco,eventTimeoutMs : 1000,debugLevel : 2.num_schedules : 5gives exit 0, with events on all five schedules (41/38/43/37/41). Noat()threw, so the container is sized correctly when the service is constructed.num_schedules : 1gives exit 0.Deliberately not in this PR
These came out of the same package review, but each one needs a decision from the trigger group, not a mechanical fix:
put()a truncated collection, and nothing in the event records that it happened.FIXME), so a later module in the same event also seescheck() == true.moduleTimeoutMshas no effect, because both consumers create the guard only when their owntimeoutMs > 0and always pass it.eventTimeoutMsis checked only by modules that have a guard.printfto message-facility, and removing the vestigialSConscript.@michaelmackenzie, this changes the
ModuleGuardconstructor signature. It is worth a look if you have trigger-side code outside Offline that constructs one.🤖 Generated with Claude Code
https://claude.ai/code/session_014DZynGvjEZXCiW6Ag6y5vG