Skip to content

Commit a83e666

Browse files
committed
Skip scheduled fires when rollout safety blocks start
Skip scheduled fires when rollout safety blocks start Treat rejected schedule starts as unavailable executions so schedule evaluation records rollout-safety blockers as skips instead of failures.
1 parent e11ac17 commit a83e666

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

‎app/Support/RemoteScheduleStarter.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use DateTimeInterface;
66
use Workflow\V2\Contracts\ScheduleWorkflowStarter;
7+
use Workflow\V2\Exceptions\WorkflowExecutionUnavailableException;
78
use Workflow\V2\Models\WorkflowSchedule;
89
use Workflow\V2\Support\ScheduleStartResult;
910

@@ -45,6 +46,29 @@ public function start(
4546
], static fn (mixed $v): bool => $v !== null);
4647

4748
$result = $this->startService->start($payload, $schedule->namespace);
49+
$started = is_bool($result['started'] ?? null)
50+
? $result['started']
51+
: ($result['reason'] ?? null) === null;
52+
53+
if (! $started) {
54+
$blockedReason = is_string($result['reason'] ?? null) && trim($result['reason']) !== ''
55+
? trim($result['reason'])
56+
: 'start_rejected';
57+
$message = is_string($result['message'] ?? null) && trim($result['message']) !== ''
58+
? trim($result['message'])
59+
: sprintf(
60+
'Schedule [%s] could not start workflow type [%s].',
61+
$schedule->schedule_id,
62+
$action['workflow_type'] ?? 'unknown',
63+
);
64+
65+
throw new WorkflowExecutionUnavailableException(
66+
'schedule_start',
67+
$schedule->schedule_id,
68+
$blockedReason,
69+
$message,
70+
);
71+
}
4872

4973
return new ScheduleStartResult(
5074
instanceId: (string) $result['workflow_id'],

‎tests/Feature/ScheduleEvaluateTest.php‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
use App\Models\WorkflowNamespace;
66
use App\Support\WorkflowStartService;
77
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Illuminate\Support\Facades\DB;
89
use Mockery\MockInterface;
910
use Tests\TestCase;
10-
use Workflow\V2\Contracts\WorkflowControlPlane;
1111
use Workflow\V2\Models\WorkflowInstance;
1212
use Workflow\V2\Models\WorkflowRun;
1313
use Workflow\V2\Models\WorkflowSchedule;
@@ -188,6 +188,41 @@ public function test_it_records_failures_and_returns_exit_code_1(): void
188188
$this->assertStringContainsString('Workflow type not found', $lastAction['reason']);
189189
}
190190

191+
public function test_it_records_rollout_safety_start_rejections_as_skips(): void
192+
{
193+
$this->fakeStartService(result: [
194+
'started' => false,
195+
'workflow_id' => 'wf-skip-compatibility-blocked',
196+
'run_id' => null,
197+
'workflow_type' => 'TestWorkflow',
198+
'outcome' => 'rejected_compatibility_blocked',
199+
'reason' => 'compatibility_blocked',
200+
'rejection_reason' => 'compatibility_blocked',
201+
'message' => 'Workflow instance [wf-skip-compatibility-blocked] cannot start.',
202+
]);
203+
204+
WorkflowSchedule::create([
205+
'schedule_id' => 'skip-compatibility-blocked',
206+
'namespace' => 'default',
207+
'spec' => ['cron_expressions' => ['* * * * *']],
208+
'action' => ['workflow_type' => 'TestWorkflow'],
209+
'next_fire_at' => now()->subMinute(),
210+
]);
211+
212+
$this->artisan('schedule:evaluate')
213+
->assertExitCode(0)
214+
->expectsOutputToContain('skipped');
215+
216+
$schedule = WorkflowSchedule::where('schedule_id', 'skip-compatibility-blocked')->firstOrFail();
217+
$this->assertSame('compatibility_blocked', $schedule->last_skip_reason);
218+
$this->assertSame(1, (int) $schedule->skipped_trigger_count);
219+
$this->assertSame(0, (int) $schedule->fires_count);
220+
$this->assertSame(0, (int) $schedule->failures_count);
221+
$this->assertNull($schedule->last_fired_at);
222+
$this->assertNull($schedule->latest_workflow_instance_id);
223+
$this->assertSame([], $schedule->recent_actions ?? []);
224+
}
225+
191226
// ── next_fire_at advancement ────────────────────────────────────
192227

193228
public function test_it_advances_next_fire_at_after_successful_fire(): void
@@ -552,7 +587,7 @@ public function test_it_normalizes_legacy_timeout_fields_when_firing(): void
552587
]);
553588

554589
// Bypass the model accessor to write raw legacy JSON
555-
\Illuminate\Support\Facades\DB::table('workflow_schedules')
590+
DB::table('workflow_schedules')
556591
->where('id', $schedule->id)
557592
->update(['action' => json_encode([
558593
'workflow_type' => 'TestWorkflow',
@@ -605,7 +640,7 @@ public function test_it_normalizes_legacy_timeout_fields_when_draining_buffer():
605640
]);
606641

607642
// Write raw legacy JSON directly
608-
\Illuminate\Support\Facades\DB::table('workflow_schedules')
643+
DB::table('workflow_schedules')
609644
->where('id', $schedule->id)
610645
->update(['action' => json_encode([
611646
'workflow_type' => 'TestWorkflow',

0 commit comments

Comments
 (0)