This commit is contained in:
2026-05-09 01:18:51 +02:00
parent 7116ee4619
commit 959970c150
132 changed files with 21310 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Temporal\DataEnrichment;
use App\Models\EnrichmentResult;
use App\Temporal\Shared\FaultSimulator;
use Temporal\Activity;
class DataEnrichmentActivity implements DataEnrichmentActivityInterface
{
public function geocodeAddress(array $record, array $simulationConfig = []): array
{
FaultSimulator::maybeApply($simulationConfig, 'geocodeAddress');
// Simulate geocoding API latency
usleep(mt_rand(200000, 500000));
return [
'lat' => round(mt_rand(2500, 4800) / 100, 6),
'lng' => round(mt_rand(-12200, -7100) / 100, 6),
'confidence' => round(mt_rand(70, 99) / 100, 2),
'attempt' => Activity::getInfo()->attempt,
];
}
public function validateEmail(array $record, array $simulationConfig = []): array
{
FaultSimulator::maybeApply($simulationConfig, 'validateEmail');
// Simulate email validation API latency
usleep(mt_rand(50000, 150000));
$email = $record['customer_email'] ?? 'unknown@example.com';
$isValid = filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
return [
'valid' => $isValid,
'disposable' => mt_rand(1, 100) <= 5,
'deliverable' => $isValid && mt_rand(1, 100) <= 95,
'attempt' => Activity::getInfo()->attempt,
];
}
public function calculateCreditScore(array $record, array $simulationConfig = []): array
{
FaultSimulator::maybeApply($simulationConfig, 'calculateCreditScore');
// Simulate credit scoring API latency
usleep(mt_rand(300000, 600000));
return [
'score' => mt_rand(300, 850),
'risk_level' => ['low', 'medium', 'high'][array_rand(['low', 'medium', 'high'])],
'attempt' => Activity::getInfo()->attempt,
];
}
public function saveEnrichedRecord(int $recordId, array $enrichments): bool
{
EnrichmentResult::updateOrCreate(
['record_type' => 'order', 'record_id' => $recordId],
[
'geocode_result' => $enrichments['geocode'] ?? null,
'email_valid' => $enrichments['email']['valid'] ?? null,
'credit_score' => $enrichments['credit']['score'] ?? null,
'enriched_at' => now(),
]
);
return true;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Temporal\DataEnrichment;
use Temporal\Activity\ActivityInterface;
use Temporal\Activity\ActivityMethod;
#[ActivityInterface]
interface DataEnrichmentActivityInterface
{
#[ActivityMethod]
public function geocodeAddress(array $record, array $simulationConfig = []): array;
#[ActivityMethod]
public function validateEmail(array $record, array $simulationConfig = []): array;
#[ActivityMethod]
public function calculateCreditScore(array $record, array $simulationConfig = []): array;
#[ActivityMethod]
public function saveEnrichedRecord(int $recordId, array $enrichments): bool;
}

View File

@@ -0,0 +1,198 @@
<?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,
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Temporal\DataEnrichment;
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
use Temporal\Workflow\QueryMethod;
#[WorkflowInterface]
interface DataEnrichmentWorkflowInterface
{
#[WorkflowMethod]
public function enrich(array $recordIds, array $simulationConfig = []);
#[QueryMethod]
public function getProgress(): array;
}