diff --git a/docs/pom.xml b/docs/pom.xml index e8bc8c007..b78c31e86 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -28,7 +28,7 @@ com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index efbefa446..5988705f4 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT pom Worker Framework @@ -273,52 +273,52 @@ com.github.workerframework standard-worker-container pom - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework util-rabbitmq - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-api - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-caf - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-configs - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-core - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-default-configs - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-queue-rabbit - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-store-fs - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-tracking-report - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.google.code.findbugs diff --git a/release-notes-10.0.0.md b/release-notes-10.0.0.md new file mode 100644 index 000000000..73dbc2f0f --- /dev/null +++ b/release-notes-10.0.0.md @@ -0,0 +1,14 @@ +#### Version Number +${version-number} + +#### Breaking Changes +- US1009117: Remove reliance on large messages being supported by the underlying queue provider. + - The TaskCallback interface has been updated to expect a TaskMessage in place of a byte array. + - The WorkerQueueProvider interface has been updated to expect a ManagedDataStore for storing large messages + and a Codec for serialization/deserialization of messages prior to storage/retrieval from the datastore. + +#### New Features +- None + +#### Known Issues +- None diff --git a/release-notes-9.1.2.md b/release-notes-9.1.2.md deleted file mode 100644 index b36546391..000000000 --- a/release-notes-9.1.2.md +++ /dev/null @@ -1,8 +0,0 @@ -!not-ready-for-release! - -#### Version Number -${version-number} - -#### New Features - -#### Known Issues diff --git a/standard-worker-container/pom.xml b/standard-worker-container/pom.xml index 2c00ee9bf..f57b48678 100644 --- a/standard-worker-container/pom.xml +++ b/standard-worker-container/pom.xml @@ -28,7 +28,7 @@ com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/util-rabbitmq/pom.xml b/util-rabbitmq/pom.xml index 77944e46f..e85f45f93 100644 --- a/util-rabbitmq/pom.xml +++ b/util-rabbitmq/pom.xml @@ -22,12 +22,12 @@ 4.0.0 util-rabbitmq - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/util-rabbitmq/src/main/java/com/github/workerframework/util/rabbitmq/RabbitHeaders.java b/util-rabbitmq/src/main/java/com/github/workerframework/util/rabbitmq/RabbitHeaders.java index 061e958b5..20b3021b7 100644 --- a/util-rabbitmq/src/main/java/com/github/workerframework/util/rabbitmq/RabbitHeaders.java +++ b/util-rabbitmq/src/main/java/com/github/workerframework/util/rabbitmq/RabbitHeaders.java @@ -21,6 +21,8 @@ public class RabbitHeaders { public static final String RABBIT_HEADER_CAF_WORKER_REJECTED = "x-caf-worker-rejected"; + public static final String RABBIT_HEADER_CAF_WORKER_INVALID = "x-caf-worker-invalid"; public static final String RABBIT_HEADER_CAF_WORKER_RETRY = "x-caf-worker-retry"; public static final String RABBIT_HEADER_CAF_DELIVERY_COUNT = "x-delivery-count"; + public static final String RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF = "x-caf-payload-offloading-storage-ref"; } diff --git a/worker-api/pom.xml b/worker-api/pom.xml index 2dd4a8e34..0997cec40 100644 --- a/worker-api/pom.xml +++ b/worker-api/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-api - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-api/src/main/java/com/github/workerframework/api/TaskCallback.java b/worker-api/src/main/java/com/github/workerframework/api/TaskCallback.java index e463e2be1..20bd311de 100644 --- a/worker-api/src/main/java/com/github/workerframework/api/TaskCallback.java +++ b/worker-api/src/main/java/com/github/workerframework/api/TaskCallback.java @@ -27,12 +27,12 @@ public interface TaskCallback * Announce to the worker core that a new task has been picked off the queue for processing. * * @param taskInformation contains an arbitrary task reference - * @param taskData the task data that is specific to the workers hosted + * @param taskMessage the task data that is specific to the workers hosted * @param headers the map of key/value paired headers on the message * @throws TaskRejectedException if the worker framework rejected execution of the task at this time * @throws InvalidTaskException if the worker framework indicates this task is invalid and cannot possibly be executed */ - void registerNewTask(TaskInformation taskInformation, byte[] taskData, Map headers) + void registerNewTask(TaskInformation taskInformation, TaskMessage taskMessage, Map headers) throws TaskRejectedException, InvalidTaskException; /** diff --git a/worker-api/src/main/java/com/github/workerframework/api/TaskMessage.java b/worker-api/src/main/java/com/github/workerframework/api/TaskMessage.java index e08fc22c3..1ed080162 100644 --- a/worker-api/src/main/java/com/github/workerframework/api/TaskMessage.java +++ b/worker-api/src/main/java/com/github/workerframework/api/TaskMessage.java @@ -53,7 +53,6 @@ public final class TaskMessage /** * The serialised data of the task-specific message. */ - @NotNull private byte[] taskData; /** @@ -129,7 +128,7 @@ public TaskMessage(final String taskId, final String taskClassifier, final int t this.taskId = Objects.requireNonNull(taskId); this.taskClassifier = Objects.requireNonNull(taskClassifier); this.taskApiVersion = Objects.requireNonNull(taskApiVersion); - this.taskData = Objects.requireNonNull(taskData); + this.taskData = taskData; this.taskStatus = Objects.requireNonNull(taskStatus); this.context = Objects.requireNonNull(context); this.to = to; diff --git a/worker-api/src/main/java/com/github/workerframework/api/TaskRejectedException.java b/worker-api/src/main/java/com/github/workerframework/api/TaskRejectedException.java index fb628064e..b3894f93d 100644 --- a/worker-api/src/main/java/com/github/workerframework/api/TaskRejectedException.java +++ b/worker-api/src/main/java/com/github/workerframework/api/TaskRejectedException.java @@ -17,6 +17,8 @@ /** * Indicates that a task cannot be accepted right now, but that it should be retried at a later time. + * This exception does not result in the message being placed on the reject queue. + * The reject queue is only used for tasks that cannot be processed at all and have been identified as poisonous. */ public class TaskRejectedException extends WorkerException { diff --git a/worker-api/src/main/java/com/github/workerframework/api/WorkerQueue.java b/worker-api/src/main/java/com/github/workerframework/api/WorkerQueue.java index 68264e8d8..8ec7f350c 100644 --- a/worker-api/src/main/java/com/github/workerframework/api/WorkerQueue.java +++ b/worker-api/src/main/java/com/github/workerframework/api/WorkerQueue.java @@ -32,7 +32,7 @@ public interface WorkerQueue * @param isLastMessage the boolean to indicate if current message is final message for the task * @throws QueueException if the message cannot be submitted */ - void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQueue, Map headers, + void publish(TaskInformation taskInformation, TaskMessage taskMessage, String targetQueue, Map headers, boolean isLastMessage) throws QueueException; /** @@ -45,7 +45,7 @@ void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQ * @throws QueueException if the message cannot be submitted */ - void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQueue, Map headers) + void publish(TaskInformation taskInformation, TaskMessage taskMessage, String targetQueue, Map headers) throws QueueException; /** diff --git a/worker-api/src/main/java/com/github/workerframework/api/WorkerQueueProvider.java b/worker-api/src/main/java/com/github/workerframework/api/WorkerQueueProvider.java index 3e430fcdc..a377af01f 100644 --- a/worker-api/src/main/java/com/github/workerframework/api/WorkerQueueProvider.java +++ b/worker-api/src/main/java/com/github/workerframework/api/WorkerQueueProvider.java @@ -15,6 +15,7 @@ */ package com.github.workerframework.api; +import com.github.cafapi.common.api.Codec; import com.github.cafapi.common.api.ConfigurationSource; /** @@ -26,10 +27,15 @@ public interface WorkerQueueProvider * Create a new WorkerQueue instance. * * @param configurationSource used for configuring the WorkerQueue - * @param maxTasks the maximum number of tasks the worker can perform at once + * @param maxTasks the maximum number of tasks the worker can perform at once + * @param invalidQueue the queue in which to place tasks that are invalid or cannot be processed + * @param dataStore the managed data store that the worker will use to store data that exceeds a threshold. + * @param codec the codec used for serialization deserialization of data. + * @param workerConfiguration * @return a new WorkerQueue instance * @throws QueueException if a WorkerQueue could not be created */ - ManagedWorkerQueue getWorkerQueue(ConfigurationSource configurationSource, int maxTasks) + ManagedWorkerQueue getWorkerQueue(ConfigurationSource configurationSource, int maxTasks, String invalidQueue, + ManagedDataStore dataStore, Codec codec, WorkerConfiguration workerConfiguration) throws QueueException; } diff --git a/worker-caf/pom.xml b/worker-caf/pom.xml index 63f2253c4..14624db44 100644 --- a/worker-caf/pom.xml +++ b/worker-caf/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-caf - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-configs/pom.xml b/worker-configs/pom.xml index cdab3e711..c2db02d0c 100644 --- a/worker-configs/pom.xml +++ b/worker-configs/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-configs - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-core/pom.xml b/worker-core/pom.xml index 0903cb8e2..1d497bcfb 100644 --- a/worker-core/pom.xml +++ b/worker-core/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-core - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-core/readme.md b/worker-core/readme.md index 6b7d3e71f..d1bedb1f1 100644 --- a/worker-core/readme.md +++ b/worker-core/readme.md @@ -266,7 +266,6 @@ the current input queue. Default is True. indicated as requeued by the WorkerQueue - core.currentIdleTime: the time in milliseconds since the worker was doing anything useful. - - core.inputSizes: histogram of input (task) message sizes in bytes - core.outputSize: histogram of output (result) messages sizes in bytes - config.lookups: the number of configuration lookups performed by the ConfigurationSource. diff --git a/worker-core/src/main/java/com/github/workerframework/core/WorkerApplication.java b/worker-core/src/main/java/com/github/workerframework/core/WorkerApplication.java index e2f836c2a..774b720fb 100644 --- a/worker-core/src/main/java/com/github/workerframework/core/WorkerApplication.java +++ b/worker-core/src/main/java/com/github/workerframework/core/WorkerApplication.java @@ -125,7 +125,8 @@ public void run(final WorkerConfiguration workerConfiguration, final Environment WorkerFactory workerFactory = workerProvider.getWorkerFactory(config, store, codec); WorkerThreadPool wtp = WorkerThreadPool.create(workerFactory); final int nThreads = workerFactory.getWorkerThreads(); - ManagedWorkerQueue workerQueue = queueProvider.getWorkerQueue(config, nThreads); + ManagedWorkerQueue workerQueue = queueProvider.getWorkerQueue(config, nThreads, workerFactory.getInvalidTaskQueue(), store, codec, + workerFactory.getWorkerConfiguration()); TransientHealthCheck transientHealthCheck = new TransientHealthCheck(); WorkerCore core = new WorkerCore(codec, wtp, workerQueue, workerFactory, path, environment.healthChecks(), transientHealthCheck); HealthConfiguration healthConfiguration = config.getConfiguration(HealthConfiguration.class); diff --git a/worker-core/src/main/java/com/github/workerframework/core/WorkerCore.java b/worker-core/src/main/java/com/github/workerframework/core/WorkerCore.java index 3d67c2e6d..f379e2b21 100644 --- a/worker-core/src/main/java/com/github/workerframework/core/WorkerCore.java +++ b/worker-core/src/main/java/com/github/workerframework/core/WorkerCore.java @@ -16,8 +16,6 @@ package com.github.workerframework.core; import com.github.cafapi.common.api.Codec; -import com.github.cafapi.common.api.CodecException; -import com.github.cafapi.common.api.DecodeMethod; import com.github.cafapi.common.util.naming.ServicePath; import com.github.workerframework.api.InvalidJobTaskIdException; import com.github.workerframework.api.InvalidTaskException; @@ -64,7 +62,7 @@ final class WorkerCore public WorkerCore(final Codec codec, final WorkerThreadPool pool, final ManagedWorkerQueue queue, final WorkerFactory factory, final ServicePath path, final HealthCheckRegistry healthCheckRegistry, final TransientHealthCheck transientHealthCheck) { - WorkerCallback taskCallback = new CoreWorkerCallback(codec, queue, stats, healthCheckRegistry, transientHealthCheck); + WorkerCallback taskCallback = new CoreWorkerCallback(queue, stats, healthCheckRegistry, transientHealthCheck); this.threadPool = Objects.requireNonNull(pool); this.callback = new CoreTaskCallback(codec, stats, new WorkerExecutor(path, taskCallback, factory, pool), pool, queue); this.workerQueue = Objects.requireNonNull(queue); @@ -151,12 +149,14 @@ public CoreTaskCallback(final Codec codec, final WorkerStats stats, final Worker * Use the factory to get a new worker to handle the task, wrap this in a handler and hand it off to the thread pool. */ @Override - public void registerNewTask(final TaskInformation taskInformation, final byte[] taskMessage, Map headers) + public void registerNewTask(final TaskInformation taskInformation, final TaskMessage taskMessage, Map headers) throws InvalidTaskException, TaskRejectedException { Objects.requireNonNull(taskInformation); stats.incrementTasksReceived(); - stats.getInputSizes().update(taskMessage.length); + if(taskMessage.getTaskData() != null) { + stats.getInputSizes().update(taskMessage.getTaskData().length); + } try { registerNewTaskImpl(taskInformation, taskMessage, headers); @@ -166,12 +166,10 @@ public void registerNewTask(final TaskInformation taskInformation, final byte[] } } - private void registerNewTaskImpl(final TaskInformation taskInformation, final byte[] taskMessage, Map headers) + private void registerNewTaskImpl(final TaskInformation taskInformation, final TaskMessage tm, Map headers) throws InvalidTaskException, TaskRejectedException { try { - final TaskMessage tm = codec.deserialise(taskMessage, TaskMessage.class, DecodeMethod.LENIENT); - LOG.debug("Received task {} (message id: {})", tm.getTaskId(), taskInformation.getInboundMessageId()); validateTaskMessage(tm); final JobStatus jobStatus; @@ -219,9 +217,7 @@ private void registerNewTaskImpl(final TaskInformation taskInformation, final by taskInformation.getInboundMessageId()); executor.discardTask(tm, taskInformation); } - } catch (CodecException e) { - throw new InvalidTaskException("Queue data did not deserialise to a TaskMessage", e); - } catch (InvalidJobTaskIdException ijte) { + } catch (final InvalidJobTaskIdException ijte) { throw new InvalidTaskException("TaskMessage contains an invalid job task identifier", ijte); } } @@ -432,15 +428,13 @@ public void setStatusCheckIntervalMillis(long statusCheckIntervalMillis) */ private static class CoreWorkerCallback implements WorkerCallback { - private final Codec codec; private final ManagedWorkerQueue workerQueue; private final WorkerStats stats; private final HealthCheckRegistry healthCheckRegistry; private final TransientHealthCheck transientHealthCheck; - public CoreWorkerCallback(final Codec codec, final ManagedWorkerQueue workerQueue, final WorkerStats stats, final HealthCheckRegistry healthCheckRegistry, final TransientHealthCheck transientHealthCheck) + public CoreWorkerCallback(final ManagedWorkerQueue workerQueue, final WorkerStats stats, final HealthCheckRegistry healthCheckRegistry, final TransientHealthCheck transientHealthCheck) { - this.codec = Objects.requireNonNull(codec); this.workerQueue = Objects.requireNonNull(workerQueue); this.stats = Objects.requireNonNull(stats); this.healthCheckRegistry = Objects.requireNonNull(healthCheckRegistry); @@ -457,15 +451,8 @@ public void send(final TaskInformation taskInformation, final TaskMessage respon final String queue = responseMessage.getTo(); checkForTrackingTermination(taskInformation, queue, responseMessage); - final byte[] output; - try { - output = codec.serialise(responseMessage); - } catch (final CodecException ex) { - throw new RuntimeException(ex); - } - try { - workerQueue.publish(taskInformation, output, queue, Collections.emptyMap()); + workerQueue.publish(taskInformation, responseMessage, queue, Collections.emptyMap()); } catch (final QueueException ex) { throw new RuntimeException(ex); } @@ -502,9 +489,10 @@ public void complete(final TaskInformation taskInformation, final String queue, } else { // **** Normal Worker **** // A worker with an input and output queue. - final byte[] output = codec.serialise(responseMessage); - workerQueue.publish(taskInformation, output, queue, Collections.emptyMap(), true); - stats.getOutputSizes().update(output.length); + workerQueue.publish(taskInformation, responseMessage, queue, Collections.emptyMap(), true); + if(responseMessage.getTaskData() != null) { + stats.getOutputSizes().update(responseMessage.getTaskData().length); + } } stats.updatedLastTaskFinishedTime(); if (TaskStatus.isSuccessfulResponse(responseMessage.getTaskStatus())) { @@ -512,7 +500,7 @@ public void complete(final TaskInformation taskInformation, final String queue, } else { stats.incrementTasksFailed(); } - } catch (CodecException | QueueException e) { + } catch (final QueueException e) { LOG.error("Cannot publish data for task {}, rejecting", responseMessage.getTaskId(), e); abandon(taskInformation, e); } @@ -543,13 +531,12 @@ public void forward(TaskInformation taskInformation, String queue, TaskMessage f workerQueue.acknowledgeTask(taskInformation); } else { // Else forward the task - final byte[] output = codec.serialise(forwardedMessage); - workerQueue.publish(taskInformation, output, queue, headers, true); + workerQueue.publish(taskInformation, forwardedMessage, queue, headers, true); stats.incrementTasksForwarded(); //TODO - I'm guessing this stat should not be updated for forwarded messages: // stats.getOutputSizes().update(output.length); } - } catch (CodecException | QueueException e) { + } catch (final QueueException e) { LOG.error("Cannot publish data for forwarded task {}, rejecting", forwardedMessage.getTaskId(), e); abandon(taskInformation, e); } @@ -565,10 +552,9 @@ public void pause(final TaskInformation taskInformation, final String pausedQueu LOG.debug("Task {} (message id: {}) being forwarded to paused queue {}", taskMessage.getTaskId(), taskInformation.getInboundMessageId(), pausedQueue); try { - final byte[] taskMessageBytes = codec.serialise(taskMessage); - workerQueue.publish(taskInformation, taskMessageBytes, pausedQueue, headers, true); + workerQueue.publish(taskInformation, taskMessage, pausedQueue, headers, true); stats.incrementTasksPaused(); - } catch (final CodecException | QueueException e) { + } catch (final QueueException e) { LOG.error("Cannot publish data for task: {} to paused queue: {}, rejecting", taskMessage.getTaskId(), pausedQueue, e); abandon(taskInformation, e); } @@ -589,16 +575,8 @@ public void reportUpdate(final TaskInformation taskInformation, final TaskMessag Objects.requireNonNull(taskInformation); Objects.requireNonNull(reportUpdateMessage); LOG.debug("Sending report updates to queue {})", reportUpdateMessage.getTo()); - - final byte[] output; - try { - output = codec.serialise(reportUpdateMessage); - } catch (final CodecException ex) { - throw new RuntimeException(ex); - } - try { - workerQueue.publish(taskInformation, output, reportUpdateMessage.getTo(), Collections.emptyMap()); + workerQueue.publish(taskInformation, reportUpdateMessage, reportUpdateMessage.getTo(), Collections.emptyMap()); } catch (final QueueException ex) { throw new RuntimeException(ex); } diff --git a/worker-core/src/test/java/com/github/workerframework/core/WorkerCoreTest.java b/worker-core/src/test/java/com/github/workerframework/core/WorkerCoreTest.java index 1f66f6666..55b0bcb0b 100644 --- a/worker-core/src/test/java/com/github/workerframework/core/WorkerCoreTest.java +++ b/worker-core/src/test/java/com/github/workerframework/core/WorkerCoreTest.java @@ -24,6 +24,7 @@ import com.github.cafapi.common.codecs.json.JsonCodec; import com.github.cafapi.common.util.naming.ServicePath; import com.github.workerframework.api.InvalidTaskException; +import com.github.workerframework.api.ManagedDataStore; import com.github.workerframework.api.ManagedWorkerQueue; import com.github.workerframework.api.QueueException; import com.github.workerframework.api.TaskCallback; @@ -32,6 +33,7 @@ import com.github.workerframework.api.TaskStatus; import com.github.workerframework.api.TrackingInfo; import com.github.workerframework.api.Worker; +import com.github.workerframework.api.WorkerConfiguration; import com.github.workerframework.api.WorkerException; import com.github.workerframework.api.WorkerFactory; import com.github.workerframework.api.WorkerQueueMetricsReporter; @@ -70,6 +72,7 @@ public class WorkerCoreTest private static final String SUCCESS = "success"; private static final String FAILURE = "failure"; + private static final String INVALID = "invalid"; private static final String WORKER_NAME = "testWorker"; private static final int WORKER_API_VER = 1; private static final String QUEUE_IN = "inQueue"; @@ -90,7 +93,7 @@ private void before() { public void testWorkerCore() throws CodecException, InterruptedException, WorkerException, ConfigurationException, QueueException, InvalidNameException { - BlockingQueue q = new LinkedBlockingQueue<>(); + BlockingQueue q = new LinkedBlockingQueue<>(); Codec codec = new JsonCodec(); WorkerThreadPool wtp = WorkerThreadPool.create(5); ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -104,14 +107,11 @@ public void testWorkerCore() core.start(); // at this point, the queue should hand off the task to the app, the app should get a worker from the mocked WorkerFactory, // and the Worker itself is a mock wrapped in a WorkerWrapper, which should return success and the appropriate result data - byte[] stuff = codec.serialise(getTaskMessage(task, codec, WORKER_NAME)); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, getTaskMessage(task, codec, WORKER_NAME)); // the worker's task result should eventually be passed back to our dummy WorkerQueue and onto our blocking queue - byte[] result = q.poll(5000, TimeUnit.MILLISECONDS); + TaskMessage taskMessage = q.poll(5000, TimeUnit.MILLISECONDS); // if the result didn't get back to us, then result will be null - Assert.assertNotNull(result); - // deserialise and verify result data - TaskMessage taskMessage = codec.deserialise(result, TaskMessage.class); + Assert.assertNotNull(taskMessage); Assert.assertEquals(TaskStatus.RESULT_SUCCESS, taskMessage.getTaskStatus()); Assert.assertEquals(WORKER_NAME, taskMessage.getTaskClassifier()); Assert.assertEquals(WORKER_API_VER, taskMessage.getTaskApiVersion()); @@ -128,7 +128,7 @@ public void testWorkerCore() public void testWorkerCoreWithTracking() throws CodecException, InterruptedException, WorkerException, ConfigurationException, QueueException, InvalidNameException { - final BlockingQueue q = new LinkedBlockingQueue<>(); + final BlockingQueue q = new LinkedBlockingQueue<>(); final Codec codec = new JsonCodec(); final WorkerThreadPool wtp = WorkerThreadPool.create(5); final ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -143,17 +143,15 @@ public void testWorkerCoreWithTracking() // at this point, the queue should hand off the task to the app, the app should get a worker from the mocked WorkerFactory, // and the Worker itself is a mock wrapped in a WorkerWrapper, which should return success and the appropriate result data final TrackingInfo tracking = new TrackingInfo("J23.1.2", new Date(), 0, "http://thehost:1234/job-service/v1/jobs/23/status", "trackingQueue", "trackTo"); - final byte[] stuff = codec.serialise(getTaskMessage(task, codec, WORKER_NAME, tracking)); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, getTaskMessage(task, codec, WORKER_NAME, tracking)); // Two results expected back. One for the report progress update and another for the message completion. // // Verify result for the report update call. - final byte[] rutResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage rutTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); // if the result didn't get back to us, then rutResult will be null - Assert.assertNotNull(rutResult); - // deserialise and verify rutResult data - final TaskMessage rutTaskMessage = codec.deserialise(rutResult, TaskMessage.class); + Assert.assertNotNull(rutTaskMessage); + // verify rutResult data Assert.assertEquals(TaskStatus.NEW_TASK, rutTaskMessage.getTaskStatus()); Assert.assertEquals(TrackingReportConstants.TRACKING_REPORT_TASK_NAME, rutTaskMessage.getTaskClassifier()); Assert.assertEquals(TrackingReportConstants.TRACKING_REPORT_TASK_API_VER, rutTaskMessage.getTaskApiVersion()); @@ -161,11 +159,10 @@ public void testWorkerCoreWithTracking() Assert.assertEquals("J23.1.2", rutWorkerResult.trackingReports.get(0).jobTaskId); Assert.assertEquals(TrackingReportStatus.Progress, rutWorkerResult.trackingReports.get(0).status); // Verify result for message completion. - final byte[] msgCompletionResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage msgCompletionTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); // if the result didn't get back to us, then result will be null - Assert.assertNotNull(msgCompletionResult); - // deserialise and verify msgCompletionResult data - final TaskMessage msgCompletionTaskMessage = codec.deserialise(msgCompletionResult, TaskMessage.class); + Assert.assertNotNull(msgCompletionTaskMessage); + // verify msgCompletionResult data Assert.assertEquals(TaskStatus.RESULT_SUCCESS, msgCompletionTaskMessage.getTaskStatus()); Assert.assertEquals(WORKER_NAME, msgCompletionTaskMessage.getTaskClassifier()); Assert.assertEquals(WORKER_API_VER, msgCompletionTaskMessage.getTaskApiVersion()); @@ -182,7 +179,7 @@ public void testWorkerCoreWithTracking() public void testInvalidWrapper() throws InvalidNameException, WorkerException, QueueException, CodecException { - BlockingQueue q = new LinkedBlockingQueue<>(); + BlockingQueue q = new LinkedBlockingQueue<>(); Codec codec = new JsonCodec(); WorkerThreadPool wtp = WorkerThreadPool.create(5); ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -194,8 +191,7 @@ public void testInvalidWrapper() WorkerCore core = new WorkerCore(codec, wtp, queue, getWorkerFactory(task, codec), path, healthCheckRegistry, transientHealthCheck); core.start(); - byte[] stuff = codec.serialise("nonsense"); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, new TaskMessage()); } /** @@ -205,7 +201,7 @@ public void testInvalidWrapper() public void testInvalidTask() throws QueueException, InvalidNameException, WorkerException, CodecException, InterruptedException { - BlockingQueue q = new LinkedBlockingQueue<>(); + BlockingQueue q = new LinkedBlockingQueue<>(); Codec codec = new JsonCodec(); WorkerThreadPool wtp = WorkerThreadPool.create(5); ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -224,11 +220,9 @@ public void testInvalidTask() byte[] testContextData = testContext.getBytes(StandardCharsets.UTF_8); context.put(testContext, testContextData); tm.setContext(context); - byte[] stuff = codec.serialise(tm); - queue.submitTask(taskInformation, stuff); - byte[] result = q.poll(5000, TimeUnit.MILLISECONDS); - Assert.assertNotNull(result); - TaskMessage taskMessage = codec.deserialise(result, TaskMessage.class); + queue.submitTask(taskInformation, tm); + TaskMessage taskMessage = q.poll(5000, TimeUnit.MILLISECONDS); + Assert.assertNotNull(taskMessage); Assert.assertEquals(TaskStatus.INVALID_TASK, taskMessage.getTaskStatus()); Assert.assertEquals(WORKER_NAME, taskMessage.getTaskClassifier()); Assert.assertEquals(WORKER_API_VER, taskMessage.getTaskApiVersion()); @@ -245,7 +239,7 @@ public void testInvalidTask() public void testInvalidTaskWithTracking() throws QueueException, InvalidNameException, WorkerException, CodecException, InterruptedException { - final BlockingQueue q = new LinkedBlockingQueue<>(); + final BlockingQueue q = new LinkedBlockingQueue<>(); final Codec codec = new JsonCodec(); final WorkerThreadPool wtp = WorkerThreadPool.create(5); final ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -266,17 +260,15 @@ public void testInvalidTaskWithTracking() final byte[] testContextData = testContext.getBytes(StandardCharsets.UTF_8); context.put(testContext, testContextData); tm.setContext(context); - final byte[] stuff = codec.serialise(tm); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, tm); // Two results expected back. One for the report progress update and another for the message completion. // // Verify result for the report update call. - final byte[] rutResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage rutTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); // if the result didn't get back to us, then rutResult will be null - Assert.assertNotNull(rutResult); - // deserialise and verify rutResult data - final TaskMessage rutTaskMessage = codec.deserialise(rutResult, TaskMessage.class); + Assert.assertNotNull(rutTaskMessage); + // verify rutResult data Assert.assertEquals(TaskStatus.NEW_TASK, rutTaskMessage.getTaskStatus()); Assert.assertEquals(TrackingReportConstants.TRACKING_REPORT_TASK_NAME, rutTaskMessage.getTaskClassifier()); Assert.assertEquals(TrackingReportConstants.TRACKING_REPORT_TASK_API_VER, rutTaskMessage.getTaskApiVersion()); @@ -286,11 +278,10 @@ public void testInvalidTaskWithTracking() Assert.assertEquals(TaskStatus.INVALID_TASK.name(), rutWorkerResult.trackingReports.get(0).failure.failureId); Assert.assertEquals(WORKER_NAME, rutWorkerResult.trackingReports.get(0).failure.failureSource); // Verify result for message completion. - final byte[] msgCompletionResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage msgCompletionTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); // if the result didn't get back to us, then result will be null - Assert.assertNotNull(msgCompletionResult); - // deserialise and verify msgCompletionResult data - final TaskMessage msgCompletionTaskMessage = codec.deserialise(msgCompletionResult, TaskMessage.class); + Assert.assertNotNull(msgCompletionTaskMessage); + // verify msgCompletionResult data Assert.assertEquals(TaskStatus.INVALID_TASK, msgCompletionTaskMessage.getTaskStatus()); Assert.assertEquals(WORKER_NAME, msgCompletionTaskMessage.getTaskClassifier()); Assert.assertEquals(WORKER_API_VER, msgCompletionTaskMessage.getTaskApiVersion()); @@ -307,7 +298,7 @@ public void testInvalidTaskWithTracking() public void testAbortTasks() throws CodecException, InterruptedException, WorkerException, ConfigurationException, QueueException, InvalidNameException { - BlockingQueue q = new LinkedBlockingQueue<>(); + BlockingQueue q = new LinkedBlockingQueue<>(); Codec codec = new JsonCodec(); WorkerThreadPool wtp = WorkerThreadPool.create(2); ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -320,12 +311,9 @@ public void testAbortTasks() WorkerCore core = new WorkerCore(codec, wtp, queue, getSlowWorkerFactory(latch, task, codec), path, healthCheckRegistry, transientHealthCheck); core.start(); - byte[] task1 = codec.serialise(getTaskMessage(task, codec, UUID.randomUUID().toString())); - byte[] task2 = codec.serialise(getTaskMessage(task, codec, UUID.randomUUID().toString())); - byte[] task3 = codec.serialise(getTaskMessage(task, codec, UUID.randomUUID().toString())); - queue.submitTask(getMockTaskInformation("task1"), task1); - queue.submitTask(getMockTaskInformation("task2"), task2); - queue.submitTask(getMockTaskInformation("task3"), task3); // there are only 2 threads, so this task should not even start + queue.submitTask(getMockTaskInformation("task1"), getTaskMessage(task, codec, UUID.randomUUID().toString())); + queue.submitTask(getMockTaskInformation("task2"), getTaskMessage(task, codec, UUID.randomUUID().toString())); + queue.submitTask(getMockTaskInformation("task3"), getTaskMessage(task, codec, UUID.randomUUID().toString())); // there are only 2 threads, so this task should not even start Thread.sleep(500); // give the test a little breathing room queue.triggerAbort(); latch.await(1, TimeUnit.SECONDS); @@ -341,7 +329,7 @@ public void testAbortTasks() public void testInterupptedTask() throws CodecException, WorkerException, ConfigurationException, QueueException, InvalidNameException { - BlockingQueue q = new LinkedBlockingQueue<>(); + BlockingQueue q = new LinkedBlockingQueue<>(); Codec codec = new JsonCodec(); WorkerThreadPool wtp = WorkerThreadPool.create(2); ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -358,21 +346,19 @@ public void testInterupptedTask() final TaskMessage tm = getTaskMessage(task, codec, WORKER_NAME); tm.setTaskData(codec.serialise("invalid task data")); - final byte[] stuff = codec.serialise(tm); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, tm); - byte[] msgCompletionTaskMessage = null; + TaskMessage rutTaskMessage = null; try { //give some time for the worker to be pick the task and create a response Thread.sleep(10000); - msgCompletionTaskMessage = q.poll(10000, TimeUnit.MILLISECONDS); + rutTaskMessage = q.poll(10000, TimeUnit.MILLISECONDS); } catch (InterruptedException ex) { Logger.error("InterruptedException" + ex); } // if the result didn't get back to us, then rutResult will be null - Assert.assertNotNull(msgCompletionTaskMessage); - // deserialise and verify rutResult data - final TaskMessage rutTaskMessage = codec.deserialise(msgCompletionTaskMessage, TaskMessage.class); + Assert.assertNotNull(rutTaskMessage); + // verify rutResult data //check if the status is NEW_TASK which qualify as a successful response (but not necessarily a successful result) Assert.assertEquals(TaskStatus.RESULT_FAILURE, rutTaskMessage.getTaskStatus()); Assert.assertEquals(WORKER_API_VER, rutTaskMessage.getTaskApiVersion()); @@ -388,7 +374,7 @@ public void testPausedTaskWithNonNullPausedQueue() throws CodecException, InterruptedException, WorkerException, ConfigurationException, QueueException, InvalidNameException, MalformedURLException { - final BlockingQueue q = new LinkedBlockingQueue<>(); + final BlockingQueue q = new LinkedBlockingQueue<>(); final Codec codec = new JsonCodec(); final WorkerThreadPool wtp = WorkerThreadPool.create(5); final ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -405,15 +391,13 @@ public void testPausedTaskWithNonNullPausedQueue() // and the Worker itself is a mock wrapped in a WorkerWrapper, which should return success and the appropriate result data final TrackingInfo tracking = new TrackingInfo("J23.1.2", new Date(), 0, new File("src/test/resources/paused-status-check-url-response.json").toURI().toURL().toString(), "trackingQueue", "trackTo"); - final byte[] stuff = codec.serialise(getTaskMessage(task, codec, WORKER_NAME, tracking)); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, getTaskMessage(task, codec, WORKER_NAME, tracking)); // Verify result for paused message. - final byte[] pausedMsgResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage pausedMsgTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); // if the result didn't get back to us, then result will be null - Assert.assertNotNull(pausedMsgResult); - // deserialise and verify pausedMsgResult data - final TaskMessage pausedMsgTaskMessage = codec.deserialise(pausedMsgResult, TaskMessage.class); + Assert.assertNotNull(pausedMsgTaskMessage); + // verify pausedMsgResult data Assert.assertEquals(pausedMsgTaskMessage.getTaskStatus(), TaskStatus.NEW_TASK); Assert.assertEquals(pausedMsgTaskMessage.getTaskClassifier(), WORKER_NAME); Assert.assertEquals(pausedMsgTaskMessage.getTaskApiVersion(), WORKER_API_VER); @@ -431,7 +415,7 @@ public void testPausedTaskWithNullPausedQueue() throws CodecException, InterruptedException, WorkerException, ConfigurationException, QueueException, InvalidNameException, MalformedURLException { - final BlockingQueue q = new LinkedBlockingQueue<>(); + final BlockingQueue q = new LinkedBlockingQueue<>(); final Codec codec = new JsonCodec(); final WorkerThreadPool wtp = WorkerThreadPool.create(5); final ConfigurationSource config = Mockito.mock(ConfigurationSource.class); @@ -448,18 +432,16 @@ public void testPausedTaskWithNullPausedQueue() // and the Worker itself is a mock wrapped in a WorkerWrapper, which should return success and the appropriate result data final TrackingInfo tracking = new TrackingInfo("J23.1.2", new Date(), 0, new File("src/test/resources/paused-status-check-url-response.json").toURI().toURL().toString(), "trackingQueue", "trackTo"); - final byte[] stuff = codec.serialise(getTaskMessage(task, codec, WORKER_NAME, tracking)); - queue.submitTask(taskInformation, stuff); + queue.submitTask(taskInformation, getTaskMessage(task, codec, WORKER_NAME, tracking)); // Two results expected back. One for the report progress update and another for the message completion. // // Verify result for the report update call. - final byte[] rutResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage rutTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); Assert.assertNotEquals(queue.getLastQueue(), QUEUE_PAUSED); // if the result didn't get back to us, then rutResult will be null - Assert.assertNotNull(rutResult); - // deserialise and verify rutResult data - final TaskMessage rutTaskMessage = codec.deserialise(rutResult, TaskMessage.class); + Assert.assertNotNull(rutTaskMessage); + // verify rutResult data Assert.assertEquals(TaskStatus.NEW_TASK, rutTaskMessage.getTaskStatus()); Assert.assertEquals(TrackingReportConstants.TRACKING_REPORT_TASK_NAME, rutTaskMessage.getTaskClassifier()); Assert.assertEquals(TrackingReportConstants.TRACKING_REPORT_TASK_API_VER, rutTaskMessage.getTaskApiVersion()); @@ -467,12 +449,11 @@ public void testPausedTaskWithNullPausedQueue() Assert.assertEquals("J23.1.2", rutWorkerResult.trackingReports.get(0).jobTaskId); Assert.assertEquals(TrackingReportStatus.Progress, rutWorkerResult.trackingReports.get(0).status); // Verify result for message completion. - final byte[] msgCompletionResult = q.poll(5000, TimeUnit.MILLISECONDS); + final TaskMessage msgCompletionTaskMessage = q.poll(5000, TimeUnit.MILLISECONDS); Assert.assertNotEquals(queue.getLastQueue(), QUEUE_PAUSED); // if the result didn't get back to us, then result will be null - Assert.assertNotNull(msgCompletionResult); - // deserialise and verify msgCompletionResult data - final TaskMessage msgCompletionTaskMessage = codec.deserialise(msgCompletionResult, TaskMessage.class); + Assert.assertNotNull(msgCompletionTaskMessage); + // verify msgCompletionResult data Assert.assertEquals(TaskStatus.RESULT_SUCCESS, msgCompletionTaskMessage.getTaskStatus()); Assert.assertEquals(WORKER_NAME, msgCompletionTaskMessage.getTaskClassifier()); Assert.assertEquals(WORKER_API_VER, msgCompletionTaskMessage.getTaskApiVersion()); @@ -567,15 +548,31 @@ public int getWorkerApiVersion() private class TestWorkerQueueProvider implements WorkerQueueProvider { - private final BlockingQueue results; + private final BlockingQueue results; - public TestWorkerQueueProvider(final BlockingQueue results) + public TestWorkerQueueProvider(final BlockingQueue results) { this.results = results; } + public final TestWorkerQueue getWorkerQueue( + final ConfigurationSource configurationSource, + final int maxTasks) + { + final ManagedDataStore dataStore = Mockito.mock(ManagedDataStore.class); + final Codec codec = new JsonCodec(); + return getWorkerQueue(configurationSource, maxTasks, INVALID, dataStore, codec, + Mockito.mock(WorkerConfiguration.class)); + } + @Override - public final TestWorkerQueue getWorkerQueue(final ConfigurationSource configurationSource, final int maxTasks) + public final TestWorkerQueue getWorkerQueue( + final ConfigurationSource configurationSource, + final int maxTasks, + final String invalidQueue, + final ManagedDataStore dataStore, + final Codec codec, + WorkerConfiguration workerConfiguration) { return new TestWorkerQueue(this.results); } @@ -584,10 +581,10 @@ public final TestWorkerQueue getWorkerQueue(final ConfigurationSource configurat private class TestWorkerQueue implements ManagedWorkerQueue { private TaskCallback callback; - private final BlockingQueue results; + private final BlockingQueue results; private String lastQueue; - public TestWorkerQueue(final BlockingQueue results) + public TestWorkerQueue(final BlockingQueue results) { this.results = results; } @@ -600,7 +597,7 @@ public void start(final TaskCallback callback) } @Override - public void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQueue, Map headers, boolean isLastMessage) + public void publish(TaskInformation taskInformation, TaskMessage taskMessage, String targetQueue, Map headers, boolean isLastMessage) throws QueueException { this.lastQueue = targetQueue; @@ -608,7 +605,7 @@ public void publish(TaskInformation taskInformation, byte[] taskMessage, String } @Override - public void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQueue, Map headers) + public void publish(TaskInformation taskInformation, TaskMessage taskMessage, String targetQueue, Map headers) throws QueueException { this.lastQueue = targetQueue; @@ -629,7 +626,7 @@ public void rejectTask(final TaskInformation taskInformation) tm.setTracking(null); try { tm.setTaskData(codec.serialise(taskInformation.getInboundMessageId().getBytes())); - results.offer(codec.serialise(tm)); + results.offer(tm); } catch (CodecException ex) { Logger.error("CodecException" + ex); } @@ -689,7 +686,7 @@ public void triggerAbort() callback.abortTasks(); } - public void submitTask(final TaskInformation taskInformation, final byte[] stuff) + public void submitTask(final TaskInformation taskInformation, TaskMessage stuff) throws WorkerException { callback.registerNewTask(taskInformation, stuff, new HashMap<>()); @@ -708,15 +705,29 @@ public void reconnectIncoming() private class TestWorkerQueueWithNullPausedQueueProvider implements WorkerQueueProvider { - private final BlockingQueue results; + private final BlockingQueue results; - public TestWorkerQueueWithNullPausedQueueProvider(final BlockingQueue results) + public TestWorkerQueueWithNullPausedQueueProvider(final BlockingQueue results) { this.results = results; } + public final TestWorkerQueueWithNullPausedQueue getWorkerQueue( + final ConfigurationSource configurationSource, + final int maxTasks) + { + return getWorkerQueue(configurationSource, maxTasks, INVALID, Mockito.mock(ManagedDataStore.class), new JsonCodec(), + Mockito.mock(WorkerConfiguration.class)); + } + @Override - public final TestWorkerQueueWithNullPausedQueue getWorkerQueue(final ConfigurationSource configurationSource, final int maxTasks) + public final TestWorkerQueueWithNullPausedQueue getWorkerQueue( + final ConfigurationSource configurationSource, + final int maxTasks, + final String invalidQueue, + final ManagedDataStore dataStore, + final Codec codec, + WorkerConfiguration workerConfiguration) { return new TestWorkerQueueWithNullPausedQueue(this.results); } @@ -724,7 +735,7 @@ public final TestWorkerQueueWithNullPausedQueue getWorkerQueue(final Configurati private class TestWorkerQueueWithNullPausedQueue extends TestWorkerQueue { - public TestWorkerQueueWithNullPausedQueue(final BlockingQueue results) + public TestWorkerQueueWithNullPausedQueue(final BlockingQueue results) { super(results); } diff --git a/worker-default-configs/README.md b/worker-default-configs/README.md index cf9cd8536..3604d3ef4 100644 --- a/worker-default-configs/README.md +++ b/worker-default-configs/README.md @@ -41,16 +41,18 @@ The default Rabbit configuration file checks for values as below; The default RabbitWorkerQueue configuration file checks for values as below; -| Property | Checked Environment Variables | Default | -|----------|-------------------------------|-----------------------| -| prefetchBuffer | `CAF_RABBITMQ_PREFETCH_BUFFER` | 1 | -| inputQueue | `CAF_WORKER_INPUT_QUEUE` | worker-in | -| | `CAF_WORKER_BASE_QUEUE_NAME` with '-in' appended to the value if present | | -| | `CAF_WORKER_NAME` with '-in' appended to the value if present | | -| pausedQueue | `CAF_WORKER_PAUSED_QUEUE` | | -| retryQueue | `CAF_WORKER_RETRY_QUEUE` | | -| rejectedQueue | | worker-rejected | -| retryLimit | `CAF_WORKER_RETRY_LIMIT` | 10 | +| Property | Checked Environment Variables | Default | +|----------------------------|--------------------------------------------------------------------------|-----------------| +| prefetchBuffer | `CAF_RABBITMQ_PREFETCH_BUFFER` | 1 | +| inputQueue | `CAF_WORKER_INPUT_QUEUE` | worker-in | +| | `CAF_WORKER_BASE_QUEUE_NAME` with '-in' appended to the value if present | | +| | `CAF_WORKER_NAME` with '-in' appended to the value if present | | +| pausedQueue | `CAF_WORKER_PAUSED_QUEUE` | | +| retryQueue | `CAF_WORKER_RETRY_QUEUE` | | +| rejectedQueue | | worker-rejected | +| retryLimit | `CAF_WORKER_RETRY_LIMIT` | 10 | +| isPayloadOffloadingEnabled | `CAF_WORKER_PAYLOAD_OFFLOADING_ENABLED` | false | +| payloadOffloadingThreshold | `CAF_WORKER_PAYLOAD_OFFLOADING_THRESHOLD_BYTES` | 16777216 | ## HealthConfiguration diff --git a/worker-default-configs/config/cfg~caf~worker~RabbitWorkerQueueConfiguration.js b/worker-default-configs/config/cfg~caf~worker~RabbitWorkerQueueConfiguration.js index 2bf870dc6..db599ed4b 100644 --- a/worker-default-configs/config/cfg~caf~worker~RabbitWorkerQueueConfiguration.js +++ b/worker-default-configs/config/cfg~caf~worker~RabbitWorkerQueueConfiguration.js @@ -22,5 +22,8 @@ rejectedQueue: "worker-rejected", retryLimit: getenv("CAF_WORKER_RETRY_LIMIT") || 10, maxPriority: getenv("CAF_RABBITMQ_MAX_PRIORITY") || 0, - queueType: getenv("CAF_RABBITMQ_QUEUE_TYPE") || "quorum" + queueType: getenv("CAF_RABBITMQ_QUEUE_TYPE") || "quorum", + isPayloadOffloadingEnabled: getenv("CAF_WORKER_PAYLOAD_OFFLOADING_ENABLED") || false, + payloadOffloadingThreshold: getenv("CAF_WORKER_PAYLOAD_OFFLOADING_THRESHOLD_BYTES") || 16777216, + payloadOffloadingDirectory: getenv("CAF_WORKER_PAYLOAD_OFFLOADING_DIRECTORY") || 'queues' }); diff --git a/worker-default-configs/pom.xml b/worker-default-configs/pom.xml index f9a7382d1..831b49146 100644 --- a/worker-default-configs/pom.xml +++ b/worker-default-configs/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-default-configs - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-queue-rabbit/pom.xml b/worker-queue-rabbit/pom.xml index 246856787..36d9149a2 100644 --- a/worker-queue-rabbit/pom.xml +++ b/worker-queue-rabbit/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-queue-rabbit - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT @@ -47,6 +47,14 @@ com.github.workerframework worker-configs + + com.github.workerframework + worker-tracking-report + + + com.google.guava + guava + com.rabbitmq amqp-client @@ -60,6 +68,16 @@ com.github.cafapi.common caf-api + + com.github.cafapi.common + codec-json + test + + + com.github.workerframework + worker-store-fs + test + org.testng testng diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/InvalidDeliveryException.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/InvalidDeliveryException.java new file mode 100644 index 000000000..dac17ec20 --- /dev/null +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/InvalidDeliveryException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2025 Open Text. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.workerframework.queues.rabbit; + +class InvalidDeliveryException extends Exception { + private static final long serialVersionUID = 1L; + private final long messageId; + + InvalidDeliveryException(final String message, final long messageId) { + super(message); + this.messageId = messageId; + } + + InvalidDeliveryException(final String message, final long messageId, final Throwable cause) { + super(message, cause); + this.messageId = messageId; + } + + long getMessageId() { + return messageId; + } +} diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitTaskInformation.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitTaskInformation.java index ad531798e..3999f9b80 100644 --- a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitTaskInformation.java +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitTaskInformation.java @@ -16,6 +16,7 @@ package com.github.workerframework.queues.rabbit; import com.github.workerframework.api.TaskInformation; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; @@ -30,12 +31,22 @@ public class RabbitTaskInformation implements TaskInformation { private final AtomicInteger acknowledgementCount; private static final Logger LOG = LoggerFactory.getLogger(RabbitTaskInformation.class); private final boolean isPoison; + private final Optional trackingJobTaskId; public RabbitTaskInformation(final String inboundMessageId) { this(inboundMessageId, false); } public RabbitTaskInformation(final String inboundMessageId, final boolean isPoison) { + this(inboundMessageId, isPoison, Optional.empty()); + } + + public RabbitTaskInformation( + final String inboundMessageId, + final boolean isPoison, + final Optional trackingJobTaskId + ) + { this.inboundMessageId = inboundMessageId; this.responseCount = new AtomicInteger(0); this.isResponseCountFinal = new AtomicBoolean(false); @@ -43,6 +54,7 @@ public RabbitTaskInformation(final String inboundMessageId, final boolean isPois this.negativeAckEventSent = new AtomicBoolean(false); this.ackEventSent = new AtomicBoolean(false); this.isPoison = isPoison; + this.trackingJobTaskId = trackingJobTaskId; } @Override @@ -141,4 +153,8 @@ public void markAckEventAsSent() public boolean isPoison() { return isPoison; } + + public Optional getTrackingJobTaskId() { + return trackingJobTaskId; + } } diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueue.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueue.java index 58a0ebfc5..0c57ef7f3 100644 --- a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueue.java +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueue.java @@ -15,12 +15,18 @@ */ package com.github.workerframework.queues.rabbit; +import com.github.cafapi.common.api.Codec; +import com.github.cafapi.common.api.CodecException; import com.github.cafapi.common.api.HealthResult; import com.github.cafapi.common.api.HealthStatus; +import com.github.workerframework.api.DataStoreException; +import com.github.workerframework.api.ManagedDataStore; import com.github.workerframework.api.ManagedWorkerQueue; import com.github.workerframework.api.QueueException; import com.github.workerframework.api.TaskCallback; import com.github.workerframework.api.TaskInformation; +import com.github.workerframework.api.TaskMessage; +import com.github.workerframework.api.WorkerConfiguration; import com.github.workerframework.api.WorkerQueueMetricsReporter; import com.github.workerframework.util.rabbitmq.ConsumerAckEvent; import com.github.workerframework.util.rabbitmq.ConsumerDropEvent; @@ -29,6 +35,7 @@ import com.github.workerframework.util.rabbitmq.Event; import com.github.workerframework.util.rabbitmq.EventPoller; import com.github.workerframework.util.rabbitmq.QueueConsumer; +import com.github.workerframework.util.rabbitmq.RabbitHeaders; import com.github.workerframework.util.rabbitmq.RabbitUtil; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; @@ -38,12 +45,15 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Paths; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * This implementation uses a separate thread for a consumer and producer, each with their own Channel. These threads handle operations @@ -72,15 +82,30 @@ public final class RabbitWorkerQueue implements ManagedWorkerQueue private final RabbitMetricsReporter metrics = new RabbitMetricsReporter(); private final RabbitWorkerQueueConfiguration config; private final int maxTasks; + private final String invalidQueue; + private final ManagedDataStore dataStore; + private final Codec codec; + private final WorkerConfiguration workerConfiguration; private static final Logger LOG = LoggerFactory.getLogger(RabbitWorkerQueue.class); - + private static final Pattern JOB_TASK_ID_PATTERN = Pattern.compile("^([^\\.]*)\\.?(.*)$"); + /** * Setup a new RabbitWorkerQueue. */ - public RabbitWorkerQueue(RabbitWorkerQueueConfiguration config, int maxTasks) + public RabbitWorkerQueue( + RabbitWorkerQueueConfiguration config, + int maxTasks, + final String invalidQueue, + final ManagedDataStore dataStore, + final Codec codec, + final WorkerConfiguration workerConfiguration) { this.config = Objects.requireNonNull(config); this.maxTasks = maxTasks; + this.invalidQueue = Objects.requireNonNull(invalidQueue); + this.dataStore = Objects.requireNonNull(dataStore); + this.codec = Objects.requireNonNull(codec); + this.workerConfiguration = Objects.requireNonNull(workerConfiguration); LOG.debug("Initialised"); } @@ -107,10 +132,27 @@ public void start(TaskCallback callback) incomingChannel = conn.createChannel(); int prefetch = Math.max(1, maxTasks + config.getPrefetchBuffer()); incomingChannel.basicQos(prefetch); - WorkerQueueConsumerImpl consumerImpl = new WorkerQueueConsumerImpl(callback, metrics, consumerQueue, incomingChannel, - publisherQueue, config.getRetryQueue(), config.getRetryLimit()); + final var rabbitWorkerQueue = this; + WorkerQueueConsumerImpl consumerImpl = new WorkerQueueConsumerImpl( + callback, + metrics, + consumerQueue, + incomingChannel, + publisherQueue, + config.getRetryQueue(), + config.getRetryLimit(), + invalidQueue, + dataStore, + codec, + rabbitWorkerQueue::disconnectIncoming, + workerConfiguration); consumer = new DefaultRabbitConsumer(consumerQueue, consumerImpl); - WorkerPublisherImpl publisherImpl = new WorkerPublisherImpl(outgoingChannel, metrics, consumerQueue, confirmListener); + WorkerPublisherImpl publisherImpl = new WorkerPublisherImpl( + outgoingChannel, + metrics, + consumerQueue, + confirmListener + ); publisher = new EventPoller<>(2, publisherQueue, publisherImpl); declareWorkerQueue(incomingChannel, config.getInputQueue()); declareWorkerQueue(outgoingChannel, config.getRetryQueue()); @@ -129,7 +171,7 @@ public void start(TaskCallback callback) } @Override - public void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQueue, Map headers, + public void publish(TaskInformation taskInformation, TaskMessage taskMessage, String targetQueue, Map headers, boolean isLastMessage) throws QueueException { try { @@ -140,11 +182,54 @@ public void publish(TaskInformation taskInformation, byte[] taskMessage, String RabbitTaskInformation rabbitTaskInformation = (RabbitTaskInformation)taskInformation; //increment the total responseCount (including task, sub task and tracking info) rabbitTaskInformation.incrementResponseCount(isLastMessage); - publisherQueue.add(new WorkerPublishQueueEvent(taskMessage, targetQueue, rabbitTaskInformation, headers)); + + final HashMap publishHeaders = new HashMap<>(headers); + + final byte[] serializedTaskMessage; + try { + if (config.getIsPayloadOffloadingEnabled() && config.getPayloadOffloadingThreshold() < taskMessage.getTaskData().length) { + LOG.debug("Offloading TaskMessage's TaskData to DataStore for message id '{}'", rabbitTaskInformation.getInboundMessageId()); + final byte[] taskData = taskMessage.getTaskData(); + taskMessage.setTaskData(null); + serializedTaskMessage = codec.serialise(taskMessage); + final String trackingJobTaskId = rabbitTaskInformation.getTrackingJobTaskId().isPresent() ? + rabbitTaskInformation.getTrackingJobTaskId().get() : "untracked"; + final String partialReference = getStoragePath(targetQueue, trackingJobTaskId); + final String taskDataStorageRef = dataStore.store(taskData, partialReference); + publishHeaders.put(RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF, taskDataStorageRef); + } else { + LOG.debug("Not offloading task message for task {}", rabbitTaskInformation.getInboundMessageId()); + serializedTaskMessage = codec.serialise(taskMessage); + } + } + catch (final CodecException e) { + metrics.incremementErrors(); + LOG.error("Failed to serialize task message for task {}", rabbitTaskInformation.getInboundMessageId(), e); + throw new QueueException("Failed to serialize task message", e); + } + catch (final DataStoreException e) { + metrics.incremementErrors(); + LOG.error("Failed to store task message for task {}", rabbitTaskInformation.getInboundMessageId(), e); + throw new QueueException("Failed to store task message", e); + } + + publisherQueue.add(new WorkerPublishQueueEvent(serializedTaskMessage, targetQueue, rabbitTaskInformation, publishHeaders)); } - + + private String getStoragePath(final String routingKey, final String trackingJobTaskId) + { + final StringBuilder path = new StringBuilder(Paths.get(config.getPayloadOffloadingDirectory(), routingKey).toString()); + final Matcher matcher = JOB_TASK_ID_PATTERN.matcher(trackingJobTaskId); + if (matcher.find()) { + path.append('/').append(matcher.group(1).replace(":", "/")); + if (matcher.group(2) != null && !matcher.group(2).isEmpty()) { + path.append('/').append(matcher.group(2)); + } + } + return path.toString(); + } @Override - public void publish(TaskInformation taskInformation, byte[] taskMessage, String targetQueue, Map headers) throws QueueException + public void publish(TaskInformation taskInformation, TaskMessage taskMessage, String targetQueue, Map headers) throws QueueException { publish(taskInformation, taskMessage, targetQueue, headers, false); } @@ -282,7 +367,7 @@ public void reconnectIncoming() try { consumerTag = incomingChannel.basicConsume(config.getInputQueue(), consumer); } catch (IOException ioe) { - LOG.error("Failed to reconnect consumer {}", ioe); + LOG.error("Failed to reconnect consumer {}", consumerTag, ioe); } } } diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConfiguration.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConfiguration.java index db7ec1373..0898ff3fb 100644 --- a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConfiguration.java +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConfiguration.java @@ -87,6 +87,22 @@ public class RabbitWorkerQueueConfiguration @NotNull private String queueType; + /** + * Indicates if payload offloading is enabled. + */ + private boolean isPayloadOffloadingEnabled = false; + + /** + * The threshold at which message payloads will be offloaded before publishing to RabbitMQ. + */ + @Min(1) + private int payloadOffloadingThreshold = 16777216; + + /** + * The datastore directory to use for offloading payloads. + */ + private String payloadOffloadingDirectory = "queues"; + public RabbitWorkerQueueConfiguration() { } @@ -181,4 +197,32 @@ public void setQueueType(String queueType) { this.queueType = queueType; } + + public boolean getIsPayloadOffloadingEnabled() + { + return isPayloadOffloadingEnabled; + } + + public void setPayloadOffloadingEnabled(boolean payloadOffloadingEnabled) + { + isPayloadOffloadingEnabled = payloadOffloadingEnabled; + } + + public int getPayloadOffloadingThreshold() + { + return payloadOffloadingThreshold; + } + + public void setPayloadOffloadingThreshold(int payloadOffloadingThreshold) + { + this.payloadOffloadingThreshold = payloadOffloadingThreshold; + } + + public String getPayloadOffloadingDirectory() { + return payloadOffloadingDirectory; + } + + public void setPayloadOffloadingDirectory(String payloadOffloadingDirectory) { + this.payloadOffloadingDirectory = payloadOffloadingDirectory; + } } diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueProvider.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueProvider.java index 0c112bd60..d5346f10a 100644 --- a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueProvider.java +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueProvider.java @@ -15,21 +15,36 @@ */ package com.github.workerframework.queues.rabbit; +import com.github.cafapi.common.api.Codec; import com.github.cafapi.common.api.ConfigurationException; import com.github.cafapi.common.api.ConfigurationSource; +import com.github.workerframework.api.ManagedDataStore; import com.github.workerframework.api.ManagedWorkerQueue; import com.github.workerframework.api.QueueException; +import com.github.workerframework.api.WorkerConfiguration; import com.github.workerframework.api.WorkerQueueProvider; public class RabbitWorkerQueueProvider implements WorkerQueueProvider { @Override - public ManagedWorkerQueue getWorkerQueue(final ConfigurationSource configurationSource, final int maxTasks) - throws QueueException + public ManagedWorkerQueue getWorkerQueue( + final ConfigurationSource configurationSource, + final int maxTasks, + final String invalidQueue, + final ManagedDataStore dataStore, + final Codec codec, + final WorkerConfiguration workerConfiguration) throws QueueException { try { - return new RabbitWorkerQueue(configurationSource.getConfiguration(RabbitWorkerQueueConfiguration.class), maxTasks); - } catch (ConfigurationException e) { + return new RabbitWorkerQueue( + configurationSource.getConfiguration(RabbitWorkerQueueConfiguration.class), + maxTasks, + invalidQueue, + dataStore, + codec, + workerConfiguration + ); + } catch (final ConfigurationException e) { throw new QueueException("Cannot create worker queue", e); } } diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/TransientDeliveryException.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/TransientDeliveryException.java new file mode 100644 index 000000000..50164e06b --- /dev/null +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/TransientDeliveryException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015-2025 Open Text. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.workerframework.queues.rabbit; + +class TransientDeliveryException extends Exception { + private static final long serialVersionUID = 1L; + private final long messageId; + + TransientDeliveryException(final String message, final long messageId, final Throwable cause) { + super(message, cause); + this.messageId = messageId; + } + + long getMessageId() { + return messageId; + } +} diff --git a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/WorkerQueueConsumerImpl.java b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/WorkerQueueConsumerImpl.java index 30636cff3..ef557111f 100644 --- a/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/WorkerQueueConsumerImpl.java +++ b/worker-queue-rabbit/src/main/java/com/github/workerframework/queues/rabbit/WorkerQueueConsumerImpl.java @@ -15,9 +15,24 @@ */ package com.github.workerframework.queues.rabbit; +import com.github.cafapi.common.api.Codec; +import com.github.cafapi.common.api.CodecException; +import com.github.cafapi.common.api.DecodeMethod; +import com.github.workerframework.api.DataStoreException; import com.github.workerframework.api.InvalidTaskException; +import com.github.workerframework.api.ManagedDataStore; +import com.github.workerframework.api.ReferenceNotFoundException; import com.github.workerframework.api.TaskCallback; +import com.github.workerframework.api.TaskMessage; import com.github.workerframework.api.TaskRejectedException; +import com.github.workerframework.api.TaskStatus; +import com.github.workerframework.api.TrackingInfo; +import com.github.workerframework.api.WorkerConfiguration; +import com.github.workerframework.tracking.report.TrackingReport; +import com.github.workerframework.tracking.report.TrackingReportConstants; +import com.github.workerframework.tracking.report.TrackingReportFailure; +import com.github.workerframework.tracking.report.TrackingReportStatus; +import com.github.workerframework.tracking.report.TrackingReportTask; import com.github.workerframework.util.rabbitmq.QueueConsumer; import com.github.workerframework.util.rabbitmq.ConsumerAckEvent; import com.github.workerframework.util.rabbitmq.Event; @@ -25,17 +40,28 @@ import com.github.workerframework.util.rabbitmq.RabbitHeaders; import com.github.workerframework.util.rabbitmq.ConsumerRejectEvent; import com.github.workerframework.util.rabbitmq.ConsumerDropEvent; +import com.google.common.base.MoreObjects; import com.rabbitmq.client.Channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; import java.util.concurrent.BlockingQueue; +import static com.github.workerframework.util.rabbitmq.RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF; +import static com.github.workerframework.util.rabbitmq.RabbitHeaders.RABBIT_HEADER_CAF_WORKER_INVALID; + /** * QueueConsumer implementation for a WorkerQueue. This QueueConsumer hands off messages to worker-core upon delivery assuming the message * is not marked 'redelivered'. Redelivered messages are republished to the retry queue with an incremented retry count. Redelivered @@ -43,7 +69,6 @@ */ public class WorkerQueueConsumerImpl implements QueueConsumer { - public static final String REJECTED_REASON_TASKMESSAGE = "TASKMESSAGE_INVALID"; private final TaskCallback callback; private final RabbitMetricsReporter metrics; private final BlockingQueue> consumerEventQueue; @@ -51,10 +76,28 @@ public class WorkerQueueConsumerImpl implements QueueConsumer private final Channel channel; private final String retryRoutingKey; private final int retryLimit; + private final String invalidRoutingKey; + private final ManagedDataStore dataStore; + private final Codec codec; + private final Runnable disconnectCallback; + private final SortedMap offloadedPayloadsToDelete; + private final WorkerConfiguration workerConfiguration; + private static final Logger LOG = LoggerFactory.getLogger(WorkerQueueConsumerImpl.class); - public WorkerQueueConsumerImpl(TaskCallback callback, RabbitMetricsReporter metrics, BlockingQueue> queue, Channel ch, - BlockingQueue> pubQueue, String retryKey, int retryLimit) + private enum PoisonMessageStatus + { + NOT_POISON, + CLASSIC_POSSIBLY_POISON, + POISON + } + + public WorkerQueueConsumerImpl(TaskCallback callback, RabbitMetricsReporter metrics, + BlockingQueue> queue, Channel ch, + BlockingQueue> pubQueue, String retryKey, int retryLimit, + final String invalidKey, + final ManagedDataStore dataStore, final Codec codec, + final Runnable disconnectCallback, final WorkerConfiguration workerConfiguration) { this.callback = Objects.requireNonNull(callback); this.metrics = Objects.requireNonNull(metrics); @@ -63,57 +106,247 @@ public WorkerQueueConsumerImpl(TaskCallback callback, RabbitMetricsReporter metr this.publisherEventQueue = Objects.requireNonNull(pubQueue); this.retryRoutingKey = Objects.requireNonNull(retryKey); this.retryLimit = retryLimit; + this.invalidRoutingKey = Objects.requireNonNull(invalidKey); + this.dataStore = Objects.requireNonNull(dataStore); + this.codec = Objects.requireNonNull(codec); + this.disconnectCallback = Objects.requireNonNull(disconnectCallback); + this.offloadedPayloadsToDelete = Collections.synchronizedSortedMap(new TreeMap<>()); + this.workerConfiguration = Objects.requireNonNull(workerConfiguration); } /** * {@inheritDoc} - * + *

