Leave old SlateDB authorization-code records in place

Drop the startup retirement of the auth/code keyspace instead of
carrying one-shot cleanup code forever. The records it deleted are
inert: at most a handful exist at cutover, every binary (old or new)
rejects them within 60 seconds of issue via the expiry check, and
nothing reads the keyspace after the move to SQLite. The refresh-token
retirement keeps its original inline shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-08-24 13:24:13 -04:00
parent a2f0167844
commit 68ef8c7e89
2 changed files with 12 additions and 73 deletions

View file

@ -783,24 +783,13 @@ where
)
.await
.context("activating SQLite blob storage")?;
// Refresh tokens and authorization codes now live in SQLite. Nothing reads
// the old records and no reaper collects them any more, so clear them out
// once rather than leaving them in the object store forever. Retiring the
// authorization-code keyspace is fatal on failure; refresh-token cleanup
// stays best-effort.
let (retired_authorization_codes, retired_refresh_tokens) = tokio::join!(
store.retire_authorization_code_keyspace(),
store.retire_refresh_token_keyspace(),
);
let retired_authorization_codes =
retired_authorization_codes.context("retiring SlateDB authorization code records")?;
if retired_authorization_codes > 0 {
info!(
removed = retired_authorization_codes,
"Removed retired SlateDB authorization code records"
);
}
match retired_refresh_tokens {
// Refresh tokens now live in SQLite. Nothing reads the old records and no
// reaper collects them any more, so clear them out once rather than
// leaving them in the object store forever. Pending authorization codes
// also moved to SQLite, but their old records are left in place: at most a
// handful exist at cutover, every binary rejects them within 60 seconds of
// issue, and nothing reads their keyspace again.
match store.retire_refresh_token_keyspace().await {
Ok(0) => {}
Ok(removed) => info!(removed, "Removed retired SlateDB refresh token records"),
Err(err) => warn!(error = %err, "Failed to remove retired SlateDB refresh token records"),

View file

@ -436,24 +436,10 @@ impl Database {
/// nothing else would ever remove them. Returns the number of records
/// deleted; a later boot finds the prefix empty and does nothing.
pub async fn retire_refresh_token_keyspace(&self) -> Result<u64> {
self.retire_keyspace(keys::SlateKey::new("auth").with("refresh"))
.await
}
/// Delete every record under the retired `auth/code` prefix.
///
/// Authorization codes move to SQLite without an import. Their short
/// lifetime makes them safe to discard, while deletion prevents an older
/// binary from accepting a code issued before the storage cutover.
/// Returns the number of records deleted; later boots are no-ops.
pub async fn retire_authorization_code_keyspace(&self) -> Result<u64> {
self.retire_keyspace(keys::SlateKey::new("auth").with("code"))
.await
}
async fn retire_keyspace(&self, keyspace: keys::SlateKey) -> Result<u64> {
let db = self.open_db().await?;
let mut iter = db.scan_prefix(keyspace.into_prefix()).await?;
let mut iter = db
.scan_prefix(keys::SlateKey::new("auth").with("refresh").into_prefix())
.await?;
let mut batch = slatedb::WriteBatch::new();
let mut deletes = 0_u64;
while let Some(entry) = iter.next().await? {
@ -584,8 +570,8 @@ mod tests {
.as_ref()
.to_vec()
});
// "auth/code" sorts adjacent to "auth/refresh" and is still live, so
// it is the neighbour a too-wide prefix delete would take with it.
// "auth/code" sorts adjacent to "auth/refresh", so it is the
// neighbour a too-wide prefix delete would take with it.
let auth_code_key = keys::SlateKey::new("auth")
.with("code")
.with("keep")
@ -610,42 +596,6 @@ mod tests {
);
}
#[tokio::test]
async fn retire_authorization_code_keyspace_clears_only_its_prefix_and_is_idempotent() {
let (_object_store, store) = make_store();
let db = store.open_db().await.unwrap();
let authorization_code_keys = ["aaa", "bbb"].map(|id| {
keys::SlateKey::new("auth")
.with("code")
.with(id)
.as_ref()
.to_vec()
});
let neighboring_key = keys::SlateKey::new("auth")
.with("refresh")
.with("keep")
.as_ref()
.to_vec();
let mut batch = slatedb::WriteBatch::new();
for key in &authorization_code_keys {
batch.put(key.as_slice(), b"{}".as_slice());
}
batch.put(neighboring_key.as_slice(), b"{}".as_slice());
db.write(batch).await.unwrap();
assert_eq!(store.retire_authorization_code_keyspace().await.unwrap(), 2);
assert_eq!(store.retire_authorization_code_keyspace().await.unwrap(), 0);
for key in &authorization_code_keys {
assert!(db.get(key.as_slice()).await.unwrap().is_none());
}
assert!(
db.get(neighboring_key.as_slice()).await.unwrap().is_some(),
"retiring authorization codes must not touch neighboring auth prefixes"
);
}
async fn make_summary_store() -> (tempfile::TempDir, Arc<RunSummaryStore>) {
let (directory, store) = store_test_support::sqlite_summary_store().await;
(directory, Arc::new(store))