Init
This commit is contained in:
140
app/Temporal/UserMigration/UserMigrationActivity.php
Normal file
140
app/Temporal/UserMigration/UserMigrationActivity.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\UserMigration;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Temporal\Shared\FaultSimulator;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Temporal\Activity;
|
||||
|
||||
class UserMigrationActivity implements UserMigrationActivityInterface
|
||||
{
|
||||
public function fetchExternalUsers(int $count, array $simulationConfig = []): array
|
||||
{
|
||||
$users = [];
|
||||
$pageSize = 25;
|
||||
$totalPages = (int) ceil($count / $pageSize);
|
||||
|
||||
for ($page = 0; $page < $totalPages; $page++) {
|
||||
Activity::heartbeat(['page' => $page + 1, 'totalPages' => $totalPages]);
|
||||
|
||||
FaultSimulator::maybeApply($simulationConfig, 'fetchExternalUsers');
|
||||
|
||||
$start = $page * $pageSize + 1;
|
||||
$end = min(($page + 1) * $pageSize, $count);
|
||||
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$users[] = [
|
||||
'name' => 'User ' . $i,
|
||||
'email' => 'user_' . $i . '_' . Str::random(4) . '@example.com',
|
||||
'legacy_id' => 'LEG-' . $i,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function validateUsers(array $users, array $simulationConfig = []): array
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'validateUsers');
|
||||
|
||||
$validUsers = array_filter($users, function (array $user) {
|
||||
if (empty($user['name'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filter_var($user['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$validUsers = array_values($validUsers);
|
||||
|
||||
Log::info(sprintf(
|
||||
'Validated users: %d valid out of %d total',
|
||||
count($validUsers),
|
||||
count($users)
|
||||
));
|
||||
|
||||
return $validUsers;
|
||||
}
|
||||
|
||||
public function createAccounts(array $users, array $simulationConfig = []): array
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'createAccounts');
|
||||
|
||||
$created = 0;
|
||||
$failed = 0;
|
||||
$createdIds = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($users as $index => $userData) {
|
||||
Activity::heartbeat(sprintf('Creating account %d of %d', $index + 1, count($users)));
|
||||
|
||||
try {
|
||||
$user = User::create([
|
||||
'name' => $userData['name'],
|
||||
'email' => $userData['email'],
|
||||
'password' => Hash::make(Str::random(16)),
|
||||
]);
|
||||
|
||||
$createdIds[] = $user->id;
|
||||
$created++;
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
$errors[] = [
|
||||
'email' => $userData['email'],
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
|
||||
Log::warning(sprintf(
|
||||
'Failed to create account for %s: %s',
|
||||
$userData['email'],
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'created' => $created,
|
||||
'failed' => $failed,
|
||||
'created_ids' => $createdIds,
|
||||
'errors' => $errors,
|
||||
'attempt' => Activity::getInfo()->attempt,
|
||||
];
|
||||
}
|
||||
|
||||
public function sendWelcomeEmails(array $userIds, array $simulationConfig = []): bool
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'sendWelcomeEmails');
|
||||
|
||||
foreach ($userIds as $id) {
|
||||
Log::info(sprintf('Sending welcome email to user ID: %s', $id));
|
||||
usleep(50000);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function generateMigrationReport(int $totalUsers, int $processedUsers, int $failedUsers, array $simulationConfig = []): array
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'generateMigrationReport');
|
||||
|
||||
$successRate = $totalUsers > 0
|
||||
? round(($processedUsers / $totalUsers) * 100, 2)
|
||||
: 0.0;
|
||||
|
||||
return [
|
||||
'total_users' => $totalUsers,
|
||||
'processed_users' => $processedUsers,
|
||||
'failed_users' => $failedUsers,
|
||||
'success_rate' => $successRate,
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\UserMigration;
|
||||
|
||||
use Temporal\Activity\ActivityInterface;
|
||||
use Temporal\Activity\ActivityMethod;
|
||||
|
||||
#[ActivityInterface]
|
||||
interface UserMigrationActivityInterface
|
||||
{
|
||||
#[ActivityMethod]
|
||||
public function fetchExternalUsers(int $count, array $simulationConfig = []): array;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function validateUsers(array $users, array $simulationConfig = []): array;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function createAccounts(array $users, array $simulationConfig = []): array;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function sendWelcomeEmails(array $userIds, array $simulationConfig = []): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function generateMigrationReport(int $totalUsers, int $processedUsers, int $failedUsers, array $simulationConfig = []): array;
|
||||
}
|
||||
122
app/Temporal/UserMigration/UserMigrationWorkflow.php
Normal file
122
app/Temporal/UserMigration/UserMigrationWorkflow.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\UserMigration;
|
||||
|
||||
use Carbon\CarbonInterval;
|
||||
use Temporal\Activity\ActivityOptions;
|
||||
use Temporal\Common\RetryOptions;
|
||||
use Temporal\Workflow;
|
||||
|
||||
class UserMigrationWorkflow implements UserMigrationWorkflowInterface
|
||||
{
|
||||
private string $status = 'pending';
|
||||
private int $totalUsers = 0;
|
||||
private int $batchSize = 0;
|
||||
private int $processedUsers = 0;
|
||||
private int $failedUsers = 0;
|
||||
private bool $isPaused = false;
|
||||
private int $currentBatch = 0;
|
||||
private int $totalBatches = 0;
|
||||
private int $retryCount = 0;
|
||||
private int $rateLimitHits = 0;
|
||||
|
||||
/** @var UserMigrationActivityInterface */
|
||||
private $activityStub;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->activityStub = Workflow::newActivityStub(
|
||||
UserMigrationActivityInterface::class,
|
||||
ActivityOptions::new()
|
||||
->withStartToCloseTimeout(CarbonInterval::minutes(10))
|
||||
->withHeartbeatTimeout(CarbonInterval::seconds(60))
|
||||
->withRetryOptions(
|
||||
RetryOptions::new()
|
||||
->withMaximumAttempts(3)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function migrate(int $totalUsers, int $batchSize, array $simulationConfig = []): \Generator
|
||||
{
|
||||
$this->totalUsers = $totalUsers;
|
||||
$this->batchSize = $batchSize;
|
||||
$this->totalBatches = (int) ceil($totalUsers / $batchSize);
|
||||
|
||||
try {
|
||||
$this->status = 'fetching_users';
|
||||
$users = yield $this->activityStub->fetchExternalUsers($totalUsers, $simulationConfig);
|
||||
|
||||
$this->status = 'processing';
|
||||
|
||||
for ($i = 0; $i < $this->totalBatches; $i++) {
|
||||
if ($this->isPaused) {
|
||||
$this->status = 'paused';
|
||||
yield Workflow::await(fn () => !$this->isPaused);
|
||||
$this->status = 'processing';
|
||||
}
|
||||
|
||||
$this->currentBatch = $i + 1;
|
||||
|
||||
$batchUsers = array_slice($users, $i * $this->batchSize, $this->batchSize);
|
||||
|
||||
$validUsers = yield $this->activityStub->validateUsers($batchUsers, $simulationConfig);
|
||||
|
||||
$result = yield $this->activityStub->createAccounts($validUsers, $simulationConfig);
|
||||
|
||||
$this->processedUsers += $result['created'];
|
||||
$this->failedUsers += $result['failed'];
|
||||
|
||||
$attempt = $result['attempt'] ?? 1;
|
||||
if ($attempt > 1) {
|
||||
$this->retryCount += ($attempt - 1);
|
||||
}
|
||||
|
||||
yield $this->activityStub->sendWelcomeEmails($result['created_ids'], $simulationConfig);
|
||||
}
|
||||
|
||||
$this->status = 'generating_report';
|
||||
$report = yield $this->activityStub->generateMigrationReport(
|
||||
$this->totalUsers,
|
||||
$this->processedUsers,
|
||||
$this->failedUsers,
|
||||
$simulationConfig
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->status = 'failed';
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->status = 'completed';
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
public function pause(): void
|
||||
{
|
||||
$this->isPaused = true;
|
||||
}
|
||||
|
||||
public function resume(): void
|
||||
{
|
||||
$this->isPaused = false;
|
||||
}
|
||||
|
||||
public function getProgress(): array
|
||||
{
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'totalUsers' => $this->totalUsers,
|
||||
'processedUsers' => $this->processedUsers,
|
||||
'failedUsers' => $this->failedUsers,
|
||||
'isPaused' => $this->isPaused,
|
||||
'currentBatch' => $this->currentBatch,
|
||||
'totalBatches' => $this->totalBatches,
|
||||
'percentComplete' => $this->totalUsers > 0
|
||||
? round(($this->processedUsers / $this->totalUsers) * 100, 2)
|
||||
: 0.0,
|
||||
'retryCount' => $this->retryCount,
|
||||
'rateLimitHits' => $this->rateLimitHits,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\UserMigration;
|
||||
|
||||
use Temporal\Workflow\WorkflowInterface;
|
||||
use Temporal\Workflow\WorkflowMethod;
|
||||
use Temporal\Workflow\SignalMethod;
|
||||
use Temporal\Workflow\QueryMethod;
|
||||
|
||||
#[WorkflowInterface]
|
||||
interface UserMigrationWorkflowInterface
|
||||
{
|
||||
#[WorkflowMethod]
|
||||
public function migrate(int $totalUsers, int $batchSize, array $simulationConfig = []);
|
||||
|
||||
#[SignalMethod]
|
||||
public function pause(): void;
|
||||
|
||||
#[SignalMethod]
|
||||
public function resume(): void;
|
||||
|
||||
#[QueryMethod]
|
||||
public function getProgress(): array;
|
||||
}
|
||||
Reference in New Issue
Block a user