Repository navigation
Renew the CAS mount lease until a definitive answer; an expired lease is a pause - #2517
Conversation
The plane already carries the claim and the remount re-anchor, and the background renewal moves onto it next; "farewell" names one of its users. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Retry::untilDefinitive` has no window and no lease bound. Its reissues wait a draw from [0.8, 1.2] of 1000 ms minus what the failed attempt took (`Retry::pauseAfterAttempt`) instead of the growing backoff, so the first good second after an outage ends a retry series. Existing policies are unchanged. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The background renewal gave up at the lease deadline, fenced the mount and remounted under a fresh epoch, although the store had not answered and nothing had touched the slot. It now keeps one tuple and retries it on the lease plane under `Retry::untilDefinitive`: - a refusal waits one renewal period and sends the same body again; - a refused precondition whose read failed repeats only the read; - a commit, a slot that moved, a local failure, a stop or a park ends the series. A stop between engine calls, after a refusal or a read that found the slot unchanged, leaves the renewer active so the farewell still runs; a stop during a call that sent an attempt, or while the reads repeat, is terminal and classified as cancelled. A committed deadline is published in `DriverLease::finish`, under the driver mutex, so a remount cannot arm a new epoch between a commit and its deadline. The lease plane sleeps interruptibly on a stop or a park, like the mount plane. The startup and remount re-anchors stay bounded by the lease, and so does a runtime with `bounded_background_renewal`. Closes: #2431 Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decommission tool opens its pool with the background renewal. An admin claim that cannot renew should end at its lease rather than pause, so `Pool::openForDecommission` sets `bounded_background_renewal`. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writes stop being admitted once the lease has less than two attempt envelopes plus the safety margin left. `system.cas_mounts` now shows `not_live` / `lease_expired` from that same moment, computed by the predicate admission uses, and one WARNING per pause names the writer epoch and the last renewal error. The refusal texts a write meets during a pause say that writes resume once the renewal is confirmed. A clean shutdown during a pause skips the farewell, which has no lease room left, and leaves the slot for GC to observe. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared fixtures for the renewal series (`CASHeartbeatUntilDefinitive`, `BackgroundRenewal`) and tests for every way a series ends, the refusal pause, stop and park, the late commit against GC, the pause report and the teardown during a pause. Three runtime tests that relied on a transient fault ending the background renewal fence the slot instead. The integration test checks that a mount PUT outage longer than the lease is a pause under the same `writer_epoch`. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2f650d9 to
e3588e3
Compare
Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k-morozov
left a comment
There was a problem hiding this comment.
LGTM. My comments are mostly related to refactoring and don't affect the resolution of the issue.
| /// The background renewal, on the lease plane, retried until a definitive answer: a commit, a | ||
| /// conflict the read observed, a local failure, or `environment.live` refusing. A refusal sends the | ||
| /// same body again, and a refused precondition whose read failed repeats only the read. | ||
| MountRenewResult renewUntilDefinitive(const MountRenewOperationEnvironment & environment); |
| ? Retry::untilDefinitive() | ||
| : Retry::untilLeaseSafe(confirmed_deadline_boot_ms, static_cast<uint64_t>(lease_safety_margin.count())); |
There was a problem hiding this comment.
untilLeaseSafe use deadline. Maybe could specify this info?
|
|
||
| MountRenewResult MountLeaseRenewer::renewOn( | ||
| CasRequests & plane, const MountRenewOperationEnvironment & environment) | ||
| CasRequests & plane, const MountRenewOperationEnvironment & environment, bool until_definitive) |
There was a problem hiding this comment.
Suggest to use particular policy instead bool flag. Like policies in CasOperation::writeLoop.
| break; | ||
| } |
There was a problem hiding this comment.
I am wondering that we could forget to process new value in std::variants.
| written = op.replace(key, body, precondition(), | ||
| Retry::untilLeaseSafe(confirmed_deadline_boot_ms, static_cast<uint64_t>(lease_safety_margin.count()))); | ||
| written = op.replace(key, body, expected, policy); | ||
| while (until_definitive) |
There was a problem hiding this comment.
Maybe we could update this place because it depends on the specific policy.
| { | ||
| public: | ||
| MountLeaseRenewer( | ||
| CasRequests & mount_requests_, CasRequests & open_requests_, const Layout & layout_, |
There was a problem hiding this comment.
It is not related with current PR.
I suggest rename open_requests_ to `lease_requests' becasee in the next it is confusing.
| /// When set, a reissue in `CasOperation`'s write and read loops waits until a draw from [0.8, 1.2] | ||
| /// of this has passed since the failed attempt started, instead of the growing backoff. A | ||
| /// first-attempt fuse timeout is still reissued at once. | ||
| std::optional<uint64_t> attempt_spacing_ms = std::nullopt; |
There was a problem hiding this comment.
Suggest change name. For example reissue_interval_ms?
| result = active == RenewalDriverState::RemountCall | ||
| ? call.renewer->renewForRemount(renewalEnvironment(worker_call)) | ||
| : call.renewer->renew(renewalEnvironment(worker_call)); | ||
| if (active == RenewalDriverState::RemountCall) |
There was a problem hiding this comment.
Different cas requests are being used here inside the methods, though it’s not immediately obvious. I can’t suggest any simple refactoring for this. Need to think.
The base changed `mountObservationThresholdMs` and `computeHeartbeatFloor` to take the observer and declared leases; the test kept the old signatures and broke the build: https://altinity-build-artifacts.s3.amazonaws.com/json.html?PR=2517&sha=d410298e2c4717738d9c3c8adc055ab99a240b01&name_0=PR&name_1=Build%20%28amd_debug%29 Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI triage for 54d1258. No red is caused by this PR.
|
Brings in the base merge of PR B: #2566 (member lifecycle PR A, final review) and #2517 (renewal until a definitive answer). - The foreign-owner refusal takes the #2566 text, which names `SYSTEM CAS DROP POOL MEMBER` for a dead owner. - Docs keep this branch's rejoin rules and add the #2566 rows for the terminal-slot claim and ids with `/`. - The unverified-tombstone test expects this branch's `decommission is not finished` refusal. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The background CAS mount lease renewal used to give up when it got no definitive answer from the object store inside its budget. It then tripped the fence and started a remount under a fresh epoch. A short object-store outage (tens of seconds) cost about 40 s without writes, restarted merges and counted a
CASMountLeaseLost, although nothing had touched the slot and the holder still had its lease on its own clock. Now the renewal retries until the store gives a definitive answer. While the lease cannot cover a write, writes pause and resume under the same epoch; the mount is no longer lost.The branch is rebased on
antalya-26.6-20261008, which already contains #2509 and #2513. It is not stacked any more. Read it commit by commit.Rollout
cas_mount_lease_ttl_ms.How it works
cas_mount_renew_period_msthe renewal thread builds one tuple and keeps it for the whole series of attempts: key, body bytes, the expected ETag of the last confirmed body,write_attempt_id, andstart, taken before the first request.PUTruns on the lease plane underRetry::untilDefinitive, which has no deadline. After an unclear attempt, the next one waits a uniform draw in [0.8, 1.2] of 1000 ms minus what the attempt took (Retry::pauseAfterAttempt). There is no growing backoff, so the first good second after an outage ends the series.AccessDenied,MalformedXML, …) waits one renewal period, then a new engine call sends the same tuple. A refused read waits the same period. A clean412whose read failed repeats only the read. A refusal after an unclear attempt is settled inside the call.DriverLease::finishpublishes the deadlinestart + leaseunderdriver_mutex, before the driver leaves the call, so a remount cannot arm a new epoch in between. The store is unconditional; it is monotonic because a renewal starts only after the previous one finished, and the renewer cannot be replaced during a call.gc_fenced, a deleted slot, or our token over other bytes. This goes to the existing loss handling.NotAttempted: nothing of ours is in flight, and the farewell still hands the slot back.cancelled: the request may still land, or the slot may have moved. No farewell runs.2 × envelope + cas_lease_safety_margin_msleft. This rule is unchanged.system.cas_mountsshowslifecycle = 'not_live'andlifecycle_reason = 'lease_expired', withlifecycle_since= when writes paused.WARNINGper pause. It names thewriter_epochand the last renewal error:CAS mount lease of '<srid>' under writer_epoch <E> has too little time left for a write … Last renewal error: ….INFO, and leaves the slot for GC to observe.bounded_background_renewal).expires_at_msalready in the past. GC does not fence the slot for it, because GC fences only a token it watched unchanged for its whole threshold.Settings (no new ones)
cas_mount_lease_ttl_ms: the length of the right to write from each confirmed renewal.cas_mount_renew_period_ms: the interval between renewal starts, and the wait after a refusal.cas_attempt_timeout_ms: the budget of one attempt, hence the admission envelope.cas_lease_safety_margin_ms: unchanged.Risks / notes
CASMountRenewalDeadlineExceedednow counts only bounded renewals: the startup and remount re-anchors, and a decommission's background renewal. An outage of a normal mount no longer moves it orCASMountLeaseLost; alert onsystem.cas_mounts.lifecycle_reason = 'lease_expired'instead.Livewhile admission is closed). Tracked as backlog task CAS-256.mounts-and-leases.md,debugging.md,monitoring.md,troubleshooting.md,metrics_and_events.md,cas_mounts.md.Testing
CASRetrySpacing.*andCASRequestsSpacing.*: no deadline, spaced attempts, the standard backoff unchanged.CASHeartbeatUntilDefinitive.*, one tuple past the lease and the refusal pause:RetriesOneTuplePastTheLease,ARefusedRenewalIsSentAgainAfterTheRefusalPause,AfterACleanPreconditionRefusalOnlyTheReadRepeats.CASHeartbeatUntilDefinitive.*, every way a series ends after a refused precondition:AReadThatFindsAForeignHolderEndsTheSeries,AReadThatFindsOurSlotFencedEndsTheSeries,AReadThatFindsTheSlotDeletedEndsTheSeries,OurTokenOverOtherBytesEndsTheSeries.CASHeartbeatUntilDefinitive.*, stops and liveness:AStopBetweenCallsEndsTheSeriesAsCancelled,AStopDuringTheReadsEndsTheSeriesWithoutAFarewell,ALostLivenessDuringTheReadsEndsTheSeriesAsALoss.CASHeartbeatUntilDefinitive.ALateCommitRestartsTheGcObservation: a late commit against GC.CASMountRuntime.*: a long outage is a pause under the same epoch; an old epoch's renewal does not move the new deadline; a late commit reports a true reason; a remount request ends a series.CASMountRuntime.*, stop and park: they end the refusal wait at once, and the farewell survives a stop.CASMountRuntime.*, the rest: one refusal waits one period; the decommission renewal ends inside its lease; the pause is logged once with the last error; a clean shutdown during a pause leaves the slot.CASLifecycleSnapshot.*: the row isnot_livefrom the admission edge.CASHeartbeat.CancellationAfterSendIsTerminalAndForbidsReleasenow also asserts classificationcancelled.test_long_mount_put_outage_pauses_under_the_same_epoch;test_cas_mount_renewal_retry6 passed.CAS*gate: 2670 passed (release), 2674 passed (ASan, no error).models/mount-lease-series-v2@1e4f3222015, all batteries green. Pause safetyResumedEpochNeverWritesAfterGcActedholds inCaRenewalDriver.Closes: #2431
Related: #2509
Related: #2513
Related: #2516
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
A CAS mount lease renewal retries until the object store gives a definitive answer instead of giving up at a deadline. While the lease is too short to admit a write, writes pause and resume under the same epoch once a renewal is confirmed, instead of losing the mount and remounting.
Documentation entry for user-facing changes
CI/CD Options
Exclude tests:
Regression jobs to run: