2828
2929import com .google .inject .Inject ;
3030import com .google .inject .Singleton ;
31+ import io .vertx .core .Future ;
3132import org .apache .cassandra .sidecar .common .server .utils .DurationSpec ;
32- import org .apache .cassandra .sidecar .common .utils .Preconditions ;
3333import org .apache .cassandra .sidecar .concurrent .ExecutorPools ;
3434import org .apache .cassandra .sidecar .concurrent .TaskExecutorPool ;
3535import org .apache .cassandra .sidecar .exceptions .OperationalJobConflictException ;
@@ -116,21 +116,59 @@ public void trySubmitJob(OperationalJob job,
116116 try
117117 {
118118 checkConflict (job );
119-
120- // New job is submitted for all cases when we do not have a corresponding downstream job
121- OperationalJob tracked = jobTracker .computeIfAbsent (job .jobId (), jobId -> job );
122- if (tracked == job )
123- {
124- tryCoordination (job );
125- internalExecutorPool .executeBlocking (job ::execute );
126- }
127119 }
128120 catch (OperationalJobConflictException oje )
129121 {
130122 onComplete .accept (job , oje );
131123 return ;
132124 }
133125
126+ if (!job .requiresCoordination ())
127+ {
128+ trackAndExecute (job , onComplete , serviceExecutorPool , waitTime );
129+ return ;
130+ }
131+
132+ // Acquiring the active operation lock might perform blocking storage I/O so it must
133+ // run off the event loop
134+ acquireActiveOperationLock (job )
135+ .onComplete (ar ->
136+ {
137+ if (ar .succeeded () && Boolean .TRUE .equals (ar .result ()))
138+ {
139+ trackAndExecute (job , onComplete , serviceExecutorPool , waitTime );
140+ }
141+ else
142+ {
143+ OperationalJobConflictException conflict = coordinationConflict (job , ar .cause ());
144+ job .failToStart (conflict );
145+ jobTracker .computeIfAbsent (job .jobId (), jobId -> job );
146+ onComplete .accept (job , conflict );
147+ }
148+ });
149+ }
150+
151+ /**
152+ * Tracks the job and submits it for asynchronous execution on the internal executor pool, then arranges for
153+ * {@code onComplete} to be invoked with the result (or after {@code waitTime} elapses).
154+ *
155+ * @param job the job to track and execute
156+ * @param onComplete callback to invoke when the job completes
157+ * @param serviceExecutorPool the executor pool to use for waiting on job completion
158+ * @param waitTime the maximum time to wait for job completion before returning
159+ */
160+ private void trackAndExecute (OperationalJob job ,
161+ BiConsumer <OperationalJob , OperationalJobConflictException > onComplete ,
162+ TaskExecutorPool serviceExecutorPool ,
163+ DurationSpec waitTime )
164+ {
165+ // New job is submitted for all cases when we do not have a corresponding downstream job
166+ OperationalJob tracked = jobTracker .computeIfAbsent (job .jobId (), jobId -> job );
167+ if (tracked == job )
168+ {
169+ internalExecutorPool .executeBlocking (job ::execute );
170+ }
171+
134172 // Get the result, waiting for the specified wait time for result
135173 job .asyncResult (serviceExecutorPool , waitTime )
136174 .onComplete (v -> onComplete .accept (job , null ));
@@ -152,24 +190,39 @@ private void checkConflict(OperationalJob job) throws OperationalJobConflictExce
152190 }
153191
154192 /**
155- * For jobs that require cluster-wide coordination, attempts to acquire the active operation lock
156- * via the coordinator. Throws a conflict if another operation is already active.
193+ * For jobs that require cluster-wide coordination, attempts to acquire the active operation lock via the
194+ * coordinator. The acquisition runs on the internal executor pool because it might performs blocking storage I/O
157195 *
158- * @param job instance of the job to coordinate
159- * @throws OperationalJobConflictException when the coordinator cannot activate the operation
196+ * @param job the job requiring coordination
197+ * @return a future resolving to {@code true} if the lock was acquired, {@code false} if another operation
198+ * already holds it, or a failed future if coordination could not be attempted (e.g. no coordinator
199+ * is configured or the storage call failed)
160200 */
161- private void tryCoordination (OperationalJob job ) throws OperationalJobConflictException
201+ private Future < Boolean > acquireActiveOperationLock (OperationalJob job )
162202 {
163- if (job . requiresCoordination () )
203+ if (coordinator == null )
164204 {
165- Preconditions .checkState (coordinator != null ,
166- "Job requires coordination but no OperationalJobCoordinator is configured" );
167- boolean activated = coordinator .trySetActive (job .operationType (), job .jobId ());
168- if (!activated )
169- {
170- throw new OperationalJobConflictException ("An active operation already exists. operationType='"
171- + job .operationType () + '\'' );
172- }
205+ return Future .failedFuture ("Job requires coordination but no OperationalJobCoordinator is configured" );
206+ }
207+ return internalExecutorPool .executeBlocking (() -> coordinator .trySetActive (job .operationType (), job .jobId ()), false );
208+ }
209+
210+ /**
211+ * Builds the conflict exception describing why the active operation lock could not be acquired.
212+ *
213+ * @param job the job that could not be coordinated
214+ * @param cause the failure cause when coordination could not be attempted, or {@code null} when the lock is
215+ * simply held by another active operation
216+ * @return the conflict exception to report to the caller
217+ */
218+ private OperationalJobConflictException coordinationConflict (OperationalJob job , @ Nullable Throwable cause )
219+ {
220+ if (cause != null )
221+ {
222+ return new OperationalJobConflictException ("Unable to coordinate operation. operationType='"
223+ + job .operationType () + "', reason='" + cause .getMessage () + '\'' );
173224 }
225+ return new OperationalJobConflictException ("An active operation already exists. operationType='"
226+ + job .operationType () + '\'' );
174227 }
175228}
0 commit comments