Repository navigation
Conversation
SchedulerExecutionQueue.schedule writes the backstop row honouring an active lease, then queued an in-memory copy whatever that write did. When the row was leased by a worker, or by a copy queued in another JVM, the copy ran alongside it and the workflow ran twice at once. The scheduler now reports whether it honoured a lease (Scheduler.scheduleWithOutcome), and the queue keeps no copy in that case. The lease holder reruns the task through the version bump the honoured write records. The engine fails the caller's result future with ResultUnavailable when no copy was queued, so a synchronous caller reads the result from storage instead of waiting forever. The TLA+ model's InMemorySchedule follows suit: inmem_overlap passes, its calibration finds the overlap, the stranded start no longer needs its own path, and NoLongPauses no longer assumes a start never stalls.
…t and overwrite returns Wrapping each return inside the statements' use blocks gave the mutation gate closeFinally mutants on lines no test can observe. Only the honour-lease branch now records the outcome.
This branch has not been deployed
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.
Stacked on #46 (base:
feat/child-workflow-builder).Problem
For a due task,
SchedulerExecutionQueue.schedulewrites a backstop row that honours any active lease, then queues an in-memory copy in this JVM. It queued that copy no matter what the write did. So when the row was already leased, by a worker that fetched it or by a copy queued in another JVM, the workflow ran twice at once. Three paths get there:startWorkflowof a running workflow withCREATE_EXISTING_WORKFLOW_IS_NOOPoff;FORCE_SIGNAL_WORKFLOW_EXEC_IN_SCHEDULERoff), including one sent while this JVM's own copy is running;#46 sidestepped this for the stranded-start recovery only (
scheduleAfterLease). This fixes it in the queue.Approach
Scheduler.scheduleWithOutcomereturnsScheduleOutcome(task, leaseHonored). The lease check and the write are one transaction, so only the store can answer this without a race. SQLite and MySQL implement it, andscheduledelegates to it. The interface default reports "not honoured", so a third-partySchedulerkeeps today's behaviour until it overrides it.Scheduled(task, queuedInMemory), so callers know whether a copy carrying their payload will run here.BUMP_TASK_VERSION_ON_HONORED_LEASE(on by default), the honoured write bumps the row version. The holder's finalremovethen fails, andfinishTaskreruns the task at once. A row that nobody is running is delivered when it comes due or its lease lapses.ResultUnavailable, as arunAsyncstart does. The proxy then reads the result from storage. Without this, a synchronous caller would wait forever on a future nobody completes. This also covers the existing "a copy is already queued here" case, which had the same problem.Trade-offs
resultPollingTimeLimit, 30 s by default) instead of getting it from a second, concurrent run. A run that takes longer surfaces as aTimeoutException, as it does forrunAsync.Task.hasActiveLeasealso holds for a row that is only due within a lease: a short retry backoff, or feat: retry the parent when a child workflow started with asChildOf fails to start #46's after-a-lease row. An in-process schedule now waits for that row instead of running at once. The persistent path already has this latency, and it can't double-run.scheduleAfterLeaseis no longer needed for safety: the model passes with the stranded start scheduled like any other start. I left it alone to keep this change narrow. Switching the recovery back to the ordinary path would be a small follow-up, and it would also make the recovering caller's result available again.bydelegation doesn't forward Java default methods, so aSchedulerwrapper must forwardscheduleWithOutcomeitself. The interface Javadoc says so, and the three test wrappers that usebynow do.Testing
Unit
QueueBasedSchedulerTest(two queues sharing one persistent scheduler, i.e. two nodes):removefails, which is the rerun signal;scheduleAfterLease-then-in-memory case is updated: the row now delivers the task after the lease.All four fail with the honour check disabled.
BaseSchedulerTestcoversscheduleWithOutcomeon SQLite and MySQL.SkipperEngineTestchecks that an in-process start with no copy queued getsResultUnavailable.Integration
InMemoryScheduleOnLeasedTaskTest: an in-process signal arrives during an in-process run. With the check disabled it fails withexpected: <1> but was: <2>executions, a real second run entering the action. With the fix it passes.Full suite: green locally, except the MySQL suites: MariaDB4j can't start on my machine, and CI runs them.
spotlessCheckis clean.Model check (
formal/tla/check.sh, tla2tools v1.7.4). All 16 models match their expected verdicts. What changed:inmem_overlap(new): two JVMs start a running workflow in process, plus an in-process signal;NoLongPausesNoOverlappingRunsviolated in 8 steps (= the calibration)inmem_overlap_unguarded(new calibration: copy queued on an honoured lease)NoOverlappingRunsviolatedstart_race_unguardedwith onlyRecoverAfterLeaseoffNoOverlappingRunsviolatedstart_race_recover_in_memory;start_race_unguardednow turns off both guards and still failsstart_race/inmem_overlap, withNoLongPausesno longer assuming a start never stalls between its two writesinmem_startRunningIsDrivenRunningIsDriven(unchanged)inmem_start+NoOverlappingRuns(side experiment, not committed)All other models are unchanged.
Trace validation (
record_traces.sh, thencheck_traces.sh --self-test):