Skip to content

Commit c397396

Browse files
framework: do not double-execute an async job after returning its queue item
In executeQueueItem, when persisting the executing MS id fails (the DB-deadlock case the catch block exists for), the queue item is returned to the queue so it can be retried. Execution then fell through to scheduleExecution(job), so the job was dispatched now AND re-dequeued and dispatched again by the heartbeat, running the same job (VM start/deploy, volume create, snapshot, etc.) twice concurrently and defeating the sync queue's serialization. Return after returning the item.
1 parent 3d70ce4 commit c397396

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

framework/jobs/src/main/java/org/apache/cloudstack/framework/jobs/impl/AsyncJobManagerImpl.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ public String obfuscatePassword(String result, boolean hidePassword) {
561561
return StringUtils.obfuscatePasswordInJsonLikeString(result);
562562
}
563563

564-
private void scheduleExecution(final AsyncJobVO job) {
564+
protected void scheduleExecution(final AsyncJobVO job) {
565565
scheduleExecution(job, false);
566566
}
567567

@@ -739,7 +739,7 @@ private int getAndResetPendingSignals(AsyncJob job) {
739739
return signals;
740740
}
741741

742-
private void executeQueueItem(SyncQueueItemVO item, boolean fromPreviousSession) {
742+
protected void executeQueueItem(SyncQueueItemVO item, boolean fromPreviousSession) {
743743
AsyncJobVO job = _jobDao.findById(item.getContentId());
744744
if (job != null) {
745745
if (logger.isDebugEnabled()) {
@@ -764,6 +764,10 @@ private void executeQueueItem(SyncQueueItemVO item, boolean fromPreviousSession)
764764
} catch (Throwable thr) {
765765
logger.error("Unexpected exception while returning job-" + item.getContentId() + " to queue", thr);
766766
}
767+
// The item was returned to the queue for a later retry; do not fall through and also
768+
// schedule the job now, or it would execute twice (once here and once when the
769+
// returned item is re-dequeued by the heartbeat).
770+
return;
767771
}
768772

769773
try {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.framework.jobs.impl;
18+
19+
import org.apache.cloudstack.framework.jobs.dao.AsyncJobDao;
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.mockito.InjectMocks;
23+
import org.mockito.Mock;
24+
import org.mockito.Mockito;
25+
import org.mockito.Spy;
26+
import org.mockito.junit.MockitoJUnitRunner;
27+
28+
import com.cloud.utils.exception.CloudRuntimeException;
29+
30+
@RunWith(MockitoJUnitRunner.Silent.class)
31+
public class AsyncJobManagerImplExecuteQueueItemTest {
32+
33+
@Mock
34+
AsyncJobDao _jobDao;
35+
@Mock
36+
SyncQueueManager _queueMgr;
37+
38+
@Spy
39+
@InjectMocks
40+
AsyncJobManagerImpl asyncJobManager = new AsyncJobManagerImpl();
41+
42+
@Test
43+
public void executeQueueItemDoesNotScheduleWhenTheJobUpdateFailsAndItemIsReturned() {
44+
long contentId = 10L;
45+
long itemId = 20L;
46+
long jobId = 1L;
47+
48+
SyncQueueItemVO item = Mockito.mock(SyncQueueItemVO.class);
49+
Mockito.when(item.getContentId()).thenReturn(contentId);
50+
Mockito.when(item.getId()).thenReturn(itemId);
51+
52+
AsyncJobVO job = Mockito.mock(AsyncJobVO.class);
53+
Mockito.when(job.getId()).thenReturn(jobId);
54+
Mockito.when(_jobDao.findById(contentId)).thenReturn(job);
55+
56+
// Simulate the DB deadlock the catch block was written to survive.
57+
Mockito.doThrow(new CloudRuntimeException("simulated DB deadlock"))
58+
.when(_jobDao).update(Mockito.anyLong(), Mockito.any(AsyncJobVO.class));
59+
60+
// Stub the executor path so we can assert whether it is reached (and avoid the real submit).
61+
Mockito.doNothing().when(asyncJobManager).scheduleExecution(Mockito.any(AsyncJobVO.class));
62+
63+
asyncJobManager.executeQueueItem(item, false);
64+
65+
// The queue item was returned for a later retry; the job must NOT also be scheduled now, or it
66+
// would run twice (once here and once when the heartbeat re-dequeues the returned item).
67+
Mockito.verify(_queueMgr).returnItem(itemId);
68+
Mockito.verify(asyncJobManager, Mockito.never()).scheduleExecution(Mockito.any(AsyncJobVO.class));
69+
}
70+
}

0 commit comments

Comments
 (0)