test(temporal): heartbeats and their cancellation, on a Laravel- and a Magento-hosted activity worker (#518) - #545
Conversation
…vel or the Magento factory (#518) worker.php takes two more roles, laravel-activity and magento-activity: the activity worker DurableServiceProvider builds on a bare container with backend: temporal, and the one RuntimeFactory::activityWorker() builds. The heartbeating activities inject the sender each host wires (#510). TemporalServerTestCase lets a subclass pick the activity role. Each host loop exits after a bounded time. DURABLE_HEARTBEAT=noop, read by these two scripts only, hands the activities the no-op sender to show the heartbeat test red; nothing under src/ knows it. phpstan.neon's existing bare-container exception extends to the host scripts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eaches it, on Laravel and Magento (#518) Two workflows on the suite's workflow worker, their activities on the host's: thirty seconds of heartbeats under a five-second heartbeat timeout and one attempt, which completes; and a deadline that cancels a heartbeating activity, whose next heartbeat returns true. The run stays open for a few heartbeats after the cancellation: once it closes, a heartbeat hears "not found", not "cancel requested". Every wait is bounded by wall-clock time: the status is read with a describe, which does not block, and the marker file is polled with a deadline. The workers die in tearDown whether the test passed or not. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#518) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gplanchat
left a comment
There was a problem hiding this comment.
Review of a3824cc (antoine), run against my own temporalio/temporal:1.9.1 container. Verdict: OK, no finding.
Runs
--filter '(Laravel|Magento)HostedHeartbeatTest' --fail-on-skipped: OK (4 tests). That is the heartbeat and the cancellation, on each host.DURABLE_HEARTBEAT=nooponLaravelHostedHeartbeatTest::testAHeartbeatingActivityOutlivesItsHeartbeatTimeout: fails after exactly 90 s with "The HeartbeatsThroughItsTimeout run did not close within 90 s; its activities are hosted by the laravel-activity worker". The bound holds and the run does not hang, even with #544's TIMED_OUT still ignored.
(1) Each host's real wiring
- Laravel:
DurableServiceProvideris registered on a bare container. The activities come from$app->make(), so they inject the provider'sActivityHeartbeatSenderInterfacesingleton, the one the worker binds tokens onto. The registration target,$app->make(RegistryActivityExecutor::class), is the provider's singleton, whichActivityExecutoralso resolves to (DurableServiceProvider:410,414). So the activities reach the worker's processor. - Magento:
RuntimeFactoryis givenheartbeat: $shared, soactivityWorker()delegates theSharedActivityHeartbeatSenderto its own Temporal sender, and the activity holds that same$shared. That is whatdi.xml's preference plus the factory argument give on a real install. - In noop mode, Laravel overrides the interface binding before the worker is built, so both sides get the no-op sender. Magento hands only the activity the no-op sender.
(2) Bounds and teardown. Every wait has a wall-clock bound:
runWithin()pollsDescribeWorkflowExecutionwith a 5 s RPC timeout, up to 90 s or 60 s;- the marker wait is capped at 30 s;
- the host loops stop at
DURABLE_HOST_WORKER_MAX_TIME(180 s).
The parent tearDown() terminates the started workflows and SIGKILLs every worker, and it runs whether the test passed or failed.
(3) The noop switch is read only in tests/integration/Temporal/Hosts/*. grep DURABLE_HEARTBEAT src finds nothing.
(4) The cancellation fixture sleeps 10 s after DeadlineExceededException before closing (IntegrationWorkflows.php:190-192), so heartbeats hear "cancel requested" rather than "not found". The marker assertion passes on both hosts.
phpstan.neon: tests/integration/Temporal/Hosts/* joins one existing narrow ignore, argument.type with the "expects Illuminate Application" message, the documented bare-container case. It is not an exclusion.
Gates: PHPStan, Psalm and cs:check 0.
|
Second review (bob) of a3824cc, alongside antoine. Verdict: changes needed, 1 finding. F1:
|
…run without it (#518, review) PHPStan on PHP 8.5 reported $argv as possibly undefined in the two host scripts, which it analyses as their own files. They now read $_SERVER['argv'] and exit with a usage line when an argument is missing. The heartbeat test's docblock says what the no-op run shows: the server times the activity out, and until #544 the run did not even fail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Re-check (bob) at 46f0a1a: OK, F1 is resolved.
|
Closes #518. Refs #510, #544.
Done when
temporal-integrationjob. That job already runsphpunit --testsuite integration,temporal --fail-on-skippedovertests/integration/Temporal/withDURABLE_TEMPORAL_ADDRESSset, and the rootvendor/hasilluminate/*. The new tests live there, so no.github/workflowschange is needed, and there is no patch to apply.RuntimeFactory::activityWorker(), the class the module'sdi.xmlwires, built with the sameSharedActivityHeartbeatSender. The factory is plain PHP and runs from the rootvendor/, so the Mage-OS job is not needed to host it.How it works
worker.phpgains two roles,laravel-activityandmagento-activity, each in a script undertests/integration/Temporal/Hosts/:DurableServiceProvideron a bare container withbackend: temporal. The activities are resolved from that container, so they inject the provider's sender. The loop isdurable:temporal-worker --role=activity --max-time;illuminate/consoleis not a root dependency, so the command itself cannot run.RuntimeFactory(activityHandlers: …, temporalDsn: …, heartbeat: $shared)->activityWorker().TemporalServerTestCase::activityWorkerRole()lets a test pick the host.HeartbeatOnAHostTestCaseholds the two scenarios, run byLaravelHostedHeartbeatTestandMagentoHostedHeartbeatTest.RetryLimit::once().true, and it writes a marker file the test reads. The run then stays open for 10 s: once it closes, a heartbeat hears "not found" rather than "cancel requested".The project lead's conditions
DURABLE_HEARTBEAT=nooplives in the two host scripts only. Nothing undersrc/reads it.DescribeWorkflowExecution, which does not block, until a deadline; the result is read only once the run has closed.DURABLE_HOST_WORKER_MAX_TIME(180 s by default).tearDown()whether the test passed or not; that is the existing base behaviour.temporal-integration(the four tests took 1:27 locally).Red once, then green
Private
temporalio/temporal:1.9.1server (Server 1.32), extensiongrpc.The cancellation test goes red with the no-op sender as well: the run reaches its deadline, and no heartbeat reports the cancellation, so the marker file is never written.
Why the red run shows a hang rather than an activity failure. With the no-op sender, the server does time the activity out (
ActivityTaskTimedOutin the history). But the Temporal bridge ignores that event, so the workflow never fails and staysRunning. That is a separate bug, filed as #544 (milestone 1), where the fix belongs. Here the bounded wait turns it into a red test instead of a stuck job.Checks
SearchAttributesTest, because my container was started without CI's--search-attributeflags.phpstan.neon's existing bare-container exception now also coverstests/integration/Temporal/Hosts/*.