Skip to content

Commit 38928f1

Browse files
committed
mirror_worker: return 409 when the mirror commit is skipped
dispatch_commit discarded the DO response and cosign_and_serve always returned 200 with a cosignature, even when the DO refused to rewind because a concurrent add-entries had already advanced the mirror checkpoint past upload_end. The client read that as "my checkpoint is served" while the mirror was actually at a larger size. Return the DO's CommittedCheckpoint and, when its size is ahead of upload_end, respond 409 with mirror-info per the spec's "upload_end too small" case so the client resyncs. Addresses bonk #264 review on cosign_and_serve.
1 parent 0986400 commit 38928f1

1 file changed

Lines changed: 37 additions & 9 deletions

File tree

crates/mirror_worker/src/add_entries.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ use crate::{
5050
frontend_worker::{ApiResult, AppError},
5151
load_mirror_signer, load_ticket_sealer, log_verifiers,
5252
mirror_state_do::{
53-
AdvanceNextEntryRequest, CommitRequest, MirrorStateSnapshot, NextEntry, PendingCheckpoint,
54-
state_stub,
53+
AdvanceNextEntryRequest, CommitRequest, CommittedCheckpoint, MirrorStateSnapshot,
54+
NextEntry, PendingCheckpoint, state_stub,
5555
},
5656
storage::load_origin_bucket,
5757
};
@@ -337,7 +337,7 @@ async fn verify_and_persist(
337337
));
338338
}
339339

340-
cosign_and_serve(env, header, target).await
340+
cosign_and_serve(env, header, target, snapshot).await
341341
}
342342

343343
/// The spec's `excess_entries = min(upload_end, next_entry) -
@@ -387,6 +387,13 @@ fn read_package(cursor: &mut Cursor<&[u8]>, num_entries: u64) -> PackageOutcome
387387
/// concurrent commits cannot rewind the served checkpoint. The frontend
388388
/// therefore does not write R2 itself.
389389
///
390+
/// If the DO reports a committed size ahead of `upload_end`, a concurrent
391+
/// `add-entries` already advanced the mirror checkpoint past this upload.
392+
/// Per the spec's final step ("If `upload_end` was too small, the mirror
393+
/// MUST respond with a 409 Conflict") this returns a 409 with mirror-info
394+
/// so the client resyncs, rather than a 200 that would falsely claim the
395+
/// client's smaller checkpoint is being served.
396+
///
390397
/// # Errors
391398
///
392399
/// Returns an error if the target note/checkpoint fails to parse, signing
@@ -395,6 +402,7 @@ async fn cosign_and_serve(
395402
env: &Env,
396403
header: &AddEntriesRequestHeader,
397404
target: &PendingCheckpoint,
405+
snapshot: &MirrorStateSnapshot,
398406
) -> ApiResult<axum::response::Response> {
399407
// The checkpoint text comes from the log-signed pending note; the
400408
// response is the bare cosignature line(s), identical to a witness's
@@ -415,7 +423,7 @@ async fn cosign_and_serve(
415423
// durable checkpoint under its commit lock.
416424
let mut checkpoint_obj = target.signed_note_bytes.clone();
417425
checkpoint_obj.extend_from_slice(&cosig_body);
418-
dispatch_commit(
426+
let committed = dispatch_commit(
419427
env,
420428
&header.log_origin,
421429
&CommitRequest {
@@ -426,6 +434,18 @@ async fn cosign_and_serve(
426434
)
427435
.await?;
428436

437+
if committed.size != header.upload_end {
438+
// The DO refused to rewind: a concurrent commit already advanced
439+
// the mirror checkpoint past upload_end, so ours was skipped.
440+
log::info!(
441+
"add-entries: commit skipped, mirror checkpoint {} already past upload_end {}; \
442+
returning 409",
443+
committed.size,
444+
header.upload_end,
445+
);
446+
return Ok(mirror_info_409(env, snapshot, &header.log_origin));
447+
}
448+
429449
Ok((
430450
StatusCode::OK,
431451
[(CONTENT_TYPE, "text/plain; charset=utf-8")],
@@ -473,10 +493,18 @@ async fn first_package_prefix(
473493
}
474494

475495
/// POST the [`CommitRequest`] to the per-origin DO, advancing the mirror
476-
/// checkpoint. A non-200 status (or a `/commit` beyond pending) is a
477-
/// frontend/mirror bug, so it is surfaced as a transport error that the
478-
/// handler maps to 500.
479-
async fn dispatch_commit(env: &Env, origin: &str, commit_req: &CommitRequest) -> Result<()> {
496+
/// checkpoint, and return the DO's resulting [`CommittedCheckpoint`].
497+
///
498+
/// The returned checkpoint may be ahead of `commit_req` when a concurrent
499+
/// `add-entries` already advanced past it (the DO refuses to rewind); the
500+
/// caller compares sizes to detect that skip. A non-200 status (a
501+
/// `/commit` beyond the persisted frontier) is a frontend/mirror bug,
502+
/// surfaced as a transport error that the handler maps to 500.
503+
async fn dispatch_commit(
504+
env: &Env,
505+
origin: &str,
506+
commit_req: &CommitRequest,
507+
) -> Result<CommittedCheckpoint> {
480508
let stub = state_stub(env, origin)?;
481509
let mut resp = stub
482510
.fetch_with_request(Request::new_with_init(
@@ -494,7 +522,7 @@ async fn dispatch_commit(env: &Env, origin: &str, commit_req: &CommitRequest) ->
494522
)?)
495523
.await?;
496524
match resp.status_code() {
497-
200 => Ok(()),
525+
200 => Ok(resp.json().await?),
498526
status => {
499527
let msg = resp.text().await.unwrap_or_default();
500528
log::error!("add-entries: DO /commit returned {status}: {msg}");

0 commit comments

Comments
 (0)