fix(service): Move TTI renewal to the background - #614
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #614 +/- ##
==========================================
+ Coverage 89.22% 89.89% +0.66%
==========================================
Files 109 110 +1
Lines 19089 20308 +1219
==========================================
+ Hits 17033 18256 +1223
+ Misses 2056 2052 -4
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…newal * origin/main: feat(gcs): Add resumable uploads (#609)
| /// of an absent row. | ||
| /// | ||
| /// Returns `true` when the update was applied or its requested state was | ||
| /// already satisfied. Returns `false` for an absent, expired, manual-policy, |
There was a problem hiding this comment.
| /// already satisfied. Returns `false` for an absent, expired, manual-policy, | |
| /// already satisfied. Returns `false` for an absent, expired, |
Given that this performs a generic TieredUpdate, the fact that we fail the op when the policy is manual is probably more suited to rather be documented on TieredUpdate::SetExpiry.
| // leave its update intact and let a future read evaluate the TTI again. | ||
| if response.status() == StatusCode::PRECONDITION_FAILED { | ||
| // A concurrent metadata writer won the CAS race. Leave its update | ||
| // intact; automatic renewal can be retried by a later read. |
There was a problem hiding this comment.
| // intact; automatic renewal can be retried by a later read. | |
| // intact. |
| download_url | ||
| .query_pairs_mut() | ||
| .append_pair("alt", "media") | ||
| .append_pair("ifGenerationMatch", &gcs_metadata.generation); |
There was a problem hiding this comment.
Why only ifGenerationMatch but not ifMetagenerationMatch I wonder?
There was a problem hiding this comment.
We read metadata first and now need a matching payload. If only metadata changes in the meanwhile, the read is still valid.
| // Renewal is an implementation detail of the original client read and | ||
| // must not add another client-operation COGS unit. |
There was a problem hiding this comment.
Right now this is true, but when we change the API to require explicit bumps, we'll have to change this.
There was a problem hiding this comment.
That is true. Since we will only call set_expiry conditionally, I think we can change this right away.
| /// | ||
| /// Millisecond expiry timestamps are not unique revisions. A replacement inline | ||
| /// object with the same row kind and expiry can therefore still be overwritten | ||
| /// by this rewrite. A dedicated persisted revision token is deferred. |
There was a problem hiding this comment.
| /// | |
| /// Millisecond expiry timestamps are not unique revisions. A replacement inline | |
| /// object with the same row kind and expiry can therefore still be overwritten | |
| /// by this rewrite. A dedicated persisted revision token is deferred. |
It seems that this is not relevant.
| worker: Option<RenewalWorker>, | ||
| } | ||
|
|
||
| impl Clone for RenewalScheduler { |
There was a problem hiding this comment.
The scheduler is like a handle to the worker. Instead of externally Arc'ing it, we clone it directly for the background metrics emitter and for stream executor.
| pub fn schedule(&self, id: ObjectId, expire_at: SystemTime) { | ||
| let pending = Arc::clone(&self.inner.pending); | ||
| if !pending.pin().insert(id.clone()) { | ||
| objectstore_metrics::count!( | ||
| "service.expiry_renewal", | ||
| outcome = "skipped", | ||
| reason = "duplicate" | ||
| ); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Bug: A race condition in RenewalScheduler::schedule() can cause TTI renewal requests to be silently dropped due to incorrect ordering of deduplication and queue capacity checks.
Severity: LOW
Suggested Fix
In the schedule() method, perform the queue capacity check using try_reserve() before inserting the object ID into the pending deduplication set. This ensures an ID is only marked as pending if a slot in the queue has been successfully reserved for it, preventing the race condition where a request is dropped after being falsely identified as a duplicate.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: objectstore-service/src/background.rs#L215-L224
Potential issue: A race condition exists in the `schedule()` method of the
`RenewalScheduler`. An object ID is added to the `pending` deduplication set before
checking if the renewal queue has capacity. If two threads call `schedule()` for the
same ID concurrently while the queue is full, the first thread adds the ID but then
fails the capacity check. The second thread sees the ID in the `pending` set,
incorrectly assumes it's a duplicate, and returns. As a result, the renewal request is
silently dropped by both threads.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c5f179f. Configure here.
| Ok(None) | ||
| } | ||
|
|
||
| async fn compare_and_update( |
There was a problem hiding this comment.
Expired tombstones still redirect deletes
Low Severity
delete_non_tombstone still returns an expired tombstone as live. The same change treats expired entries as absent on reads, put_non_tombstone, and compare_and_write, so a delete can still follow the redirect and remove the long-term blob after get already reports not found.
Reviewed by Cursor Bugbot for commit c5f179f. Configure here.


Backend GET and HEAD operations currently renew TTI deadlines inline. These rewrites can race with concurrent tiered-storage commits and overwrite newer state.
This change makes backend reads side-effect-free. The service schedules bounded, best-effort renewals after successful reads. It adds extend-only conditional expiry updates for each backend and renews tiered objects blob-first before updating the matching redirect.
Ref FS-405