Repository navigation
feat(jobs): per-campaign and per-remote-operation locks - #177
Merged
Merged
Conversation
Two workers, or a worker and an operator, can no longer act on the same
campaign or the same provider list at once.
- Schema 8 adds {prefix}campaignbridge_locks. Acquiring inserts the name,
so the primary key admits one holder; taking over an expired lock is a
conditional update with one winner; only the holder can release.
- Lock_Manager holds the lock for a whole operation, provider calls
included, and releases it in finally. A caller that finds it held does
nothing and gets the new `locked` workflow error (REST 409) with
retry_after, the seconds until the lock expires. A lock left by a process
that stopped expires after five minutes and the takeover is audited.
- Draft handoff, test send, schedule, unschedule, send, and reconcile hold
the campaign lock; a discovery refresh holds a lock per account and list
and, when one is already running, returns the cached list with a
retryable refusal instead of calling the provider again.
- The campaign timeline labels a takeover "Recovered an interrupted
operation" with the operation that stopped.
Verified on MySQL and on the Studio SQLite site.
Closes #169
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… suppressions (#178) 116 files under includes/ disabled Squiz.Commenting.FunctionComment, Generic.Commenting.DocComment.MissingShort, and FunctionCommentThrowTag at the top of the file, which hid 1,378 findings: 401 functions with no doc comment, 567 missing @PARAM tags, and tags that no longer matched their signatures. Every one of those disables is removed and the code is now documented instead: - Each function has a summary describing what it does, and a complete, ordered @PARAM list with the signature's types (keeping the existing PHPStan generic types) and a description. - Interface implementations use {@inheritdoc}, so each port documents its contract once; phpcs.xml.dist enables Squiz's skipIfInheritdoc for them. - Every exception a function throws has a @throws tag. - Typed callable and Closure signatures move to @phpstan-param, which Squiz's type-hint check cannot parse. Database_Values::decode_json() now reports malformed JSON as the InvalidArgumentException it already documented, with the JsonException as the previous exception, instead of leaking an undeclared exception type; the three repository hydrators that caught both now catch the one. Database-boundary disables are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TheAggressive
force-pushed
the
feat/locks
branch
from
October 11, 2026 10:56
4d7205a to
7adb6c4
Compare
TheAggressive
enabled auto-merge (squash)
October 11, 2026 11:06
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.
Closes #169. Part of #66 (M5). Rebased onto
masterafter #176 merged. It also carries #178 (the documentation pass), which was merged into this branch while it was #178's base, so landing this PR brings both tomaster.What this adds
{prefix}campaignbridge_locks, behind aLock_Sourceport.Workflow/Lock/Lock_Manager. Holds the lock for the whole operation, provider calls included, and releases it infinally.lockedworkflow error: REST409 campaignbridge_campaign_lockedwithretry_after, the seconds until the lock expires.campaign_lock_takeoverorprovider_lock_takeover.mailchimp_refresh_in_progressinstead of a second provider call.Evidence (#169)
Campaign_Schedule_Route_Test: while a background worker holds the campaign, send, schedule, test send, and reconcile each return409 lockedwithretry_afterof 300 seconds or less, and no request reaches Mailchimp. After release, the send goes through with exactly one action and releases its own lock.Lock_Manager_Test:Provider_Discovery_Routes_Test: a refresh run while another holds the lock makes no Mailchimp call and returns the cached list with a retryable refusal.locks.held_until, takeover with the interrupted purpose, one winner of a racing takeover, and owner-only release. Studio was re-stamped to schema 6 afterwards.Verification
pnpm qa:fastpasses.🤖 Generated with Claude Code