Skip to content

Commit 58bb7d4

Browse files
Add releaseOnCompletion hook to OperationalJob
1 parent 9868be7 commit 58bb7d4

4 files changed

Lines changed: 85 additions & 25 deletions

File tree

server/src/main/java/org/apache/cassandra/sidecar/job/OperationalJob.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,24 @@ public boolean requiresCoordination()
183183
return false;
184184
}
185185

186+
/**
187+
* Whether the manager should release the active operation lock (via
188+
* {@link OperationalJobCoordinator#clearActive}) when this job completes locally. Only consulted for jobs that
189+
* {@link #requiresCoordination() require coordination}.
190+
* <p>
191+
* Single-node coordinated jobs return {@code true} (the default): the Sidecar that acquires the lock also
192+
* finishes the work, so releasing on local completion is correct. Distributed cluster-wide jobs whose work
193+
* finishes on other nodes return {@code false} and rely on the orchestration layer to call
194+
* {@link OperationalJobCoordinator#clearActive} once all nodes reach a terminal state.
195+
*
196+
* @return {@code true} if the manager should release the lock on local completion; {@code false} to defer
197+
* release to the orchestration layer
198+
*/
199+
public boolean releasesOnCompletion()
200+
{
201+
return true;
202+
}
203+
186204
@Override
187205
public final Void result()
188206
{
@@ -240,11 +258,6 @@ public String name()
240258
return simpleName.isEmpty() ? this.getClass().getName() : simpleName;
241259
}
242260

243-
/**
244-
* @return the type of operation this job performs
245-
*/
246-
public abstract OperationType operationType();
247-
248261
/**
249262
* {@inheritDoc}
250263
*/

server/src/main/java/org/apache/cassandra/sidecar/job/OperationalJobManager.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ public void trySubmitJob(OperationalJob job,
127127
{
128128
if (ar.succeeded() && Boolean.TRUE.equals(ar.result()))
129129
{
130-
job.asyncResult().onComplete(result -> releaseActiveOperationLock(job));
130+
// Distributed jobs that finish on other nodes opt out of auto-release; the orchestration
131+
// layer clears the lock once all nodes reach a terminal state.
132+
if (job.releasesOnCompletion())
133+
{
134+
job.asyncResult().onComplete(result -> releaseActiveOperationLock(job));
135+
}
131136
trackAndExecute(job, onComplete, serviceExecutorPool, waitTime);
132137
}
133138
else

server/src/test/java/org/apache/cassandra/sidecar/job/OperationalJobManagerTest.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import static org.apache.cassandra.testing.utils.AssertionUtils.loopAssert;
5050
import static org.assertj.core.api.Assertions.assertThat;
5151
import static org.mockito.ArgumentMatchers.any;
52+
import static org.mockito.Mockito.after;
5253
import static org.mockito.Mockito.doThrow;
5354
import static org.mockito.Mockito.mock;
5455
import static org.mockito.Mockito.never;
@@ -198,7 +199,7 @@ void testJobRemovedFromTrackerWhenPersistenceFails()
198199
DurableOperationalJobTracker durableTracker = new DurableOperationalJobTracker(new ServiceConfigurationImpl(),
199200
storageProvider,
200201
executorPool.service());
201-
OperationalJobManager manager = new OperationalJobManager(durableTracker, executorPool);
202+
OperationalJobManager manager = new OperationalJobManager(durableTracker, new DisabledOperationalJobCoordinator(), executorPool);
202203

203204
UUID jobId = UUIDs.timeBased();
204205
OperationalJob job = OperationalJobTest.createOperationalJob(jobId, MillisecondBoundConfiguration.parse("50ms"));
@@ -283,6 +284,29 @@ void testCoordinationFailsWhenCoordinationDisabled() throws InterruptedException
283284
assertThat(tracker.inflightJobsByOperation(job.name())).doesNotContain(job);
284285
}
285286

287+
@Test
288+
void testLockNotReleasedWhenJobOptsOut() throws InterruptedException
289+
{
290+
OperationalJobTracker tracker = new InMemoryOperationalJobTracker(4);
291+
OperationalJobCoordinator coordinator = mock(OperationalJobCoordinator.class);
292+
when(coordinator.trySetActive(any(), any())).thenReturn(true);
293+
OperationalJobManager manager = new OperationalJobManager(tracker, coordinator, executorPool);
294+
CountDownLatch latch = new CountDownLatch(1);
295+
296+
// A distributed cluster-wide job that acquires the lock locally but relies on the orchestration
297+
// layer to clear it once all nodes finish, so the manager must not auto-release on local completion.
298+
OperationalJob job = createNonReleasingCoordinatedJob(UUIDs.timeBased());
299+
BiConsumer<OperationalJob, OperationalJobConflictException> onComplete = (j, ex) -> {
300+
assertThat(ex).isNull();
301+
latch.countDown();
302+
};
303+
304+
manager.trySubmitJob(job, onComplete, executorPool.service(), SecondBoundConfiguration.parse("5s"));
305+
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
306+
verify(coordinator).trySetActive(OperationType.MOVE, job.jobId());
307+
verify(coordinator, after(1000).never()).clearActive(any(), any());
308+
}
309+
286310
@Test
287311
void testCoordinatorNotCalledWhenJobDoesNotRequireCoordination() throws InterruptedException
288312
{
@@ -331,4 +355,40 @@ protected Future<Void> executeInternal()
331355
}
332356
};
333357
}
358+
359+
private static OperationalJob createNonReleasingCoordinatedJob(UUID jobId)
360+
{
361+
return new OperationalJob(jobId)
362+
{
363+
@Override
364+
public boolean hasConflict(@NotNull List<OperationalJob> sameOperationJobs)
365+
{
366+
return false;
367+
}
368+
369+
@Override
370+
public OperationType operationType()
371+
{
372+
return OperationType.MOVE;
373+
}
374+
375+
@Override
376+
public boolean requiresCoordination()
377+
{
378+
return true;
379+
}
380+
381+
@Override
382+
public boolean releasesOnCompletion()
383+
{
384+
return false;
385+
}
386+
387+
@Override
388+
protected Future<Void> executeInternal()
389+
{
390+
return Future.succeededFuture();
391+
}
392+
};
393+
}
334394
}

server/src/test/java/org/apache/cassandra/sidecar/job/OperationalJobTest.java

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,6 @@ public OperationalJobStatus status()
8585
return jobStatus;
8686
}
8787

88-
@Override
89-
public OperationType operationType()
90-
{
91-
return OperationType.DRAIN;
92-
}
93-
9488
@Override
9589
public String name()
9690
{
@@ -127,12 +121,6 @@ public OperationalJobStatus status()
127121
return jobStatus;
128122
}
129123

130-
@Override
131-
public OperationType operationType()
132-
{
133-
return OperationType.DRAIN;
134-
}
135-
136124
@Override
137125
public String name()
138126
{
@@ -177,12 +165,6 @@ protected Future<Void> executeInternal() throws OperationalJobException
177165
return Future.succeededFuture();
178166
}
179167

180-
@Override
181-
public OperationType operationType()
182-
{
183-
return OperationType.DRAIN;
184-
}
185-
186168
@Override
187169
public String name()
188170
{

0 commit comments

Comments
 (0)