Skip to content

Commit 38f438a

Browse files
authored
feat: Implement migrator read and write methods (#133)
1 parent 6f220fa commit 38f438a

File tree

5 files changed

+477
-10
lines changed

5 files changed

+477
-10
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaunchDarkly\Impl\Migrations;
6+
7+
use Closure;
8+
use Exception;
9+
use LaunchDarkly\Impl\Util;
10+
use LaunchDarkly\Migrations\OperationResult;
11+
use LaunchDarkly\Migrations\OpTracker;
12+
use LaunchDarkly\Migrations\Origin;
13+
use LaunchDarkly\Types\Result;
14+
15+
/**
16+
* Utility class for executing migration operations while also tracking our
17+
* built-in migration measurements.
18+
*/
19+
class Executor
20+
{
21+
/**
22+
* @param Closure(mixed): Result $fn
23+
*/
24+
public function __construct(
25+
public readonly Origin $origin,
26+
private Closure $fn,
27+
private OpTracker $tracker,
28+
private bool $trackLatency,
29+
private bool $trackErrors,
30+
private mixed $payload,
31+
) {
32+
}
33+
34+
public function run(): OperationResult
35+
{
36+
$start = Util::currentTimeUnixMillis();
37+
38+
try {
39+
$result = ($this->fn)($this->payload);
40+
} catch (Exception $e) {
41+
$result = Result::error($e->getMessage(), $e);
42+
}
43+
44+
if ($this->trackLatency) {
45+
$this->tracker->latency($this->origin, Util::currentTimeUnixMillis() - $start);
46+
}
47+
48+
if ($this->trackErrors && !$result->isSuccessful()) {
49+
$this->tracker->error($this->origin);
50+
}
51+
52+
$this->tracker->invoked($this->origin);
53+
54+
return new OperationResult($this->origin, $result);
55+
}
56+
}

src/LaunchDarkly/Migrations/Migrator.php

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
namespace LaunchDarkly\Migrations;
66

7+
use LaunchDarkly\Impl\Migrations\Executor;
78
use LaunchDarkly\LDClient;
89
use LaunchDarkly\LDContext;
910
use LaunchDarkly\LDUser;
10-
use LaunchDarkly\Types\Result;
1111

1212
/**
1313
* Migrator is a class for performing a technology migration.
@@ -22,8 +22,8 @@ public function __construct(
2222
private ExecutionOrder $executionOrder,
2323
private MigrationConfig $readConfig,
2424
private MigrationConfig $writeConfig,
25-
private bool $measureLatency,
26-
private bool $measureErrors,
25+
private bool $trackLatency,
26+
private bool $trackErrors,
2727
) {
2828
}
2929

@@ -36,9 +36,28 @@ public function read(
3636
Stage $defaultStage,
3737
mixed $payload = null
3838
): OperationResult {
39-
// TODO(sc-219376): Implement later
39+
$variationResult = $this->client->migrationVariation($key, $context, $defaultStage);
40+
/** @var Stage */
41+
$stage = $variationResult['stage'];
42+
/** @var OpTracker */
43+
$tracker = $variationResult['tracker'];
44+
$tracker->operation(Operation::READ);
4045

41-
return new OperationResult(Origin::OLD, Result::success(null));
46+
$old = new Executor(Origin::OLD, $this->readConfig->old, $tracker, $this->trackLatency, $this->trackErrors, $payload);
47+
$new = new Executor(Origin::NEW, $this->readConfig->new, $tracker, $this->trackLatency, $this->trackErrors, $payload);
48+
49+
$result = match ($stage) {
50+
Stage::OFF => $old->run(),
51+
Stage::DUALWRITE => $old->run(),
52+
Stage::SHADOW => $this->readBoth($old, $new, $tracker),
53+
Stage::LIVE => $this->readBoth($new, $old, $tracker),
54+
Stage::RAMPDOWN => $new->run(),
55+
Stage::COMPLETE => $new->run(),
56+
};
57+
58+
// TODO(sc-219377): Emit the event here
59+
60+
return $result;
4261
}
4362

