Skip to content

Commit d1e2378

Browse files
Relax workflow cache admission to warning-only diagnostics
Relax cache admission to warning-only diagnostics
1 parent c953e8d commit d1e2378

8 files changed

Lines changed: 256 additions & 16 deletions

File tree

src/V2/Support/BackendCapabilities.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,19 @@ private static function cache(?string $configuredStore = null): array
175175
if ($store === null || $driver === null) {
176176
$issues[] = self::issue(
177177
'cache',
178-
'error',
178+
'warning',
179179
'cache_store_missing',
180-
'Workflow v2 requires a configured cache store for worker-loop throttles and compatibility heartbeat fallbacks.',
180+
'No cache store is configured. Wake acceleration, repair-loop throttles, and cache-backed fleet fallbacks may be degraded, but durable dispatch remains correct.',
181181
);
182182
} elseif (! $lockSupported) {
183183
$issues[] = self::issue(
184184
'cache',
185-
'error',
185+
'warning',
186186
'cache_locks_unsupported',
187-
sprintf('The [%s] cache store does not advertise Laravel atomic lock support.', $store),
187+
sprintf(
188+
'The [%s] cache store does not advertise Laravel atomic lock support. Wake acceleration remains optional, but repair-loop throttles and cache-backed fleet fallbacks may be degraded.',
189+
$store,
190+
),
188191
);
189192
}
190193

