199 lines
7.8 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|