['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, ]; } }