4463
/**
@@ -49,9 +68,65 @@ public function write(
4968
LDContext|LDUser $context,
5069
Stage $defaultStage,
5170
mixed $payload = null
52-
): OperationResult {
53-
// TODO(sc-219376): Implement later
54-
//
55-
return new OperationResult(Origin::OLD, Result::success(null));
71+
): WriteResult {
72+
$variationResult = $this->client->migrationVariation($key, $context, $defaultStage);
73+
/** @var Stage */
74+
$stage = $variationResult['stage'];
75+
/** @var OpTracker */
76+
$tracker = $variationResult['tracker'];
77+
$tracker->operation(Operation::READ);
78+
79+
$old = new Executor(Origin::OLD, $this->writeConfig->old, $tracker, $this->trackLatency, $this->trackErrors, $payload);
80+
$new = new Executor(Origin::NEW, $this->writeConfig->new, $tracker, $this->trackLatency, $this->trackErrors, $payload);
81+
82+
$writeResult = match ($stage) {
83+
Stage::OFF => new WriteResult($old->run()),
84+
Stage::DUALWRITE => $this->writeBoth($old, $new, $tracker),
85+
Stage::SHADOW => $this->writeBoth($old, $new, $tracker),
86+
Stage::LIVE => $this->writeBoth($new, $old, $tracker),
87+
Stage::RAMPDOWN => $this->writeBoth($new, $old, $tracker),
88+
Stage::COMPLETE => new WriteResult($new->run()),
89+
};
90+
91+
// TODO(sc-219377): Emit the event here
92+
93+
return $writeResult;
94+
}
95+
96+
private function readBoth(Executor $authoritative, Executor $nonauthoritative, OpTracker $tracker): OperationResult
97+
{
98+
// TODO(sc-219378): Add sampling to limit to 50% chance
99+
if ($this->executionOrder == ExecutionOrder::RANDOM) {
100+
$nonauthoritativeResult = $nonauthoritative->run();
101+
$authoritativeResult = $authoritative->run();
102+
} else {
103+
$authoritativeResult = $authoritative->run();
104+
$nonauthoritativeResult = $nonauthoritative->run();
105+
}
106+
107+
if ($this->readConfig->comparison === null) {
108+
return $authoritativeResult;
109+
}
110+
111+
if ($authoritativeResult->isSuccessful() && $nonauthoritativeResult->isSuccessful()) {
112+
$tracker->consistent(fn (): bool => ($this->readConfig->comparison)($authoritativeResult->value, $nonauthoritativeResult->value));
113+
}
114+
115+
return $authoritativeResult;
116+
}
117+
118+
private function writeBoth(Executor $authoritative, Executor $nonauthoritative, OpTracker $tracker): WriteResult
119+
{
120+
$authoritativeResult = $authoritative->run();
121+
$tracker->invoked($authoritative->origin);
122+
123+
if (!$authoritativeResult->isSuccessful()) {
124+
return new WriteResult($authoritativeResult);
125+
}
126+
127+
$nonauthoritativeResult = $nonauthoritative->run();
128+
$tracker->invoked($nonauthoritative->origin);
129+
130+
return new WriteResult($authoritativeResult, $nonauthoritativeResult);
56131
}
57132
}

src/LaunchDarkly/Migrations/OperationResult.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,32 @@
44

55
namespace LaunchDarkly\Migrations;
66

7+
use Exception;
78
use LaunchDarkly\Types\Result;
89

910
/**
1011
* The OperationResult pairs an origin with a result.
1112
*/
1213
class OperationResult
1314
{
15+
public readonly mixed $value;
16+
public readonly ?string $error;
17+
public readonly ?Exception $exception;
18+
1419
public function __construct(
1520
public readonly Origin $origin,
16-
public readonly Result $result
21+
private readonly Result $result
1722
) {
23+
$this->value = $result->value;
24+
$this->error = $result->error;
25+
$this->exception = $result->exception;
26+
}
27+
28+
/**
29+
* Determine whether this result represents success or failure.
30+
*/
31+
public function isSuccessful(): bool
32+
{
33+
return $this->result->isSuccessful();
1834
}
1935
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LaunchDarkly\Migrations;
6+
7+
/**
8+
* The WriteResult pairs an origin with a result.
9+
*/
10+
class WriteResult
11+
{
12+
public function __construct(
13+
public readonly OperationResult $authoritative,
14+
public readonly ?OperationResult $nonauthoritative = null
15+
) {
16+
}
17+
}

0 commit comments

Comments
 (0)