2020import java .io .InputStream ;
2121import java .nio .charset .StandardCharsets ;
2222import java .util .UUID ;
23+ import java .util .concurrent .CountDownLatch ;
2324import java .util .concurrent .atomic .AtomicBoolean ;
25+ import java .util .concurrent .atomic .AtomicReference ;
2426import java .util .regex .Pattern ;
2527import java .util .zip .GZIPInputStream ;
2628import java .util .zip .GZIPOutputStream ;
@@ -45,7 +47,13 @@ public final class BlobPayloadStore extends PayloadStore {
4547
4648 private final BlobContainerClient containerClient ;
4749 private final LargePayloadStorageOptions options ;
48- private final AtomicBoolean containerVerified = new AtomicBoolean (false );
50+
51+ // Container-creation guard. The first thread to call ensureContainerExists() creates the
52+ // latch and performs the RPC. Concurrent callers await the latch so they don't race ahead
53+ // and upload to a container that hasn't been created yet. On failure the reference is
54+ // reset to null so a subsequent call can retry.
55+ private final AtomicReference <CountDownLatch > containerLatch = new AtomicReference <>();
56+ private volatile boolean containerVerified ;
4957
5058 /**
5159 * Creates a new {@code BlobPayloadStore} from the given options.
@@ -115,25 +123,9 @@ public String upload(String payload) {
115123
116124 byte [] payloadBytes = payload .getBytes (StandardCharsets .UTF_8 );
117125
118- // Ensure container exists (idempotent) — skip after first successful check.
119- // compareAndSet lets only one concurrent caller perform the RPC; others skip.
120- // On failure we reset the flag so a later call can retry.
121- if (this .containerVerified .compareAndSet (false , true )) {
122- try {
123- this .containerClient .createIfNotExists ();
124- } catch (BlobStorageException e ) {
125- // 409 Conflict means it already exists — safe to ignore, leave flag set.
126- if (e .getStatusCode () != 409 ) {
127- this .containerVerified .set (false ); // allow a future upload to retry
128- throw new PayloadStorageException (
129- "Failed to create blob container '" + this .containerClient .getBlobContainerName () + "'." , e );
130- }
131- } catch (RuntimeException e ) {
132- // Any other transport/SDK failure: also allow retry on next upload.
133- this .containerVerified .set (false );
134- throw e ;
135- }
136- }
126+ // Ensure container exists before uploading. Thread-safe: the first caller creates
127+ // the container while concurrent callers wait for it to complete.
128+ ensureContainerExists ();
137129
138130 try {
139131 // Defense-in-depth: require the blob to not already exist (If-None-Match: *).
@@ -193,6 +185,54 @@ public String upload(String payload) {
193185 return encodeToken (this .containerClient .getBlobContainerName (), blobName );
194186 }
195187
188+ /**
189+ * Ensures the blob container exists, creating it if necessary. Thread-safe: the first
190+ * caller performs the RPC while concurrent callers wait for it to complete. On success
191+ * the check is skipped on all future calls. On failure the guard is reset so a later
192+ * call can retry.
193+ */
194+ private void ensureContainerExists () {
195+ if (this .containerVerified ) {
196+ return ;
197+ }
198+
199+ CountDownLatch latch = new CountDownLatch (1 );
200+ CountDownLatch existing = this .containerLatch .compareAndExchange (null , latch );
201+ if (existing != null ) {
202+ // Another thread is already creating the container — wait for it.
203+ try {
204+ existing .await ();
205+ } catch (InterruptedException e ) {
206+ Thread .currentThread ().interrupt ();
207+ throw new PayloadStorageException ("Interrupted while waiting for container creation." , e );
208+ }
209+ // If the creating thread failed, containerVerified is still false; the next
210+ // upload attempt will retry. For now, return and let the upload proceed
211+ // (it will fail fast with a clear error if the container doesn't exist).
212+ return ;
213+ }
214+
215+ // This thread is responsible for creating the container.
216+ try {
217+ this .containerClient .createIfNotExists ();
218+ this .containerVerified = true ;
219+ } catch (BlobStorageException e ) {
220+ if (e .getStatusCode () == 409 ) {
221+ // 409 Conflict means it already exists — safe to ignore.
222+ this .containerVerified = true ;
223+ } else {
224+ this .containerLatch .set (null ); // allow a future call to retry
225+ throw new PayloadStorageException (
226+ "Failed to create blob container '" + this .containerClient .getBlobContainerName () + "'." , e );
227+ }
228+ } catch (RuntimeException e ) {
229+ this .containerLatch .set (null ); // allow a future call to retry
230+ throw e ;
231+ } finally {
232+ latch .countDown (); // unblock waiting threads
233+ }
234+ }
235+
196236 @ Override
197237 public String download (String token ) {
198238 String [] decoded = decodeToken (token );
0 commit comments