* If an incoming message is marked as redelivered, hand it off to another method to deal with retry/rejection. Otherwise, hand it off - * to worker-core, and potentially repbulish or reject it depending upon exceptions thrown. + * to worker-core, and potentially republish or reject it depending upon exceptions thrown. */ @Override public void processDelivery(Delivery delivery) { - final int retries = delivery.getHeaders().containsKey(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT) ? - Integer.parseInt(String.valueOf(delivery.getHeaders() - .getOrDefault(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT, "0"))) : - Integer.parseInt(String.valueOf(delivery.getHeaders() - .getOrDefault(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_RETRY, "0"))); - + final long inboundMessageId = delivery.getEnvelope().getDeliveryTag(); + final String routingKey = delivery.getEnvelope().getRoutingKey(); + final Map deliveryHeaders = delivery.getHeaders(); + final boolean isRedelivered = delivery.getEnvelope().isRedeliver(); + final int retries = deliveryHeaders.containsKey(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT) + ? Integer.parseInt(String.valueOf(deliveryHeaders.getOrDefault(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT, "0"))) + : Integer.parseInt(String.valueOf(deliveryHeaders.getOrDefault(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_RETRY, "0"))); + final Optional taskMessageStorageRefOpt + = Optional.ofNullable(deliveryHeaders.get(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF)).map(Object::toString); metrics.incrementReceived(); - final boolean isPoison; - if (delivery.getEnvelope().isRedeliver()) { - if (!delivery.getHeaders().containsKey(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT)) { - //RABBIT_HEADER_CAF_DELIVERY_COUNT is not available, message was delivered from CLASSIC queue + + final byte[] deliveryMessageData = delivery.getMessageData(); + final TaskMessage taskMessage; + try { + try { + taskMessage = codec.deserialise(deliveryMessageData, TaskMessage.class, DecodeMethod.LENIENT); + } catch (final CodecException e) { + handleInvalidDelivery(inboundMessageId, Optional.empty(), deliveryMessageData, deliveryHeaders, + "Cannot deserialize delivery messageData to TaskMessage"); + return; + } + + try { + handleTaskDataInjection(taskMessage, inboundMessageId, taskMessageStorageRefOpt); + } catch (final InvalidDeliveryException ex) { + handleInvalidDelivery(inboundMessageId, Optional.of(taskMessage), deliveryMessageData, deliveryHeaders, + ex.getMessage()); + return; + } + + final PoisonMessageStatus poisonMessageStatus = getPoisonMessageStatus( + isRedelivered, deliveryHeaders, retries); + + if (poisonMessageStatus == PoisonMessageStatus.CLASSIC_POSSIBLY_POISON) { + republishClassicRedelivery( + delivery.getEnvelope().getRoutingKey(), + inboundMessageId, + deliveryMessageData, + taskMessage.getTaskData(), + deliveryHeaders, + retries, + taskMessage.getTracking(), + taskMessageStorageRefOpt + ); + return; + } + + processDelivery( + inboundMessageId, + routingKey, + deliveryHeaders, + taskMessage, + deliveryMessageData, + poisonMessageStatus == PoisonMessageStatus.POISON + ); + } catch (final TransientDeliveryException e) { + LOG.warn("Transient error processing message id {}, disconnecting.", inboundMessageId, e); + offloadedPayloadsToDelete.remove(inboundMessageId); + //Disconnect the channel to allow for a reconnect when the HealthCheck passes. + disconnectCallback.run(); + } + } + + /** + * Handles the logic for injecting taskData from the store into the TaskMessage if required. + * Returns true if processing should continue, false if it should stop (e.g. error). + * If invalid, handles as poison message (publishes to retry queue) and returns false. + */ + private void handleTaskDataInjection(final TaskMessage taskMessage, final long inboundMessageId, + final Optional taskMessageStorageRefOpt) + throws InvalidDeliveryException, TransientDeliveryException + { + final byte[] currentTaskData = taskMessage.getTaskData(); + final boolean hasStorageRef = taskMessageStorageRefOpt.isPresent(); + final boolean hasTaskData = currentTaskData != null; + + if (hasTaskData && hasStorageRef) { + throw new InvalidDeliveryException( + "TaskMessage contains both taskData and a storage reference. This is invalid.", inboundMessageId); + } + if (!hasTaskData && !hasStorageRef) { + throw new InvalidDeliveryException( + "TaskMessage contains neither taskData nor a storage reference. This is invalid.", inboundMessageId); + } + if (hasStorageRef) { + final byte[] offloadedTaskData; + try { + offloadedTaskData = retrieveTaskDataFromStore(taskMessageStorageRefOpt.get(), inboundMessageId); + } catch (final ReferenceNotFoundException e) { + throw new InvalidDeliveryException(e.getMessage(), inboundMessageId); + } + taskMessage.setTaskData(offloadedTaskData); + } + // If hasTaskData and !hasStorageRef, nothing to do + } + + private byte[] retrieveTaskDataFromStore(final String taskMessageStorageRef, final long inboundMessageId) + throws ReferenceNotFoundException, TransientDeliveryException + { + try (final var inputStream = dataStore.retrieve(taskMessageStorageRef)) { + final var taskData = inputStream.readAllBytes(); + offloadedPayloadsToDelete.put(inboundMessageId, taskMessageStorageRef); + return taskData; + } catch (final IOException | DataStoreException ex) { + if (ex instanceof ReferenceNotFoundException) { + throw (ReferenceNotFoundException)ex; + } + throw new TransientDeliveryException( + "TaskMessage's TaskData could not be retrieved from DataStore", inboundMessageId, ex); + } + } + + private void handleInvalidDelivery( + final long inboundMessageId, + final Optional deliveredTaskMessageOpt, + final byte[] deliveryMessageData, + final Map deliveryHeaders, + final String exceptionMesssage + ) + { + try { + final RabbitTaskInformation taskInformation = new RabbitTaskInformation(String.valueOf(inboundMessageId), true); + taskInformation.incrementResponseCount(true); + final var publishHeaders = new HashMap<>(deliveryHeaders); + publishHeaders.put(RABBIT_HEADER_CAF_WORKER_INVALID, exceptionMesssage); + + publisherEventQueue.add(new WorkerPublishQueueEvent(deliveryMessageData, invalidRoutingKey, taskInformation, publishHeaders)); + + if (deliveredTaskMessageOpt.isPresent()) { + final var taskMessage = deliveredTaskMessageOpt.get(); + if(taskMessage.getTracking() != null) { + sendFailureTrackingReport(taskMessage, exceptionMesssage, taskInformation); + } + } + } catch (CodecException e) { + LOG.error("Failed to serialise report update task data."); + throw new RuntimeException(e); + } + } + + private void sendFailureTrackingReport( + final TaskMessage taskMessage, + final String invalidDeliveryExceptionMessage, + final RabbitTaskInformation rabbitTaskInformation + ) throws CodecException { + final TrackingReportFailure failure = new TrackingReportFailure(); + failure.failureId = TaskStatus.INVALID_TASK.toString(); + failure.failureTime = new Date(); + failure.failureSource = getWorkerName(taskMessage); + failure.failureMessage = invalidDeliveryExceptionMessage; + + final List trackingReports = new ArrayList<>(); + + final TrackingReport trackingReport = new TrackingReport(); + trackingReport.failure = failure; + trackingReport.status = TrackingReportStatus.Failed; + + trackingReports.add(trackingReport); + + final TrackingReportTask trackingReportTask = new TrackingReportTask(); + trackingReportTask.trackingReports = trackingReports; + + final byte[] trackingReportTaskTaskData = codec.serialise(trackingReportTask); + + final TrackingInfo trackingInfo = taskMessage.getTracking(); + + final TaskMessage failureReportTaskMessage = new TaskMessage( + UUID.randomUUID().toString(), TrackingReportConstants.TRACKING_REPORT_TASK_NAME, + TrackingReportConstants.TRACKING_REPORT_TASK_API_VER, trackingReportTaskTaskData, TaskStatus.NEW_TASK, + Collections.emptyMap(), trackingInfo.getTrackingPipe(), null, null, + taskMessage.getCorrelationId()); + + publisherEventQueue.add(new WorkerPublishQueueEvent(codec.serialise(failureReportTaskMessage), + trackingInfo.getTrackingPipe(), rabbitTaskInformation, Collections.emptyMap())); + } + + private PoisonMessageStatus getPoisonMessageStatus( + final boolean isRedelivered, + final Map deliveryHeaders, + final int retries + ) { + // If the message is being redelivered it is potentially a poison message. + if (isRedelivered) { + // If the headers do not contain the delivery count, then it is a classic queue. + if (!deliveryHeaders.containsKey(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT)) { + // If the retries have not been exceeded, then republish the message + // with a header recording the retry count if (retries < retryLimit) { - //Republish the delivery with a header recording the incremented number of retries. - //Classic queues do not record delivery count, so we republish the message with an incremented - //retry count. This allows us to track the number of attempts to process the message. - republishClassicRedelivery(delivery, retries); - return; + return PoisonMessageStatus.CLASSIC_POSSIBLY_POISON; } - isPoison = true; - } else { - isPoison = retries > retryLimit; } - } else { - isPoison = false; + return (retries >= retryLimit) + ? PoisonMessageStatus.POISON + : PoisonMessageStatus.NOT_POISON; } + return PoisonMessageStatus.NOT_POISON; + } - final RabbitTaskInformation taskInformation = new RabbitTaskInformation(String.valueOf(delivery.getEnvelope().getDeliveryTag()), isPoison); + private void processDelivery( + final long inboundMessageId, + final String routingKey, + final Map deliveryHeaders, + final TaskMessage taskMessage, + final byte[] taskMessageByteArray, + final boolean isPoison + ) { + final TrackingInfo trackingInfo = taskMessage.getTracking(); + final String trackingJobTaskId = trackingInfo != null ? trackingInfo.getJobTaskId() : "untracked"; + final RabbitTaskInformation taskInformation = new RabbitTaskInformation( + String.valueOf(inboundMessageId), isPoison, Optional.of(trackingJobTaskId) + ); try { - LOG.debug("Registering new message {}", taskInformation.getInboundMessageId()); - callback.registerNewTask(taskInformation, delivery.getMessageData(), delivery.getHeaders()); - } catch (InvalidTaskException e) { - LOG.error("Cannot register new message, rejecting {}", taskInformation.getInboundMessageId(), e); + LOG.debug("Registering new message {}", inboundMessageId); + callback.registerNewTask(taskInformation, taskMessage, deliveryHeaders); + } catch (final InvalidTaskException e) { + LOG.error("Cannot register new message, rejecting {}", inboundMessageId, e); taskInformation.incrementResponseCount(true); - publisherEventQueue.add(new WorkerPublishQueueEvent(delivery.getMessageData(), retryRoutingKey, taskInformation, - Collections.singletonMap(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_REJECTED, REJECTED_REASON_TASKMESSAGE))); - } catch (TaskRejectedException e) { - LOG.warn("Message {} rejected as a task at this time, returning to queue", taskInformation.getInboundMessageId(), e); + final var publishHeaders = new HashMap(); + publishHeaders.put(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_INVALID, e); + publisherEventQueue.add(new WorkerPublishQueueEvent(taskMessageByteArray, invalidRoutingKey, taskInformation, publishHeaders)); + } catch (final TaskRejectedException e) { + LOG.warn("Message {} rejected as a task at this time, returning to queue", inboundMessageId, e); taskInformation.incrementResponseCount(true); - publisherEventQueue.add(new WorkerPublishQueueEvent(delivery.getMessageData(), delivery.getEnvelope().getRoutingKey(), - taskInformation)); + publisherEventQueue.add(new WorkerPublishQueueEvent(taskMessageByteArray, routingKey, taskInformation, deliveryHeaders)); } } @@ -131,6 +364,17 @@ public void processAck(long tag) LOG.warn("Couldn't ack message {}, will retry", tag, e); metrics.incremementErrors(); consumerEventQueue.add(new ConsumerAckEvent(tag)); + return; + } + + final String datastorePayloadReference = offloadedPayloadsToDelete.remove(tag); + if (datastorePayloadReference != null) { + try { + dataStore.delete(datastorePayloadReference); + } catch (final DataStoreException e) { + LOG.warn("Couldn't delete offloaded payload '{}' for delivery tag '{}' from datastore message.", + datastorePayloadReference, tag, e); + } } } @@ -175,21 +419,59 @@ private void processReject(long id, boolean requeue) } } - /** - * Republish the delivery to the retry queue with the retry count stamped in the headers. - * - * @param delivery the redelivered message - */ - private void republishClassicRedelivery(final Delivery delivery, final int retries) { - - final RabbitTaskInformation taskInformation = - new RabbitTaskInformation(String.valueOf(delivery.getEnvelope().getDeliveryTag())); + private void republishClassicRedelivery( + final String deliveryQueue, + final long inboundMessageId, + final byte[] serializedTaskMessage, + final byte[] serializedTaskData, + final Map deliveryHeaders, + final int retries, + final TrackingInfo tracking, + final Optional taskMessageStorageRefOpt + ) + { + final String trackingJobTaskId = tracking != null ? tracking.getJobTaskId() : "untracked"; + final RabbitTaskInformation taskInformation = new RabbitTaskInformation( + String.valueOf(inboundMessageId), false, Optional.of(trackingJobTaskId)); LOG.debug("Received redelivered message with id {}, retry count {}, retry limit {}, republishing to retry queue", - delivery.getEnvelope().getDeliveryTag(), retryLimit, retries + 1); - final Map headers = new HashMap<>(); - headers.put(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_RETRY, String.valueOf(retries + 1)); + inboundMessageId, retryLimit, retries + 1); + final Map publishHeaders = new HashMap<>(deliveryHeaders); + publishHeaders.put(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_RETRY, String.valueOf(retries + 1)); taskInformation.incrementResponseCount(true); - publisherEventQueue.add(new WorkerPublishQueueEvent(delivery.getMessageData(), retryRoutingKey, - taskInformation, headers)); + if(taskMessageStorageRefOpt.isPresent()) { + if (!retryRoutingKey.equals(deliveryQueue)) { + try { + final String newStorageReference = + dataStore.store(serializedTaskData, + taskMessageStorageRefOpt.get().replace(deliveryQueue, retryRoutingKey)); + publishHeaders.put(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF, newStorageReference); + } + catch (final DataStoreException e) { + LOG.error("Failed to relocate offloaded payload for message id {} from {} to {}", + inboundMessageId, deliveryQueue, retryRoutingKey, e); + //Disconnect the channel to allow for a reconnect when the HealthCheck passes. + disconnectCallback.run(); + } + } + else { + //We are reusing the same routing key, so we do not need to relocate the payload. + offloadedPayloadsToDelete.remove(inboundMessageId); + } + } + publisherEventQueue.add(new WorkerPublishQueueEvent(serializedTaskMessage, retryRoutingKey, taskInformation, publishHeaders)); + } + + private String getWorkerName(final TaskMessage taskMessage) + { + final var taskClassifier = MoreObjects.firstNonNull(taskMessage.getTaskClassifier(), ""); + if (workerConfiguration != null) { + final String workerName = workerConfiguration.getWorkerName(); + + if (workerName != null) { + return workerName; + } + } + + return taskClassifier; } } diff --git a/worker-queue-rabbit/src/test/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConsumerTest.java b/worker-queue-rabbit/src/test/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConsumerTest.java index 13c39e1f9..fed7f93a9 100644 --- a/worker-queue-rabbit/src/test/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConsumerTest.java +++ b/worker-queue-rabbit/src/test/java/com/github/workerframework/queues/rabbit/RabbitWorkerQueueConsumerTest.java @@ -15,11 +15,22 @@ */ package com.github.workerframework.queues.rabbit; +import com.github.cafapi.common.api.Codec; +import com.github.cafapi.common.api.CodecException; +import com.github.cafapi.common.codecs.json.JsonCodec; +import com.github.workerframework.api.DataStoreException; import com.github.workerframework.api.InvalidTaskException; +import com.github.workerframework.api.ManagedDataStore; import com.github.workerframework.api.TaskCallback; import com.github.workerframework.api.TaskInformation; +import com.github.workerframework.api.TaskMessage; import com.github.workerframework.api.TaskRejectedException; +import com.github.workerframework.api.TaskStatus; +import com.github.workerframework.api.TrackingInfo; +import com.github.workerframework.api.WorkerConfiguration; import com.github.workerframework.api.WorkerException; +import com.github.workerframework.datastores.fs.FileSystemDataStore; +import com.github.workerframework.datastores.fs.FileSystemDataStoreConfiguration; import com.github.workerframework.util.rabbitmq.ConsumerAckEvent; import com.github.workerframework.util.rabbitmq.ConsumerDropEvent; import com.github.workerframework.util.rabbitmq.ConsumerRejectEvent; @@ -31,15 +42,19 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Envelope; import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.stubbing.Answer; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; @@ -47,24 +62,63 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import static org.mockito.Mockito.mock; + public class RabbitWorkerQueueConsumerTest { private String testQueue = "testQueue"; - private RabbitTaskInformation taskInformation; - private byte[] data = "test123".getBytes(StandardCharsets.UTF_8); + private RabbitTaskInformation taskInformation; private Envelope newEnv; private Envelope poisonEnv; private Envelope redeliveredEnv; private String retryKey = "retry"; + private String invalidKey = "invalid"; private RabbitMetricsReporter metrics = new RabbitMetricsReporter(); - private TaskCallback mockCallback = Mockito.mock(TaskCallback.class); + private TaskCallback mockCallback = mock(TaskCallback.class); + private File tempDataStore; + private ManagedDataStore dataStore; + private static Codec codec; + private static byte[] data; + + @BeforeClass + public static void beforeClass() throws CodecException { + codec = new JsonCodec(); + data = getNewTaskMessage(); + } @BeforeMethod - public void beforeMethod() { + public void beforeMethod() throws DataStoreException { taskInformation = new RabbitTaskInformation("101"); newEnv = new Envelope(Long.valueOf(taskInformation.getInboundMessageId()), false, "", testQueue); poisonEnv = new Envelope(Long.valueOf(taskInformation.getInboundMessageId()), true, "", testQueue); redeliveredEnv = new Envelope(Long.valueOf(taskInformation.getInboundMessageId()), true, "", testQueue); + tempDataStore = new File("RabbitWorkerQueueConsumerTest"); + dataStore = new FileSystemDataStore(createConfig()); + } + + @AfterMethod + public void tearDown() + { + deleteDir(tempDataStore); + } + + private void deleteDir(File file) + { + File[] contents = file.listFiles(); + if (contents != null) { + for (File f : contents) { + deleteDir(f); + } + } + file.delete(); + } + + private FileSystemDataStoreConfiguration createConfig() + { + final FileSystemDataStoreConfiguration conf = new FileSystemDataStoreConfiguration(); + conf.setDataDir(tempDataStore.getAbsolutePath()); + conf.setDataDirHealthcheckTimeoutSeconds(10); + return conf; } /** @@ -76,19 +130,21 @@ public void testHandleDelivery() { BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); - Channel channel = Mockito.mock(Channel.class); + Channel channel = mock(Channel.class); CountDownLatch latch = new CountDownLatch(1); - TaskCallback callback = Mockito.mock(TaskCallback.class); + TaskCallback callback = mock(TaskCallback.class); Answer a = invocationOnMock -> { latch.countDown(); return null; }; Mockito.doAnswer(a).when(callback).registerNewTask(Mockito.any(), Mockito.any(), Mockito.anyMap()); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); - AMQP.BasicProperties prop = Mockito.mock(AMQP.BasicProperties.class); + AMQP.BasicProperties prop = mock(AMQP.BasicProperties.class); Mockito.when(prop.getHeaders()).thenReturn(Collections.emptyMap()); consumer.handleDelivery("consumer", newEnv, prop, data); Assert.assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); @@ -105,19 +161,21 @@ public void testPoisonDelivery() { BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); - Channel channel = Mockito.mock(Channel.class); + Channel channel = mock(Channel.class); CountDownLatch latch = new CountDownLatch(1); - TaskCallback callback = Mockito.mock(TaskCallback.class); + TaskCallback callback = mock(TaskCallback.class); Answer a = invocationOnMock -> { latch.countDown(); return null; }; Mockito.doAnswer(a).when(callback).registerNewTask(Mockito.any(), Mockito.any(), Mockito.anyMap()); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); - AMQP.BasicProperties prop = Mockito.mock(AMQP.BasicProperties.class); + AMQP.BasicProperties prop = mock(AMQP.BasicProperties.class); Map headers = new HashMap<>(); headers.put(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_RETRY, "1"); Mockito.when(prop.getHeaders()).thenReturn(headers); @@ -131,8 +189,8 @@ public void testPoisonDelivery() } /** - * Send in a new message and verify that if the task registration throws an InvalidTaskException that a new publish request to the - * reject queue is sent. + * Send in a new message and verify that if the task registration throws an InvalidTaskException that a new publish + * request to the invalid queue is sent. */ @Test public void testHandleDeliveryInvalid() @@ -140,28 +198,30 @@ public void testHandleDeliveryInvalid() { BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); - Channel channel = Mockito.mock(Channel.class); - TaskCallback callback = Mockito.mock(TaskCallback.class); + Channel channel = mock(Channel.class); + TaskCallback callback = mock(TaskCallback.class); Answer a = invocationOnMock -> { throw new InvalidTaskException("blah"); }; Mockito.doAnswer(a).when(callback).registerNewTask(Mockito.any(), Mockito.any(), Mockito.anyMap()); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); - AMQP.BasicProperties prop = Mockito.mock(AMQP.BasicProperties.class); + AMQP.BasicProperties prop = mock(AMQP.BasicProperties.class); Mockito.when(prop.getHeaders()).thenReturn(Collections.emptyMap()); consumer.handleDelivery("consumer", newEnv, prop, data); Event pubEvent = publisherEvents.poll(1, TimeUnit.SECONDS); Assert.assertNotNull(pubEvent); - WorkerPublisher publisher = Mockito.mock(WorkerPublisher.class); + WorkerPublisher publisher = mock(WorkerPublisher.class); ArgumentCaptor> captor = buildStringObjectMapCaptor(); pubEvent.handleEvent(publisher); - Mockito.verify(publisher, Mockito.times(1)).handlePublish(Mockito.eq(data), Mockito.eq(retryKey), Mockito.any(RabbitTaskInformation.class), captor.capture()); - Assert.assertTrue(captor.getValue().containsKey(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_REJECTED)); - Assert.assertEquals(WorkerQueueConsumerImpl.REJECTED_REASON_TASKMESSAGE, - captor.getValue().get(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_REJECTED)); + Mockito.verify(publisher, Mockito.times(1)).handlePublish(Mockito.eq(data), Mockito.eq(invalidKey), Mockito.any(RabbitTaskInformation.class), captor.capture()); + Assert.assertTrue(captor.getValue().containsKey(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_INVALID)); + Assert.assertEquals(captor.getValue().get(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_INVALID).toString(), + "com.github.workerframework.api.InvalidTaskException: blah"); consumer.shutdown(); } @@ -175,22 +235,24 @@ public void testHandleDeliveryRejected() { BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); - Channel channel = Mockito.mock(Channel.class); - TaskCallback callback = Mockito.mock(TaskCallback.class); + Channel channel = mock(Channel.class); + TaskCallback callback = mock(TaskCallback.class); Answer a = invocationOnMock -> { throw new TaskRejectedException("blah"); }; Mockito.doAnswer(a).when(callback).registerNewTask(Mockito.any(), Mockito.any(), Mockito.anyMap()); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); - AMQP.BasicProperties prop = Mockito.mock(AMQP.BasicProperties.class); + AMQP.BasicProperties prop = mock(AMQP.BasicProperties.class); Mockito.when(prop.getHeaders()).thenReturn(Collections.emptyMap()); consumer.handleDelivery("consumer", newEnv, prop, data); Event pubEvent = publisherEvents.poll(1, TimeUnit.SECONDS); Assert.assertNotNull(pubEvent); - WorkerPublisher publisher = Mockito.mock(WorkerPublisher.class); + WorkerPublisher publisher = mock(WorkerPublisher.class); ArgumentCaptor> captor = buildStringObjectMapCaptor(); pubEvent.handleEvent(publisher); Mockito.verify(publisher, Mockito.times(1)).handlePublish(Mockito.eq(data), Mockito.eq(testQueue), Mockito.any(RabbitTaskInformation.class), captor.capture()); @@ -208,18 +270,20 @@ public void testHandleRedelivery() { BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); - Channel channel = Mockito.mock(Channel.class); - TaskCallback callback = Mockito.mock(TaskCallback.class); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + Channel channel = mock(Channel.class); + TaskCallback callback = mock(TaskCallback.class); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + callback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); - AMQP.BasicProperties prop = Mockito.mock(AMQP.BasicProperties.class); + AMQP.BasicProperties prop = mock(AMQP.BasicProperties.class); Mockito.when(prop.getHeaders()).thenReturn(Collections.emptyMap()); consumer.handleDelivery("consumer", redeliveredEnv, prop, data); Event pubEvent = publisherEvents.poll(1, TimeUnit.SECONDS); Assert.assertNotNull(pubEvent); - WorkerPublisher publisher = Mockito.mock(WorkerPublisher.class); + WorkerPublisher publisher = mock(WorkerPublisher.class); ArgumentCaptor> captor = buildStringObjectMapCaptor(); pubEvent.handleEvent(publisher); Mockito.verify(publisher, Mockito.times(1)).handlePublish(Mockito.eq(data), Mockito.eq(retryKey), Mockito.any(RabbitTaskInformation.class), captor.capture()); @@ -238,13 +302,15 @@ public void testHandleDeliveryAck() BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); CountDownLatch channelLatch = new CountDownLatch(1); - Channel channel = Mockito.mock(Channel.class); + Channel channel = mock(Channel.class); Answer a = invocationOnMock -> { channelLatch.countDown(); return null; }; Mockito.doAnswer(a).when(channel).basicAck(Mockito.eq(Long.valueOf(taskInformation.getInboundMessageId())), Mockito.anyBoolean()); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(mockCallback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + mockCallback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); @@ -263,13 +329,15 @@ public void testHandleDeliveryReject() BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); CountDownLatch channelLatch = new CountDownLatch(1); - Channel channel = Mockito.mock(Channel.class); + Channel channel = mock(Channel.class); Answer a = invocationOnMock -> { channelLatch.countDown(); return null; }; Mockito.doAnswer(a).when(channel).basicReject(Mockito.eq(Long.valueOf(taskInformation.getInboundMessageId())), Mockito.eq(true)); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(mockCallback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + mockCallback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); @@ -288,13 +356,15 @@ public void testHandleDeliveryDrop() BlockingQueue> consumerEvents = new LinkedBlockingQueue<>(); BlockingQueue> publisherEvents = new LinkedBlockingQueue<>(); CountDownLatch channelLatch = new CountDownLatch(1); - Channel channel = Mockito.mock(Channel.class); + Channel channel = mock(Channel.class); Answer a = invocationOnMock -> { channelLatch.countDown(); return null; }; Mockito.doAnswer(a).when(channel).basicReject(Long.valueOf(taskInformation.getInboundMessageId()), false); - WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl(mockCallback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1); + WorkerQueueConsumerImpl impl = new WorkerQueueConsumerImpl( + mockCallback, metrics, consumerEvents, channel, publisherEvents, retryKey, 1, invalidKey, + dataStore, codec, () -> {}, mock(WorkerConfiguration.class)); DefaultRabbitConsumer consumer = new DefaultRabbitConsumer(consumerEvents, impl); Thread t = new Thread(consumer); t.start(); @@ -308,4 +378,18 @@ private static ArgumentCaptor> buildStringObjectMapCaptor() { return ArgumentCaptor.forClass(Map.class); } + + private static byte[] getNewTaskMessage() throws CodecException { + final var trackingInfo = new TrackingInfo("task1", new Date(), 1, "http://hello.com", "pipe", "to"); + return codec.serialise(new TaskMessage( + "task1", + "ACTUAL_CLASSIFIER", + 1, + "test123".getBytes(StandardCharsets.UTF_8), + TaskStatus.NEW_TASK, + new HashMap<>(), + "to", + trackingInfo + )); + } } diff --git a/worker-store-fs/pom.xml b/worker-store-fs/pom.xml index ea8ae1ee5..c2ff6b3cf 100644 --- a/worker-store-fs/pom.xml +++ b/worker-store-fs/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-store-fs - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-store-http/pom.xml b/worker-store-http/pom.xml index 6ac9543c9..4a7a30876 100644 --- a/worker-store-http/pom.xml +++ b/worker-store-http/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-store-http - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-store-mem/pom.xml b/worker-store-mem/pom.xml index 0023f3c0d..d25553782 100644 --- a/worker-store-mem/pom.xml +++ b/worker-store-mem/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-store-mem - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-store-s3/pom.xml b/worker-store-s3/pom.xml index 96036adac..841b32479 100644 --- a/worker-store-s3/pom.xml +++ b/worker-store-s3/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-store-s3 - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT diff --git a/worker-test/pom.xml b/worker-test/pom.xml index 238fb5b1b..51ac51e87 100644 --- a/worker-test/pom.xml +++ b/worker-test/pom.xml @@ -24,7 +24,7 @@ com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT worker-test @@ -88,6 +88,29 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-test + integration-test + + integration-test + + + + http://${docker.host.address}:${webdav.port}/webdav + ${docker.host.address} + ${rabbitmq.node.port} + guest + guest + + + + + + org.apache.maven.plugins @@ -250,7 +273,7 @@ - worker-test + PayloadOffloadingIT ${targetDockerRegistryPath}/worker-test:${project.version} ${projectDockerRegistry}/cafapi/opensuse-jre17 @@ -295,11 +318,110 @@ linux/amd64 - ${worker.adminport}:8081 - ${worker.debugport}:5005 + ${PayloadOffloadingIT.httpport}:8080 + ${PayloadOffloadingIT.adminport}:8081 + ${PayloadOffloadingIT.debugport}:5005 + + + PayloadOffloadingIT-in + PayloadOffloadingIT-out + PayloadOffloadingIT-reject + PayloadOffloadingIT-invalid + /srv/common/webdav + true + 1 + queues + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + + 1 + amqps + 5671 + /test-keystore + ca_certificate.pem + + + + webdav + keystore + + + + rabbitmq + + + true + + + + http://${docker.host.address}:${PayloadOffloadingIT.adminport} + + + 500 + + + + + PoisonMessageIT-1 + ${targetDockerRegistryPath}/worker-test:${project.version} + + linux/amd64 + + ${PoisonMessageIT-1.httpport}:8080 + ${PoisonMessageIT-1.adminport}:8081 + ${PoisonMessageIT-1.debugport}:5005 + + + PoisonMessageIT-in + PoisonMessageIT-out + PoisonMessageIT-reject + PoisonMessageIT-invalid + /srv/common/webdav + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + + 1 + amqps + 5671 + /test-keystore + ca_certificate.pem + + + + webdav + keystore + + + + rabbitmq + + + true + + + + http://${docker.host.address}:${PoisonMessageIT-1.adminport} + + + 500 + + + + + PoisonMessageIT-2 + ${targetDockerRegistryPath}/worker-test:${project.version} + + linux/amd64 + + ${PoisonMessageIT-2.httpport}:8080 + ${PoisonMessageIT-2.adminport}:8081 + ${PoisonMessageIT-2.debugport}:5005 - 10 + PoisonMessageIT-in + PoisonMessageIT-out + PoisonMessageIT-reject + PoisonMessageIT-invalid /srv/common/webdav -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 @@ -324,25 +446,34 @@ - http://${docker.host.address}:${worker.adminport} + http://${docker.host.address}:${PoisonMessageIT-2.adminport} 500 + + - worker-test-2 + PoisonMessageIT-Offloading-1 ${targetDockerRegistryPath}/worker-test:${project.version} linux/amd64 - ${worker.testhttpport2}:8080 - ${worker.testadminport2}:8081 - ${worker.testdebugport2}:5005 + ${PoisonMessageIT-Offloading-1.httpport}:8080 + ${PoisonMessageIT-Offloading-1.adminport}:8081 + ${PoisonMessageIT-Offloading-1.debugport}:5005 + PoisonMessageIT-Offloading-in + PoisonMessageIT-Offloading-out + PoisonMessageIT-Offloading-reject + PoisonMessageIT-Offloading-invalid /srv/common/webdav + true + 1 + queues -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 @@ -366,13 +497,114 @@ - http://${docker.host.address}:${worker.testadminport2} + http://${docker.host.address}:${PoisonMessageIT-Offloading-1.adminport} 500 + + PoisonMessageIT-Offloading-2 + ${targetDockerRegistryPath}/worker-test:${project.version} + + linux/amd64 + + ${PoisonMessageIT-Offloading-2.httpport}:8080 + ${PoisonMessageIT-Offloading-2.adminport}:8081 + ${PoisonMessageIT-Offloading-2.debugport}:5005 + + + PoisonMessageIT-Offloading-in + PoisonMessageIT-Offloading-out + PoisonMessageIT-Offloading-reject + PoisonMessageIT-Offloading-invalid + /srv/common/webdav + true + 1 + queues + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + + 1 + amqps + 5671 + /test-keystore + ca_certificate.pem + + + + webdav + keystore + + + + rabbitmq + + + true + + + + http://${docker.host.address}:${PoisonMessageIT-Offloading-2.adminport} + + + 500 + + + + + + + PayloadOffloadingIT-Terminal + ${targetDockerRegistryPath}/worker-test:${project.version} + + linux/amd64 + + ${PayloadOffloadingIT-Terminal.httpport}:8080 + ${PayloadOffloadingIT-Terminal.adminport}:8081 + ${PayloadOffloadingIT-Terminal.debugport}:5005 + + + PayloadOffloadingIT-Terminal-in + PayloadOffloadingIT-Terminal-out + PayloadOffloadingIT-Terminal-reject + PayloadOffloadingIT-Terminal-invalid + /srv/common/webdav + true + 1 + queues + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + + 1 + amqps + 5671 + /test-keystore + ca_certificate.pem + + + + webdav + keystore + + + + rabbitmq + + + true + + + + http://${docker.host.address}:${PayloadOffloadingIT-Terminal.adminport} + + + 500 + + + + worker-test-no-valid-cert @@ -380,9 +612,9 @@ linux/amd64 - ${worker.testhttpport3}:8080 - ${worker.testadminport3}:8081 - ${worker.testdebugport3}:5005 + ${worker-test-no-valid-cert.httpport}:8080 + ${worker-test-no-valid-cert.adminport}:8081 + ${worker-test-no-valid-cert.debugport}:5005 /srv/common/webdav @@ -407,7 +639,7 @@ - http://${docker.host.address}:${worker.testhttpport3}/health-check?type=READY + http://${docker.host.address}:${worker-test-no-valid-cert.httpport}/health-check?type=READY 503 @@ -428,14 +660,38 @@ 15672 25672 9090 - 8081 - 5005 - 8084 - 8082 - 8083 - 8085 - 5006 - 5007 + + 8080 + 8081 + 5010 + + 8082 + 8083 + 5011 + + 8084 + 8085 + 5012 + + 8086 + 8087 + 5013 + + 8088 + 8089 + 5014 + + 8090 + 8091 + 5015 + + 8092 + 8093 + 5016 + + 8094 + 8095 + 5017 diff --git a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorker.java b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorker.java index 780bf5204..f21bb0a4c 100644 --- a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorker.java +++ b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorker.java @@ -28,7 +28,7 @@ final class TestWorker implements Worker { - private static final byte[] TEST_WORKER_RESULT = "TestWorkerResult".getBytes(StandardCharsets.UTF_8); + private static final byte[] TEST_WORKER_RESULT = "TestWorkerResultTaskData".getBytes(StandardCharsets.UTF_8); private final TestWorkerConfiguration config; private final Codec codec; @@ -51,7 +51,7 @@ public WorkerResponse doWork() throws InterruptedException, TaskRejectedExceptio try { testWorkerTask = codec.deserialise(workerTask.getData(), TestWorkerTask.class); if(testWorkerTask.isPoison()){ - System.exit(1); + Runtime.getRuntime().halt(0); } } catch (final CodecException e) { throw new RuntimeException(e); @@ -65,7 +65,7 @@ public WorkerResponse doWork() throws InterruptedException, TaskRejectedExceptio } return new WorkerResponse( - outputQueue, + testWorkerTask.isTerminalWorker() ? null : outputQueue, TaskStatus.RESULT_SUCCESS, TEST_WORKER_RESULT, "TestWorkerResult", diff --git a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerConfiguration.java b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerConfiguration.java index bd9c26da6..e8507433b 100644 --- a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerConfiguration.java +++ b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerConfiguration.java @@ -22,6 +22,7 @@ final class TestWorkerConfiguration extends WorkerConfiguration { private String outputQueue; + private String invalidQueue; private int threads; public String getOutputQueue() @@ -45,4 +46,12 @@ public void setThreads(final int threads) { this.threads = threads; } + + public String getInvalidQueue() { + return invalidQueue; + } + + public void setInvalidQueue(String invalidQueue) { + this.invalidQueue = invalidQueue; + } } diff --git a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerFactory.java b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerFactory.java index 934930e80..e24698cb1 100644 --- a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerFactory.java +++ b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerFactory.java @@ -69,7 +69,7 @@ public TestWorkerConfiguration getWorkerConfiguration(){ @Override public String getInvalidTaskQueue() { - return config.getOutputQueue(); + return config.getInvalidQueue(); } @Nonnull diff --git a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerTask.java b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerTask.java index 4eb22c693..89966f45e 100644 --- a/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerTask.java +++ b/worker-test/src/main/java/com/github/workerframework/testworker/TestWorkerTask.java @@ -18,6 +18,8 @@ public class TestWorkerTask { private boolean isPoison; + private boolean isTerminalWorker; + /** * Configurable delay in processing a message */ @@ -31,6 +33,14 @@ public void setPoison(boolean poison) { isPoison = poison; } + public boolean isTerminalWorker() { + return isTerminalWorker; + } + + public void setTerminalWorker(boolean terminalWorker) { + isTerminalWorker = terminalWorker; + } + public int getDelaySeconds() { return delaySeconds; } diff --git a/worker-test/src/main/resources/com/github/workerframework/testworker/config/cfg~caf~worker~TestWorkerConfiguration.js b/worker-test/src/main/resources/com/github/workerframework/testworker/config/cfg~caf~worker~TestWorkerConfiguration.js index 66ea32019..824ed0902 100644 --- a/worker-test/src/main/resources/com/github/workerframework/testworker/config/cfg~caf~worker~TestWorkerConfiguration.js +++ b/worker-test/src/main/resources/com/github/workerframework/testworker/config/cfg~caf~worker~TestWorkerConfiguration.js @@ -17,6 +17,7 @@ workerName: "worker-test", workerVersion: "1.0.0", outputQueue: getenv("CAF_WORKER_OUTPUT_QUEUE") || "testworker-out", - rejectQueue: getenv("CAF_WORKER_REJECT_QUEUE") || "testworker-reject", + invalidQueue: getenv("CAF_WORKER_INVALID_QUEUE") || "testworker-invalid", + rejectQueue: getenv("CAF_WORKER_REJECT_QUEUE") || "worker-rejected", threads: getenv("CAF_WORKER_THREADS") || 1 }); diff --git a/worker-test/src/test/java/com/github/workerframework/workertest/GetWorkerNameIT.java b/worker-test/src/test/java/com/github/workerframework/workertest/GetWorkerNameIT.java deleted file mode 100644 index d4fec0902..000000000 --- a/worker-test/src/test/java/com/github/workerframework/workertest/GetWorkerNameIT.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2015-2025 Open Text. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.workerframework.workertest; - -import com.github.cafapi.common.api.Codec; -import com.github.cafapi.common.api.CodecException; -import com.github.cafapi.common.codecs.json.JsonCodec; -import com.github.workerframework.testworker.TestWorkerTask; -import com.github.workerframework.api.TaskMessage; -import com.github.workerframework.api.TaskStatus; -import com.github.workerframework.util.rabbitmq.QueueCreator; -import com.github.workerframework.util.rabbitmq.RabbitHeaders; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.AMQP; -import org.testng.annotations.Test; -import org.testng.Assert; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.HashMap; -import java.util.concurrent.TimeoutException; - -public class GetWorkerNameIT extends TestWorkerTestBase { - private static final String POISON_ERROR_MESSAGE = "could not process the item."; - private static final String WORKER_FRIENDLY_NAME = "TestWorker"; - private static final String TEST_WORKER_NAME = "testWorkerIdentifier"; - private static final String WORKER_IN = "worker-in"; - private static final String TESTWORKER_OUT = "testworker-out"; - private static final int TASK_NUMBER = 1; - private static final Codec codec = new JsonCodec(); - - @Test - public void getWorkerNameInPoisonMessageTest() throws IOException, TimeoutException, CodecException { - - try(final Connection connection = connectionFactory.newConnection()) { - - final Channel channel = connection.createChannel(); - - final Map args = new HashMap<>(); - args.put(QueueCreator.RABBIT_PROP_QUEUE_TYPE, QueueCreator.RABBIT_PROP_QUEUE_TYPE_QUORUM); - - channel.queueDeclare(TESTWORKER_OUT, true, false, false, args); - - final TestWorkerQueueConsumer poisonConsumer = new TestWorkerQueueConsumer(); - channel.basicConsume(TESTWORKER_OUT, true, poisonConsumer); - - final Map retryLimitHeaders = new HashMap<>(); - retryLimitHeaders.put(RabbitHeaders.RABBIT_HEADER_CAF_DELIVERY_COUNT, 2); - - final AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() - .headers(retryLimitHeaders) - .contentType("application/json") - .deliveryMode(2) - .build(); - - final TaskMessage requestTaskMessage = new TaskMessage(); - - final TestWorkerTask documentWorkerTask = new TestWorkerTask(); - documentWorkerTask.setPoison(false); - requestTaskMessage.setTaskId(Integer.toString(TASK_NUMBER)); - requestTaskMessage.setTaskClassifier(TEST_WORKER_NAME); - requestTaskMessage.setTaskApiVersion(TASK_NUMBER); - requestTaskMessage.setTaskStatus(TaskStatus.NEW_TASK); - requestTaskMessage.setTaskData(codec.serialise(documentWorkerTask)); - requestTaskMessage.setTo(WORKER_IN); - - channel.basicPublish("", WORKER_IN, properties, codec.serialise(requestTaskMessage)); - - try { - for (int i=0; i<100; i++){ - - Thread.sleep(100); - - if (poisonConsumer.getLastDeliveredBody() != null){ - break; - } - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - Assert.assertNotNull(poisonConsumer.getLastDeliveredBody()); - final TaskMessage decodedBody = codec.deserialise(poisonConsumer.getLastDeliveredBody(), TaskMessage.class); - final String taskData = new String(decodedBody.getTaskData(), StandardCharsets.UTF_8); - - Assert.assertTrue(taskData.contains(POISON_ERROR_MESSAGE)); - Assert.assertTrue(taskData.contains(WORKER_FRIENDLY_NAME)); - } - } -} diff --git a/worker-test/src/test/java/com/github/workerframework/workertest/PayloadOffloadingIT.java b/worker-test/src/test/java/com/github/workerframework/workertest/PayloadOffloadingIT.java new file mode 100644 index 000000000..f4dd598b9 --- /dev/null +++ b/worker-test/src/test/java/com/github/workerframework/workertest/PayloadOffloadingIT.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015-2025 Open Text. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.workerframework.workertest; + +import com.github.workerframework.api.TaskMessage; +import com.github.workerframework.testworker.TestWorkerTask; +import com.github.workerframework.util.rabbitmq.RabbitHeaders; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static com.github.workerframework.util.rabbitmq.RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF; + +public class PayloadOffloadingIT extends WorkerTestBase { + private static final String TEST_WORKER_NAME = "PayloadOffloadingIT"; + private static final String WORKER_IN = "PayloadOffloadingIT-in"; + private static final String WORKER_OUT = "PayloadOffloadingIT-out"; + private static final String WORKER_INVALID = "PayloadOffloadingIT-invalid"; + + private static final String TERMINAL_WORKER_IN = "PayloadOffloadingIT-Terminal-in"; + private static final String TERMINAL_WORKER_OUT = "PayloadOffloadingIT-Terminal-out"; + + @Test + public void checkOffloadedPayloadIsConsumedAndDeletedOnAck() throws Exception { + final TestWorkerTask documentWorkerTask = new TestWorkerTask(); + documentWorkerTask.setPoison(false); + + final TaskMessage taskMessage = getTaskMessage(TEST_WORKER_NAME, 1, documentWorkerTask, WORKER_IN); + final byte[] taskData = taskMessage.getTaskData(); + taskMessage.setTaskData(null); + + final var setupPayloadOffloadStorageRef = UUID.randomUUID().toString(); + writeFileToWebDav(setupPayloadOffloadStorageRef, taskData); + final var readWebDAVFile = readFileFromWebDAV(setupPayloadOffloadStorageRef); + Assert.assertTrue(readWebDAVFile.isPresent(), "The file should be present in the datastore"); + + + try(final Connection connection = connectionFactory.newConnection(); + final Channel channel = prepareChannel(connection)) { + createQueues(channel, WORKER_IN, WORKER_OUT); + + // Now we can send a message which expects to find the setupPayloadOffloadStorageRef. + final Map headers = new HashMap<>(); + headers.put(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF, setupPayloadOffloadStorageRef); + // this publish will result in an offloaded payload being recovered by the consumer. + // the body will be ignored as the offloaded payload will be used. + publish(channel, codec.serialise(taskMessage), headers, WORKER_IN); + + final TestWorkerQueueConsumer outboundConsumer = new TestWorkerQueueConsumer(); + consume(channel, outboundConsumer, WORKER_OUT); + final var consumedTaskMessageStorageRef = getTaskMessageStorageRef(outboundConsumer); + Assert.assertTrue(consumedTaskMessageStorageRef.isPresent(), "The payload offloading header was missing"); + Assert.assertNotEquals(consumedTaskMessageStorageRef.get(), setupPayloadOffloadStorageRef, "Storage refs should have been different"); + Assert.assertTrue(consumedTaskMessageStorageRef.get().contains(WORKER_OUT), "The storage reference does not contain the output queue name"); + + // The previously offloaded payload should now have been deleted when the inbound message is ack'd + final var storedSetupByteArrayOpt = readFileFromWebDAV(setupPayloadOffloadStorageRef); + Assert.assertTrue(storedSetupByteArrayOpt.isEmpty(), "setup message should not have been found"); + + // The outbound message should be present in the datastore + final var consumedByteArrayOpt = readFileFromWebDAV(consumedTaskMessageStorageRef.get()); + Assert.assertTrue(consumedByteArrayOpt.isPresent(), "Offloaded payload should have been found"); + Assert.assertEquals(new String(consumedByteArrayOpt.get(), StandardCharsets.UTF_8), "TestWorkerResultTaskData"); + } + } + + @Test + public void invalidOffloadedPayloadReference() throws Exception { + final TestWorkerTask documentWorkerTask = new TestWorkerTask(); + documentWorkerTask.setPoison(false); + + final TaskMessage taskMessage = getTaskMessage(TEST_WORKER_NAME, 1, documentWorkerTask, WORKER_IN); + taskMessage.setTaskData(null); + + try(final Connection connection = connectionFactory.newConnection(); + final Channel channel = prepareChannel(connection)) { + createQueues(channel, WORKER_IN, WORKER_OUT, WORKER_INVALID); + + // Now we can send a message with header that contains an invalid payload offloading reference. + final Map headers = new HashMap<>(); + final String invalidReference = UUID.randomUUID().toString(); + headers.put(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF, invalidReference); + publish(channel, codec.serialise(taskMessage), headers, WORKER_IN); + + final TestWorkerQueueConsumer invalidConsumer = new TestWorkerQueueConsumer(); + consume(channel, invalidConsumer, WORKER_INVALID); + + final TaskMessage invalidTaskMessage = codec.deserialise(invalidConsumer.getLastDeliveredBody(), + TaskMessage.class); + + Assert.assertEquals(invalidTaskMessage.getTaskId(), taskMessage.getTaskId()); + + Assert.assertNotNull(invalidConsumer.getHeaders().get(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_INVALID), + "RABBIT_HEADER_CAF_WORKER_INVALID is missing"); + + Assert.assertEquals( + invalidConsumer.getHeaders().get(RabbitHeaders.RABBIT_HEADER_CAF_WORKER_INVALID).toString(), + "Reference not found: /srv/common/webdav/" + invalidReference); + + Assert.assertNotNull(invalidConsumer.getHeaders().get(RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF), + "RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF is missing"); + + Assert.assertEquals( + invalidConsumer.getHeaders().get(RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF).toString(), + invalidReference); + } + } + + @Test + public void checkOffloadedPayloadIsDeletedOnTerminalWorker() throws Exception { + // First we need a message stored in the datastore + final TestWorkerTask terminalDocumentWorkerTask = new TestWorkerTask(); + terminalDocumentWorkerTask.setTerminalWorker(true); + + final TaskMessage taskMessage = getTaskMessage(TEST_WORKER_NAME, 3, terminalDocumentWorkerTask, + TERMINAL_WORKER_IN); + final byte[] taskData = taskMessage.getTaskData(); + taskMessage.setTaskData(null); + + final var storageRef = UUID.randomUUID().toString(); + writeFileToWebDav(storageRef, taskData); + final var readWebDAVFile = readFileFromWebDAV(storageRef); + Assert.assertTrue(readWebDAVFile.isPresent(), "The file should be present in the datastore"); + + try(final Connection connection = connectionFactory.newConnection(); + final Channel channel = prepareChannel(connection)) { + createQueues(channel, TERMINAL_WORKER_IN, TERMINAL_WORKER_OUT); + + // Now we can send a message which expects to find the taskMessageStorageRef. + final Map headers = new HashMap<>(); + headers.put(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF, storageRef); + + // this publish will result the worker thinking it a terminal worker. + publish(channel,codec.serialise(taskMessage), headers, TERMINAL_WORKER_IN); + + final TestWorkerQueueConsumer outboundConsumer = new TestWorkerQueueConsumer(); + consume(channel, outboundConsumer, TERMINAL_WORKER_OUT); + + Assert.assertNull(outboundConsumer.getLastDeliveredBody(), "The message should not have been output to the queue"); + + final var reReadWebDAVFile = readFileFromWebDAV(storageRef); + Assert.assertFalse(reReadWebDAVFile.isPresent(), "The file should be gone from the datastore"); + } + } + +} diff --git a/worker-test/src/test/java/com/github/workerframework/workertest/PoisonMessageIT.java b/worker-test/src/test/java/com/github/workerframework/workertest/PoisonMessageIT.java index d9eb45d2f..5b3985c2e 100644 --- a/worker-test/src/test/java/com/github/workerframework/workertest/PoisonMessageIT.java +++ b/worker-test/src/test/java/com/github/workerframework/workertest/PoisonMessageIT.java @@ -16,7 +16,6 @@ package com.github.workerframework.workertest; import com.github.cafapi.common.api.Codec; -import com.github.cafapi.common.api.CodecException; import com.github.cafapi.common.codecs.json.JsonCodec; import com.github.workerframework.testworker.TestWorkerTask; import com.github.workerframework.api.TaskMessage; @@ -28,74 +27,165 @@ import org.testng.Assert; import org.testng.annotations.Test; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeoutException; +import java.util.UUID; -public class PoisonMessageIT extends TestWorkerTestBase{ +import static com.github.workerframework.util.rabbitmq.RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF; + +public class PoisonMessageIT extends WorkerTestBase { private static final String POISON_ERROR_MESSAGE = "could not process the item."; private static final String WORKER_FRIENDLY_NAME = "TestWorker"; - private static final String TEST_WORKER_NAME = "testWorkerIdentifier"; - private static final String WORKER_IN = "worker-in"; - private static final String TESTWORKER_OUT = "testworker-out"; + private static final String TEST_WORKER_NAME = "PoisonMessageIT"; + private static final String POISON_MESSAGE_IT_IN = "PoisonMessageIT-in"; + private static final String POISON_MESSAGE_IT_OUT = "PoisonMessageIT-out"; + private static final String POISON_MESSAGE_IT_REJECT = "PoisonMessageIT-reject"; + + private static final String POISON_MESSAGE_IT_OFFLOADING_IN = "PoisonMessageIT-Offloading-in"; + private static final String POISON_MESSAGE_IT_OFFLOADING_OUT = "PoisonMessageIT-Offloading-out"; + private static final String POISON_MESSAGE_IT_OFFLOADING_REJECT = "PoisonMessageIT-Offloading-reject"; + private static final int TASK_NUMBER = 1; private static final Codec codec = new JsonCodec(); @Test - public void getWorkerNameInPoisonMessageTest() throws IOException, TimeoutException, CodecException { - - try(final Connection connection = connectionFactory.newConnection()) { + public void getWorkerNameInPoisonMessageTest() throws Exception { - final Channel channel = connection.createChannel(); - - final Map args = new HashMap<>(); - args.put(QueueCreator.RABBIT_PROP_QUEUE_TYPE, QueueCreator.RABBIT_PROP_QUEUE_TYPE_QUORUM); - channel.queueDeclare(WORKER_IN, true, false, false, args); + try(final Connection connection = connectionFactory.newConnection(); + final Channel channel = connection.createChannel()) { + createQueues(channel, POISON_MESSAGE_IT_IN, POISON_MESSAGE_IT_OUT, POISON_MESSAGE_IT_REJECT); final TaskMessage requestTaskMessage = new TaskMessage(); - final TestWorkerTask documentWorkerTask = new TestWorkerTask(); - documentWorkerTask.setPoison(true); + final TestWorkerTask testWorkerTask = new TestWorkerTask(); + testWorkerTask.setPoison(true); requestTaskMessage.setTaskId(Integer.toString(TASK_NUMBER)); requestTaskMessage.setTaskClassifier(TEST_WORKER_NAME); requestTaskMessage.setTaskApiVersion(TASK_NUMBER); requestTaskMessage.setTaskStatus(TaskStatus.NEW_TASK); - requestTaskMessage.setTaskData(codec.serialise(documentWorkerTask)); - requestTaskMessage.setTo(WORKER_IN); + requestTaskMessage.setTaskData(codec.serialise(testWorkerTask)); + requestTaskMessage.setTo(POISON_MESSAGE_IT_IN); final AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() .contentType("application/json") .deliveryMode(2) .build(); - channel.basicPublish("", WORKER_IN, properties, codec.serialise(requestTaskMessage)); - - final TestWorkerQueueConsumer poisonConsumer = new TestWorkerQueueConsumer(); - channel.queueDeclare(TESTWORKER_OUT, true, false, false, args); - - channel.basicConsume(TESTWORKER_OUT, false, poisonConsumer); - - try { - for (int i=0; i<10000; i++){ - - Thread.sleep(100); - - if (poisonConsumer.getLastDeliveredBody() != null){ - break; - } - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - Assert.assertNotNull(poisonConsumer.getLastDeliveredBody()); - final TaskMessage decodedBody = codec.deserialise(poisonConsumer.getLastDeliveredBody(), TaskMessage.class); - final String taskData = new String(decodedBody.getTaskData(), StandardCharsets.UTF_8); + channel.basicPublish("", POISON_MESSAGE_IT_IN, properties, codec.serialise(requestTaskMessage)); + + final TestWorkerQueueConsumer rejectConsumer = new TestWorkerQueueConsumer(); + + //Verify a copy was placed on the reject queue for later inspection + consume(channel, rejectConsumer, POISON_MESSAGE_IT_REJECT); + + Assert.assertNotNull(rejectConsumer.getLastDeliveredBody(), + "Message was not delivered to the queue before timeout or not at all."); + + final TaskMessage rejectTaskMessage = codec.deserialise(rejectConsumer.getLastDeliveredBody(), TaskMessage.class); + + Assert.assertEquals(rejectTaskMessage.getTaskStatus(), TaskStatus.RESULT_EXCEPTION); + + final TestWorkerTask copyOfTestWorkerTask = codec.deserialise(rejectTaskMessage.getTaskData(), TestWorkerTask.class); + + Assert.assertEquals(copyOfTestWorkerTask.isPoison(), testWorkerTask.isPoison()); + + final TestWorkerQueueConsumer outConsumer = new TestWorkerQueueConsumer(); + consume(channel, outConsumer, POISON_MESSAGE_IT_OUT); + //Verify a response was placed on the out queue for further processing + + Assert.assertNotNull(outConsumer.getLastDeliveredBody(), + "Message was not delivered to the queue before timeout or not at all."); + + final TaskMessage outputTaskMessage = codec.deserialise(outConsumer.getLastDeliveredBody(), TaskMessage.class); + final String outputTaskData = new String(outputTaskMessage.getTaskData(), StandardCharsets.UTF_8); + + Assert.assertTrue(outputTaskData.contains(WORKER_FRIENDLY_NAME)); + Assert.assertTrue(outputTaskData.contains(POISON_ERROR_MESSAGE)); + + Assert.assertEquals(outputTaskMessage.getTaskStatus(), TaskStatus.RESULT_SUCCESS); + + } + } - Assert.assertTrue(taskData.contains(POISON_ERROR_MESSAGE)); - Assert.assertTrue(taskData.contains(WORKER_FRIENDLY_NAME)); + @Test + public void offloadedPoisonMessageGoesToRejectFolderTest() throws Exception { + try(final Connection connection = connectionFactory.newConnection(); + final Channel channel = prepareChannel(connection)) { + createQueues(channel, + POISON_MESSAGE_IT_OFFLOADING_IN, POISON_MESSAGE_IT_OFFLOADING_OUT, POISON_MESSAGE_IT_OFFLOADING_REJECT); + + final TestWorkerTask testWorkerTask = new TestWorkerTask(); + testWorkerTask.setPoison(true); + + final var taskMessage = getTaskMessage( + TEST_WORKER_NAME, + TASK_NUMBER, + testWorkerTask, + POISON_MESSAGE_IT_OFFLOADING_IN + ); + + final byte[] taskData = taskMessage.getTaskData(); + taskMessage.setTaskData(null); + final var storageRef = UUID.randomUUID().toString(); + writeFileToWebDav(storageRef, taskData); + final HashMap publishHeaders = new HashMap<>(); + publishHeaders.put(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF, storageRef); + + // Publish a message to the test worker, the worker should detect this as a poison message + // because the payload is already offloaded it should remain offloaded and a copy should be placed on + // the reject queue. + publish( + channel, + codec.serialise(taskMessage), + publishHeaders, + POISON_MESSAGE_IT_OFFLOADING_IN + ); + + // Now we can consume the outgoing message from the reject queue. + final TestWorkerQueueConsumer rejectConsumer = new TestWorkerQueueConsumer(); + consume(channel, rejectConsumer, POISON_MESSAGE_IT_OFFLOADING_REJECT); + + final var rejectedTaskMessageStorageRef = getTaskMessageStorageRef(rejectConsumer); + + Assert.assertTrue(rejectedTaskMessageStorageRef.isPresent(), "The payload offloading header was missing"); + + // The rejected message should be present in the datastore + final var rejectedByteArrayOpt = readFileFromWebDAV(rejectedTaskMessageStorageRef.get()); + Assert.assertTrue(rejectedByteArrayOpt.isPresent(), "Offloaded payload should have been found"); + + final TestWorkerTask rejectTestWorkerTask = codec.deserialise(rejectedByteArrayOpt.get(), + TestWorkerTask.class); + + Assert.assertEquals(rejectTestWorkerTask.isPoison(), testWorkerTask.isPoison()); + + final TestWorkerQueueConsumer outConsumer = new TestWorkerQueueConsumer(); + consume(channel, outConsumer, POISON_MESSAGE_IT_OFFLOADING_OUT); + //Verify a response was placed on the out queue for further processing + + Assert.assertNotNull(outConsumer.getLastDeliveredBody(), + "Message was not delivered to the queue before timeout or not at all."); + + final TaskMessage outputTaskMessage = codec.deserialise(outConsumer.getLastDeliveredBody(), TaskMessage.class); + outputTaskMessage.setTaskStatus(TaskStatus.RESULT_SUCCESS); + + final var outputTaskMessageStorageRef = getTaskMessageStorageRef(outConsumer); + + Assert.assertTrue(outputTaskMessageStorageRef.isPresent(), "Offloaded payload should have been found"); + + Assert.assertNotEquals(outputTaskMessageStorageRef.get(), rejectedTaskMessageStorageRef.get(), + "The output reference should not match the rejected reference"); + + final var outputOffloadedPayloadBytes = readFileFromWebDAV(outputTaskMessageStorageRef.get()); + Assert.assertTrue(outputOffloadedPayloadBytes.isPresent(), "Offloaded payload bytes should have been found"); + + final var outputTaskData = new String(outputOffloadedPayloadBytes.get(), StandardCharsets.UTF_8); + + Assert.assertTrue(outputTaskData.contains(WORKER_FRIENDLY_NAME)); + Assert.assertTrue(outputTaskData.contains(POISON_ERROR_MESSAGE)); + + Assert.assertEquals(outputTaskMessage.getTaskStatus(), TaskStatus.RESULT_SUCCESS); + } } } diff --git a/worker-test/src/test/java/com/github/workerframework/workertest/ShutdownDeveloperTest.java b/worker-test/src/test/java/com/github/workerframework/workertest/ShutdownDeveloperIT.java similarity index 94% rename from worker-test/src/test/java/com/github/workerframework/workertest/ShutdownDeveloperTest.java rename to worker-test/src/test/java/com/github/workerframework/workertest/ShutdownDeveloperIT.java index 272bfbb6b..092e242d9 100644 --- a/worker-test/src/test/java/com/github/workerframework/workertest/ShutdownDeveloperTest.java +++ b/worker-test/src/test/java/com/github/workerframework/workertest/ShutdownDeveloperIT.java @@ -33,7 +33,7 @@ import java.util.Map; import java.util.concurrent.TimeoutException; -public class ShutdownDeveloperTest extends TestWorkerTestBase { +public class ShutdownDeveloperIT extends WorkerTestBase { private static final String TEST_WORKER_NAME = "testWorkerIdentifier"; private static final String WORKER_IN = "worker-in"; private static final String TESTWORKER_OUT = "testworker-out"; @@ -45,7 +45,8 @@ public class ShutdownDeveloperTest extends TestWorkerTestBase { public void shutdownTest() throws IOException, TimeoutException, CodecException { // Usage instructions - // Comment out the iages for test worker 2 and 3 in this module's pom.xml + // This test is only to be ran manually by developers never by automation. + // Comment out the images for test worker 2 and 3 in this module's pom.xml // Use mvn docker:start to start test worker // Remove the @Ignore and run the test to create 100 test messages // From a terminal execute docker stop -t 300 CONTAINER_ID diff --git a/worker-test/src/test/java/com/github/workerframework/workertest/TestWorkerTestBase.java b/worker-test/src/test/java/com/github/workerframework/workertest/TestWorkerTestBase.java deleted file mode 100644 index 81a37715f..000000000 --- a/worker-test/src/test/java/com/github/workerframework/workertest/TestWorkerTestBase.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2015-2025 Open Text. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.workerframework.workertest; - -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; - -import java.io.IOException; -import java.util.Objects; - -public class TestWorkerTestBase { - final protected ConnectionFactory connectionFactory; - private static final String CAF_RABBITMQ_HOST = "CAF_RABBITMQ_HOST"; - private static final String CAF_RABBITMQ_PORT = "CAF_RABBITMQ_PORT"; - private static final String CAF_RABBITMQ_USERNAME = "CAF_RABBITMQ_USERNAME"; - private static final String CAF_RABBITMQ_PASSWORD = "CAF_RABBITMQ_PASSWORD"; - - public TestWorkerTestBase() { - connectionFactory = new ConnectionFactory(); - connectionFactory.setHost(getEnvOrDefault(CAF_RABBITMQ_HOST, "localhost")); - connectionFactory.setPort(Integer.parseInt(getEnvOrDefault(CAF_RABBITMQ_PORT, "25672"))); - connectionFactory.setUsername(getEnvOrDefault(CAF_RABBITMQ_USERNAME, "guest")); - connectionFactory.setPassword(getEnvOrDefault(CAF_RABBITMQ_PASSWORD, "guest")); - connectionFactory.setVirtualHost("/"); - } - - private static String getEnvOrDefault(final String name, final String defaultValue) { - final String value = System.getenv(name); - - return value != null && !Objects.equals(value, "") ? value : defaultValue; - } - public static class TestWorkerQueueConsumer implements Consumer { - private byte[] lastDeliveredBody = null; - @Override - public void handleConsumeOk(String consumerTag) { - - } - - @Override - public void handleCancelOk(String consumerTag) { - - } - - @Override - public void handleCancel(String consumerTag) throws IOException { - - } - - @Override - public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { - - } - - @Override - public void handleRecoverOk(String consumerTag) { - - } - - public byte[] getLastDeliveredBody() { - return lastDeliveredBody; - } - - @Override - public void handleDelivery(final String consumerTag, final Envelope envelope, final AMQP.BasicProperties properties, - final byte[] body) throws IOException { - lastDeliveredBody = body; - } - } -} diff --git a/worker-test/src/test/java/com/github/workerframework/workertest/WorkerTestBase.java b/worker-test/src/test/java/com/github/workerframework/workertest/WorkerTestBase.java new file mode 100644 index 000000000..dad1c3c1d --- /dev/null +++ b/worker-test/src/test/java/com/github/workerframework/workertest/WorkerTestBase.java @@ -0,0 +1,231 @@ +/* + * Copyright 2015-2025 Open Text. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.workerframework.workertest; + +import com.github.cafapi.common.api.Codec; +import com.github.cafapi.common.api.CodecException; +import com.github.cafapi.common.codecs.json.JsonCodec; +import com.github.workerframework.api.TaskMessage; +import com.github.workerframework.api.TaskStatus; +import com.github.workerframework.api.TrackingInfo; +import com.github.workerframework.testworker.TestWorkerTask; +import com.github.workerframework.util.rabbitmq.QueueCreator; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; +import org.testng.Assert; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.github.workerframework.util.rabbitmq.RabbitHeaders.RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF; + +public class WorkerTestBase { + final protected ConnectionFactory connectionFactory; + private static final String CAF_RABBITMQ_HOST = "CAF_RABBITMQ_HOST"; + private static final String CAF_RABBITMQ_PORT = "CAF_RABBITMQ_PORT"; + private static final String CAF_RABBITMQ_USERNAME = "CAF_RABBITMQ_USERNAME"; + private static final String CAF_RABBITMQ_PASSWORD = "CAF_RABBITMQ_PASSWORD"; + private static final String WEBDAV_URL = System.getProperty("WEBDAV_URL", "http://localhost:9090/webdav"); + protected static final Codec codec = new JsonCodec(); + + public WorkerTestBase() { + connectionFactory = new ConnectionFactory(); + connectionFactory.setHost(System.getProperty(CAF_RABBITMQ_HOST, "localhost")); + connectionFactory.setPort(Integer.parseInt(System.getProperty(CAF_RABBITMQ_PORT, "25672"))); + connectionFactory.setUsername(System.getProperty(CAF_RABBITMQ_USERNAME, "guest")); + connectionFactory.setPassword(System.getProperty(CAF_RABBITMQ_PASSWORD, "guest")); + connectionFactory.setVirtualHost("/"); + } + + public void publish( + final Channel channel, + final byte[] taskMessage, + final Map headers, + final String workerIn + ) throws IOException { + final AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() + .contentType("application/json") + .deliveryMode(2) + .headers(headers) + .build(); + + channel.basicPublish("", workerIn, properties, taskMessage); + } + + public void consume( + final Channel channel, + final TestWorkerQueueConsumer messageConsumer, + final String workerOut + ) throws IOException { + final String consumerTag = channel.basicConsume(workerOut, false, messageConsumer); + try { + for (int i = 0; i < 1000; i++) { + + Thread.sleep(100); + + if (messageConsumer.getLastDeliveredBody() != null) { + break; + } + } + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + channel.basicCancel(consumerTag); + } + + public TaskMessage getTaskMessage( + final String testWorkerName, + final int taskNumber, + final TestWorkerTask documentWorkerTask, + final String workerIn + ) throws CodecException { + final var trackingInfo = new TrackingInfo(testWorkerName + taskNumber, new Date(), 1, null, "pipe", "to"); + final TaskMessage requestTaskMessage = new TaskMessage(); + requestTaskMessage.setTaskId(Integer.toString(taskNumber)); + requestTaskMessage.setTaskClassifier(testWorkerName); + requestTaskMessage.setTaskApiVersion(1); + requestTaskMessage.setTaskStatus(TaskStatus.NEW_TASK); + requestTaskMessage.setTaskData(codec.serialise(documentWorkerTask)); + requestTaskMessage.setTo(workerIn); + requestTaskMessage.setTracking(trackingInfo); + return requestTaskMessage; + } + + public Channel prepareChannel(final Connection connection) throws IOException + { + final Channel channel = connection.createChannel(); + return channel; + } + + void createQueues(final Channel channel, final String... queueNames) throws IOException { + final Map args = new HashMap<>(); + args.put(QueueCreator.RABBIT_PROP_QUEUE_TYPE, QueueCreator.RABBIT_PROP_QUEUE_TYPE_QUORUM); + for(final String queueName : queueNames) { + channel.queueDeclare(queueName, true, false, false, args); + } + + } + + /** + * This method will return the storage ref of the offloaded payload stored in the datastore on publish to the + * worker-out queue. + * @param messageConsumer + * @return + */ + public static Optional getTaskMessageStorageRef(final TestWorkerQueueConsumer messageConsumer) { + final Map outgoingHeaders = messageConsumer.getHeaders(); + final Optional outgoingTaskMessageStorageRef = outgoingHeaders.containsKey(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF) ? + Optional.of(outgoingHeaders.get(RABBIT_HEADER_CAF_PAYLOAD_OFFLOADING_STORAGE_REF).toString()) : + Optional.empty(); + return outgoingTaskMessageStorageRef; + } + + public static Optional readFileFromWebDAV(final String messageStorageRef) throws Exception { + final String fileUrl = String.format("%s/%s", WEBDAV_URL, messageStorageRef); + final URL url = new URL(fileUrl); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + return Optional.empty(); + } + + try (InputStream in = conn.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + final byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + } + return Optional.of(out.toByteArray()); + } + } + + public static void writeFileToWebDav(final String messageStorageRef, byte[] fileData) throws Exception { + final String fileUrl = String.format("%s/%s", WEBDAV_URL, messageStorageRef); + final URL url = new URL(fileUrl); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("PUT"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/octet-stream"); + + try (OutputStream out = conn.getOutputStream()) { + out.write(fileData); + } + + final int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_CREATED && responseCode != HttpURLConnection.HTTP_NO_CONTENT) { + Assert.fail("Failed to write file. HTTP response code: " + responseCode); + } + } + + public static class TestWorkerQueueConsumer implements Consumer { + private byte[] lastDeliveredBody = null; + private Map headers = null; + @Override + public void handleConsumeOk(String consumerTag) { + + } + + @Override + public void handleCancelOk(String consumerTag) { + + } + + @Override + public void handleCancel(String consumerTag) throws IOException { + + } + + @Override + public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { + + } + + @Override + public void handleRecoverOk(String consumerTag) { + + } + + public byte[] getLastDeliveredBody() { + return lastDeliveredBody; + } + + public Map getHeaders() { + return headers; + } + + @Override + public void handleDelivery(final String consumerTag, final Envelope envelope, final AMQP.BasicProperties properties, + final byte[] body) throws IOException { + lastDeliveredBody = body; + headers = properties.getHeaders(); + } + } +} diff --git a/worker-tracking-report/pom.xml b/worker-tracking-report/pom.xml index 41d1e4aa9..5e70cc125 100644 --- a/worker-tracking-report/pom.xml +++ b/worker-tracking-report/pom.xml @@ -22,12 +22,12 @@ 4.0.0 worker-tracking-report - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT com.github.workerframework worker-framework-aggregator - 9.1.2-SNAPSHOT + 10.0.0-SNAPSHOT