Init
This commit is contained in:
72
app/Temporal/DataEnrichment/DataEnrichmentActivity.php
Normal file
72
app/Temporal/DataEnrichment/DataEnrichmentActivity.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
198
app/Temporal/DataEnrichment/DataEnrichmentWorkflow.php
Normal file
198
app/Temporal/DataEnrichment/DataEnrichmentWorkflow.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user