Init
This commit is contained in:
94
app/Temporal/ExternalApiSync/ExternalApiSyncActivity.php
Normal file
94
app/Temporal/ExternalApiSync/ExternalApiSyncActivity.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\ExternalApiSync;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Temporal\Shared\FaultSimulator;
|
||||
use Illuminate\Support\Str;
|
||||
use Temporal\Activity;
|
||||
|
||||
class ExternalApiSyncActivity implements ExternalApiSyncActivityInterface
|
||||
{
|
||||
private const PAGE_SIZE = 10;
|
||||
private const TOTAL_SIMULATED_RECORDS = 50;
|
||||
|
||||
public function refreshToken(array $simulationConfig = []): string
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'refreshToken');
|
||||
|
||||
// Simulate OAuth token refresh
|
||||
usleep(200000);
|
||||
|
||||
return 'tok_' . Str::random(32);
|
||||
}
|
||||
|
||||
public function fetchPage(string $cursor, string $token, array $simulationConfig = []): array
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'fetchPage');
|
||||
|
||||
Activity::heartbeat(['cursor' => $cursor, 'token_prefix' => substr($token, 0, 8)]);
|
||||
|
||||
// Simulate API latency
|
||||
usleep(150000);
|
||||
|
||||
$offset = (int) $cursor;
|
||||
$records = [];
|
||||
|
||||
for ($i = $offset; $i < min($offset + self::PAGE_SIZE, self::TOTAL_SIMULATED_RECORDS); $i++) {
|
||||
$records[] = [
|
||||
'external_id' => 'EXT-' . str_pad((string) ($i + 1), 5, '0', STR_PAD_LEFT),
|
||||
'name' => 'Synced Product ' . ($i + 1),
|
||||
'description' => 'Product synced from external API',
|
||||
'price' => round(mt_rand(500, 50000) / 100, 2),
|
||||
'stock' => mt_rand(10, 500),
|
||||
'category' => ['Electronics', 'Clothing', 'Home & Garden', 'Sports'][array_rand(['Electronics', 'Clothing', 'Home & Garden', 'Sports'])],
|
||||
];
|
||||
}
|
||||
|
||||
$nextOffset = $offset + self::PAGE_SIZE;
|
||||
$hasMore = $nextOffset < self::TOTAL_SIMULATED_RECORDS;
|
||||
|
||||
return [
|
||||
'records' => $records,
|
||||
'nextCursor' => (string) $nextOffset,
|
||||
'hasMore' => $hasMore,
|
||||
'attempt' => Activity::getInfo()->attempt,
|
||||
];
|
||||
}
|
||||
|
||||
public function transformAndStore(array $records, array $simulationConfig = []): array
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'transformAndStore');
|
||||
|
||||
$stored = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($records as $record) {
|
||||
$sku = 'SYNC-' . $record['external_id'];
|
||||
|
||||
try {
|
||||
Product::updateOrCreate(
|
||||
['sku' => $sku],
|
||||
[
|
||||
'name' => $record['name'],
|
||||
'description' => $record['description'],
|
||||
'price' => $record['price'],
|
||||
'stock_quantity' => $record['stock'],
|
||||
'category' => $record['category'],
|
||||
'status' => 'active',
|
||||
'imported_at' => now(),
|
||||
]
|
||||
);
|
||||
$stored++;
|
||||
} catch (\Throwable) {
|
||||
$skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'stored' => $stored,
|
||||
'skipped' => $skipped,
|
||||
'attempt' => Activity::getInfo()->attempt,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\ExternalApiSync;
|
||||
|
||||
use Temporal\Activity\ActivityInterface;
|
||||
use Temporal\Activity\ActivityMethod;
|
||||
|
||||
#[ActivityInterface]
|
||||
interface ExternalApiSyncActivityInterface
|
||||
{
|
||||
#[ActivityMethod]
|
||||
public function refreshToken(array $simulationConfig = []): string;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function fetchPage(string $cursor, string $token, array $simulationConfig = []): array;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function transformAndStore(array $records, array $simulationConfig = []): array;
|
||||
}
|
||||
123
app/Temporal/ExternalApiSync/ExternalApiSyncWorkflow.php
Normal file
123
app/Temporal/ExternalApiSync/ExternalApiSyncWorkflow.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\ExternalApiSync;
|
||||
|
||||
use Carbon\CarbonInterval;
|
||||
use Temporal\Activity\ActivityOptions;
|
||||
use Temporal\Common\RetryOptions;
|
||||
use Temporal\Workflow;
|
||||
|
||||
class ExternalApiSyncWorkflow implements ExternalApiSyncWorkflowInterface
|
||||
{
|
||||
private string $status = 'pending';
|
||||
private int $pagesFetched = 0;
|
||||
private int $recordsSynced = 0;
|
||||
private int $rateLimitHits = 0;
|
||||
private int $retryCount = 0;
|
||||
private string $currentCursor = '0';
|
||||
private bool $isPaused = false;
|
||||
|
||||
/** @var ExternalApiSyncActivityInterface */
|
||||
private $activityStub;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->activityStub = Workflow::newActivityStub(
|
||||
ExternalApiSyncActivityInterface::class,
|
||||
ActivityOptions::new()
|
||||
->withStartToCloseTimeout(CarbonInterval::minutes(5))
|
||||
->withHeartbeatTimeout(CarbonInterval::seconds(30))
|
||||
->withRetryOptions(
|
||||
RetryOptions::new()
|
||||
->withMaximumAttempts(5)
|
||||
->withInitialInterval(CarbonInterval::seconds(2))
|
||||
->withBackoffCoefficient(2.0)
|
||||
->withMaximumInterval(CarbonInterval::seconds(30))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function sync(string $apiEndpoint, array $simulationConfig = []): \Generator
|
||||
{
|
||||
try {
|
||||
// Step 1: Refresh API token
|
||||
$this->status = 'authenticating';
|
||||
$token = yield $this->activityStub->refreshToken($simulationConfig);
|
||||
|
||||
// Step 2: Paginated fetch loop
|
||||
$this->status = 'syncing';
|
||||
$hasMore = true;
|
||||
|
||||
while ($hasMore) {
|
||||
// Check for pause
|
||||
if ($this->isPaused) {
|
||||
$this->status = 'paused';
|
||||
yield Workflow::await(fn () => !$this->isPaused);
|
||||
$this->status = 'syncing';
|
||||
}
|
||||
|
||||
// Fetch one page
|
||||
$pageResult = yield $this->activityStub->fetchPage($this->currentCursor, $token, $simulationConfig);
|
||||
$pageResult = (array) $pageResult;
|
||||
|
||||
$attempt = $pageResult['attempt'] ?? 1;
|
||||
if ($attempt > 1) {
|
||||
$this->retryCount += ($attempt - 1);
|
||||
}
|
||||
|
||||
$this->pagesFetched++;
|
||||
$this->currentCursor = $pageResult['nextCursor'];
|
||||
$hasMore = $pageResult['hasMore'];
|
||||
|
||||
// Transform and store records
|
||||
if (!empty($pageResult['records'])) {
|
||||
$storeResult = yield $this->activityStub->transformAndStore($pageResult['records'], $simulationConfig);
|
||||
$storeResult = (array) $storeResult;
|
||||
|
||||
$this->recordsSynced += $storeResult['stored'] ?? 0;
|
||||
|
||||
$storeAttempt = $storeResult['attempt'] ?? 1;
|
||||
if ($storeAttempt > 1) {
|
||||
$this->retryCount += ($storeAttempt - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->status = 'failed';
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->status = 'completed';
|
||||
|
||||
return [
|
||||
'status' => 'completed',
|
||||
'pagesFetched' => $this->pagesFetched,
|
||||
'recordsSynced' => $this->recordsSynced,
|
||||
'rateLimitHits' => $this->rateLimitHits,
|
||||
'retryCount' => $this->retryCount,
|
||||
];
|
||||
}
|
||||
|
||||
public function pause(): void
|
||||
{
|
||||
$this->isPaused = true;
|
||||
}
|
||||
|
||||
public function resume(): void
|
||||
{
|
||||
$this->isPaused = false;
|
||||
}
|
||||
|
||||
public function getProgress(): array
|
||||
{
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'pagesFetched' => $this->pagesFetched,
|
||||
'recordsSynced' => $this->recordsSynced,
|
||||
'rateLimitHits' => $this->rateLimitHits,
|
||||
'retryCount' => $this->retryCount,
|
||||
'currentCursor' => $this->currentCursor,
|
||||
'isPaused' => $this->isPaused,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\ExternalApiSync;
|
||||
|
||||
use Temporal\Workflow\WorkflowInterface;
|
||||
use Temporal\Workflow\WorkflowMethod;
|
||||
use Temporal\Workflow\SignalMethod;
|
||||
use Temporal\Workflow\QueryMethod;
|
||||
|
||||
#[WorkflowInterface]
|
||||
interface ExternalApiSyncWorkflowInterface
|
||||
{
|
||||
#[WorkflowMethod]
|
||||
public function sync(string $apiEndpoint, array $simulationConfig = []);
|
||||
|
||||
#[SignalMethod]
|
||||
public function pause(): void;
|
||||
|
||||
#[SignalMethod]
|
||||
public function resume(): void;
|
||||
|
||||
#[QueryMethod]
|
||||
public function getProgress(): array;
|
||||
}
|
||||
Reference in New Issue
Block a user