1414
1515package com .google .devtools .build .lib .remote ;
1616
17+ import static com .google .common .util .concurrent .MoreExecutors .directExecutor ;
1718import static com .google .devtools .build .lib .remote .util .Utils .getFromFuture ;
1819
1920import build .bazel .remote .execution .v2 .Digest ;
2324import com .google .devtools .build .lib .remote .common .RemoteActionExecutionContext ;
2425import java .io .IOException ;
2526import java .io .OutputStream ;
27+ import java .util .ArrayList ;
28+ import java .util .HashMap ;
2629import java .util .List ;
30+ import java .util .Map ;
31+ import java .util .concurrent .LinkedBlockingQueue ;
2732
28- /** Downloads blobs by sequentially fetching chunks via the SplitBlob API. */
33+ /** Downloads blobs by fetching chunks through a per-blob sliding window via the SplitBlob API. */
2934public class ChunkedBlobDownloader {
35+ // Guard against pathological fanout from a single large chunked blob. This is only a per-blob
36+ // cap; chunk requests still flow through CombinedCache and the shared remote cache transport
37+ // stack below it, which is what bounds active remote RPC concurrency across blobs.
38+ private static final int MAX_IN_FLIGHT_CHUNK_DOWNLOADS = 32 ;
39+
3040 private final GrpcCacheClient grpcCacheClient ;
3141 private final CombinedCache combinedCache ;
3242
@@ -37,8 +47,8 @@ public ChunkedBlobDownloader(GrpcCacheClient grpcCacheClient, CombinedCache comb
3747
3848 /**
3949 * Downloads a blob using chunked download via the SplitBlob API. This should be called with
40- * virtual threads, as it blocks on futures via {@link
41- * com.google.devtools.build.lib.remote.util.Utils#getFromFuture} .
50+ * virtual threads, as it may block while waiting for chunk metadata and completed chunk
51+ * downloads .
4252 */
4353 public void downloadChunked (
4454 RemoteActionExecutionContext context , Digest blobDigest , OutputStream out )
@@ -64,11 +74,123 @@ private List<Digest> getChunkDigests(RemoteActionExecutionContext context, Diges
6474 return chunkDigests ;
6575 }
6676
77+ private static final class PendingDownload {
78+ private final Digest digest ;
79+ private final ListenableFuture <byte []> future ;
80+ private final List <Integer > chunkIndices = new ArrayList <>(1 );
81+
82+ PendingDownload (Digest digest , ListenableFuture <byte []> future , int firstChunkIndex ) {
83+ this .digest = digest ;
84+ this .future = future ;
85+ chunkIndices .add (firstChunkIndex );
86+ }
87+
88+ void addChunkIndex (int chunkIndex ) {
89+ chunkIndices .add (chunkIndex );
90+ }
91+
92+ Digest digest () {
93+ return digest ;
94+ }
95+
96+ ListenableFuture <byte []> future () {
97+ return future ;
98+ }
99+
100+ List <Integer > chunkIndices () {
101+ return chunkIndices ;
102+ }
103+ }
104+
67105 private void downloadAndReassembleChunks (
68106 RemoteActionExecutionContext context , List <Digest > chunkDigests , OutputStream out )
69107 throws IOException , InterruptedException {
70- for (Digest chunkDigest : chunkDigests ) {
71- getFromFuture (combinedCache .downloadBlob (context , chunkDigest , out ));
108+ new DownloadSession (context , chunkDigests , out ).run ();
109+ }
110+
111+ private final class DownloadSession {
112+ private final LinkedBlockingQueue <PendingDownload > completedDownloads =
113+ new LinkedBlockingQueue <>();
114+ private final Map <Digest , PendingDownload > activeDownloads =
115+ new HashMap <>(MAX_IN_FLIGHT_CHUNK_DOWNLOADS );
116+ private final Map <Integer , byte []> readyChunks =
117+ new HashMap <>(MAX_IN_FLIGHT_CHUNK_DOWNLOADS );
118+ private final RemoteActionExecutionContext context ;
119+ private final List <Digest > chunkDigests ;
120+ private final OutputStream out ;
121+ private int nextToStart = 0 ;
122+ private int nextToWrite = 0 ;
123+
124+ DownloadSession (
125+ RemoteActionExecutionContext context , List <Digest > chunkDigests , OutputStream out ) {
126+ this .context = context ;
127+ this .chunkDigests = chunkDigests ;
128+ this .out = out ;
129+ }
130+
131+ void run () throws IOException , InterruptedException {
132+ try {
133+ fillWindow ();
134+ while (nextToWrite < chunkDigests .size ()) {
135+ awaitCompletedDownload ();
136+ fillWindow ();
137+ drainReadyChunks ();
138+ }
139+ } catch (IOException | InterruptedException | RuntimeException | Error e ) {
140+ cancelAllDownloads ();
141+ throw e ;
142+ }
143+ }
144+
145+ private void fillWindow () {
146+ while (nextToStart < chunkDigests .size ()) {
147+ Digest chunkDigest = chunkDigests .get (nextToStart );
148+ PendingDownload existing = activeDownloads .get (chunkDigest );
149+ if (existing != null ) {
150+ existing .addChunkIndex (nextToStart );
151+ nextToStart ++;
152+ continue ;
153+ }
154+ if (activeDownloads .size () >= MAX_IN_FLIGHT_CHUNK_DOWNLOADS ) {
155+ return ;
156+ }
157+ startDownload (chunkDigest , nextToStart );
158+ nextToStart ++;
159+ }
160+ }
161+
162+ private void startDownload (Digest chunkDigest , int chunkIndex ) {
163+ PendingDownload download =
164+ new PendingDownload (
165+ chunkDigest , combinedCache .downloadBlob (context , chunkDigest ), chunkIndex );
166+ activeDownloads .put (chunkDigest , download );
167+ download .future ().addListener (() -> completedDownloads .add (download ), directExecutor ());
168+ }
169+
170+ private void awaitCompletedDownload () throws IOException , InterruptedException {
171+ PendingDownload download = completedDownloads .take ();
172+ activeDownloads .remove (download .digest ());
173+ byte [] chunkData = getFromFuture (download .future ());
174+ for (int chunkIndex : download .chunkIndices ()) {
175+ readyChunks .put (chunkIndex , chunkData );
176+ }
177+ }
178+
179+ private void drainReadyChunks () throws IOException {
180+ while (true ) {
181+ byte [] chunk = readyChunks .remove (nextToWrite );
182+ if (chunk == null ) {
183+ return ;
184+ }
185+ out .write (chunk );
186+ nextToWrite ++;
187+ }
188+ }
189+
190+ private void cancelAllDownloads () {
191+ for (PendingDownload download : activeDownloads .values ()) {
192+ download .future ().cancel (/* mayInterruptIfRunning= */ true );
193+ }
72194 }
73195 }
74196}
0 commit comments