2323import com .google .api .gax .rpc .ApiException ;
2424import com .google .common .util .concurrent .MoreExecutors ;
2525import com .google .pubsub .v1 .PublishResponse ;
26+ import java .util .HashMap ;
2627import java .util .Map ;
27- import java .util .concurrent .ConcurrentHashMap ;
2828import java .util .concurrent .atomic .AtomicBoolean ;
29- import java .util .concurrent .atomic .AtomicReference ;
29+ import java .util .concurrent .locks .Lock ;
30+ import java .util .concurrent .locks .ReentrantLock ;
3031
3132/**
3233 * Coordinates multiple publish attempts for a single batch of messages.
3334 *
34- * <p>Implements {@link ApiFuture} to act as the single future returned to the
35- * publisher's client. It manages the lifecycle of the original attempt and any
36- * subsequent hedged attempts.
35+ * <p>Implements {@link ApiFuture} to act as the single future returned to the publisher's client.
36+ * It manages the lifecycle of the original attempt and any subsequent hedged attempts.
3737 */
3838class CancellationSharer extends AbstractApiFuture <PublishResponse > {
39- /** The message batch being coordinated. */
4039 private final Publisher .OutstandingBatch batch ;
41-
42- /** The publisher instance. */
4340 private final Publisher publisher ;
4441
45- /** Map of active attempt numbers to their respective gRPC futures. */
46- private final Map <Integer , ApiFuture <PublishResponse >> runningAttempts =
47- new ConcurrentHashMap <>();
48-
49- /** Boolean indicating whether the overall batch has resolved or failed. */
50- private final AtomicBoolean done = new AtomicBoolean (false );
42+ // Guarded by lock
43+ private final Map <Integer , ApiFuture <PublishResponse >> runningAttempts = new HashMap <>();
44+ private boolean done = false ;
45+ private Throwable lastError ;
5146
52- /** Boolean indicating whether the batch is currently waiting in the hedging queue. */
47+ private final Lock lock = new ReentrantLock ();
5348 private final AtomicBoolean isInQueue = new AtomicBoolean (false );
5449
55- /** The last error encountered by any failed attempt. */
56- private final AtomicReference <Throwable > lastError = new AtomicReference <>();
57-
5850 CancellationSharer (final Publisher .OutstandingBatch batch , final Publisher publisher ) {
5951 this .batch = batch ;
6052 this .publisher = publisher ;
6153 }
6254
63- /**
64- * Adds an attempt to be tracked by this coordinator.
65- *
66- * @param attemptNumber the 1-based index of the attempt (1 is original, 2+ are hedged)
67- * @param future the future representing the gRPC call for this attempt
68- */
6955 void addAttempt (final int attemptNumber , final ApiFuture <PublishResponse > future ) {
70- runningAttempts .put (attemptNumber , future );
71-
72- if (done .get ()) {
73- future .cancel (true );
74- runningAttempts .remove (attemptNumber );
75- return ;
56+ lock .lock ();
57+ try {
58+ if (done ) {
59+ future .cancel (true );
60+ return ;
61+ }
62+ runningAttempts .put (attemptNumber , future );
63+ } finally {
64+ lock .unlock ();
7665 }
7766
7867 ApiFutures .addCallback (
@@ -92,80 +81,109 @@ public void onFailure(final Throwable t) {
9281 }
9382
9483 private void handleAttemptSuccess (final int attemptNumber , final PublishResponse response ) {
95- if (done .compareAndSet (false , true )) {
84+ lock .lock ();
85+ try {
86+ if (done ) {
87+ return ;
88+ }
89+ done = true ;
9690 batch .successfulAttempt = attemptNumber ;
97- set (response ); // Resolve parent future
98- cancelAllExcept (attemptNumber );
99- publisher .refillTokenBucket ();
91+ set (response );
92+ cancelAllExceptLocked (attemptNumber );
93+ } finally {
94+ lock .unlock ();
10095 }
96+ publisher .refillTokenBucket ();
10197 }
10298
10399 private void handleAttemptFailure (final int attemptNumber , final Throwable t ) {
104- runningAttempts .remove (attemptNumber );
100+ boolean shouldRemoveFromQueue = false ;
101+ lock .lock ();
102+ try {
103+ if (done ) {
104+ return ; // <-- Exit early before modifying runningAttempts to avoid
105+ // ConcurrentModificationException
106+ }
107+ runningAttempts .remove (attemptNumber );
108+ lastError = t ;
105109
106- if (done .get ()) {
107- return ;
108- }
109- lastError .set (t );
110-
111- boolean isRetryable = true ;
112- if (t instanceof ApiException ) {
113- isRetryable =
114- publisher
115- .getRetryableCodes ()
116- .contains (((ApiException ) t ).getStatusCode ().getCode ());
117- }
110+ boolean isRetryable = true ;
111+ if (t instanceof ApiException ) {
112+ isRetryable =
113+ publisher .getRetryableCodes ().contains (((ApiException ) t ).getStatusCode ().getCode ());
114+ }
118115
119- if (runningAttempts .isEmpty () || !isRetryable ) {
120- if ( done . compareAndSet ( false , true )) {
121- setException (lastError . get () );
122- cancelAll ();
116+ if (runningAttempts .isEmpty () || !isRetryable ) {
117+ done = true ;
118+ setException (lastError );
119+ cancelAllLocked ();
123120 if (isInQueue .get ()) {
124- publisher . removeFromHedgingQueue ( this ) ;
121+ shouldRemoveFromQueue = true ;
125122 }
126123 }
124+ } finally {
125+ lock .unlock ();
127126 }
128- }
129127
130- private void cancelAll () {
131- for (ApiFuture <PublishResponse > future : runningAttempts .values ()) {
132- future .cancel (true );
128+ if (shouldRemoveFromQueue ) {
129+ publisher .removeFromHedgingQueue (this );
133130 }
134131 }
135132
136133 void checkCompletionOnQueueExit () {
137- if (!done .get () && runningAttempts .isEmpty () && !isInQueue .get ()) {
138- if (done .compareAndSet (false , true )) {
139- Throwable error = lastError .get ();
134+ lock .lock ();
135+ try {
136+ if (!done && runningAttempts .isEmpty () && !isInQueue .get ()) {
137+ done = true ;
140138 setException (
141- error != null
142- ? error
139+ lastError != null
140+ ? lastError
143141 : new RuntimeException ("Hedging failed with no active attempts" ));
144142 }
143+ } finally {
144+ lock .unlock ();
145145 }
146146 }
147147
148- private void cancelAllExcept (final int successfulAttempt ) {
149- for (Map .Entry <Integer , ApiFuture <PublishResponse >> entry : runningAttempts .entrySet ()) {
150- if (entry .getKey () != successfulAttempt ) {
151- entry .getValue ().cancel (true );
148+ @ Override
149+ public boolean cancel (final boolean mayInterruptIfRunning ) {
150+ boolean cancelled = false ;
151+ boolean shouldRemoveFromQueue = false ;
152+ lock .lock ();
153+ try {
154+ if (super .cancel (mayInterruptIfRunning )) {
155+ cancelled = true ;
156+ done = true ;
157+ if (isInQueue .get ()) {
158+ shouldRemoveFromQueue = true ;
159+ }
160+ cancelAllLocked ();
152161 }
162+ } finally {
163+ lock .unlock ();
153164 }
165+
166+ if (shouldRemoveFromQueue ) {
167+ publisher .removeFromHedgingQueue (this );
168+ }
169+ return cancelled ;
154170 }
155171
156- @ Override
157- public boolean cancel (final boolean mayInterruptIfRunning ) {
158- if (super .cancel (mayInterruptIfRunning )) {
159- done .set (true );
160- if (isInQueue .get ()) {
161- publisher .removeFromHedgingQueue (this );
162- }
163- for (ApiFuture <PublishResponse > future : runningAttempts .values ()) {
164- future .cancel (mayInterruptIfRunning );
165- }
166- return true ;
172+ private void cancelAllLocked () {
173+ for (ApiFuture <PublishResponse > future : runningAttempts .values ()) {
174+ future .cancel (true );
167175 }
168- return false ;
176+ runningAttempts .clear ();
177+ }
178+
179+ private void cancelAllExceptLocked (final int successfulAttempt ) {
180+ runningAttempts .forEach (
181+ (attempt , future ) -> {
182+ if (attempt != successfulAttempt ) {
183+ future .cancel (true );
184+ }
185+ });
186+ runningAttempts .clear ();
169187 }
170188
171189 AtomicBoolean isInQueue () {
0 commit comments