Skip to content

Commit 23ad875

Browse files
committed
mirror_worker: address lbaquerofierro review on PR 264
1 parent 50e7347 commit 23ad875

2 files changed

Lines changed: 75 additions & 24 deletions

File tree

crates/mirror_worker/src/add_entries.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,13 +441,23 @@ async fn cosign_and_serve(
441441
let cosig_body =
442442
tlog_witness::serialize_add_checkpoint_response(std::slice::from_ref(&note_sig));
443443

444+
// The served checkpoint is the log's signed note with the mirror's
445+
// cosignature appended. Build it once so both the early-return and the
446+
// `/commit` paths use the same bytes.
447+
let checkpoint_obj = [target.signed_note_bytes.as_slice(), &cosig_body].concat();
448+
444449
// When the upload only reaches the mirror checkpoint's current size,
445450
// the checkpoint at that size is already committed and served. There
446451
// is nothing to advance, and re-committing would redundantly rewrite
447452
// R2 and append a duplicate cosignature line to the served note, so
448453
// return a fresh cosignature without dispatching `/commit`. (Committed
449454
// is monotonic, so a stale-low snapshot only skips this optimization,
450455
// never the other way.)
456+
//
457+
// Note: this also means a previously-failed R2 checkpoint write won't
458+
// self-heal at the same size; it heals on the next larger commit. That
459+
// is acceptable because the durable committed state advanced before the
460+
// R2 write, and duplicate cosignatures are worse than a lagging object.
451461
if header.upload_end <= snapshot.committed.size {
452462
return Ok((
453463
StatusCode::OK,
@@ -457,22 +467,24 @@ async fn cosign_and_serve(
457467
.into_response());
458468
}
459469

460-
// The served checkpoint is the log's signed note with the mirror's
461-
// cosignature appended. The DO writes it to R2 while advancing the
470+
// The DO writes the cosigned checkpoint to R2 while advancing the
462471
// durable checkpoint under its commit lock.
463-
let mut checkpoint_obj = target.signed_note_bytes.clone();
464-
checkpoint_obj.extend_from_slice(&cosig_body);
465472
let committed = dispatch_commit(
466473
env,
467474
&header.log_origin,
468475
&CommitRequest {
469476
size: header.upload_end,
470477
hash: target.hash,
471-
signed_note_bytes: checkpoint_obj,
478+
signed_note_bytes: checkpoint_obj.clone(),
472479
},
473480
)
474481
.await?;
475482

483+
debug_assert_eq!(
484+
committed.signed_note_bytes, checkpoint_obj,
485+
"DO returned checkpoint bytes that do not match the cosigned note we sent"
486+
);
487+
476488
if committed.size != header.upload_end {
477489
// The DO refused to rewind: a concurrent commit already advanced
478490
// the mirror checkpoint past upload_end, so ours was skipped.

crates/mirror_worker/src/commit.rs

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
3030
use std::collections::HashMap;
3131

32+
use futures_util::{
33+
future::try_join_all,
34+
stream::{StreamExt as _, TryStreamExt as _, iter as stream_iter},
35+
};
3236
use generic_log_worker::{
3337
ObjectBackend,
3438
log_ops::{HashReaderWithOverlay, TileWithBytes, UploadOptions, read_edge_tiles},
@@ -44,6 +48,14 @@ use worker::*;
4448
/// tlog-tiles fixes a tile height of 8, i.e. 256 entries per full tile.
4549
const TILE_WIDTH: u64 = TlogTile::FULL_WIDTH as u64;
4650

51+
/// Max in-flight R2 uploads per commit. A single `add-entries` request is
52+
/// bounded only by the request-body size, so uploading every entry bundle
53+
/// and hash tile at once could open thousands of connections and hold all
54+
/// their bytes in memory. Workers allows only six connections to be waiting
55+
/// on response headers at a time, so a small bound captures the concurrency
56+
/// win without the unbounded fan-out.
57+
const UPLOAD_CONCURRENCY: usize = 6;
58+
4759
/// Object key for the (cosigned) checkpoint the mirror serves at
4860
/// `<monitoring>/<origin hash>/checkpoint`. Matches
4961
/// [`generic_log_worker::log_ops::CHECKPOINT_KEY`].
@@ -162,14 +174,17 @@ async fn authenticated_leaf_hashes(
162174
}
163175
}
164176

165-
let mut tiles: HashMap<TlogTile, Vec<u8>> = HashMap::new();
166-
for tile in recorder.0.into_inner() {
167-
let bytes = object
168-
.fetch(tile.path())
169-
.await?
170-
.ok_or_else(|| Error::from(format!("persisted hash tile missing: {}", tile.path())))?;
171-
tiles.insert(tile, bytes);
172-
}
177+
let tile_futures = recorder.0.into_inner().into_iter().map(|tile| {
178+
let path = tile.path();
179+
async move {
180+
let bytes = object
181+
.fetch(path.clone())
182+
.await?
183+
.ok_or_else(|| Error::from(format!("persisted hash tile missing: {path}")))?;
184+
Ok::<(TlogTile, Vec<u8>), Error>((tile, bytes))
185+
}
186+
});
187+
let tiles: HashMap<TlogTile, Vec<u8>> = try_join_all(tile_futures).await?.into_iter().collect();
173188

174189
let reader = PreloadedTlogTileReader(tiles);
175190
TileHashReader::new(tree_size, tree_hash, &reader)
@@ -317,9 +332,12 @@ pub(crate) async fn persist_entries(
317332
verify_partial_bundle(&data_tile, subtree_start, persisted_size, &edge_tiles)?;
318333
}
319334

320-
// Replay leaves, flushing entry bundles at 256-entry boundaries.
335+
// Replay leaves, buffering entry-bundle uploads until the end so they
336+
// can be issued with bounded concurrency. Bundles are immutable and
337+
// idempotent, so overlapping them is safe.
321338
let mut overlay: HashMap<u64, Hash> = HashMap::new();
322339
let mut n = persisted_size;
340+
let mut bundle_uploads = Vec::new();
323341
for entry in entries {
324342
push_tile_leaf(&mut data_tile, entry)?;
325343
let hashes = stored_hashes_for_record_hash(
@@ -336,30 +354,51 @@ pub(crate) async fn persist_entries(
336354
}
337355
n += 1;
338356
if n.is_multiple_of(TILE_WIDTH) {
339-
upload_entry_bundle(object, n, std::mem::take(&mut data_tile)).await?;
357+
bundle_uploads.push(upload_entry_bundle(
358+
object,
359+
n,
360+
std::mem::take(&mut data_tile),
361+
));
340362
}
341363
}
342364
debug_assert_eq!(n, target_size);
343365
// Trailing partial entry bundle.
344366
if !target_size.is_multiple_of(TILE_WIDTH) {
345-
upload_entry_bundle(object, target_size, std::mem::take(&mut data_tile)).await?;
367+
bundle_uploads.push(upload_entry_bundle(
368+
object,
369+
target_size,
370+
std::mem::take(&mut data_tile),
371+
));
346372
}
347-
348-
// (Re)compute and upload hash tiles.
373+
stream_iter(bundle_uploads)
374+
.buffer_unordered(UPLOAD_CONCURRENCY)
375+
.try_collect::<()>()
376+
.await?;
377+
378+
// (Re)compute hash tiles, then upload them with bounded concurrency
379+
// while keeping edge_tiles current for the final root hash.
380+
let tile_opts = immutable_tile_opts();
381+
let mut hash_uploads = Vec::new();
349382
for tile in TlogTile::new_tiles(persisted_size, target_size) {
350383
let bytes = tile
351384
.read_data(&HashReaderWithOverlay {
352385
edge_tiles: &edge_tiles,
353386
overlay: &overlay,
354387
})
355388
.map_err(|e| Error::from(format!("couldn't build hash tile {tile:?}: {e}")))?;
356-
object
357-
.upload(tile.path(), bytes.clone(), &immutable_tile_opts())
358-
.await?;
359-
// Keep edge_tiles current so read_data of a higher/next tile can
360-
// still resolve persisted hashes it depends on.
361-
edge_tiles.insert(tile.level(), TileWithBytes { tile, b: bytes });
389+
edge_tiles.insert(
390+
tile.level(),
391+
TileWithBytes {
392+
tile,
393+
b: bytes.clone(),
394+
},
395+
);
396+
hash_uploads.push(object.upload(tile.path(), bytes, &tile_opts));
362397
}
398+
stream_iter(hash_uploads)
399+
.buffer_unordered(UPLOAD_CONCURRENCY)
400+
.try_collect::<()>()
401+
.await?;
363402

364403
// Recompute the root hash from the frontier we just built.
365404
tlog_core::tree_hash(

0 commit comments

Comments
 (0)