diff --git a/app/src/SimpleBatch/ExecuteCommand.php b/app/src/SimpleBatch/ExecuteCommand.php new file mode 100755 index 0000000..3f7bfc6 --- /dev/null +++ b/app/src/SimpleBatch/ExecuteCommand.php @@ -0,0 +1,70 @@ +getArgument('batchId')); + $options = [ + 'min' => intval($input->getOption('min')), + 'max' => intval($input->getOption('max')), + ]; + + $workflow = $this->workflowClient->newWorkflowStub( + SimpleBatchWorkflowInterface::class, + WorkflowOptions::new() + ->withWorkflowId(SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId) + ->withWorkflowExecutionTimeout(CarbonInterval::week()) + ); + + $output->writeln("Starting SimpleBatchWorkflow... "); + + try { + $run = $this->workflowClient->start($workflow, $batchId, $options); + $output->writeln( + sprintf( + 'Started: WorkflowID=%s, RunID=%s', + $run->getExecution()->getID(), + $run->getExecution()->getRunID(), + ) + ); + } catch (WorkflowExecutionAlreadyStartedException $e) { + $output->writeln('Still running'); + } + + return self::SUCCESS; + } +} diff --git a/app/src/SimpleBatch/README.md b/app/src/SimpleBatch/README.md new file mode 100644 index 0000000..d08a8da --- /dev/null +++ b/app/src/SimpleBatch/README.md @@ -0,0 +1,17 @@ +# Simple batch sample + +This sample demonstrates a simple batch processing. + +Run the following command to start a batch with a number of items randomly chosen between given min and max values: + +```bash +php ./app/app.php simple-batch:start [--min ] [--max ] +``` + +The minimum and maximum item count resp. default to 10 and 20. + +Run the following command to show the batch status: + +```bash +php ./app/app.php simple-batch:status +``` diff --git a/app/src/SimpleBatch/SimpleBatchActivity.php b/app/src/SimpleBatch/SimpleBatchActivity.php new file mode 100755 index 0000000..5b39701 --- /dev/null +++ b/app/src/SimpleBatch/SimpleBatchActivity.php @@ -0,0 +1,109 @@ +logger = new Logger(); + } + + /** + * @inheritDoc + */ + public function getBatchItemIds(int $batchId, array $options): array + { + $minItemCount = $options['min'] ?? 10; + $maxItemCount = $options['max'] ?? 20; + // Return an array with between $minItemCount and $maxItemCount entries. + return array_map(fn(int $itemId) => ($batchId % 100) * 1000 + $itemId, + range(101, random_int(100 + $minItemCount, 100 + $maxItemCount))); + } + + /** + * @inheritDoc + */ + public function processItem(int $itemId, int $batchId): int + { + $this->log("Processing item %d of batch %d.", $itemId, $batchId); + + $random = random_int(0, 90); + // Wait for max 1 second. + usleep($random * 10000); + + // Randomly throw an error. + if($random > 30) + { + throw new Exception(sprintf("Error while processing of item %d of batch %d.", $itemId, $batchId)); + } + return $random; + } + + /** + * @param int $batchId + * @param array $options + * + * @return void + */ + public function batchProcessingStarted(int $batchId, array $options): void + { + $this->log("Started processing of batch %d.", $batchId); + $this->log("Batch options: %s.", json_encode($options, JSON_PRETTY_PRINT)); + } + + /** + * @param int $batchId + * @param array $results + * + * @return void + */ + public function batchProcessingEnded(int $batchId, array $results): void + { + $this->log("Ended processing of batch %d.", $batchId); + $this->log("Batch results: %s.", json_encode($results, JSON_PRETTY_PRINT)); + } + + /** + * @inheritDoc + */ + public function itemProcessingStarted(int $itemId, int $batchId): void + { + $this->log("Started processing of item %d of batch %d.", $itemId, $batchId); + } + + /** + * @inheritDoc + */ + public function itemProcessingEnded(int $itemId, int $batchId): void + { + $this->log("Ended processing of item %d of batch %d.", $itemId, $batchId); + } + + /** + * @param string $message + * @param mixed ...$arg + */ + private function log(string $message, ...$arg) + { + $this->logger->debug(sprintf($message, ...$arg)); + } +} diff --git a/app/src/SimpleBatch/SimpleBatchActivityInterface.php b/app/src/SimpleBatch/SimpleBatchActivityInterface.php new file mode 100755 index 0000000..faf43cd --- /dev/null +++ b/app/src/SimpleBatch/SimpleBatchActivityInterface.php @@ -0,0 +1,66 @@ +batchActivity = Workflow::newActivityStub( + SimpleBatchActivityInterface::class, + ActivityOptions::new() + ->withStartToCloseTimeout(CarbonInterval::seconds(10)) + ->withScheduleToStartTimeout(CarbonInterval::seconds(10)) + ->withScheduleToCloseTimeout(CarbonInterval::minutes(30)) + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(100) + ->withInitialInterval(CarbonInterval::second(10)) + ->withMaximumInterval(CarbonInterval::seconds(100)) + ) + ); + } + + /** + * @inheritDoc + */ + public function start(int $batchId, array $options) + { + // Notify the batch processing start. + yield $this->batchActivity->batchProcessingStarted($batchId, $options); + + $itemIds = yield $this->batchActivity->getBatchItemIds($batchId, $options); + + $promises = []; + foreach($itemIds as $itemId) + { + // Set the batch item as pending. + $this->pending[$itemId] = true; + // Process the batch item. + $promises[$itemId] = Workflow::async( + function() use($itemId, $batchId) { + // Notify the item processing start. + yield $this->batchActivity->itemProcessingStarted($itemId, $batchId); + + // This activity randomly throws an exception. + $output = yield $this->batchActivity->processItem($itemId, $batchId); + + // Notify the item processing end. + yield $this->batchActivity->itemProcessingEnded($itemId, $batchId); + + return $output; + } + ) + ->then( + fn($output) => $this->results[$itemId] = [ + 'success' => true, + 'output' => $output, + ], + fn(Throwable $e) => $this->results[$itemId] = [ + 'success' => false, + 'message' => $e->getMessage(), + ] + ) + // We are calling always() instead of finally() because the Temporal PHP SDK depends on + // react/promise 2.9. Need to be changed to finally() after upgrade to react/promise 3.x. + ->always(fn() => $this->pending[$itemId] = false); + } + + // Wait for all the async calls to terminate. + yield Promise::all($promises); + + // Notify the batch processing end. + yield $this->batchActivity->batchProcessingEnded($batchId, $this->results); + + return $this->results; + } + + /** + * @inheritDoc + */ + public function getAvailableResults(): array + { + return $this->results; + } + + /** + * @inheritDoc + */ + public function getPendingTasks(): array + { + return array_keys(array_filter($this->pending, fn($pending) => $pending)); + } +} diff --git a/app/src/SimpleBatch/SimpleBatchWorkflowInterface.php b/app/src/SimpleBatch/SimpleBatchWorkflowInterface.php new file mode 100755 index 0000000..1cba166 --- /dev/null +++ b/app/src/SimpleBatch/SimpleBatchWorkflowInterface.php @@ -0,0 +1,31 @@ +getArgument('batchId')); + + /** @var SimpleBatchWorkflowInterface */ + $workflow = $this->workflowClient->newRunningWorkflowStub( + SimpleBatchWorkflowInterface::class, + SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId + ); + + $results = $workflow->getAvailableResults(); + $pending = $workflow->getPendingTasks(); + $failedCount = count(array_filter($results, fn(array $result) => !$result['success'])); + + $output->writeln("SimpleBatchWorkflow (id $batchId) status"); + $output->writeln(json_encode([ + 'count' => [ + 'pending' => count($pending), + 'ended' => count($results), + 'succeeded' => count($results) - $failedCount, + 'failed' => $failedCount, + ], + 'tasks' => [ + 'pending' => $pending, + 'results' => $results, + ], + ], JSON_PRETTY_PRINT)); + + return self::SUCCESS; + } +} diff --git a/app/src/SimpleBatchChild/ExecuteCommand.php b/app/src/SimpleBatchChild/ExecuteCommand.php new file mode 100755 index 0000000..b839f11 --- /dev/null +++ b/app/src/SimpleBatchChild/ExecuteCommand.php @@ -0,0 +1,70 @@ +getArgument('batchId')); + $options = [ + 'min' => intval($input->getOption('min')), + 'max' => intval($input->getOption('max')), + ]; + + $workflow = $this->workflowClient->newWorkflowStub( + SimpleBatchWorkflowInterface::class, + WorkflowOptions::new() + ->withWorkflowId(SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId) + ->withWorkflowExecutionTimeout(CarbonInterval::week()) + ); + + $output->writeln("Starting SimpleBatchWorkflow... "); + + try { + $run = $this->workflowClient->start($workflow, $batchId, $options); + $output->writeln( + sprintf( + 'Started: WorkflowID=%s, RunID=%s', + $run->getExecution()->getID(), + $run->getExecution()->getRunID(), + ) + ); + } catch (WorkflowExecutionAlreadyStartedException $e) { + $output->writeln('Still running'); + } + + return self::SUCCESS; + } +} diff --git a/app/src/SimpleBatchChild/README.md b/app/src/SimpleBatchChild/README.md new file mode 100644 index 0000000..1f3ccf5 --- /dev/null +++ b/app/src/SimpleBatchChild/README.md @@ -0,0 +1,19 @@ +# Simple batch with child workflows sample + +This sample demonstrates a simple batch processing, with child workflows. + +Unlike the `SimpleBatch` sample, using child workflows will prevent from exceeding [the workflow history limits](https://docs.temporal.io/self-hosted-guide/defaults). + +Run the following command to start a batch with a number of items randomly chosen between given min and max values: + +```bash +php ./app/app.php simple-batch-child:start [--min ] [--max ] +``` + +The minimum and maximum item count resp. default to 10 and 20. + +Run the following command to show the batch status: + +```bash +php ./app/app.php simple-batch-child:status +``` diff --git a/app/src/SimpleBatchChild/SimpleBatchActivity.php b/app/src/SimpleBatchChild/SimpleBatchActivity.php new file mode 100755 index 0000000..0debd41 --- /dev/null +++ b/app/src/SimpleBatchChild/SimpleBatchActivity.php @@ -0,0 +1,109 @@ +logger = new Logger(); + } + + /** + * @inheritDoc + */ + public function getBatchItemIds(int $batchId, array $options): array + { + $minItemCount = $options['min'] ?? 10; + $maxItemCount = $options['max'] ?? 20; + // Return an array with between $minItemCount and $maxItemCount entries. + return array_map(fn(int $itemId) => ($batchId % 100) * 1000 + $itemId, + range(101, random_int(100 + $minItemCount, 100 + $maxItemCount))); + } + + /** + * @inheritDoc + */ + public function processItem(int $itemId, int $batchId): int + { + $this->log("Processing item %d of batch %d.", $itemId, $batchId); + + $random = random_int(0, 90); + // Wait for max 1 second. + usleep($random % 10000); + + // Randomly throw an error. + if($random > 30) + { + throw new Exception(sprintf("Error while processing of item %d of batch %d.", $itemId, $batchId)); + } + return $random; + } + + /** + * @param int $batchId + * @param array $options + * + * @return void + */ + public function batchProcessingStarted(int $batchId, array $options): void + { + $this->log("Started processing of batch %d.", $batchId); + $this->log("Batch options: %s.", json_encode($options, JSON_PRETTY_PRINT)); + } + + /** + * @param int $batchId + * @param array $results + * + * @return void + */ + public function batchProcessingEnded(int $batchId, array $results): void + { + $this->log("Ended processing of batch %d.", $batchId); + $this->log("Batch results: %s.", json_encode($results, JSON_PRETTY_PRINT)); + } + + /** + * @inheritDoc + */ + public function itemProcessingStarted(int $itemId, int $batchId): void + { + $this->log("Started processing of item %d of batch %d.", $itemId, $batchId); + } + + /** + * @inheritDoc + */ + public function itemProcessingEnded(int $itemId, int $batchId): void + { + $this->log("Ended processing of item %d of batch %d.", $itemId, $batchId); + } + + /** + * @param string $message + * @param mixed ...$arg + */ + private function log(string $message, ...$arg) + { + $this->logger->debug(sprintf($message, ...$arg)); + } +} diff --git a/app/src/SimpleBatchChild/SimpleBatchActivityInterface.php b/app/src/SimpleBatchChild/SimpleBatchActivityInterface.php new file mode 100755 index 0000000..aa04c6b --- /dev/null +++ b/app/src/SimpleBatchChild/SimpleBatchActivityInterface.php @@ -0,0 +1,66 @@ +batchActivity = Workflow::newActivityStub( + SimpleBatchActivityInterface::class, + ActivityOptions::new() + ->withStartToCloseTimeout(CarbonInterval::seconds(10)) + ->withScheduleToStartTimeout(CarbonInterval::seconds(10)) + ->withScheduleToCloseTimeout(CarbonInterval::minutes(30)) + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(100) + ->withInitialInterval(CarbonInterval::second(10)) + ->withMaximumInterval(CarbonInterval::seconds(100)) + ) + ); + } + + /** + * @inheritDoc + */ + public function processItem(int $itemId, int $batchId) + { + // Set the item processing as started. + yield $this->batchActivity->itemProcessingStarted($itemId, $batchId); + + // This activity randomly throws an exception. + $output = yield $this->batchActivity->processItem($itemId, $batchId); + + // Set the item processing as ended. + yield $this->batchActivity->itemProcessingEnded($itemId, $batchId); + + return $output; + } +} diff --git a/app/src/SimpleBatchChild/SimpleBatchChildWorkflowInterface.php b/app/src/SimpleBatchChild/SimpleBatchChildWorkflowInterface.php new file mode 100644 index 0000000..b87e533 --- /dev/null +++ b/app/src/SimpleBatchChild/SimpleBatchChildWorkflowInterface.php @@ -0,0 +1,28 @@ +batchActivity = Workflow::newActivityStub( + SimpleBatchActivityInterface::class, + ActivityOptions::new() + ->withStartToCloseTimeout(CarbonInterval::seconds(10)) + ->withScheduleToStartTimeout(CarbonInterval::seconds(10)) + ->withScheduleToCloseTimeout(CarbonInterval::minutes(30)) + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(100) + ->withInitialInterval(CarbonInterval::second(10)) + ->withMaximumInterval(CarbonInterval::seconds(100)) + ) + ); + } + + /** + * @inheritDoc + */ + public function start(int $batchId, array $options) + { + // Notify the batch processing start. + yield $this->batchActivity->batchProcessingStarted($batchId, $options); + + $itemIds = yield $this->batchActivity->getBatchItemIds($batchId, $options); + + $promises = []; + foreach($itemIds as $itemId) + { + // Set the batch item as pending. + $this->pending[$itemId] = true; + + $promises[$itemId] = Workflow::newChildWorkflowStub(SimpleBatchChildWorkflowInterface::class) + ->processItem($itemId, $batchId) + ->then( + fn($output) => $this->results[$itemId] = [ + 'success' => true, + 'output' => $output, + ], + fn(Throwable $e) => $this->results[$itemId] = [ + 'success' => false, + 'message' => $e->getMessage(), + ] + ) + // We are calling always() instead of finally() because the Temporal PHP SDK depends on + // react/promise 2.9. Will need to change to finally() when upgrading to react/promise 3.x. + ->always(fn() => $this->pending[$itemId] = false); + } + + // Wait for all the async calls to terminate. + yield Promise::all($promises); + + // Notify the batch processing end. + yield $this->batchActivity->batchProcessingEnded($batchId, $this->results); + + return $this->results; + } + + /** + * @inheritDoc + */ + public function getAvailableResults(): array + { + return $this->results; + } + + /** + * @inheritDoc + */ + public function getPendingTasks(): array + { + return array_keys(array_filter($this->pending, fn($pending) => $pending)); + } +} diff --git a/app/src/SimpleBatchChild/SimpleBatchWorkflowInterface.php b/app/src/SimpleBatchChild/SimpleBatchWorkflowInterface.php new file mode 100755 index 0000000..5c8daed --- /dev/null +++ b/app/src/SimpleBatchChild/SimpleBatchWorkflowInterface.php @@ -0,0 +1,31 @@ +getArgument('batchId')); + + /** @var SimpleBatchWorkflowInterface */ + $workflow = $this->workflowClient->newRunningWorkflowStub( + SimpleBatchWorkflowInterface::class, + SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId + ); + + $results = $workflow->getAvailableResults(); + $pending = $workflow->getPendingTasks(); + $failedCount = count(array_filter($results, fn(array $result) => !$result['success'])); + + $output->writeln("SimpleBatchWorkflow (id $batchId) status"); + $output->writeln(json_encode([ + 'count' => [ + 'pending' => count($pending), + 'ended' => count($results), + 'succeeded' => count($results) - $failedCount, + 'failed' => $failedCount, + ], + 'tasks' => [ + 'pending' => $pending, + 'results' => $results, + ], + ], JSON_PRETTY_PRINT)); + + return self::SUCCESS; + } +}