Files
temporalio-test/app/Temporal/DataEnrichment/DataEnrichmentWorkflow.php
2026-05-09 01:18:51 +02:00

199 lines
7.8 KiB
PHP

<?php
namespace App\Temporal\DataEnrichment;
use Carbon\CarbonInterval;
use Temporal\Activity\ActivityOptions;
use Temporal\Common\RetryOptions;
use Temporal\Workflow;
class DataEnrichmentWorkflow implements DataEnrichmentWorkflowInterface
{
private string $status = 'pending';
private int $totalRecords = 0;
private int $enrichedRecords = 0;
private int $failedRecords = 0;
private array $apiStats = [
'geocode' => ['calls' => 0, 'retries' => 0, 'rateLimits' => 0],
'email' => ['calls' => 0, 'retries' => 0, 'rateLimits' => 0],
'credit' => ['calls' => 0, 'retries' => 0, 'rateLimits' => 0],
];
/** @var DataEnrichmentActivityInterface - geocoding: patient retries */
private $geocodeStub;
/** @var DataEnrichmentActivityInterface - email: fast fail */
private $emailStub;
/** @var DataEnrichmentActivityInterface - credit: medium retries */
private $creditStub;
/** @var DataEnrichmentActivityInterface - save: minimal retries */
private $saveStub;
public function __construct()
{
// Geocoding: 5 attempts, 3s initial, 1.5 backoff (patient)
$this->geocodeStub = Workflow::newActivityStub(
DataEnrichmentActivityInterface::class,
ActivityOptions::new()
->withStartToCloseTimeout(CarbonInterval::minutes(2))
->withRetryOptions(
RetryOptions::new()
->withMaximumAttempts(5)
->withInitialInterval(CarbonInterval::seconds(3))
->withBackoffCoefficient(1.5)
)
);
// Email validation: 2 attempts, 500ms initial, 2.0 backoff (fast fail)
$this->emailStub = Workflow::newActivityStub(
DataEnrichmentActivityInterface::class,
ActivityOptions::new()
->withStartToCloseTimeout(CarbonInterval::seconds(30))
->withRetryOptions(
RetryOptions::new()
->withMaximumAttempts(2)
->withInitialInterval(CarbonInterval::milliseconds(500))
->withBackoffCoefficient(2.0)
)
);
// Credit scoring: 3 attempts, 2s initial, 2.0 backoff (medium)
$this->creditStub = Workflow::newActivityStub(
DataEnrichmentActivityInterface::class,
ActivityOptions::new()
->withStartToCloseTimeout(CarbonInterval::minutes(1))
->withRetryOptions(
RetryOptions::new()
->withMaximumAttempts(3)
->withInitialInterval(CarbonInterval::seconds(2))
->withBackoffCoefficient(2.0)
)
);
// Save: just 1 attempt
$this->saveStub = Workflow::newActivityStub(
DataEnrichmentActivityInterface::class,
ActivityOptions::new()
->withStartToCloseTimeout(CarbonInterval::seconds(15))
->withRetryOptions(
RetryOptions::new()->withMaximumAttempts(2)
)
);
}
public function enrich(array $recordIds, array $simulationConfig = []): \Generator
{
$this->totalRecords = count($recordIds);
$this->status = 'enriching';
try {
$batchSize = 5;
$batches = array_chunk($recordIds, $batchSize);
foreach ($batches as $batch) {
foreach ($batch as $recordId) {
// Run 3 enrichment APIs in parallel per record
$geocodePromise = Workflow::async(function () use ($recordId, $simulationConfig) {
try {
$result = yield $this->geocodeStub->geocodeAddress(
['id' => $recordId],
$simulationConfig
);
return ['success' => true, 'data' => (array) $result];
} catch (\Throwable $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
});
$emailPromise = Workflow::async(function () use ($recordId, $simulationConfig) {
try {
$result = yield $this->emailStub->validateEmail(
['id' => $recordId, 'customer_email' => "customer{$recordId}@example.com"],
$simulationConfig
);
return ['success' => true, 'data' => (array) $result];
} catch (\Throwable $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
});
$creditPromise = Workflow::async(function () use ($recordId, $simulationConfig) {
try {
$result = yield $this->creditStub->calculateCreditScore(
['id' => $recordId],
$simulationConfig
);
return ['success' => true, 'data' => (array) $result];
} catch (\Throwable $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
});
// Wait for all 3 to complete
$geocodeResult = (array) (yield $geocodePromise);
$emailResult = (array) (yield $emailPromise);
$creditResult = (array) (yield $creditPromise);
// Track per-API stats
$this->trackApiStat('geocode', $geocodeResult);
$this->trackApiStat('email', $emailResult);
$this->trackApiStat('credit', $creditResult);
// Save enriched data
$enrichments = [
'geocode' => $geocodeResult['success'] ? $geocodeResult['data'] : null,
'email' => $emailResult['success'] ? $emailResult['data'] : null,
'credit' => $creditResult['success'] ? $creditResult['data'] : null,
];
$anySuccess = $geocodeResult['success'] || $emailResult['success'] || $creditResult['success'];
if ($anySuccess) {
yield $this->saveStub->saveEnrichedRecord($recordId, $enrichments);
$this->enrichedRecords++;
} else {
$this->failedRecords++;
}
}
}
} catch (\Throwable $e) {
$this->status = 'failed';
throw $e;
}
$this->status = 'completed';
return [
'status' => 'completed',
'totalRecords' => $this->totalRecords,
'enrichedRecords' => $this->enrichedRecords,
'failedRecords' => $this->failedRecords,
'apiStats' => $this->apiStats,
];
}
private function trackApiStat(string $api, array $result): void
{
$this->apiStats[$api]['calls']++;
if ($result['success'] && isset($result['data']['attempt'])) {
$attempt = $result['data']['attempt'];
if ($attempt > 1) {
$this->apiStats[$api]['retries'] += ($attempt - 1);
}
}
}
public function getProgress(): array
{
return [
'status' => $this->status,
'totalRecords' => $this->totalRecords,
'enrichedRecords' => $this->enrichedRecords,
'failedRecords' => $this->failedRecords,
'apiStats' => $this->apiStats,
];
}
}