Skip to content

Commit 1031184

Browse files
pthirunclaude
andcommitted
[vpj] Add kill detection during data writing phase
When a push job is killed by the controller (e.g., a user push supersedes a repush), the VPJ now detects this during the data writing phase instead of waiting until pollStatusUntilComplete(). A periodic kill-check monitor queries the controller for the push status and kills the data writer job immediately if the push has been terminated, avoiding hours of wasted resource usage on large stores. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0eedead commit 1031184

2 files changed

Lines changed: 144 additions & 2 deletions

File tree

clients/venice-push-job/src/main/java/com/linkedin/venice/hadoop/VenicePushJob.java

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
import java.util.Set;
181181
import java.util.concurrent.Executors;
182182
import java.util.concurrent.ScheduledExecutorService;
183+
import java.util.concurrent.ScheduledFuture;
183184
import java.util.concurrent.TimeUnit;
184185
import java.util.stream.Collectors;
185186
import org.apache.avro.Schema;
@@ -261,6 +262,8 @@ public class VenicePushJob implements AutoCloseable {
261262
private final PushJobHeartbeatSenderFactory pushJobHeartbeatSenderFactory;
262263
private PushJobHeartbeatSender pushJobHeartbeatSender = null;
263264
private volatile boolean pushJobStatusUploadDisabledHasBeenLogged = false;
265+
private ScheduledFuture<?> pushJobKillCheckScheduledFuture;
266+
private volatile boolean pushJobKilledByController = false;
264267
private final ScheduledExecutorService timeoutExecutor;
265268
private static final int VERSION_SWAP_BUFFER_TIME_MINUTES = 20;
266269

@@ -827,7 +830,13 @@ public void run() {
827830
LOGGER.info("Incremental Push Version: {}", pushJobSetting.incrementalPushVersion);
828831
getVeniceWriter(pushJobSetting)
829832
.broadcastStartOfIncrementalPush(pushJobSetting.incrementalPushVersion, new HashMap<>());
830-
runJobAndUpdateStatus();
833+
startPushJobKillCheckMonitor();
834+
try {
835+
runJobAndUpdateStatus();
836+
} finally {
837+
stopPushJobKillCheckMonitor();
838+
}
839+
throwIfPushJobKilledByController();
831840
getVeniceWriter(pushJobSetting)
832841
.broadcastEndOfIncrementalPush(pushJobSetting.incrementalPushVersion, Collections.emptyMap());
833842
} else {
@@ -849,7 +858,13 @@ public void run() {
849858
* {@link createNewStoreVersion(PushJobSetting, long, ControllerClient, String, VeniceProperties)}
850859
*/
851860
}
852-
runJobAndUpdateStatus();
861+
startPushJobKillCheckMonitor();
862+
try {
863+
runJobAndUpdateStatus();
864+
} finally {
865+
stopPushJobKillCheckMonitor();
866+
}
867+
throwIfPushJobKilledByController();
853868

854869
if (!pushJobSetting.suppressEndOfPushMessage) {
855870
if (pushJobSetting.sendControlMessagesDirectly) {
@@ -994,6 +1009,59 @@ private void setupJobTimeoutMonitor() {
9941009
}, timeoutMs, TimeUnit.MILLISECONDS);
9951010
}
9961011

1012+
/**
1013+
* Schedules a periodic task that checks whether the push job has been killed by the controller.
1014+
* This runs during the data writing phase to detect early kills (e.g., when a user push supersedes
1015+
* a repush) and abort the data writer job promptly instead of wasting resources.
1016+
*/
1017+
void startPushJobKillCheckMonitor() {
1018+
String topicToMonitor = getTopicToMonitor(pushJobSetting);
1019+
long intervalMs = pushJobSetting.pollJobStatusIntervalMs;
1020+
LOGGER.info("Starting push job kill check monitor for topic: {} with interval: {} ms", topicToMonitor, intervalMs);
1021+
pushJobKillCheckScheduledFuture = timeoutExecutor.scheduleAtFixedRate(() -> {
1022+
try {
1023+
JobStatusQueryResponse response = ControllerClient.retryableRequest(
1024+
controllerClient,
1025+
pushJobSetting.controllerStatusPollRetries,
1026+
client -> client.queryOverallJobStatus(topicToMonitor, Optional.empty(), null, false));
1027+
if (response.isError()) {
1028+
LOGGER.warn(
1029+
"Kill check monitor could not query job status for topic: {}. Error: {}",
1030+
topicToMonitor,
1031+
response.getError());
1032+
return;
1033+
}
1034+
ExecutionStatus status = getExecutionStatusFromControllerResponse(response);
1035+
if (status.isTerminal() && status.isError()) {
1036+
LOGGER.error(
1037+
"Kill check monitor detected that push job for topic: {} has been killed. Status: {}",
1038+
topicToMonitor,
1039+
status);
1040+
pushJobKilledByController = true;
1041+
killDataWriterJob();
1042+
}
1043+
} catch (Exception e) {
1044+
LOGGER.warn("Kill check monitor encountered an error while checking job status", e);
1045+
}
1046+
}, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
1047+
}
1048+
1049+
void stopPushJobKillCheckMonitor() {
1050+
if (pushJobKillCheckScheduledFuture != null) {
1051+
pushJobKillCheckScheduledFuture.cancel(false);
1052+
pushJobKillCheckScheduledFuture = null;
1053+
LOGGER.info("Stopped push job kill check monitor");
1054+
}
1055+
}
1056+
1057+
private void throwIfPushJobKilledByController() {
1058+
if (pushJobKilledByController) {
1059+
throw new VeniceException(
1060+
"Push job for store " + pushJobSetting.storeName + " (topic: " + pushJobSetting.topic
1061+
+ ") was killed by the controller during the data writing phase.");
1062+
}
1063+
}
1064+
9971065
private void buildHDFSSchemaDir() throws IOException {
9981066
// Build the full path for HDFSRmdSchemaSource:
9991067
// RMD schemas: <job_temp_dir>/rmd_schemas

clients/venice-push-job/src/test/java/com/linkedin/venice/hadoop/VenicePushJobTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,4 +1839,78 @@ public void testTargetRegionPushWithDeferredSwapVersionStatusChecks(
18391839
}
18401840
}
18411841
}
1842+
1843+
/**
1844+
* Test that VPJ detects a killed push during the data writing phase and kills the data writer job.
1845+
* This simulates the scenario where a repush is superseded by a user push, and the controller kills
1846+
* the repush version while data is still being written.
1847+
*/
1848+
@Test(dataProvider = "DataWriterJobClasses")
1849+
public void testPushJobKilledDuringDataWriting(Class<? extends DataWriterComputeJob> dataWriterJobClass)
1850+
throws Exception {
1851+
Properties props = getVpjRequiredProperties();
1852+
props.put(KEY_FIELD_PROP, "id");
1853+
props.put(VALUE_FIELD_PROP, "name");
1854+
props.put(DATA_WRITER_COMPUTE_JOB_CLASS, dataWriterJobClass.getCanonicalName());
1855+
ControllerClient client = getClient();
1856+
1857+
// Simulate controller returning ERROR status (push was killed)
1858+
JobStatusQueryResponse killResponse = mock(JobStatusQueryResponse.class);
1859+
doReturn("ERROR").when(killResponse).getStatus();
1860+
doReturn(false).when(killResponse).isError();
1861+
doReturn(killResponse).when(client).queryOverallJobStatus(anyString(), any(), any(), anyBoolean());
1862+
1863+
try (VenicePushJob pushJob = getSpyVenicePushJob(props, client)) {
1864+
PushJobSetting pushJobSetting = pushJob.getPushJobSetting();
1865+
pushJobSetting.pollJobStatusIntervalMs = 10; // Poll quickly for the test
1866+
1867+
CountDownLatch dataWriterRunningLatch = new CountDownLatch(1);
1868+
CountDownLatch dataWriterKilledLatch = new CountDownLatch(1);
1869+
1870+
// Stall the data writer job until it gets killed by the kill-check monitor
1871+
doCallRealMethod().when(pushJob).runJobAndUpdateStatus();
1872+
doCallRealMethod().when(pushJob).startPushJobKillCheckMonitor();
1873+
doCallRealMethod().when(pushJob).stopPushJobKillCheckMonitor();
1874+
doCallRealMethod().when(pushJob).killDataWriterJob();
1875+
1876+
DataWriterComputeJob dataWriterJob = spy(pushJob.getDataWriterComputeJob());
1877+
pushJob.setDataWriterComputeJob(dataWriterJob);
1878+
doNothing().when(dataWriterJob).configure(any(), any());
1879+
doNothing().when(dataWriterJob).validateJob();
1880+
1881+
Answer<Void> stallDataWriterJob = invocation -> {
1882+
dataWriterRunningLatch.countDown();
1883+
if (!dataWriterKilledLatch.await(10, TimeUnit.SECONDS)) {
1884+
fail("Timed out waiting for the data writer job to be killed by kill-check monitor");
1885+
}
1886+
throw new VeniceException("Data writer job was killed");
1887+
};
1888+
doAnswer(stallDataWriterJob).when(dataWriterJob).runComputeJob();
1889+
1890+
// When dataWriterJob.kill() is called, release the stalled data writer
1891+
doAnswer(invocation -> {
1892+
invocation.callRealMethod();
1893+
dataWriterKilledLatch.countDown();
1894+
return null;
1895+
}).when(dataWriterJob).kill();
1896+
1897+
skipVPJValidation(pushJob);
1898+
// Override skipVPJValidation's stub on runJobAndUpdateStatus
1899+
doCallRealMethod().when(pushJob).runJobAndUpdateStatus();
1900+
1901+
try {
1902+
pushJob.run();
1903+
fail("Expected VeniceException due to push job being killed during data writing");
1904+
} catch (VeniceException e) {
1905+
assertTrue(
1906+
e.getMessage().contains("killed by the controller during the data writing phase")
1907+
|| e.getMessage().contains("Data writer job was killed"),
1908+
"Unexpected error message: " + e.getMessage());
1909+
}
1910+
1911+
assertEquals(dataWriterRunningLatch.getCount(), 0, "Data writer job should have started");
1912+
assertEquals(dataWriterKilledLatch.getCount(), 0, "Data writer job should have been killed");
1913+
verify(dataWriterJob, times(1)).kill();
1914+
}
1915+
}
18421916
}

0 commit comments

Comments
 (0)