src/V2/Support/HealthCheck.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,23 @@ private static function backendCheck(array $backend): array
6666
{
6767
$issues = is_array($backend['issues'] ?? null) ? $backend['issues'] : [];
6868
$supported = BackendCapabilities::isSupported($backend);
69+
$severity = is_string($backend['severity'] ?? null) ? $backend['severity'] : ($supported ? 'ok' : 'error');
6970

7071
return self::check(
7172
'backend_capabilities',
72-
$supported ? 'ok' : 'error',
73-
$supported
74-
? 'The configured database, queue, and cache backends satisfy the v2 capability contract.'
75-
: 'One or more configured v2 backend capabilities are unsupported.',
73+
match ($severity) {
74+
'error' => 'error',
75+
'warning' => 'warning',
76+
default => 'ok',
77+
},
78+
match ($severity) {
79+
'error' => 'One or more configured v2 backend capabilities are unsupported.',
80+
'warning' => 'One or more configured v2 backend capabilities are degraded but non-blocking.',
81+
default => 'The configured database, queue, cache, and codec backends satisfy the v2 capability contract.',
82+
},
7683
self::CATEGORY_CORRECTNESS,
7784
[
85+
'severity' => $severity,
7886
'issue_count' => count($issues),
7987
'issues' => $issues,
8088
],

src/V2/Support/LongPollCacheValidator.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
use Illuminate\Support\Str;
1111

1212
/**
13-
* Validates cache backend configuration for long-poll coordination in multi-node deployments.
13+
* Validates cache backend configuration for long-poll wake acceleration in multi-node deployments.
1414
*
15-
* Multi-node deployments require shared cache backends (Redis, database, Memcached)
16-
* for wake signal propagation. File-based cache is per-node and cannot coordinate
17-
* across nodes.
15+
* Shared cache backends (Redis, database, Memcached) are required only for
16+
* cross-node wake propagation. Durable dispatch correctness does not depend on
17+
* this layer, but latency and rollout-safety diagnostics do.
1818
*
1919
* @see \Workflow\V2\Contracts\LongPollWakeStore
2020
*/
@@ -53,7 +53,7 @@ public function validateMultiNodeCapable(CacheRepository $cache): array
5353
}
5454

5555
/**
56-
* Check if current configuration is safe for multi-node deployment.
56+
* Check if current configuration is safe for multi-node wake acceleration.
5757
*
5858
* @return array{
5959
* safe: bool,
@@ -82,7 +82,7 @@ public function checkMultiNodeSafety(CacheRepository $cache, bool $multiNode): a
8282
return [
8383
'safe' => false,
8484
'message' => sprintf(
85-
'Multi-node deployment detected (DW_V2_MULTI_NODE=true) but cache backend is "%s". %s',
85+
'Multi-node wake acceleration is enabled (DW_V2_MULTI_NODE=true) but cache backend is "%s". %s',
8686
$validation['backend'],
8787
$validation['reason']
8888
),

src/V2/Support/ReadinessContract.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static function definition(): array
6767
'dispatch' => [
6868
'authority' => BackendCapabilities::class . '::snapshot',
6969
'readiness_key' => 'supported',
70-
'gate' => 'No database, queue, cache, codec, or structural-limit issue has error severity.',
70+
'gate' => 'No database, queue, codec, or structural-limit issue has error severity; cache issues remain non-blocking warnings.',
7171
'unready_behavior' => 'Task dispatch does not claim unsupported durable work; operators see backend_capabilities errors.',
7272
],
7373
'claim' => [
@@ -107,6 +107,8 @@ public static function definition(): array
107107
],
108108
'cache' => [
109109
'required_capabilities' => ['atomic_locks'],
110+
'dispatch_blocking_severity' => 'warning',
111+
'role' => 'acceleration_only',
110112
],
111113
'codec' => [
112114
'default_for_new_v2_runs' => 'avro',
@@ -130,7 +132,7 @@ public static function forBackendCapabilities(): array
130132
$contract['effective_states'] = [
131133
'dispatch' => [
132134
'state' => 'evaluated_by_backend_capabilities_snapshot',
133-
'blocking_rule' => 'Any error-severity backend issue makes supported=false.',
135+
'blocking_rule' => 'Any error-severity database, queue, codec, or structural-limit issue makes supported=false; cache diagnostics stay warning-only.',
134136
],
135137
'claim' => [
136138
'state' => 'evaluated_per_task_queue_connection',
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Support;
6+
7+
use Illuminate\Contracts\Cache\Store;
8+
9+
final class NonLockingCacheStore implements Store
10+
{
11+
/**
12+
* @var array<string, mixed>
13+
*/
14+
private array $values = [];
15+
16+
public function get($key)
17+
{
18+
return $this->values[$key] ?? null;
19+
}
20+
21+
public function many(array $keys): array
22+
{
23+
$values = [];
24+
25+
foreach ($keys as $key) {
26+
$values[$key] = $this->get($key);
27+
}
28+
29+
return $values;
30+
}
31+
32+
public function put($key, $value, $seconds): bool
33+
{
34+
$this->values[$key] = $value;
35+
36+
return true;
37+
}
38+
39+
public function putMany(array $values, $seconds): bool
40+
{
41+
foreach ($values as $key => $value) {
42+
$this->put($key, $value, $seconds);
43+
}
44+
45+
return true;
46+
}
47+
48+
public function increment($key, $value = 1)
49+
{
50+
$current = (int) ($this->values[$key] ?? 0);
51+
$current += $value;
52+
$this->values[$key] = $current;
53+
54+
return $current;
55+
}
56+
57+
public function decrement($key, $value = 1)
58+
{
59+
$current = (int) ($this->values[$key] ?? 0);
60+
$current -= $value;
61+
$this->values[$key] = $current;
62+
63+
return $current;
64+
}
65+
66+
public function forever($key, $value): bool
67+
{
68+
return $this->put($key, $value, 0);
69+
}
70+
71+
public function forget($key): bool
72+
{
73+
unset($this->values[$key]);
74+
75+
return true;
76+
}
77+
78+
public function flush(): bool
79+
{
80+
$this->values = [];
81+
82+
return true;
83+
}
84+
85+
public function getPrefix(): string
86+
{
87+
return 'test-non-locking:';
88+
}
89+
}

tests/Unit/Commands/V2DoctorCommandTest.php

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

55
namespace Tests\Unit\Commands;
66

7+
use Illuminate\Cache\CacheManager;
8+
use Illuminate\Cache\Repository;
9+
use Tests\Support\NonLockingCacheStore;
710
use Tests\TestCase;
811

912
final class V2DoctorCommandTest extends TestCase
@@ -119,4 +122,40 @@ public function testQueueModeSyncQueueIsReportedAsErrorAndFailsStrict(): void
119122
->expectsOutputToContain('[ERROR] [queue_sync_unsupported]')
120123
->assertFailed();
121124
}
125+
126+
public function testCustomNoLockCacheStoreAppearsInJsonOutputAndStrictStillSucceeds(): void
127+
{
128+
config()->set('queue.default', 'redis');
129+
config()
130+
->set('queue.connections.redis.driver', 'redis');
131+
config()
132+
->set('workflows.serializer', 'avro');
133+
$this->configureNonLockingCacheStore();
134+
135+
$this->artisan('workflow:v2:doctor', [
136+
'--json' => true,
137+
'--strict' => true,
138+
])
139+
->expectsOutputToContain('"code":"cache_locks_unsupported"')
140+
->assertSuccessful();
141+
}
142+
143+
private function configureNonLockingCacheStore(): void
144+
{
145+
$driver = 'test-non-locking';
146+
$store = 'test-non-locking';
147+
148+
$this->app['cache']->extend($driver, function (): Repository {
149+
if (! $this instanceof CacheManager) {
150+
throw new \RuntimeException('Unexpected cache manager binding.');
151+
}
152+
153+
return new Repository(new NonLockingCacheStore());
154+
});
155+
156+
config()
157+
->set("cache.stores.{$store}.driver", $driver);
158+
config()
159+
->set('cache.default', $store);
160+
}
122161
}

