2929
3030use std:: collections:: HashMap ;
3131
32+ use futures_util:: {
33+ future:: try_join_all,
34+ stream:: { StreamExt as _, TryStreamExt as _, iter as stream_iter} ,
35+ } ;
3236use 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.
4549const 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