Skip to content

Commit d1e0f35

Browse files
Conformance blocker: PHP worker query-task polling must satisfy server contract (#606)
1 parent 3929609 commit d1e0f35

4 files changed

Lines changed: 111 additions & 6 deletions

File tree

docs/api-stability.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ These classes are covered by the same semver rules as the server-facing
186186
`Support\*` list above. They intentionally wrap only the worker-plane
187187
HTTP routes and cold-replay stepping primitives; application workers still
188188
own registration maps, process lifecycle, logging, and command-line UX.
189+
Standalone PHP workflow-worker registrations advertise the `query_tasks`
190+
capability by default whenever they register workflow types; pass an explicit
191+
empty capabilities list only for custom workers that deliberately cannot
192+
answer server-routed workflow queries.
189193
Cold PHP Fiber runners must be constructed with the bridge `history_events`
190194
payload so workflow start time plus recorded activity, timer, child-workflow,
191195
side-effect, version marker, and search-attribute outcomes can be replayed

docs/architecture/query-and-live-debug.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ and returns `poll_status = 'leased'`; an empty long-poll returns
187187
`poll_status = 'empty'` with no task. Query task long-poll timeout
188188
semantics are the same clamped `WorkerProtocolVersion::longPollSemantics()`
189189
used by workflow and activity task polling.
190+
`WorkerProtocolClient::registerWorker()` advertises `query_tasks` by
191+
default for standalone PHP workers that register workflow types, and
192+
`pollQueryTasks()` sends a stable `poll_request_id` for every query
193+
poll attempt.
190194

191195
Each leased query task carries `query_task_id`,
192196
`query_task_attempt`, `lease_owner`, `workflow_id`, `run_id`,

src/V2/Worker/WorkerProtocolClient.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,9 @@ public function registerWorker(
126126
$body['workflow_definition_fingerprints'] = $workflowDefinitionFingerprints;
127127
}
128128

129-
if ($capabilities !== null) {
130-
$body['capabilities'] = array_values(array_filter(
131-
$capabilities,
132-
static fn (mixed $capability): bool => is_string($capability) && $capability !== '',
133-
));
129+
$advertisedCapabilities = $this->workerCapabilities($supportedWorkflowTypes, $capabilities);
130+
if ($advertisedCapabilities !== null) {
131+
$body['capabilities'] = $advertisedCapabilities;
134132
}
135133

136134
if ($buildId !== null) {
@@ -369,7 +367,9 @@ public function pollQueryTasks(
369367
return [];
370368
}
371369

372-
$pollRequestId = $this->stringValue($pollRequestId) ?? 'query-poll-'.bin2hex(random_bytes(16));
370+
$pollRequestId = is_string($pollRequestId) && trim($pollRequestId) !== ''
371+
? trim($pollRequestId)
372+
: 'query-poll-'.bin2hex(random_bytes(16));
373373
$body = [
374374
'worker_id' => $this->resolveStandaloneWorkerId($workerId),
375375
'task_queue' => $this->resolveStandaloneTaskQueue($queue),
@@ -918,6 +918,39 @@ private function longPollRequestTimeoutSeconds(int $timeoutSeconds): int
918918
return max($pollTimeoutSeconds, WorkerProtocolVersion::DEFAULT_LONG_POLL_TIMEOUT) + 5;
919919
}
920920

921+
/**
922+
* @param list<string> $supportedWorkflowTypes
923+
* @param list<string>|null $capabilities
924+
* @return list<string>|null
925+
*/
926+
private function workerCapabilities(array $supportedWorkflowTypes, ?array $capabilities): ?array
927+
{
928+
if ($capabilities === null) {
929+
return $this->hasSupportedWorkflowTypes($supportedWorkflowTypes)
930+
? [WorkerProtocolVersion::CAPABILITY_QUERY_TASKS]
931+
: null;
932+
}
933+
934+
return array_values(array_filter(
935+
$capabilities,
936+
static fn (mixed $capability): bool => is_string($capability) && $capability !== '',
937+
));
938+
}
939+
940+
/**
941+
* @param list<string> $supportedWorkflowTypes
942+
*/
943+
private function hasSupportedWorkflowTypes(array $supportedWorkflowTypes): bool
944+
{
945+
foreach ($supportedWorkflowTypes as $workflowType) {
946+
if (is_string($workflowType) && trim($workflowType) !== '') {
947+
return true;
948+
}
949+
}
950+
951+
return false;
952+
}
953+
921954
private function resolveStandaloneWorkerId(?string $workerId): string
922955
{
923956
$resolved = $workerId ?? $this->registeredWorkerId;

tests/Unit/V2/WorkerProtocolClientTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,70 @@ public function testRegisterWorkerSendsWorkerProtocolHeadersAndBody(): void
7070
], $requestBody);
7171
}
7272

73+
public function testStandaloneWorkflowWorkerRegistrationAdvertisesQueryTaskCapabilityByDefault(): void
74+
{
75+
$http = new HttpFactory();
76+
$requestBody = null;
77+
78+
$http->fake(function (Request $request) use ($http, &$requestBody) {
79+
$requestBody = $request->data();
80+
81+
return $http->response(['registered' => true], 201);
82+
});
83+
84+
$client = new WorkerProtocolClient($http, 'http://server:8080', 'test-token', 'default');
85+
$client->registerWorker(
86+
workerId: 'php-worker',
87+
taskQueue: 'polyglot',
88+
supportedWorkflowTypes: ['polyglot.php.signal-query'],
89+
);
90+
91+
$this->assertSame(['query_tasks'], $requestBody['capabilities'] ?? null);
92+
}
93+
94+
public function testActivityOnlyWorkerRegistrationDoesNotAdvertiseQueryTasksByDefault(): void
95+
{
96+
$http = new HttpFactory();
97+
$requestBody = null;
98+
99+
$http->fake(function (Request $request) use ($http, &$requestBody) {
100+
$requestBody = $request->data();
101+
102+
return $http->response(['registered' => true], 201);
103+
});
104+
105+
$client = new WorkerProtocolClient($http, 'http://server:8080', 'test-token', 'default');
106+
$client->registerWorker(
107+
workerId: 'php-activity-worker',
108+
taskQueue: 'polyglot',
109+
supportedActivityTypes: ['polyglot.php.activity'],
110+
);
111+
112+
$this->assertArrayNotHasKey('capabilities', $requestBody);
113+
}
114+
115+
public function testExplicitWorkerCapabilitiesOverrideQueryTaskDefault(): void
116+
{
117+
$http = new HttpFactory();
118+
$requestBody = null;
119+
120+
$http->fake(function (Request $request) use ($http, &$requestBody) {
121+
$requestBody = $request->data();
122+
123+
return $http->response(['registered' => true], 201);
124+
});
125+
126+
$client = new WorkerProtocolClient($http, 'http://server:8080', 'test-token', 'default');
127+
$client->registerWorker(
128+
workerId: 'php-worker',
129+
taskQueue: 'polyglot',
130+
supportedWorkflowTypes: ['polyglot.php.signal-query'],
131+
capabilities: [],
132+
);
133+
134+
$this->assertSame([], $requestBody['capabilities'] ?? null);
135+
}
136+
73137
public function testStandaloneQueryPollAndCompleteUseCachedLease(): void
74138
{
75139
$http = new HttpFactory();

0 commit comments

Comments
 (0)