|
19 | 19 |
|
20 | 20 | uses(RefreshDatabase::class); |
21 | 21 |
|
22 | | -it('creates a versioned candidate and records deployment state', function (): void { |
| 22 | +it('provisions a versioned candidate before enabling concurrent writes', function (): void { |
23 | 23 | Date::setTestNow('2026-07-12 12:34:56.789'); |
24 | 24 | config()->set('opensearch-migrations.index_name_prefix', 'test_'); |
25 | 25 | config()->set('opensearch-migrations.alias_name_prefix', 'test_'); |
|
43 | 43 |
|
44 | 44 | $manager = new Deployer($repository, $indexes, $adapterIndexes, $openSearch); |
45 | 45 |
|
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 { |
47 | 47 | $mapping->text('title'); |
48 | 48 | $settings->index(['number_of_replicas' => 0]); |
49 | 49 | }); |
|
58 | 58 | ->and($deployment->alias)->toBe('test_posts_search') |
59 | 59 | ->and($deployment->activeIndex)->toBe('test_posts_blue') |
60 | 60 | ->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 | + ]); |
62 | 71 | }); |
63 | 72 |
|
64 | 73 | it('marks a candidate ready and cuts over its alias atomically', function (): void { |
65 | 74 | $repository = app(DeploymentRepository::class); |
66 | 75 | $repository->prepare(); |
67 | | - $repository->save(Deployment::backfilling( |
| 76 | + $repository->save(Deployment::provisioned( |
68 | 77 | name: 'posts', |
69 | 78 | alias: 'posts_search', |
70 | 79 | activeIndex: 'posts_blue', |
|
98 | 107 |
|
99 | 108 | $manager = new Deployer($repository, $indexes, $adapterIndexes, $openSearch); |
100 | 109 |
|
| 110 | + expect($manager->beginBackfill('posts')->status)->toBe(DeploymentStatus::Backfilling); |
101 | 111 | expect($manager->markReady('posts')->status)->toBe(DeploymentStatus::Ready); |
102 | 112 |
|
103 | 113 | $deployment = $manager->cutover('posts'); |
|
108 | 118 | ->and($deployment->status)->toBe(DeploymentStatus::Active); |
109 | 119 | }); |
110 | 120 |
|
| 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 | + |
111 | 142 | it('finishes a cutover when the alias was already moved', function (): void { |
112 | 143 | $repository = app(DeploymentRepository::class); |
113 | 144 | $repository->prepare(); |
114 | | - $repository->save(Deployment::backfilling( |
| 145 | + $repository->save(Deployment::provisioned( |
115 | 146 | name: 'posts', |
116 | 147 | alias: 'posts_search', |
117 | 148 | activeIndex: 'posts_blue', |
118 | 149 | candidateIndex: 'posts_green', |
119 | 150 | now: Date::now(), |
120 | | - )->markReady(Date::now())->stageCutover()); |
| 151 | + )->beginBackfill()->markReady(Date::now())->stageCutover()); |
121 | 152 |
|
122 | 153 | $adapterIndexes = new FakeIndexManager; |
123 | 154 | $indexes = new IndexManagerAdapter($adapterIndexes); |
|
144 | 175 | it('rolls a deployment back atomically', function (): void { |
145 | 176 | $repository = app(DeploymentRepository::class); |
146 | 177 | $repository->prepare(); |
147 | | - $repository->save(Deployment::backfilling( |
| 178 | + $repository->save(Deployment::provisioned( |
148 | 179 | name: 'posts', |
149 | 180 | alias: 'posts_search', |
150 | 181 | activeIndex: 'posts_blue', |
151 | 182 | candidateIndex: 'posts_green', |
152 | 183 | now: Date::now(), |
153 | | - )->markReady(Date::now())->stageCutover()->completeCutover(Date::now())); |
| 184 | + )->beginBackfill()->markReady(Date::now())->stageCutover()->completeCutover(Date::now())); |
154 | 185 |
|
155 | 186 | $adapterIndexes = new FakeIndexManager; |
156 | 187 | $indexes = new IndexManagerAdapter($adapterIndexes); |
|
177 | 208 | it('cancels and deletes a candidate index', function (): void { |
178 | 209 | $repository = app(DeploymentRepository::class); |
179 | 210 | $repository->prepare(); |
180 | | - $repository->save(Deployment::backfilling( |
| 211 | + $repository->save(Deployment::provisioned( |
181 | 212 | name: 'posts', |
182 | 213 | alias: 'posts_search', |
183 | 214 | activeIndex: 'posts_blue', |
|
211 | 242 | it('retires the previous physical index', function (): void { |
212 | 243 | $repository = app(DeploymentRepository::class); |
213 | 244 | $repository->prepare(); |
214 | | - $repository->save(Deployment::backfilling( |
| 245 | + $repository->save(Deployment::provisioned( |
215 | 246 | name: 'posts', |
216 | 247 | alias: 'posts_search', |
217 | 248 | activeIndex: 'posts_blue', |
218 | 249 | candidateIndex: 'posts_green', |
219 | 250 | now: Date::now(), |
220 | | - )->markReady(Date::now())->stageCutover()->completeCutover(Date::now())); |
| 251 | + )->beginBackfill()->markReady(Date::now())->stageCutover()->completeCutover(Date::now())); |
221 | 252 |
|
222 | 253 | $adapterIndexes = new FakeIndexManager; |
223 | 254 | $indexes = new IndexManagerAdapter($adapterIndexes); |
|
234 | 265 | it('restores deployment state when retirement fails', function (): void { |
235 | 266 | $repository = app(DeploymentRepository::class); |
236 | 267 | $repository->prepare(); |
237 | | - $repository->save(Deployment::backfilling( |
| 268 | + $repository->save(Deployment::provisioned( |
238 | 269 | name: 'posts', |
239 | 270 | alias: 'posts_search', |
240 | 271 | activeIndex: 'posts_blue', |
241 | 272 | candidateIndex: 'posts_green', |
242 | 273 | now: Date::now(), |
243 | | - )->markReady(Date::now())->stageCutover()->completeCutover(Date::now())); |
| 274 | + )->beginBackfill()->markReady(Date::now())->stageCutover()->completeCutover(Date::now())); |
244 | 275 |
|
245 | 276 | $adapterIndexes = mock(AdapterIndexManagerInterface::class); |
246 | 277 | $adapterIndexes->shouldReceive('delete')->once()->andThrow(new DeploymentException('Delete failed.')); |
|
0 commit comments