|
2 | 2 |
|
3 | 3 | namespace Tests\Feature; |
4 | 4 |
|
| 5 | +use App\Models\WorkerBuildIdRollout; |
| 6 | +use App\Models\WorkerRegistration; |
5 | 7 | use App\Models\WorkflowNamespace; |
6 | 8 | use Illuminate\Foundation\Testing\RefreshDatabase; |
7 | | -use Illuminate\Support\Str; |
8 | 9 | use Illuminate\Support\Facades\Queue; |
| 10 | +use Illuminate\Support\Str; |
9 | 11 | use Tests\Fixtures\AwaitApprovalWorkflow; |
10 | | -use Tests\Fixtures\InternalParentWorkflow; |
11 | | -use Tests\Fixtures\InternalChildWorkflow; |
12 | 12 | use Tests\Fixtures\InteractiveCommandWorkflow; |
| 13 | +use Tests\Fixtures\InternalChildWorkflow; |
| 14 | +use Tests\Fixtures\InternalParentWorkflow; |
13 | 15 | use Tests\TestCase; |
14 | 16 | use Workflow\V2\Contracts\WorkflowControlPlane; |
15 | 17 | use Workflow\V2\Jobs\RunWorkflowTask; |
16 | 18 | use Workflow\V2\Models\WorkflowCommand; |
17 | | -use Workflow\V2\Models\WorkflowInstance; |
18 | 19 | use Workflow\V2\Models\WorkflowHistoryEvent; |
| 20 | +use Workflow\V2\Models\WorkflowInstance; |
19 | 21 | use Workflow\V2\Models\WorkflowLink; |
20 | 22 | use Workflow\V2\Models\WorkflowRunLineageEntry; |
21 | 23 | use Workflow\V2\Models\WorkflowTask; |
@@ -278,6 +280,119 @@ public function test_start_rejects_cross_namespace_workflow_id_without_leaking_t |
278 | 280 | $this->assertStringContainsString('another namespace', $message); |
279 | 281 | } |
280 | 282 |
|
| 283 | + public function test_start_blocks_drained_task_queue_without_an_active_worker_cohort(): void |
| 284 | + { |
| 285 | + Queue::fake(); |
| 286 | + |
| 287 | + $this->configureWorkflowTypes(); |
| 288 | + $this->createNamespace('default', 'Default namespace'); |
| 289 | + |
| 290 | + WorkerRegistration::query()->create([ |
| 291 | + 'worker_id' => 'draining-worker', |
| 292 | + 'namespace' => 'default', |
| 293 | + 'task_queue' => 'drain-queue', |
| 294 | + 'runtime' => 'php', |
| 295 | + 'sdk_version' => '1.0.0', |
| 296 | + 'build_id' => 'build-draining', |
| 297 | + 'supported_workflow_types' => ['tests.await-approval-workflow'], |
| 298 | + 'workflow_definition_fingerprints' => [], |
| 299 | + 'supported_activity_types' => [], |
| 300 | + 'max_concurrent_workflow_tasks' => 100, |
| 301 | + 'max_concurrent_activity_tasks' => 100, |
| 302 | + 'last_heartbeat_at' => now(), |
| 303 | + 'status' => 'draining', |
| 304 | + ]); |
| 305 | + |
| 306 | + WorkerBuildIdRollout::query()->create([ |
| 307 | + 'namespace' => 'default', |
| 308 | + 'task_queue' => 'drain-queue', |
| 309 | + 'build_id' => 'build-draining', |
| 310 | + 'drain_intent' => 'draining', |
| 311 | + 'drained_at' => now(), |
| 312 | + ]); |
| 313 | + |
| 314 | + $response = $this->withHeaders($this->apiHeaders()) |
| 315 | + ->postJson('/api/workflows', [ |
| 316 | + 'workflow_id' => 'wf-drained-start', |
| 317 | + 'workflow_type' => 'tests.await-approval-workflow', |
| 318 | + 'task_queue' => 'drain-queue', |
| 319 | + ]); |
| 320 | + |
| 321 | + $response->assertStatus(409) |
| 322 | + ->assertJsonPath('workflow_id', 'wf-drained-start') |
| 323 | + ->assertJsonPath('workflow_type', 'tests.await-approval-workflow') |
| 324 | + ->assertJsonPath('task_queue', 'drain-queue') |
| 325 | + ->assertJsonPath('reason', 'task_queue_draining') |
| 326 | + ->assertJsonPath('routing_status', 'draining') |
| 327 | + ->assertJsonPath('active_worker_count', 0) |
| 328 | + ->assertJsonPath('draining_worker_count', 1) |
| 329 | + ->assertJsonPath('stale_worker_count', 0) |
| 330 | + ->assertJsonPath('draining_build_ids.0', 'build-draining') |
| 331 | + ->assertJsonPath('drain_intent', 'draining'); |
| 332 | + |
| 333 | + $this->assertFalse(WorkflowInstance::query()->whereKey('wf-drained-start')->exists()); |
| 334 | + } |
| 335 | + |
| 336 | + public function test_start_allows_queue_with_active_and_draining_worker_cohorts(): void |
| 337 | + { |
| 338 | + Queue::fake(); |
| 339 | + |
| 340 | + $this->configureWorkflowTypes(); |
| 341 | + $this->createNamespace('default', 'Default namespace'); |
| 342 | + |
| 343 | + WorkerRegistration::query()->create([ |
| 344 | + 'worker_id' => 'active-worker', |
| 345 | + 'namespace' => 'default', |
| 346 | + 'task_queue' => 'mixed-queue', |
| 347 | + 'runtime' => 'php', |
| 348 | + 'sdk_version' => '1.0.0', |
| 349 | + 'build_id' => 'build-active', |
| 350 | + 'supported_workflow_types' => ['tests.await-approval-workflow'], |
| 351 | + 'workflow_definition_fingerprints' => [], |
| 352 | + 'supported_activity_types' => [], |
| 353 | + 'max_concurrent_workflow_tasks' => 100, |
| 354 | + 'max_concurrent_activity_tasks' => 100, |
| 355 | + 'last_heartbeat_at' => now(), |
| 356 | + 'status' => 'active', |
| 357 | + ]); |
| 358 | + |
| 359 | + WorkerRegistration::query()->create([ |
| 360 | + 'worker_id' => 'draining-worker', |
| 361 | + 'namespace' => 'default', |
| 362 | + 'task_queue' => 'mixed-queue', |
| 363 | + 'runtime' => 'php', |
| 364 | + 'sdk_version' => '1.0.0', |
| 365 | + 'build_id' => 'build-draining', |
| 366 | + 'supported_workflow_types' => ['tests.await-approval-workflow'], |
| 367 | + 'workflow_definition_fingerprints' => [], |
| 368 | + 'supported_activity_types' => [], |
| 369 | + 'max_concurrent_workflow_tasks' => 100, |
| 370 | + 'max_concurrent_activity_tasks' => 100, |
| 371 | + 'last_heartbeat_at' => now(), |
| 372 | + 'status' => 'draining', |
| 373 | + ]); |
| 374 | + |
| 375 | + WorkerBuildIdRollout::query()->create([ |
| 376 | + 'namespace' => 'default', |
| 377 | + 'task_queue' => 'mixed-queue', |
| 378 | + 'build_id' => 'build-draining', |
| 379 | + 'drain_intent' => 'draining', |
| 380 | + 'drained_at' => now(), |
| 381 | + ]); |
| 382 | + |
| 383 | + $response = $this->withHeaders($this->apiHeaders()) |
| 384 | + ->postJson('/api/workflows', [ |
| 385 | + 'workflow_id' => 'wf-mixed-drain-start', |
| 386 | + 'workflow_type' => 'tests.await-approval-workflow', |
| 387 | + 'task_queue' => 'mixed-queue', |
| 388 | + ]); |
| 389 | + |
| 390 | + $response->assertCreated() |
| 391 | + ->assertJsonPath('workflow_id', 'wf-mixed-drain-start') |
| 392 | + ->assertJsonPath('workflow_type', 'tests.await-approval-workflow') |
| 393 | + ->assertJsonPath('outcome', 'started_new'); |
| 394 | + } |
| 395 | + |
281 | 396 | public function test_signal_is_scoped_by_namespace(): void |
282 | 397 | { |
283 | 398 | Queue::fake(); |
|
0 commit comments