tests/Unit/V2/BackendCapabilitiesTest.php

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

55
namespace Tests\Unit\V2;
66

7+
use Illuminate\Cache\CacheManager;
8+
use Illuminate\Cache\Repository;
79
use Illuminate\Support\Carbon;
10+
use Tests\Support\NonLockingCacheStore;
811
use Tests\TestCase;
912
use Workflow\V2\Support\BackendCapabilities;
1013

@@ -93,11 +96,17 @@ public function testSnapshotIncludesFrozenReadinessContractMatrix(): void
9396
'info',
9497
$contract['backend_capabilities']['queue']['poll_mode']['sync_or_missing_queue_severity']
9598
);
99+
$this->assertSame('warning', $contract['backend_capabilities']['cache']['dispatch_blocking_severity']);
100+
$this->assertSame('acceleration_only', $contract['backend_capabilities']['cache']['role']);
96101
$this->assertSame('avro', $contract['backend_capabilities']['codec']['default_for_new_v2_runs']);
97102
$this->assertSame(
98103
'evaluated_by_backend_capabilities_snapshot',
99104
$contract['effective_states']['dispatch']['state']
100105
);
106+
$this->assertStringContainsString(
107+
'cache diagnostics stay warning-only',
108+
$contract['effective_states']['dispatch']['blocking_rule']
109+
);
101110
}
102111

103112
public function testSnapshotCanInspectAnExplicitTaskQueueConnection(): void
@@ -372,6 +381,8 @@ public function testSnapshotIncludesSeverityRollupOfOkWhenAdmissionIsClean(): vo
372381
->set('cache.default', 'array');
373382
config()
374383
->set('cache.stores.array.driver', 'array');
384+
config()
385+
->set('workflows.serializer', 'avro');
375386

376387
$snapshot = BackendCapabilities::snapshot();
377388

@@ -439,4 +450,49 @@ public function testSnapshotSeverityRollupReportsErrorWhenAnyIssueIsErrorSeverit
439450
$this->assertSame('error', $snapshot['severity']);
440451
$this->assertFalse($snapshot['supported']);
441452
}
453+
454+
public function testCustomNoLockCacheStoreIsWarningOnlyAndDoesNotFailSupport(): void
455+
{
456+
config()->set('database.default', 'pgsql');
457+
config()
458+
->set('database.connections.pgsql.driver', 'pgsql');
459+
config()
460+
->set('queue.default', 'redis');
461+
config()
462+
->set('queue.connections.redis.driver', 'redis');
463+
config()
464+
->set('workflows.serializer', 'avro');
465+
$this->configureNonLockingCacheStore();
466+
467+
$snapshot = BackendCapabilities::snapshot();
468+
469+
$this->assertTrue($snapshot['supported']);
470+
$this->assertTrue(BackendCapabilities::isSupported($snapshot));
471+
$this->assertTrue($snapshot['cache']['supported']);
472+
$this->assertSame('warning', $snapshot['severity']);
473+
474+
$cacheIssue = collect($snapshot['issues'])->firstWhere('code', 'cache_locks_unsupported');
475+
$this->assertNotNull($cacheIssue);
476+
$this->assertSame('warning', $cacheIssue['severity']);
477+
$this->assertStringContainsString('Wake acceleration remains optional', $cacheIssue['message']);
478+
}
479+
480+
private function configureNonLockingCacheStore(): void
481+
{
482+
$driver = 'test-non-locking';
483+
$store = 'test-non-locking';
484+
485+
$this->app['cache']->extend($driver, function (): Repository {
486+
if (! $this instanceof CacheManager) {
487+
throw new \RuntimeException('Unexpected cache manager binding.');
488+
}
489+
490+
return new Repository(new NonLockingCacheStore());
491+
});
492+
493+
config()
494+
->set("cache.stores.{$store}.driver", $driver);
495+
config()
496+
->set('cache.default', $store);
497+
}
442498
}

0 commit comments

Comments
 (0)