Skip to content

fix(infra): stop the old task before starting the new one on the singleton services - #73

Merged
robertleifke merged 1 commit into
mainfrom
fix/singleton-services-stop-before-start
Sep 20, 2026
Merged

robertleifke merged 1 commit into
mainfrom
fix/singleton-services-stop-before-start

Conversation

@robertleifke

Copy link
Copy Markdown
Contributor

What

execution-service and market-maker-spot deploy with the ECS defaults — 100% minimum healthy, 200% maximum — so every rolling deploy runs two tasks at once. Both are singletons. The matcher already sets 0/100 for exactly this reason; this applies the same setting to the two that were missed.

service before after why
markets 100/200 unchanged HTTP API, no singleton constraint — rolling avoids downtime
matcher 0/100 unchanged already correct
execution 100/200 0/100 shared executor EOA nonce sequence
market-maker 100/200 0/100 two makers quote the same book

Why it matters for execution-service

Two tasks sign with the same KMS key, and nothing coordinates the nonce across processes. serial-queue.ts serialises within one process and states the hazard itself:

"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 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.gorevertUnknown is 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:

~ deployment_maximum_percent         = 200 -> 100
~ deployment_minimum_healthy_percent = 100 -> 0
Plan: 0 to add, 2 to change, 0 to destroy.

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

…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>
@robertleifke
robertleifke merged commit 9e36661 into main Sep 20, 2026
1 check passed
@robertleifke
robertleifke deleted the fix/singleton-services-stop-before-start branch September 20, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant