fix(infra): stop the old task before starting the new one on the singleton services - #73
Merged
Merged
Conversation
…leton services execution-service and market-maker-spot deployed with the ECS defaults, 100% minimum healthy and 200% maximum, so every rolling deploy ran two tasks at once. Both are singletons, and the matcher already sets 0/100 for exactly this reason. This applies the same setting to the two that were missed. execution-service: two tasks sign with the SAME KMS key, and nothing coordinates the executor EOA's nonce across processes. serial-queue.ts serialises within one process and states the hazard plainly -- "viem reads the next nonce from the RPC at send time, so two sends in flight at once can take the same nonce and one replaces or rejects the other". There is no load balancer to drain: Cloud Map returns an A record for BOTH tasks (MULTIVALUE, TTL 10s), so a settlement lands on whichever the matcher resolves. The deployment circuit breaker is disabled, so nothing would have caught it either. market-maker-spot: two market makers quote the same book. Already observed -- a draining task kept placing orders for seconds after its successor had finished its startup reconciliation, leaving quotes resting under the previous configuration that nothing re-examined. markets-service is deliberately left rolling. It is an HTTP API with no singleton constraint, so rolling avoids downtime and costs nothing. The cost is a gap of roughly a minute or two per deploy on those two services. For settlement that is lag, not loss: the matcher classifies an unrecognised executor error as transient (internal/matching/revert.go, revertUnknown is the zero value and documented as "treated like a transient failure") and retries on a doubling backoff -- 2s to a 5m cap, then every 30m after twelve failures -- that never cancels an order. A connection failure carries no revert selector, so it cannot be classified permanent and cannot park a pair. Applying this changes deploymentConfiguration only. It registers no task definition and forces no new deployment, so nothing restarts. Found while preparing the execution-service deploy of #71/#69: the rollout plan was a rolling one, and the nonce sequence is shared. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What
execution-serviceandmarket-maker-spotdeploy with the ECS defaults — 100% minimum healthy, 200% maximum — so every rolling deploy runs two tasks at once. Both are singletons. The matcher already sets0/100for exactly this reason; this applies the same setting to the two that were missed.Why it matters for execution-service
Two tasks sign with the same KMS key, and nothing coordinates the nonce across processes.
serial-queue.tsserialises within one process and states the hazard itself:There is no load balancer to drain — Cloud Map returns an A record for both tasks (
MULTIVALUE, TTL 10s), so a settlement lands on whichever the matcher resolves. The deployment circuit breaker is also disabled, so nothing would have caught a collision.Why it matters for market-maker
Not hypothetical. A draining task kept placing orders for seconds after its successor had finished startup reconciliation, leaving quotes resting on the live book under the previous configuration that nothing re-examined.
The cost, and why it's acceptable
A gap of roughly a minute or two per deploy on those two services. For settlement that's lag, not loss, and I checked the matcher rather than assuming:
internal/matching/revert.go—revertUnknownis the zero value, documented as "treated like a transient failure". A connection error carries no revert selector, so it cannot be classified permanent and cannot park a pair.internal/matching/backoff.go— retries on a doubling backoff, 2s to a 5-minute cap, then every 30 minutes after twelve failures, until an order expires (the venue signs for a day). It is explicitly "non-destructive: it delays retries, it never cancels an order."A ~2 minute gap accumulates ~6 failures, so settlement resumes within a couple of minutes of the service returning.
Applying this restarts nothing
The plan is two in-place service updates changing only
deploymentConfiguration:No task definition is registered and no new deployment is forced, so running tasks are untouched.
Context
Found while preparing the execution-service deploy of #69/#71 — the rollout would have been a rolling one over a shared nonce sequence. With this merged, that deploy becomes a single reviewed apply that stops before it starts, instead of a manual scale-to-zero dance that has to be remembered every time.
🤖 Generated with Claude Code