Implement Shard lease protocol: RenewShardLease, Deregister, epoch in AssignShards - #3834
Conversation
✅ Deploy Preview for golemcloud canceled.
|
…er persist before acknowledging
…sion when it lapses
…s and test the executor lease paths
…revision, and sweep on every receipt path
… race renewals against shutdown, and compact etcd history
…revokes on it, and bound the lease RPCs
a6db0f8 to
da41488
Compare
|
One open question for you, because it is a policy number rather than a protocol property.
|
| /// its own clock, anchored no later than the grant that told it, and nothing but its next | ||
| /// renewal can shorten its copy - so a re-grant under a reduced `shard_lease_duration` that | ||
| /// used the new length would lapse here before it lapses there. The configured length applies | ||
| /// from that renewal; a longer one applies at once. |
There was a problem hiding this comment.
Agent review — outstanding lease deadlines must also be preserved on renewal. This protects against shortening during startup, but the first renewal calls
renew_lease, which unconditionally replaces the deadline withnow + lease_ttl.Example: the executor has an acknowledged deadline of t=60. The manager restarts at t=15 with the duration reduced from 60s to 30s. A renewal at t=20 stores t=50; if its reply is lost and subsequent renewals fail, the executor retains t=60 while the manager can reclaim and redistribute its shards from t=50.
Please preserve the outstanding manager deadline on renewal, e.g.
max(existing_expiry, now + lease_ttl), and add coverage for an early renewal whose response is lost. Updating the executor’s deadline on receipt cannot fix a response it never receives.
| tracing::info!( | ||
| "Shard lease has lapsed; agents for the delivered set are recovered once it is renewed" | ||
| ); | ||
| return Ok(RecoveryOutcome::DeferredUntilLeaseIsLive); |
There was a problem hiding this comment.
Agent review — the initial startup caller needs to retain this deferred-recovery outcome.
WorkerExecutorImpl::newcurrently discards the successfulRecoveryOutcome. If registration returns a nonempty assignment with an already-expired request-anchored deadline, startup skips recovery without settingrecovery_pending. A subsequent unchanged renewal restores admission but does not start those agents.The queued repair push is not a guaranteed fallback: if that renewal advances the executor’s revision first, the older push is acknowledged as stale without performing recovery, ending the manager’s retries.
Please mirror the push path at startup: mark recovery pending before awaiting assignment effects, and clear it only on
Recovered. Keep interruption ordering and startup-fatal errors unchanged. Add startup-path coverage for an expired nonempty registration followed by an unchanged live renewal.
resolves GOL-448
Shard ownership was push-only: the shard manager told an executor what it held and never heard from
it again, so a wedged executor kept its shards indefinitely. This makes ownership a lease that lapses
unless it is renewed, and gets each shard's epoch to the executor so ticket 5 can fence oplog writes
on it. Every behavioural test is mutation-checked. Draining agents on lease loss and the oplog fence
itself are next steps; #3766 lands after them
What this adds
RenewShardLease. An executor renews at a third of the time left on its lease (about 20 s onthe 1 m default), claiming the epoch of every shard it holds. Failed renewals back off exponentially,
capped by the lease; a lease with no expiry (single-shard mode, the debugging service) never renews.
owner, never on a renewal, so a renewal is idempotent and a lost response is harmless.
executor, which the executor adopts exactly as it would a push. Pushes deliver a change at once;
renewals guarantee it arrives within a third of the lease even if the push was lost. The manager
logs the mismatch, which is the one signal that pushes to that executor are not landing.
revisionon every delivery (RegisterSuccess,AssignShards,ShardLease): the revisionof the persisted state the set was read from. The executor applies a delivery only if that is at
least the last one applied, so a push and a renewal that cross on the network cannot leave the older
set in place.
delivery names the revision it really lands at instead of predicting one that a renewal persisting
in the meantime would consume. And a revoke - the one delivery with no revision, so nothing can
order it against a grant - is only ever sent for a shard the store has already moved, so a renewal
served in the middle of the fan-out cannot hand it straight back. Epochs are minted in that same
write, so no executor is ever told an epoch the store does not hold, and a refused write changes
nothing anywhere.
recover the agents on gained ones, announce. Registration, pushes and corrected renewals all go
through it, so a renewal that narrows the set sweeps exactly as a push would.
lease_ttl), anchored to the executor's own clock onreceipt, so the two machines' clocks are never compared and skew cannot lengthen or shorten a lease.
Decoding is total: an absent, negative or out-of-range value is an error, never a panic and never a
lease that does not expire.
A lapsed lease is reclaimed and its shards re-homed within one tick, even in an idle cluster.
answers
ShardingNotReady, which the worker service already answers by refreshing its routing tableand retrying. Running invocations are not interrupted, and routing checks are never fenced.
Deregister. A graceful stop, including theSIGTERMan orchestrator sends, hands the leaseback so the shards move on the next tick rather than after the lease expires.
AssignShardsis a full-replace push carrying epochs, the lease TTL, the revision and theshard count. It absorbs
SetShardAssignment, which is deleted and its field number reserved.Registerpersists before it acknowledges, and is idempotent on retry, so an acknowledgedregistration is always one the next leader will find.
manager restart or failover never evicts a healthy cluster.
otherwise. The leader compacts the history behind the state after each pass, keeping the newest
compaction_retention_revisions(1000 by default;0disables it, for a cluster shared with otherapplications). A failure to compact is logged and never delays a pass.
Decisions the ticket does not make
shard_lease_duration; a second setting would have tobe kept consistent with it by hand.
That is not idempotent: one lost response leaves the executor an epoch behind for shards it still
legitimately owns.
the past, and the ticket's "call
housekeepat the top of each pass" would evict the whole clusteron pass one.
replica needs stored state to compare against, and a cold leader would otherwise persist nothing
until its first executor registered.
shard_epochs = 1, which is a wirebreak against the
shard_idsthat lives there. The old-bytes decode test passes with or without thereservedlines, because prost skips unknown fields; whatreservedbuys is protoc's build-timerefusal of a future re-use, so please don't simplify it away.
ShardLeaseErroris aoneof, mirroringQuotaError, rather than the error code the ticketnames:
ShardManagerErrorhas nothing to attach a code to.stale_epoch, so that awrong picture is never extended. But refusing it holds an executor whose pushes cannot reach it - a
one-way partition - on that wrong picture until its lease lapses and its agents restart, through the
one channel that still works. Correcting the picture in the same round trip that extends the lease
removes the objection. That is only safe with the revision order and the single receipt path above,
which is why they exist; together they are push-plus-periodic-resync with a resource version, the
shape informers use.
executor, discards its write whole, and must not take another executor's reaping with it.
ownership by the time anything is sent, so an executor that missed a revoke or a push is queued for
a full push of the set it is now recorded as holding, rather than the plan being unwound. Its own
next renewal carries the same set, so the repair is bounded even if the push fails again. This
replaces stripping the failed shards out of the plan, which could re-mint an epoch that had already
been pushed - two holders of one shard at the same epoch, the pair an oplog fence cannot separate.
LazyWorkerActivator: it closes over the service graph that owns that service, so a strongreference would be a cycle nothing could free.
WorkerExecutorImplowns it.the renewal and deregister calls take one attempt rather than the client's retries: the loop owns
the backoff, and a termination signal has to be seen inside the grace
mainwaits.SIGTERMis handled in the executor'smain. It trips the graph-wide shutdown token and waits abounded grace on a
TaskTrackerthat the renewal loop spawns through, so the deregister RPC is notcut off by runtime teardown. That is tokio-util's
rtfeature, declared on the workspace's existingdependency;
create_shard_manager_servicetakes the wholeShutdownrather than its token.Weak<Self>is not its exit; the shutdown token is. The assignment-changedhook makes the service reach itself, so
upgrade()cannot fail. The cycle is the same shape as thepre-existing
lazy_worker_activatorone.write, about
3Nper lease period, each a new etcd revision. The leader compacts the historybehind them, so the volume costs etcd throughput but not disk; ticket 7 stacks quota renewals on
top.