Skip to content

Commit 254475a

Browse files
committed
Separate provisioning from backfilling
1 parent 8d81d07 commit 254475a

10 files changed

Lines changed: 119 additions & 38 deletions

File tree

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ First, create a stable alias for the index in a migration:
145145
Index::putAlias('posts', 'posts_search');
146146
```
147147

148-
Start a deployment with the latest mapping and settings:
148+
Provision a candidate with the latest mapping and settings:
149149

150150
```php
151151
use DirectoryTree\OpenSearchAdapter\Indices\Mapping;
@@ -154,7 +154,7 @@ use DirectoryTree\OpenSearchMigrations\Deployer;
154154

155155
$deployer = app(Deployer::class);
156156

157-
$deployment = $deployer->start(
157+
$deployment = $deployer->provision(
158158
name: 'posts',
159159
alias: 'posts_search',
160160
configure: function (Mapping $mapping, Settings $settings) {
@@ -164,15 +164,18 @@ $deployment = $deployer->start(
164164
);
165165
```
166166

167-
The returned deployment exposes the physical candidate index for backfilling:
167+
The returned deployment exposes the physical candidate index for inspection while writes continue targeting only the active alias:
168168

169169
```php
170170
$deployment->candidateIndex;
171+
$deployment->writeIndexes(); // ['posts_search']
171172
```
172173

173-
While backfilling, send live writes and deletions to every deployment write index:
174+
Once the candidate has been inspected, begin backfilling before importing historical documents. This enables concurrent writes and deletions to the candidate:
174175

175176
```php
177+
$deployment = $deployer->beginBackfill('posts');
178+
176179
$deployment->writeIndexes();
177180
```
178181

src/Deployer.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public function __construct(
2525
) {}
2626

2727
/**
28-
* Create a candidate index and begin a deployment.
28+
* Provision a candidate index without enabling concurrent writes.
2929
*/
30-
public function start(
30+
public function provision(
3131
string $name,
3232
string $alias,
3333
?callable $configure = null,
@@ -48,7 +48,7 @@ public function start(
4848
$this->indexes->create($candidate, $configure);
4949

5050
return $this->deployments->save(
51-
Deployment::backfilling(
51+
Deployment::provisioned(
5252
name: $name,
5353
alias: $prefixedAlias,
5454
activeIndex: $activeIndex,
@@ -58,15 +58,31 @@ public function start(
5858
);
5959
}
6060

61+
/**
62+
* Begin backfilling a provisioned candidate index.
63+
*/
64+
public function beginBackfill(string $name): Deployment
65+
{
66+
$deployment = $this->deployments->findOrFail($name);
67+
68+
if ($deployment->status !== DeploymentStatus::Provisioned || ! $deployment->candidateIndex) {
69+
throw new DeploymentException('The candidate index must be provisioned before backfilling.');
70+
}
71+
72+
return $this->deployments->save(
73+
$deployment->beginBackfill()
74+
);
75+
}
76+
6177
/**
6278
* Mark a candidate index as ready for cutover.
6379
*/
6480
public function markReady(string $name): Deployment
6581
{
6682
$deployment = $this->deployments->findOrFail($name);
6783

68-
if (! $deployment->candidateIndex) {
69-
throw new DeploymentException('The deployment does not have a candidate index to mark as ready.');
84+
if ($deployment->status !== DeploymentStatus::Backfilling || ! $deployment->candidateIndex) {
85+
throw new DeploymentException('The candidate index must be backfilling before it can be marked as ready.');
7086
}
7187

7288
return $this->deployments->save(

src/Deployments/Deployment.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
class Deployment
1010
{
1111
/**
12-
* Create a deployment that is ready to be backfilled.
12+
* Create a deployment with a provisioned candidate index.
1313
*/
14-
public static function backfilling(
14+
public static function provisioned(
1515
string $name,
1616
string $alias,
1717
string $activeIndex,
@@ -27,7 +27,7 @@ public static function backfilling(
2727
activeIndex: $activeIndex,
2828
candidateIndex: $candidateIndex,
2929
previousIndex: null,
30-
status: DeploymentStatus::Backfilling,
30+
status: DeploymentStatus::Provisioned,
3131
readyAt: null,
3232
cutoverAt: null,
3333
createdAt: $now,
@@ -72,6 +72,21 @@ protected function __construct(
7272
public CarbonImmutable $updatedAt,
7373
) {}
7474

75+
/**
76+
* Begin backfilling the candidate index.
77+
*/
78+
public function beginBackfill(): static
79+
{
80+
return $this->transition(
81+
activeIndex: $this->activeIndex,
82+
candidateIndex: $this->candidateIndex,
83+
previousIndex: $this->previousIndex,
84+
status: DeploymentStatus::Backfilling,
85+
readyAt: $this->readyAt,
86+
cutoverAt: $this->cutoverAt,
87+
);
88+
}
89+
7590
/**
7691
* Mark the candidate index as ready for cutover.
7792
*/
@@ -184,9 +199,14 @@ public function retirePrevious(): static
184199
*/
185200
public function writeIndexes(): array
186201
{
202+
$candidate = in_array($this->status, [
203+
DeploymentStatus::Backfilling,
204+
DeploymentStatus::Ready,
205+
], true) ? $this->candidateIndex : null;
206+
187207
return array_values(array_unique(array_filter([
188208
$this->alias,
189-
$this->candidateIndex,
209+
$candidate,
190210
$this->previousIndex,
191211
])));
192212
}

src/Deployments/DeploymentStatus.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ enum DeploymentStatus: string
66
{
77
case Active = 'active';
88
case Backfilling = 'backfilling';
9+
case Provisioned = 'provisioned';
910
case Ready = 'ready';
1011
}

tests/Integration/Console/FreshCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
$index = Mockery::mock(IndexManagerInterface::class);
1919
$deployments = app(DeploymentRepository::class);
2020
$deployments->prepare();
21-
$deployments->save(Deployment::backfilling(
21+
$deployments->save(Deployment::provisioned(
2222
name: 'posts',
2323
alias: 'posts_search',
2424
activeIndex: 'posts_blue',

tests/Integration/Console/RefreshCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
it('refuses to refresh while managed deployments exist', function (): void {
3030
$deployments = app(DeploymentRepository::class);
3131
$deployments->prepare();
32-
$deployments->save(Deployment::backfilling(
32+
$deployments->save(Deployment::provisioned(
3333
name: 'posts',
3434
alias: 'posts_search',
3535
activeIndex: 'posts_blue',

tests/Integration/Console/ResetCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
it('refuses to reset while managed deployments exist', function (): void {
2929
$deployments = app(DeploymentRepository::class);
3030
$deployments->prepare();
31-
$deployments->save(Deployment::backfilling(
31+
$deployments->save(Deployment::provisioned(
3232
name: 'posts',
3333
alias: 'posts_search',
3434
activeIndex: 'posts_blue',

tests/Integration/Deployments/DeployerTest.php

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
uses(RefreshDatabase::class);
2121

22-
it('creates a versioned candidate and records deployment state', function (): void {
22+
it('provisions a versioned candidate before enabling concurrent writes', function (): void {
2323
Date::setTestNow('2026-07-12 12:34:56.789');
2424
config()->set('opensearch-migrations.index_name_prefix', 'test_');
2525
config()->set('opensearch-migrations.alias_name_prefix', 'test_');
@@ -43,7 +43,7 @@
4343

4444
$manager = new Deployer($repository, $indexes, $adapterIndexes, $openSearch);
4545

46-
$deployment = $manager->start('posts', 'posts_search', function (Mapping $mapping, Settings $settings): void {
46+
$deployment = $manager->provision('posts', 'posts_search', function (Mapping $mapping, Settings $settings): void {
4747
$mapping->text('title');
4848
$settings->index(['number_of_replicas' => 0]);
4949
});
@@ -58,13 +58,22 @@
5858
->and($deployment->alias)->toBe('test_posts_search')
5959
->and($deployment->activeIndex)->toBe('test_posts_blue')
6060
->and($deployment->candidateIndex)->toBe('test_posts_v20260712123456789')
61-
->and($deployment->status)->toBe(DeploymentStatus::Backfilling);
61+
->and($deployment->status)->toBe(DeploymentStatus::Provisioned)
62+
->and($deployment->writeIndexes())->toBe(['test_posts_search']);
63+
64+
$deployment = $manager->beginBackfill('posts');
65+
66+
expect($deployment->status)->toBe(DeploymentStatus::Backfilling)
67+
->and($deployment->writeIndexes())->toBe([
68+
'test_posts_search',
69+
'test_posts_v20260712123456789',
70+
]);
6271
});
6372

6473
it('marks a candidate ready and cuts over its alias atomically', function (): void {
6574
$repository = app(DeploymentRepository::class);
6675
$repository->prepare();
67-
$repository->save(Deployment::backfilling(
76+
$repository->save(Deployment::provisioned(
6877
name: 'posts',
6978
alias: 'posts_search',
7079
activeIndex: 'posts_blue',
@@ -98,6 +107,7 @@
98107

99108
$manager = new Deployer($repository, $indexes, $adapterIndexes, $openSearch);
100109

110+
expect($manager->beginBackfill('posts')->status)->toBe(DeploymentStatus::Backfilling);
101111
expect($manager->markReady('posts')->status)->toBe(DeploymentStatus::Ready);
102112

103113
$deployment = $manager->cutover('posts');
@@ -108,16 +118,37 @@
108118
->and($deployment->status)->toBe(DeploymentStatus::Active);
109119
});
110120

121+
it('does not mark a provisioned candidate ready before backfilling begins', function (): void {
122+
$repository = app(DeploymentRepository::class);
123+
$repository->prepare();
124+
$repository->save(Deployment::provisioned(
125+
name: 'posts',
126+
alias: 'posts_search',
127+
activeIndex: 'posts_blue',
128+
candidateIndex: 'posts_green',
129+
now: Date::now(),
130+
));
131+
132+
$adapterIndexes = new FakeIndexManager;
133+
$indexes = new IndexManagerAdapter($adapterIndexes);
134+
$openSearch = mock(OpenSearchManager::class);
135+
136+
$deployer = new Deployer($repository, $indexes, $adapterIndexes, $openSearch);
137+
138+
expect(fn () => $deployer->markReady('posts'))
139+
->toThrow(DeploymentException::class, 'The candidate index must be backfilling before it can be marked as ready.');
140+
});
141+
111142
it('finishes a cutover when the alias was already moved', function (): void {
112143
$repository = app(DeploymentRepository::class);
113144
$repository->prepare();
114-
$repository->save(Deployment::backfilling(
145+
$repository->save(Deployment::provisioned(
115146
name: 'posts',
116147
alias: 'posts_search',
117148
activeIndex: 'posts_blue',
118149
candidateIndex: 'posts_green',
119150
now: Date::now(),
120-
)->markReady(Date::now())->stageCutover());
151+
)->beginBackfill()->markReady(Date::now())->stageCutover());
121152

122153
$adapterIndexes = new FakeIndexManager;
123154
$indexes = new IndexManagerAdapter($adapterIndexes);
@@ -144,13 +175,13 @@
144175
it('rolls a deployment back atomically', function (): void {
145176
$repository = app(DeploymentRepository::class);
146177
$repository->prepare();
147-
$repository->save(Deployment::backfilling(
178+
$repository->save(Deployment::provisioned(
148179
name: 'posts',
149180
alias: 'posts_search',
150181
activeIndex: 'posts_blue',
151182
candidateIndex: 'posts_green',
152183
now: Date::now(),
153-
)->markReady(Date::now())->stageCutover()->completeCutover(Date::now()));
184+
)->beginBackfill()->markReady(Date::now())->stageCutover()->completeCutover(Date::now()));
154185

155186
$adapterIndexes = new FakeIndexManager;
156187
$indexes = new IndexManagerAdapter($adapterIndexes);
@@ -177,7 +208,7 @@
177208
it('cancels and deletes a candidate index', function (): void {
178209
$repository = app(DeploymentRepository::class);
179210
$repository->prepare();
180-
$repository->save(Deployment::backfilling(
211+
$repository->save(Deployment::provisioned(
181212
name: 'posts',
182213
alias: 'posts_search',
183214
activeIndex: 'posts_blue',
@@ -211,13 +242,13 @@
211242
it('retires the previous physical index', function (): void {
212243
$repository = app(DeploymentRepository::class);
213244
$repository->prepare();
214-
$repository->save(Deployment::backfilling(
245+
$repository->save(Deployment::provisioned(
215246
name: 'posts',
216247
alias: 'posts_search',
217248
activeIndex: 'posts_blue',
218249
candidateIndex: 'posts_green',
219250
now: Date::now(),
220-
)->markReady(Date::now())->stageCutover()->completeCutover(Date::now()));
251+
)->beginBackfill()->markReady(Date::now())->stageCutover()->completeCutover(Date::now()));
221252

222253
$adapterIndexes = new FakeIndexManager;
223254
$indexes = new IndexManagerAdapter($adapterIndexes);
@@ -234,13 +265,13 @@
234265
it('restores deployment state when retirement fails', function (): void {
235266
$repository = app(DeploymentRepository::class);
236267
$repository->prepare();
237-
$repository->save(Deployment::backfilling(
268+
$repository->save(Deployment::provisioned(
238269
name: 'posts',
239270
alias: 'posts_search',
240271
activeIndex: 'posts_blue',
241272
candidateIndex: 'posts_green',
242273
now: Date::now(),
243-
)->markReady(Date::now())->stageCutover()->completeCutover(Date::now()));
274+
)->beginBackfill()->markReady(Date::now())->stageCutover()->completeCutover(Date::now()));
244275

245276
$adapterIndexes = mock(AdapterIndexManagerInterface::class);
246277
$adapterIndexes->shouldReceive('delete')->once()->andThrow(new DeploymentException('Delete failed.'));

tests/Integration/Repositories/DeploymentRepositoryTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
$repository = app(DeploymentRepository::class);
3939
$repository->prepare();
4040

41-
$deployment = $repository->save(Deployment::backfilling(
41+
$deployment = $repository->save(Deployment::provisioned(
4242
name: 'posts',
4343
alias: 'posts_search',
4444
activeIndex: 'posts_blue',
@@ -51,7 +51,7 @@
5151
->and($deployment->activeIndex)->toBe('posts_blue')
5252
->and($deployment->candidateIndex)->toBe('posts_green')
5353
->and($deployment->previousIndex)->toBeNull()
54-
->and($deployment->status)->toBe(DeploymentStatus::Backfilling)
54+
->and($deployment->status)->toBe(DeploymentStatus::Provisioned)
5555
->and($deployment->createdAt->toDateTimeString())->toBe('2026-07-12 12:00:00');
5656
});
5757

@@ -61,7 +61,7 @@
6161
$repository = app(DeploymentRepository::class);
6262
$repository->prepare();
6363

64-
$repository->save(Deployment::backfilling(
64+
$repository->save(Deployment::provisioned(
6565
name: 'posts',
6666
alias: 'posts_search',
6767
activeIndex: 'posts_blue',
@@ -72,7 +72,7 @@
7272
Date::setTestNow('2026-07-12 13:00:00');
7373

7474
$deployment = $repository->save(
75-
$repository->findOrFail('posts')->markReady(Date::now())
75+
$repository->findOrFail('posts')->beginBackfill()->markReady(Date::now())
7676
);
7777

7878
expect($deployment->status)->toBe(DeploymentStatus::Ready)
@@ -100,7 +100,7 @@
100100

101101
expect($repository->exists())->toBeFalse();
102102

103-
$repository->save(Deployment::backfilling(
103+
$repository->save(Deployment::provisioned(
104104
name: 'posts',
105105
alias: 'posts_search',
106106
activeIndex: 'posts_blue',

0 commit comments

Comments
 (0)