mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-02-09 14:34:56 +00:00
fixing up more tests for batch2
This commit is contained in:
parent
7d5794e1fb
commit
4b22697af9
@ -18,6 +18,7 @@ import ca.uhn.fhir.jpa.dao.data.IBatch2WorkChunkRepository;
|
|||||||
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
|
import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
|
||||||
import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
|
import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
|
||||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||||
|
import ca.uhn.fhir.jpa.test.Batch2JobHelper;
|
||||||
import ca.uhn.fhir.util.JsonUtil;
|
import ca.uhn.fhir.util.JsonUtil;
|
||||||
import ca.uhn.hapi.fhir.batch2.test.AbstractIJobPersistenceSpecificationTest;
|
import ca.uhn.hapi.fhir.batch2.test.AbstractIJobPersistenceSpecificationTest;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -77,6 +78,9 @@ public class JpaJobPersistenceImplTest extends BaseJpaR4Test {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IBatch2JobInstanceRepository myJobInstanceRepository;
|
private IBatch2JobInstanceRepository myJobInstanceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public Batch2JobHelper myBatch2JobHelper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDeleteInstance() {
|
public void testDeleteInstance() {
|
||||||
// Setup
|
// Setup
|
||||||
@ -323,14 +327,19 @@ public class JpaJobPersistenceImplTest extends BaseJpaR4Test {
|
|||||||
class Batch2SpecTest extends AbstractIJobPersistenceSpecificationTest {
|
class Batch2SpecTest extends AbstractIJobPersistenceSpecificationTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected PlatformTransactionManager getTxManager() {
|
public PlatformTransactionManager getTxManager() {
|
||||||
return JpaJobPersistenceImplTest.this.getTxManager();
|
return JpaJobPersistenceImplTest.this.getTxManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected WorkChunk freshFetchWorkChunk(String chunkId) {
|
public WorkChunk freshFetchWorkChunk(String chunkId) {
|
||||||
return JpaJobPersistenceImplTest.this.freshFetchWorkChunk(chunkId);
|
return JpaJobPersistenceImplTest.this.freshFetchWorkChunk(chunkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void runMaintenancePass() {
|
||||||
|
myBatch2JobHelper.runMaintenancePass();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -23,29 +23,18 @@ package ca.uhn.hapi.fhir.batch2.test;
|
|||||||
import ca.uhn.fhir.batch2.api.IJobPersistence;
|
import ca.uhn.fhir.batch2.api.IJobPersistence;
|
||||||
import ca.uhn.fhir.batch2.api.RunOutcome;
|
import ca.uhn.fhir.batch2.api.RunOutcome;
|
||||||
import ca.uhn.fhir.batch2.coordinator.JobDefinitionRegistry;
|
import ca.uhn.fhir.batch2.coordinator.JobDefinitionRegistry;
|
||||||
import ca.uhn.fhir.batch2.maintenance.JobChunkProgressAccumulator;
|
|
||||||
import ca.uhn.fhir.batch2.maintenance.JobInstanceProcessor;
|
|
||||||
import ca.uhn.fhir.batch2.model.JobDefinition;
|
import ca.uhn.fhir.batch2.model.JobDefinition;
|
||||||
import ca.uhn.fhir.batch2.model.JobInstance;
|
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||||
import ca.uhn.fhir.batch2.model.StatusEnum;
|
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||||
import ca.uhn.fhir.batch2.model.WorkChunk;
|
|
||||||
import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent;
|
|
||||||
import ca.uhn.fhir.batch2.model.WorkChunkCreateEvent;
|
import ca.uhn.fhir.batch2.model.WorkChunkCreateEvent;
|
||||||
import ca.uhn.fhir.batch2.model.WorkChunkErrorEvent;
|
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
|
||||||
import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum;
|
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
import ca.uhn.fhir.util.StopWatch;
|
import ca.uhn.fhir.util.StopWatch;
|
||||||
import ca.uhn.hapi.fhir.batch2.test.support.TestJobParameters;
|
import ca.uhn.hapi.fhir.batch2.test.support.TestJobParameters;
|
||||||
import ca.uhn.hapi.fhir.batch2.test.support.TestJobStep2InputType;
|
import ca.uhn.hapi.fhir.batch2.test.support.TestJobStep2InputType;
|
||||||
import ca.uhn.hapi.fhir.batch2.test.support.TestJobStep3InputType;
|
import ca.uhn.hapi.fhir.batch2.test.support.TestJobStep3InputType;
|
||||||
import com.google.common.collect.ImmutableList;
|
import jakarta.annotation.Nonnull;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Nested;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.EnumSource;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
import org.springframework.transaction.TransactionDefinition;
|
import org.springframework.transaction.TransactionDefinition;
|
||||||
@ -53,572 +42,35 @@ import org.springframework.transaction.TransactionStatus;
|
|||||||
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
import jakarta.annotation.Nonnull;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
import static org.awaitility.Awaitility.await;
|
import static org.awaitility.Awaitility.await;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.emptyString;
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
|
||||||
import static org.hamcrest.Matchers.not;
|
|
||||||
import static org.hamcrest.Matchers.theInstance;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specification tests for batch2 storage and event system.
|
* Specification tests for batch2 storage and event system.
|
||||||
* These tests are abstract, and do not depend on JPA.
|
* These tests are abstract, and do not depend on JPA.
|
||||||
* Test setups should use the public batch2 api to create scenarios.
|
* Test setups should use the public batch2 api to create scenarios.
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractIJobPersistenceSpecificationTest {
|
public abstract class AbstractIJobPersistenceSpecificationTest implements IInProgressActionsTests, IInstanceStateTransitions, IWorkChunkStateTransitions, IWorkChunkStorageTests, IWorkChunkErrorActionsTests, WorkChunkTestConstants {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(AbstractIJobPersistenceSpecificationTest.class);
|
|
||||||
|
|
||||||
public static final String JOB_DEFINITION_ID = "definition-id";
|
|
||||||
public static final String TARGET_STEP_ID = "step-id";
|
|
||||||
public static final String DEF_CHUNK_ID = "definition-chunkId";
|
|
||||||
public static final String STEP_CHUNK_ID = "step-chunkId";
|
|
||||||
public static final int JOB_DEF_VER = 1;
|
|
||||||
public static final int SEQUENCE_NUMBER = 1;
|
|
||||||
public static final String CHUNK_DATA = "{\"key\":\"value\"}";
|
|
||||||
public static final String ERROR_MESSAGE_A = "This is an error message: A";
|
|
||||||
public static final String ERROR_MESSAGE_B = "This is a different error message: B";
|
|
||||||
public static final String ERROR_MESSAGE_C = "This is a different error message: C";
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IJobPersistence mySvc;
|
private IJobPersistence mySvc;
|
||||||
|
|
||||||
@Nested
|
@Autowired
|
||||||
class WorkChunkStorage {
|
private JobDefinitionRegistry myJobDefinitionRegistry;
|
||||||
|
|
||||||
@Test
|
@Autowired
|
||||||
public void testStoreAndFetchWorkChunk_NoData() {
|
private IHapiTransactionService myTransactionService;
|
||||||
JobInstance instance = createInstance();
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance);
|
|
||||||
|
|
||||||
String id = storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, instanceId, 0, null);
|
public IHapiTransactionService getTransactionManager() {
|
||||||
|
return myTransactionService;
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(id).orElseThrow(IllegalArgumentException::new);
|
|
||||||
assertNull(chunk.getData());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public IJobPersistence getSvc() {
|
||||||
public void testStoreAndFetchWorkChunk_WithData() {
|
return mySvc;
|
||||||
JobInstance instance = createInstance();
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance);
|
|
||||||
|
|
||||||
String id = storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, instanceId, 0, CHUNK_DATA);
|
|
||||||
assertNotNull(id);
|
|
||||||
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.QUEUED, freshFetchWorkChunk(id).getStatus()));
|
|
||||||
|
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(id).orElseThrow(IllegalArgumentException::new);
|
|
||||||
assertEquals(36, chunk.getInstanceId().length());
|
|
||||||
assertEquals(JOB_DEFINITION_ID, chunk.getJobDefinitionId());
|
|
||||||
assertEquals(JOB_DEF_VER, chunk.getJobDefinitionVersion());
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
|
||||||
assertEquals(CHUNK_DATA, chunk.getData());
|
|
||||||
|
|
||||||
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.IN_PROGRESS, freshFetchWorkChunk(id).getStatus()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public JobDefinition<TestJobParameters> withJobDefinition() {
|
||||||
* Should match the diagram in batch2_states.md
|
|
||||||
* @see hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_batch/batch2_states.md
|
|
||||||
*/
|
|
||||||
@Nested
|
|
||||||
class StateTransitions {
|
|
||||||
|
|
||||||
private String myInstanceId;
|
|
||||||
private String myChunkId;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
JobInstance jobInstance = createInstance();
|
|
||||||
myInstanceId = mySvc.storeNewInstance(jobInstance);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createChunk() {
|
|
||||||
return storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, myInstanceId, 0, CHUNK_DATA);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void chunkCreation_isQueued() {
|
|
||||||
|
|
||||||
myChunkId = createChunk();
|
|
||||||
|
|
||||||
WorkChunk fetchedWorkChunk = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.QUEUED, fetchedWorkChunk.getStatus(), "New chunks are QUEUED");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void chunkReceived_queuedToInProgress() {
|
|
||||||
|
|
||||||
myChunkId = createChunk();
|
|
||||||
|
|
||||||
// the worker has received the chunk, and marks it started.
|
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(myChunkId).orElseThrow(IllegalArgumentException::new);
|
|
||||||
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
|
||||||
assertEquals(CHUNK_DATA, chunk.getData());
|
|
||||||
|
|
||||||
// verify the db was updated too
|
|
||||||
WorkChunk fetchedWorkChunk = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, fetchedWorkChunk.getStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class InProgressActions {
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// setup - the worker has received the chunk, and has marked it IN_PROGRESS.
|
|
||||||
myChunkId = createChunk();
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void processingOk_inProgressToSuccess_clearsDataSavesRecordCount() {
|
|
||||||
|
|
||||||
// execution ok
|
|
||||||
mySvc.onWorkChunkCompletion(new WorkChunkCompletionEvent(myChunkId, 3, 0));
|
|
||||||
|
|
||||||
// verify the db was updated
|
|
||||||
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.COMPLETED, workChunkEntity.getStatus());
|
|
||||||
assertNull(workChunkEntity.getData());
|
|
||||||
assertEquals(3, workChunkEntity.getRecordsProcessed());
|
|
||||||
assertNull(workChunkEntity.getErrorMessage());
|
|
||||||
assertEquals(0, workChunkEntity.getErrorCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void processingRetryableError_inProgressToError_bumpsCountRecordsMessage() {
|
|
||||||
|
|
||||||
// execution had a retryable error
|
|
||||||
mySvc.onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_A));
|
|
||||||
|
|
||||||
// verify the db was updated
|
|
||||||
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.ERRORED, workChunkEntity.getStatus());
|
|
||||||
assertEquals(ERROR_MESSAGE_A, workChunkEntity.getErrorMessage());
|
|
||||||
assertEquals(1, workChunkEntity.getErrorCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void processingFailure_inProgressToFailed() {
|
|
||||||
|
|
||||||
// execution had a failure
|
|
||||||
mySvc.onWorkChunkFailed(myChunkId, "some error");
|
|
||||||
|
|
||||||
// verify the db was updated
|
|
||||||
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.FAILED, workChunkEntity.getStatus());
|
|
||||||
assertEquals("some error", workChunkEntity.getErrorMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
class ErrorActions {
|
|
||||||
public static final String FIRST_ERROR_MESSAGE = ERROR_MESSAGE_A;
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// setup - the worker has received the chunk, and has marked it IN_PROGRESS.
|
|
||||||
myChunkId = createChunk();
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
// execution had a retryable error
|
|
||||||
mySvc.onWorkChunkError(new WorkChunkErrorEvent(myChunkId, FIRST_ERROR_MESSAGE));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The consumer will retry after a retryable error is thrown
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
void errorRetry_errorToInProgress() {
|
|
||||||
|
|
||||||
// when consumer restarts chunk
|
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(myChunkId).orElseThrow(IllegalArgumentException::new);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
|
||||||
|
|
||||||
// verify the db state, error message, and error count
|
|
||||||
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, workChunkEntity.getStatus());
|
|
||||||
assertEquals(FIRST_ERROR_MESSAGE, workChunkEntity.getErrorMessage(), "Original error message kept");
|
|
||||||
assertEquals(1, workChunkEntity.getErrorCount(), "error count kept");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void errorRetry_repeatError_increasesErrorCount() {
|
|
||||||
// setup - the consumer is re-trying, and marks it IN_PROGRESS
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
|
|
||||||
|
|
||||||
// when another error happens
|
|
||||||
mySvc.onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_B));
|
|
||||||
|
|
||||||
|
|
||||||
// verify the state, new message, and error count
|
|
||||||
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.ERRORED, workChunkEntity.getStatus());
|
|
||||||
assertEquals(ERROR_MESSAGE_B, workChunkEntity.getErrorMessage(), "new error message");
|
|
||||||
assertEquals(2, workChunkEntity.getErrorCount(), "error count inc");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void errorThenRetryAndComplete_addsErrorCounts() {
|
|
||||||
// setup - the consumer is re-trying, and marks it IN_PROGRESS
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
|
|
||||||
// then it completes ok.
|
|
||||||
mySvc.onWorkChunkCompletion(new WorkChunkCompletionEvent(myChunkId, 3, 1));
|
|
||||||
|
|
||||||
// verify the state, new message, and error count
|
|
||||||
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.COMPLETED, workChunkEntity.getStatus());
|
|
||||||
assertEquals(FIRST_ERROR_MESSAGE, workChunkEntity.getErrorMessage(), "Error message kept.");
|
|
||||||
assertEquals(2, workChunkEntity.getErrorCount(), "error combined with earlier error");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void errorRetry_maxErrors_movesToFailed() {
|
|
||||||
// we start with 1 error already
|
|
||||||
|
|
||||||
// 2nd try
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
mySvc.onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_B));
|
|
||||||
var chunk = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.ERRORED, chunk.getStatus());
|
|
||||||
assertEquals(2, chunk.getErrorCount());
|
|
||||||
|
|
||||||
// 3rd try
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
mySvc.onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_B));
|
|
||||||
chunk = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.ERRORED, chunk.getStatus());
|
|
||||||
assertEquals(3, chunk.getErrorCount());
|
|
||||||
|
|
||||||
// 4th try
|
|
||||||
mySvc.onWorkChunkDequeue(myChunkId);
|
|
||||||
mySvc.onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_C));
|
|
||||||
chunk = freshFetchWorkChunk(myChunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.FAILED, chunk.getStatus());
|
|
||||||
assertEquals(4, chunk.getErrorCount());
|
|
||||||
assertThat("Error message contains last error", chunk.getErrorMessage(), containsString(ERROR_MESSAGE_C));
|
|
||||||
assertThat("Error message contains error count and complaint", chunk.getErrorMessage(), containsString("many errors: 4"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMarkChunkAsCompleted_Success() {
|
|
||||||
JobInstance instance = createInstance();
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance);
|
|
||||||
String chunkId = storeWorkChunk(DEF_CHUNK_ID, STEP_CHUNK_ID, instanceId, SEQUENCE_NUMBER, CHUNK_DATA);
|
|
||||||
assertNotNull(chunkId);
|
|
||||||
|
|
||||||
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.QUEUED, freshFetchWorkChunk(chunkId).getStatus()));
|
|
||||||
|
|
||||||
sleepUntilTimeChanges();
|
|
||||||
|
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(chunkId).orElseThrow(IllegalArgumentException::new);
|
|
||||||
assertEquals(SEQUENCE_NUMBER, chunk.getSequence());
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
|
||||||
assertNotNull(chunk.getCreateTime());
|
|
||||||
assertNotNull(chunk.getStartTime());
|
|
||||||
assertNull(chunk.getEndTime());
|
|
||||||
assertNull(chunk.getRecordsProcessed());
|
|
||||||
assertNotNull(chunk.getData());
|
|
||||||
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.IN_PROGRESS, freshFetchWorkChunk(chunkId).getStatus()));
|
|
||||||
|
|
||||||
sleepUntilTimeChanges();
|
|
||||||
|
|
||||||
runInTransaction(() -> mySvc.onWorkChunkCompletion(new WorkChunkCompletionEvent(chunkId, 50, 0)));
|
|
||||||
|
|
||||||
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.COMPLETED, entity.getStatus());
|
|
||||||
assertEquals(50, entity.getRecordsProcessed());
|
|
||||||
assertNotNull(entity.getCreateTime());
|
|
||||||
assertNotNull(entity.getStartTime());
|
|
||||||
assertNotNull(entity.getEndTime());
|
|
||||||
assertNull(entity.getData());
|
|
||||||
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
|
||||||
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMarkChunkAsCompleted_Error() {
|
|
||||||
JobInstance instance = createInstance();
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance);
|
|
||||||
String chunkId = storeWorkChunk(DEF_CHUNK_ID, STEP_CHUNK_ID, instanceId, SEQUENCE_NUMBER, null);
|
|
||||||
assertNotNull(chunkId);
|
|
||||||
|
|
||||||
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.QUEUED, freshFetchWorkChunk(chunkId).getStatus()));
|
|
||||||
|
|
||||||
sleepUntilTimeChanges();
|
|
||||||
|
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(chunkId).orElseThrow(IllegalArgumentException::new);
|
|
||||||
assertEquals(SEQUENCE_NUMBER, chunk.getSequence());
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
|
||||||
|
|
||||||
sleepUntilTimeChanges();
|
|
||||||
|
|
||||||
WorkChunkErrorEvent request = new WorkChunkErrorEvent(chunkId, ERROR_MESSAGE_A);
|
|
||||||
mySvc.onWorkChunkError(request);
|
|
||||||
runInTransaction(() -> {
|
|
||||||
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.ERRORED, entity.getStatus());
|
|
||||||
assertEquals(ERROR_MESSAGE_A, entity.getErrorMessage());
|
|
||||||
assertNotNull(entity.getCreateTime());
|
|
||||||
assertNotNull(entity.getStartTime());
|
|
||||||
assertNotNull(entity.getEndTime());
|
|
||||||
assertEquals(1, entity.getErrorCount());
|
|
||||||
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
|
||||||
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mark errored again
|
|
||||||
|
|
||||||
WorkChunkErrorEvent request2 = new WorkChunkErrorEvent(chunkId, "This is an error message 2");
|
|
||||||
mySvc.onWorkChunkError(request2);
|
|
||||||
runInTransaction(() -> {
|
|
||||||
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.ERRORED, entity.getStatus());
|
|
||||||
assertEquals("This is an error message 2", entity.getErrorMessage());
|
|
||||||
assertNotNull(entity.getCreateTime());
|
|
||||||
assertNotNull(entity.getStartTime());
|
|
||||||
assertNotNull(entity.getEndTime());
|
|
||||||
assertEquals(2, entity.getErrorCount());
|
|
||||||
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
|
||||||
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
|
||||||
});
|
|
||||||
|
|
||||||
List<WorkChunk> chunks = ImmutableList.copyOf(mySvc.fetchAllWorkChunksIterator(instanceId, true));
|
|
||||||
assertEquals(1, chunks.size());
|
|
||||||
assertEquals(2, chunks.get(0).getErrorCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testMarkChunkAsCompleted_Fail() {
|
|
||||||
JobInstance instance = createInstance();
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance);
|
|
||||||
String chunkId = storeWorkChunk(DEF_CHUNK_ID, STEP_CHUNK_ID, instanceId, SEQUENCE_NUMBER, null);
|
|
||||||
assertNotNull(chunkId);
|
|
||||||
|
|
||||||
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.QUEUED, freshFetchWorkChunk(chunkId).getStatus()));
|
|
||||||
|
|
||||||
sleepUntilTimeChanges();
|
|
||||||
|
|
||||||
WorkChunk chunk = mySvc.onWorkChunkDequeue(chunkId).orElseThrow(IllegalArgumentException::new);
|
|
||||||
assertEquals(SEQUENCE_NUMBER, chunk.getSequence());
|
|
||||||
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
|
||||||
|
|
||||||
sleepUntilTimeChanges();
|
|
||||||
|
|
||||||
mySvc.onWorkChunkFailed(chunkId, "This is an error message");
|
|
||||||
runInTransaction(() -> {
|
|
||||||
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
|
||||||
assertEquals(WorkChunkStatusEnum.FAILED, entity.getStatus());
|
|
||||||
assertEquals("This is an error message", entity.getErrorMessage());
|
|
||||||
assertNotNull(entity.getCreateTime());
|
|
||||||
assertNotNull(entity.getStartTime());
|
|
||||||
assertNotNull(entity.getEndTime());
|
|
||||||
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
|
||||||
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void markWorkChunksWithStatusAndWipeData_marksMultipleChunksWithStatus_asExpected() {
|
|
||||||
JobInstance instance = createInstance();
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance);
|
|
||||||
ArrayList<String> chunkIds = new ArrayList<>();
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
WorkChunkCreateEvent chunk = new WorkChunkCreateEvent(
|
|
||||||
"defId",
|
|
||||||
1,
|
|
||||||
"stepId",
|
|
||||||
instanceId,
|
|
||||||
0,
|
|
||||||
"{}"
|
|
||||||
);
|
|
||||||
String id = mySvc.onWorkChunkCreate(chunk);
|
|
||||||
chunkIds.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
runInTransaction(() -> mySvc.markWorkChunksWithStatusAndWipeData(instance.getInstanceId(), chunkIds, WorkChunkStatusEnum.COMPLETED, null));
|
|
||||||
|
|
||||||
Iterator<WorkChunk> reducedChunks = mySvc.fetchAllWorkChunksIterator(instanceId, true);
|
|
||||||
|
|
||||||
while (reducedChunks.hasNext()) {
|
|
||||||
WorkChunk reducedChunk = reducedChunks.next();
|
|
||||||
assertTrue(chunkIds.contains(reducedChunk.getId()));
|
|
||||||
assertEquals(WorkChunkStatusEnum.COMPLETED, reducedChunk.getStatus());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test
|
|
||||||
* * @see hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_batch/batch2_states.md
|
|
||||||
*/
|
|
||||||
@Nested
|
|
||||||
class InstanceStateTransitions {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createInstance_createsInQueuedWithChunk() {
|
|
||||||
// given
|
|
||||||
JobDefinition<?> jd = withJobDefinition();
|
|
||||||
|
|
||||||
// when
|
|
||||||
IJobPersistence.CreateResult createResult =
|
|
||||||
newTxTemplate().execute(status->
|
|
||||||
mySvc.onCreateWithFirstChunk(jd, "{}"));
|
|
||||||
|
|
||||||
// then
|
|
||||||
ourLog.info("job and chunk created {}", createResult);
|
|
||||||
assertNotNull(createResult);
|
|
||||||
assertThat(createResult.jobInstanceId, not(emptyString()));
|
|
||||||
assertThat(createResult.workChunkId, not(emptyString()));
|
|
||||||
|
|
||||||
JobInstance jobInstance = freshFetchJobInstance(createResult.jobInstanceId);
|
|
||||||
assertThat(jobInstance.getStatus(), equalTo(StatusEnum.QUEUED));
|
|
||||||
assertThat(jobInstance.getParameters(), equalTo("{}"));
|
|
||||||
|
|
||||||
WorkChunk firstChunk = freshFetchWorkChunk(createResult.workChunkId);
|
|
||||||
assertThat(firstChunk.getStatus(), equalTo(WorkChunkStatusEnum.QUEUED));
|
|
||||||
assertNull(firstChunk.getData(), "First chunk data is null - only uses parameters");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCreateInstance_firstChunkDequeued_movesToInProgress() {
|
|
||||||
// given
|
|
||||||
JobDefinition<?> jd = withJobDefinition();
|
|
||||||
IJobPersistence.CreateResult createResult = newTxTemplate().execute(status->
|
|
||||||
mySvc.onCreateWithFirstChunk(jd, "{}"));
|
|
||||||
assertNotNull(createResult);
|
|
||||||
|
|
||||||
// when
|
|
||||||
newTxTemplate().execute(status -> mySvc.onChunkDequeued(createResult.jobInstanceId));
|
|
||||||
|
|
||||||
// then
|
|
||||||
JobInstance jobInstance = freshFetchJobInstance(createResult.jobInstanceId);
|
|
||||||
assertThat(jobInstance.getStatus(), equalTo(StatusEnum.IN_PROGRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@EnumSource(StatusEnum.class)
|
|
||||||
void cancelRequest_cancelsJob_whenNotFinalState(StatusEnum theState) {
|
|
||||||
// given
|
|
||||||
JobInstance cancelledInstance = createInstance();
|
|
||||||
cancelledInstance.setStatus(theState);
|
|
||||||
String instanceId1 = mySvc.storeNewInstance(cancelledInstance);
|
|
||||||
mySvc.cancelInstance(instanceId1);
|
|
||||||
|
|
||||||
JobInstance normalInstance = createInstance();
|
|
||||||
normalInstance.setStatus(theState);
|
|
||||||
String instanceId2 = mySvc.storeNewInstance(normalInstance);
|
|
||||||
|
|
||||||
JobDefinitionRegistry jobDefinitionRegistry = new JobDefinitionRegistry();
|
|
||||||
jobDefinitionRegistry.addJobDefinitionIfNotRegistered(withJobDefinition());
|
|
||||||
|
|
||||||
|
|
||||||
// when
|
|
||||||
runInTransaction(()-> {
|
|
||||||
new JobInstanceProcessor(
|
|
||||||
mySvc,
|
|
||||||
null,
|
|
||||||
instanceId1,
|
|
||||||
new JobChunkProgressAccumulator(),
|
|
||||||
null,
|
|
||||||
jobDefinitionRegistry,
|
|
||||||
null // transaction manager
|
|
||||||
).process();
|
|
||||||
});
|
|
||||||
|
|
||||||
// then
|
|
||||||
JobInstance freshInstance1 = mySvc.fetchInstance(instanceId1).orElseThrow();
|
|
||||||
if (theState.isCancellable()) {
|
|
||||||
assertEquals(StatusEnum.CANCELLED, freshInstance1.getStatus(), "cancel request processed");
|
|
||||||
assertThat(freshInstance1.getErrorMessage(), containsString("Job instance cancelled"));
|
|
||||||
} else {
|
|
||||||
assertEquals(theState, freshInstance1.getStatus(), "cancel request ignored - state unchanged");
|
|
||||||
assertNull(freshInstance1.getErrorMessage(), "no error message");
|
|
||||||
}
|
|
||||||
JobInstance freshInstance2 = mySvc.fetchInstance(instanceId2).orElseThrow();
|
|
||||||
assertEquals(theState, freshInstance2.getStatus(), "cancel request ignored - cancelled not set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testDeleteChunksAndMarkInstanceAsChunksPurged_doesWhatItSays() {
|
|
||||||
// given
|
|
||||||
JobDefinition<?> jd = withJobDefinition();
|
|
||||||
IJobPersistence.CreateResult createResult = newTxTemplate().execute(status->
|
|
||||||
mySvc.onCreateWithFirstChunk(jd, "{}"));
|
|
||||||
String instanceId = createResult.jobInstanceId;
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, instanceId, i, CHUNK_DATA);
|
|
||||||
}
|
|
||||||
JobInstance readback = freshFetchJobInstance(instanceId);
|
|
||||||
assertFalse(readback.isWorkChunksPurged());
|
|
||||||
assertTrue(mySvc.fetchAllWorkChunksIterator(instanceId, true).hasNext(), "has chunk");
|
|
||||||
|
|
||||||
// when
|
|
||||||
mySvc.deleteChunksAndMarkInstanceAsChunksPurged(instanceId);
|
|
||||||
|
|
||||||
// then
|
|
||||||
readback = freshFetchJobInstance(instanceId);
|
|
||||||
assertTrue(readback.isWorkChunksPurged(), "purged set");
|
|
||||||
assertFalse(mySvc.fetchAllWorkChunksIterator(instanceId, true).hasNext(), "chunks gone");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testInstanceUpdate_modifierApplied() {
|
|
||||||
// given
|
|
||||||
String instanceId = mySvc.storeNewInstance(createInstance());
|
|
||||||
|
|
||||||
// when
|
|
||||||
mySvc.updateInstance(instanceId, instance ->{
|
|
||||||
instance.setErrorCount(42);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// then
|
|
||||||
JobInstance jobInstance = freshFetchJobInstance(instanceId);
|
|
||||||
assertEquals(42, jobInstance.getErrorCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testInstanceUpdate_modifierNotAppliedWhenPredicateReturnsFalse() {
|
|
||||||
// given
|
|
||||||
JobInstance instance1 = createInstance();
|
|
||||||
boolean initialValue = true;
|
|
||||||
instance1.setFastTracking(initialValue);
|
|
||||||
String instanceId = mySvc.storeNewInstance(instance1);
|
|
||||||
|
|
||||||
// when
|
|
||||||
mySvc.updateInstance(instanceId, instance ->{
|
|
||||||
instance.setFastTracking(false);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// then
|
|
||||||
JobInstance jobInstance = freshFetchJobInstance(instanceId);
|
|
||||||
assertEquals(initialValue, jobInstance.isFastTracking());
|
|
||||||
}
|
|
||||||
|
|
||||||
private JobDefinition<TestJobParameters> withJobDefinition() {
|
|
||||||
return JobDefinition.newBuilder()
|
return JobDefinition.newBuilder()
|
||||||
.setJobDefinitionId(JOB_DEFINITION_ID)
|
.setJobDefinitionId(JOB_DEFINITION_ID)
|
||||||
.setJobDefinitionVersion(JOB_DEF_VER)
|
.setJobDefinitionVersion(JOB_DEF_VER)
|
||||||
@ -630,11 +82,20 @@ public abstract class AbstractIJobPersistenceSpecificationTest {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void after() {
|
||||||
|
myJobDefinitionRegistry.removeJobDefinition(JOB_DEFINITION_ID, JOB_DEF_VER);
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private JobInstance createInstance() {
|
public JobInstance createInstance() {
|
||||||
|
JobDefinition<TestJobParameters> jobDefinition = withJobDefinition();
|
||||||
|
if (myJobDefinitionRegistry.getJobDefinition(JOB_DEFINITION_ID, JOB_DEF_VER).isEmpty()) {
|
||||||
|
myJobDefinitionRegistry.addJobDefinition(jobDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
JobInstance instance = new JobInstance();
|
JobInstance instance = new JobInstance();
|
||||||
instance.setJobDefinitionId(JOB_DEFINITION_ID);
|
instance.setJobDefinitionId(jobDefinition.getJobDefinitionId());
|
||||||
instance.setStatus(StatusEnum.QUEUED);
|
instance.setStatus(StatusEnum.QUEUED);
|
||||||
instance.setJobDefinitionVersion(JOB_DEF_VER);
|
instance.setJobDefinitionVersion(JOB_DEF_VER);
|
||||||
instance.setParameters(CHUNK_DATA);
|
instance.setParameters(CHUNK_DATA);
|
||||||
@ -642,15 +103,14 @@ public abstract class AbstractIJobPersistenceSpecificationTest {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String storeWorkChunk(String theJobDefinitionId, String theTargetStepId, String theInstanceId, int theSequence, String theSerializedData) {
|
public String storeWorkChunk(String theJobDefinitionId, String theTargetStepId, String theInstanceId, int theSequence, String theSerializedData) {
|
||||||
WorkChunkCreateEvent batchWorkChunk = new WorkChunkCreateEvent(theJobDefinitionId, JOB_DEF_VER, theTargetStepId, theInstanceId, theSequence, theSerializedData);
|
WorkChunkCreateEvent batchWorkChunk = new WorkChunkCreateEvent(theJobDefinitionId, JOB_DEF_VER, theTargetStepId, theInstanceId, theSequence, theSerializedData);
|
||||||
return mySvc.onWorkChunkCreate(batchWorkChunk);
|
return mySvc.onWorkChunkCreate(batchWorkChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract PlatformTransactionManager getTxManager();
|
||||||
|
|
||||||
protected abstract PlatformTransactionManager getTxManager();
|
public JobInstance freshFetchJobInstance(String theInstanceId) {
|
||||||
protected abstract WorkChunk freshFetchWorkChunk(String theChunkId);
|
|
||||||
protected JobInstance freshFetchJobInstance(String theInstanceId) {
|
|
||||||
return runInTransaction(() -> mySvc.fetchInstance(theInstanceId).orElseThrow());
|
return runInTransaction(() -> mySvc.fetchInstance(theInstanceId).orElseThrow());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -689,6 +149,14 @@ public abstract class AbstractIJobPersistenceSpecificationTest {
|
|||||||
await().until(() -> sw.getMillis() > 0);
|
await().until(() -> sw.getMillis() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String createAndStoreJobInstance() {
|
||||||
|
JobInstance jobInstance = createInstance();
|
||||||
|
return mySvc.storeNewInstance(jobInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createAndDequeueWorkChunk(String theJobInstanceId) {
|
||||||
|
String chunkId = createChunk(theJobInstanceId);
|
||||||
|
mySvc.onWorkChunkDequeue(chunkId);
|
||||||
|
return chunkId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkErrorEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
public interface IInProgressActionsTests extends IWorkChunkCommon, WorkChunkTestConstants {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void processingOk_inProgressToSuccess_clearsDataSavesRecordCount() {
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
// execution ok
|
||||||
|
getSvc().onWorkChunkCompletion(new WorkChunkCompletionEvent(myChunkId, 3, 0));
|
||||||
|
|
||||||
|
// verify the db was updated
|
||||||
|
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.COMPLETED, workChunkEntity.getStatus());
|
||||||
|
assertNull(workChunkEntity.getData());
|
||||||
|
assertEquals(3, workChunkEntity.getRecordsProcessed());
|
||||||
|
assertNull(workChunkEntity.getErrorMessage());
|
||||||
|
assertEquals(0, workChunkEntity.getErrorCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void processingRetryableError_inProgressToError_bumpsCountRecordsMessage() {
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
// execution had a retryable error
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_A));
|
||||||
|
|
||||||
|
// verify the db was updated
|
||||||
|
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.ERRORED, workChunkEntity.getStatus());
|
||||||
|
assertEquals(ERROR_MESSAGE_A, workChunkEntity.getErrorMessage());
|
||||||
|
assertEquals(1, workChunkEntity.getErrorCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void processingFailure_inProgressToFailed() {
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
// execution had a failure
|
||||||
|
getSvc().onWorkChunkFailed(myChunkId, "some error");
|
||||||
|
|
||||||
|
// verify the db was updated
|
||||||
|
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.FAILED, workChunkEntity.getStatus());
|
||||||
|
assertEquals("some error", workChunkEntity.getErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.batch2.api.IJobPersistence;
|
||||||
|
import ca.uhn.fhir.batch2.coordinator.JobDefinitionRegistry;
|
||||||
|
import ca.uhn.fhir.batch2.maintenance.JobChunkProgressAccumulator;
|
||||||
|
import ca.uhn.fhir.batch2.maintenance.JobInstanceProcessor;
|
||||||
|
import ca.uhn.fhir.batch2.model.JobDefinition;
|
||||||
|
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||||
|
import ca.uhn.fhir.batch2.model.StatusEnum;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.emptyString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
public interface IInstanceStateTransitions extends IWorkChunkCommon, WorkChunkTestConstants {
|
||||||
|
Logger ourLog = LoggerFactory.getLogger(IInstanceStateTransitions.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void createInstance_createsInQueuedWithChunk() {
|
||||||
|
// given
|
||||||
|
JobDefinition<?> jd = withJobDefinition();
|
||||||
|
|
||||||
|
// when
|
||||||
|
IJobPersistence.CreateResult createResult =
|
||||||
|
newTxTemplate().execute(status->
|
||||||
|
getSvc().onCreateWithFirstChunk(jd, "{}"));
|
||||||
|
|
||||||
|
// then
|
||||||
|
ourLog.info("job and chunk created {}", createResult);
|
||||||
|
assertNotNull(createResult);
|
||||||
|
assertThat(createResult.jobInstanceId, not(emptyString()));
|
||||||
|
assertThat(createResult.workChunkId, not(emptyString()));
|
||||||
|
|
||||||
|
JobInstance jobInstance = freshFetchJobInstance(createResult.jobInstanceId);
|
||||||
|
assertThat(jobInstance.getStatus(), equalTo(StatusEnum.QUEUED));
|
||||||
|
assertThat(jobInstance.getParameters(), equalTo("{}"));
|
||||||
|
|
||||||
|
WorkChunk firstChunk = freshFetchWorkChunk(createResult.workChunkId);
|
||||||
|
assertThat(firstChunk.getStatus(), equalTo(WorkChunkStatusEnum.READY));
|
||||||
|
assertNull(firstChunk.getData(), "First chunk data is null - only uses parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void testCreateInstance_firstChunkDequeued_movesToInProgress() {
|
||||||
|
// given
|
||||||
|
JobDefinition<?> jd = withJobDefinition();
|
||||||
|
IJobPersistence.CreateResult createResult = newTxTemplate().execute(status->
|
||||||
|
getSvc().onCreateWithFirstChunk(jd, "{}"));
|
||||||
|
assertNotNull(createResult);
|
||||||
|
|
||||||
|
// when
|
||||||
|
newTxTemplate().execute(status -> getSvc().onChunkDequeued(createResult.jobInstanceId));
|
||||||
|
|
||||||
|
// then
|
||||||
|
JobInstance jobInstance = freshFetchJobInstance(createResult.jobInstanceId);
|
||||||
|
assertThat(jobInstance.getStatus(), equalTo(StatusEnum.IN_PROGRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(StatusEnum.class)
|
||||||
|
default void cancelRequest_cancelsJob_whenNotFinalState(StatusEnum theState) {
|
||||||
|
// given
|
||||||
|
JobInstance cancelledInstance = createInstance();
|
||||||
|
cancelledInstance.setStatus(theState);
|
||||||
|
String instanceId1 = getSvc().storeNewInstance(cancelledInstance);
|
||||||
|
getSvc().cancelInstance(instanceId1);
|
||||||
|
|
||||||
|
JobInstance normalInstance = createInstance();
|
||||||
|
normalInstance.setStatus(theState);
|
||||||
|
String instanceId2 = getSvc().storeNewInstance(normalInstance);
|
||||||
|
|
||||||
|
JobDefinitionRegistry jobDefinitionRegistry = new JobDefinitionRegistry();
|
||||||
|
jobDefinitionRegistry.addJobDefinitionIfNotRegistered(withJobDefinition());
|
||||||
|
|
||||||
|
// when
|
||||||
|
runInTransaction(()-> {
|
||||||
|
new JobInstanceProcessor(
|
||||||
|
getSvc(),
|
||||||
|
null,
|
||||||
|
instanceId1,
|
||||||
|
new JobChunkProgressAccumulator(),
|
||||||
|
null,
|
||||||
|
jobDefinitionRegistry,
|
||||||
|
getTransactionManager()
|
||||||
|
).process();
|
||||||
|
});
|
||||||
|
|
||||||
|
// then
|
||||||
|
JobInstance freshInstance1 = getSvc().fetchInstance(instanceId1).orElseThrow();
|
||||||
|
if (theState.isCancellable()) {
|
||||||
|
assertEquals(StatusEnum.CANCELLED, freshInstance1.getStatus(), "cancel request processed");
|
||||||
|
assertThat(freshInstance1.getErrorMessage(), containsString("Job instance cancelled"));
|
||||||
|
} else {
|
||||||
|
assertEquals(theState, freshInstance1.getStatus(), "cancel request ignored - state unchanged");
|
||||||
|
assertNull(freshInstance1.getErrorMessage(), "no error message");
|
||||||
|
}
|
||||||
|
JobInstance freshInstance2 = getSvc().fetchInstance(instanceId2).orElseThrow();
|
||||||
|
assertEquals(theState, freshInstance2.getStatus(), "cancel request ignored - cancelled not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
default JobInstance createInstance() {
|
||||||
|
JobInstance instance = new JobInstance();
|
||||||
|
instance.setJobDefinitionId(JOB_DEFINITION_ID);
|
||||||
|
instance.setStatus(StatusEnum.QUEUED);
|
||||||
|
instance.setJobDefinitionVersion(JOB_DEF_VER);
|
||||||
|
instance.setParameters(CHUNK_DATA);
|
||||||
|
instance.setReport("TEST");
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.batch2.api.IJobPersistence;
|
||||||
|
import ca.uhn.fhir.batch2.model.JobDefinition;
|
||||||
|
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||||
|
import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService;
|
||||||
|
import ca.uhn.hapi.fhir.batch2.test.support.TestJobParameters;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
public interface IWorkChunkCommon extends WorkChunkTestConstants {
|
||||||
|
String createAndStoreJobInstance();
|
||||||
|
|
||||||
|
String createAndDequeueWorkChunk(String theJobInstanceId);
|
||||||
|
|
||||||
|
WorkChunk freshFetchWorkChunk(String theChunkId);
|
||||||
|
|
||||||
|
JobInstance createInstance();
|
||||||
|
|
||||||
|
String storeWorkChunk(String theJobDefinitionId, String theTargetStepId, String theInstanceId, int theSequence, String theSerializedData);
|
||||||
|
|
||||||
|
void runInTransaction(Runnable theRunnable);
|
||||||
|
|
||||||
|
public void sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
JobDefinition<TestJobParameters> withJobDefinition();
|
||||||
|
|
||||||
|
TransactionTemplate newTxTemplate();
|
||||||
|
|
||||||
|
JobInstance freshFetchJobInstance(String theInstanceId);
|
||||||
|
|
||||||
|
void runMaintenancePass();
|
||||||
|
|
||||||
|
IHapiTransactionService getTransactionManager();
|
||||||
|
|
||||||
|
IJobPersistence getSvc();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This assumes a creation of JOB_DEFINITION already
|
||||||
|
* @param theJobInstanceId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default String createChunk(String theJobInstanceId) {
|
||||||
|
return storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, theJobInstanceId, 0, CHUNK_DATA);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkErrorEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public interface IWorkChunkErrorActionsTests extends IWorkChunkCommon, WorkChunkTestConstants {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The consumer will retry after a retryable error is thrown
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
default void errorRetry_errorToInProgress() {
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, FIRST_ERROR_MESSAGE));
|
||||||
|
|
||||||
|
// when consumer restarts chunk
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(myChunkId).orElseThrow(IllegalArgumentException::new);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
||||||
|
|
||||||
|
// verify the db state, error message, and error count
|
||||||
|
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, workChunkEntity.getStatus());
|
||||||
|
assertEquals(FIRST_ERROR_MESSAGE, workChunkEntity.getErrorMessage(), "Original error message kept");
|
||||||
|
assertEquals(1, workChunkEntity.getErrorCount(), "error count kept");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void errorRetry_repeatError_increasesErrorCount() {
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, FIRST_ERROR_MESSAGE));
|
||||||
|
|
||||||
|
// setup - the consumer is re-trying, and marks it IN_PROGRESS
|
||||||
|
getSvc().onWorkChunkDequeue(myChunkId);
|
||||||
|
|
||||||
|
|
||||||
|
// when another error happens
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_B));
|
||||||
|
|
||||||
|
|
||||||
|
// verify the state, new message, and error count
|
||||||
|
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.ERRORED, workChunkEntity.getStatus());
|
||||||
|
assertEquals(ERROR_MESSAGE_B, workChunkEntity.getErrorMessage(), "new error message");
|
||||||
|
assertEquals(2, workChunkEntity.getErrorCount(), "error count inc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void errorThenRetryAndComplete_addsErrorCounts() {
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, FIRST_ERROR_MESSAGE));
|
||||||
|
|
||||||
|
// setup - the consumer is re-trying, and marks it IN_PROGRESS
|
||||||
|
getSvc().onWorkChunkDequeue(myChunkId);
|
||||||
|
|
||||||
|
// then it completes ok.
|
||||||
|
getSvc().onWorkChunkCompletion(new WorkChunkCompletionEvent(myChunkId, 3, 1));
|
||||||
|
|
||||||
|
// verify the state, new message, and error count
|
||||||
|
var workChunkEntity = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.COMPLETED, workChunkEntity.getStatus());
|
||||||
|
assertEquals(FIRST_ERROR_MESSAGE, workChunkEntity.getErrorMessage(), "Error message kept.");
|
||||||
|
assertEquals(2, workChunkEntity.getErrorCount(), "error combined with earlier error");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void errorRetry_maxErrors_movesToFailed() {
|
||||||
|
// we start with 1 error already
|
||||||
|
String jobId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createAndDequeueWorkChunk(jobId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, FIRST_ERROR_MESSAGE));
|
||||||
|
|
||||||
|
|
||||||
|
// 2nd try
|
||||||
|
getSvc().onWorkChunkDequeue(myChunkId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_B));
|
||||||
|
var chunk = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.ERRORED, chunk.getStatus());
|
||||||
|
assertEquals(2, chunk.getErrorCount());
|
||||||
|
|
||||||
|
// 3rd try
|
||||||
|
getSvc().onWorkChunkDequeue(myChunkId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_B));
|
||||||
|
chunk = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.ERRORED, chunk.getStatus());
|
||||||
|
assertEquals(3, chunk.getErrorCount());
|
||||||
|
|
||||||
|
// 4th try
|
||||||
|
getSvc().onWorkChunkDequeue(myChunkId);
|
||||||
|
getSvc().onWorkChunkError(new WorkChunkErrorEvent(myChunkId, ERROR_MESSAGE_C));
|
||||||
|
chunk = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.FAILED, chunk.getStatus());
|
||||||
|
assertEquals(4, chunk.getErrorCount());
|
||||||
|
assertThat("Error message contains last error", chunk.getErrorMessage(), containsString(ERROR_MESSAGE_C));
|
||||||
|
assertThat("Error message contains error count and complaint", chunk.getErrorMessage(), containsString("many errors: 4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public interface IWorkChunkStateTransitions extends IWorkChunkCommon, WorkChunkTestConstants {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void chunkCreation_isQueued() {
|
||||||
|
String jobInstanceId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createChunk(jobInstanceId);
|
||||||
|
|
||||||
|
WorkChunk fetchedWorkChunk = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.READY, fetchedWorkChunk.getStatus(), "New chunks are READY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void chunkReceived_queuedToInProgress() {
|
||||||
|
String jobInstanceId = createAndStoreJobInstance();
|
||||||
|
String myChunkId = createChunk(jobInstanceId);
|
||||||
|
|
||||||
|
runMaintenancePass();
|
||||||
|
// the worker has received the chunk, and marks it started.
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(myChunkId).orElseThrow(IllegalArgumentException::new);
|
||||||
|
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
||||||
|
assertEquals(CHUNK_DATA, chunk.getData());
|
||||||
|
|
||||||
|
// verify the db was updated too
|
||||||
|
WorkChunk fetchedWorkChunk = freshFetchWorkChunk(myChunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, fetchedWorkChunk.getStatus());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.batch2.model.JobInstance;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunk;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkCompletionEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkCreateEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkErrorEvent;
|
||||||
|
import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public interface IWorkChunkStorageTests extends IWorkChunkCommon, WorkChunkTestConstants {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void testStoreAndFetchWorkChunk_NoData() {
|
||||||
|
JobInstance instance = createInstance();
|
||||||
|
String instanceId = getSvc().storeNewInstance(instance);
|
||||||
|
|
||||||
|
String id = storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, instanceId, 0, null);
|
||||||
|
runMaintenancePass();
|
||||||
|
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(id).orElseThrow(IllegalArgumentException::new);
|
||||||
|
assertNull(chunk.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void testStoreAndFetchWorkChunk_WithData() {
|
||||||
|
JobInstance instance = createInstance();
|
||||||
|
String instanceId = getSvc().storeNewInstance(instance);
|
||||||
|
|
||||||
|
String id = storeWorkChunk(JOB_DEFINITION_ID, TARGET_STEP_ID, instanceId, 0, CHUNK_DATA);
|
||||||
|
assertNotNull(id);
|
||||||
|
runMaintenancePass();
|
||||||
|
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.QUEUED, freshFetchWorkChunk(id).getStatus()));
|
||||||
|
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(id).orElseThrow(IllegalArgumentException::new);
|
||||||
|
assertEquals(36, chunk.getInstanceId().length());
|
||||||
|
assertEquals(JOB_DEFINITION_ID, chunk.getJobDefinitionId());
|
||||||
|
assertEquals(JOB_DEF_VER, chunk.getJobDefinitionVersion());
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
||||||
|
assertEquals(CHUNK_DATA, chunk.getData());
|
||||||
|
|
||||||
|
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.IN_PROGRESS, freshFetchWorkChunk(id).getStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void testMarkChunkAsCompleted_Success() {
|
||||||
|
JobInstance instance = createInstance();
|
||||||
|
String instanceId = getSvc().storeNewInstance(instance);
|
||||||
|
String chunkId = storeWorkChunk(DEF_CHUNK_ID, TARGET_STEP_ID, instanceId, SEQUENCE_NUMBER, CHUNK_DATA);
|
||||||
|
assertNotNull(chunkId);
|
||||||
|
|
||||||
|
runMaintenancePass();
|
||||||
|
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.QUEUED, freshFetchWorkChunk(chunkId).getStatus()));
|
||||||
|
sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(chunkId).orElseThrow(IllegalArgumentException::new);
|
||||||
|
assertEquals(SEQUENCE_NUMBER, chunk.getSequence());
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
||||||
|
assertNotNull(chunk.getCreateTime());
|
||||||
|
assertNotNull(chunk.getStartTime());
|
||||||
|
assertNull(chunk.getEndTime());
|
||||||
|
assertNull(chunk.getRecordsProcessed());
|
||||||
|
assertNotNull(chunk.getData());
|
||||||
|
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.IN_PROGRESS, freshFetchWorkChunk(chunkId).getStatus()));
|
||||||
|
|
||||||
|
sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
runInTransaction(() -> getSvc().onWorkChunkCompletion(new WorkChunkCompletionEvent(chunkId, 50, 0)));
|
||||||
|
|
||||||
|
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.COMPLETED, entity.getStatus());
|
||||||
|
assertEquals(50, entity.getRecordsProcessed());
|
||||||
|
assertNotNull(entity.getCreateTime());
|
||||||
|
assertNotNull(entity.getStartTime());
|
||||||
|
assertNotNull(entity.getEndTime());
|
||||||
|
assertNull(entity.getData());
|
||||||
|
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
||||||
|
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void testMarkChunkAsCompleted_Error() {
|
||||||
|
JobInstance instance = createInstance();
|
||||||
|
String instanceId = getSvc().storeNewInstance(instance);
|
||||||
|
String chunkId = storeWorkChunk(DEF_CHUNK_ID, TARGET_STEP_ID, instanceId, SEQUENCE_NUMBER, null);
|
||||||
|
assertNotNull(chunkId);
|
||||||
|
|
||||||
|
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.READY, freshFetchWorkChunk(chunkId).getStatus()));
|
||||||
|
runMaintenancePass();
|
||||||
|
sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(chunkId).orElseThrow(IllegalArgumentException::new);
|
||||||
|
assertEquals(SEQUENCE_NUMBER, chunk.getSequence());
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
||||||
|
|
||||||
|
sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
WorkChunkErrorEvent request = new WorkChunkErrorEvent(chunkId, ERROR_MESSAGE_A);
|
||||||
|
getSvc().onWorkChunkError(request);
|
||||||
|
runInTransaction(() -> {
|
||||||
|
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.ERRORED, entity.getStatus());
|
||||||
|
assertEquals(ERROR_MESSAGE_A, entity.getErrorMessage());
|
||||||
|
assertNotNull(entity.getCreateTime());
|
||||||
|
assertNotNull(entity.getStartTime());
|
||||||
|
assertNotNull(entity.getEndTime());
|
||||||
|
assertEquals(1, entity.getErrorCount());
|
||||||
|
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
||||||
|
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark errored again
|
||||||
|
|
||||||
|
WorkChunkErrorEvent request2 = new WorkChunkErrorEvent(chunkId, "This is an error message 2");
|
||||||
|
getSvc().onWorkChunkError(request2);
|
||||||
|
runInTransaction(() -> {
|
||||||
|
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.ERRORED, entity.getStatus());
|
||||||
|
assertEquals("This is an error message 2", entity.getErrorMessage());
|
||||||
|
assertNotNull(entity.getCreateTime());
|
||||||
|
assertNotNull(entity.getStartTime());
|
||||||
|
assertNotNull(entity.getEndTime());
|
||||||
|
assertEquals(2, entity.getErrorCount());
|
||||||
|
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
||||||
|
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
List<WorkChunk> chunks = ImmutableList.copyOf(getSvc().fetchAllWorkChunksIterator(instanceId, true));
|
||||||
|
assertEquals(1, chunks.size());
|
||||||
|
assertEquals(2, chunks.get(0).getErrorCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void testMarkChunkAsCompleted_Fail() {
|
||||||
|
JobInstance instance = createInstance();
|
||||||
|
String instanceId = getSvc().storeNewInstance(instance);
|
||||||
|
String chunkId = storeWorkChunk(DEF_CHUNK_ID, TARGET_STEP_ID, instanceId, SEQUENCE_NUMBER, null);
|
||||||
|
assertNotNull(chunkId);
|
||||||
|
|
||||||
|
runInTransaction(() -> assertEquals(WorkChunkStatusEnum.READY, freshFetchWorkChunk(chunkId).getStatus()));
|
||||||
|
runMaintenancePass();
|
||||||
|
sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
WorkChunk chunk = getSvc().onWorkChunkDequeue(chunkId).orElseThrow(IllegalArgumentException::new);
|
||||||
|
assertEquals(SEQUENCE_NUMBER, chunk.getSequence());
|
||||||
|
assertEquals(WorkChunkStatusEnum.IN_PROGRESS, chunk.getStatus());
|
||||||
|
|
||||||
|
sleepUntilTimeChanges();
|
||||||
|
|
||||||
|
getSvc().onWorkChunkFailed(chunkId, "This is an error message");
|
||||||
|
runInTransaction(() -> {
|
||||||
|
WorkChunk entity = freshFetchWorkChunk(chunkId);
|
||||||
|
assertEquals(WorkChunkStatusEnum.FAILED, entity.getStatus());
|
||||||
|
assertEquals("This is an error message", entity.getErrorMessage());
|
||||||
|
assertNotNull(entity.getCreateTime());
|
||||||
|
assertNotNull(entity.getStartTime());
|
||||||
|
assertNotNull(entity.getEndTime());
|
||||||
|
assertTrue(entity.getCreateTime().getTime() < entity.getStartTime().getTime());
|
||||||
|
assertTrue(entity.getStartTime().getTime() < entity.getEndTime().getTime());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
default void markWorkChunksWithStatusAndWipeData_marksMultipleChunksWithStatus_asExpected() {
|
||||||
|
JobInstance instance = createInstance();
|
||||||
|
String instanceId = getSvc().storeNewInstance(instance);
|
||||||
|
ArrayList<String> chunkIds = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
WorkChunkCreateEvent chunk = new WorkChunkCreateEvent(
|
||||||
|
"defId",
|
||||||
|
1,
|
||||||
|
"stepId",
|
||||||
|
instanceId,
|
||||||
|
0,
|
||||||
|
"{}"
|
||||||
|
);
|
||||||
|
String id = getSvc().onWorkChunkCreate(chunk);
|
||||||
|
chunkIds.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
runInTransaction(() -> getSvc().markWorkChunksWithStatusAndWipeData(instance.getInstanceId(), chunkIds, WorkChunkStatusEnum.COMPLETED, null));
|
||||||
|
|
||||||
|
Iterator<WorkChunk> reducedChunks = getSvc().fetchAllWorkChunksIterator(instanceId, true);
|
||||||
|
|
||||||
|
while (reducedChunks.hasNext()) {
|
||||||
|
WorkChunk reducedChunk = reducedChunks.next();
|
||||||
|
assertTrue(chunkIds.contains(reducedChunk.getId()));
|
||||||
|
assertEquals(WorkChunkStatusEnum.COMPLETED, reducedChunk.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package ca.uhn.hapi.fhir.batch2.test;
|
||||||
|
|
||||||
|
public interface WorkChunkTestConstants {
|
||||||
|
public static final String JOB_DEFINITION_ID = "definition-id";
|
||||||
|
public static final String TARGET_STEP_ID = "step-id";
|
||||||
|
public static final String DEF_CHUNK_ID = "definition-chunkId";
|
||||||
|
// public static final String STEP_CHUNK_ID = "step-chunkId";
|
||||||
|
public static final int JOB_DEF_VER = 1;
|
||||||
|
public static final int SEQUENCE_NUMBER = 1;
|
||||||
|
public static final String CHUNK_DATA = "{\"key\":\"value\"}";
|
||||||
|
public static final String ERROR_MESSAGE_A = "This is an error message: A";
|
||||||
|
public static final String ERROR_MESSAGE_B = "This is a different error message: B";
|
||||||
|
public static final String ERROR_MESSAGE_C = "This is a different error message: C";
|
||||||
|
|
||||||
|
final String FIRST_ERROR_MESSAGE = ERROR_MESSAGE_A;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user