Skip to content

Commit cb322f9

Browse files
committed
worker: expose activity heartbeat recording
Core\Worker::recordActivityHeartbeat(bytes) hands a serialized coresdk ActivityHeartbeat to the core. The bridge call is synchronous (in-memory record; throttling and the server RPC run on the core's own threads), so the method never parks the coroutine and needs no cross-thread call slot. The finalized guard covers it: the bridge unwraps None after finalize. Also fixes the stale "activity-only worker" comment in temporal_core.h (doc drift noted in the 2026-06-11 review).
1 parent 73a1280 commit cb322f9

8 files changed

Lines changed: 128 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ transport through a `ServiceClientInterface` adapter — see the
3636
enabled pollers to wind down.
3737
- The connection now defaults its identity to `<pid>@<hostname>` when none is
3838
given (required by workers).
39+
- `Core\Worker::recordActivityHeartbeat(bytes)` — records an activity heartbeat
40+
(serialized coresdk ActivityHeartbeat). Synchronous and never parks the
41+
coroutine: the core stores the heartbeat in memory and throttles/sends it to
42+
the server on its own threads. A pending cancellation is delivered separately,
43+
as a cancel-variant task from `pollActivityTask()`.
3944
- The worker now caches workflow runs (sticky execution) so a fired timer or
4045
resolved activity resumes the live instance instead of replaying from scratch.
4146

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ Just the transport seam:
4949

5050
- `TrueAsync\Temporal\Core\Connection``connect` plus an async
5151
`rpcCall(service, method, requestBytes): responseBytes`.
52+
- `TrueAsync\Temporal\Core\Worker` — the worker transport: poll/complete for
53+
activity tasks and workflow activations, activity heartbeat recording, and
54+
the shutdown lifecycle.
5255
- `TrueAsync\Temporal\{TemporalException, ConnectionException, ServiceException}`.
5356

5457
Everything user-facing (workflow client, options, data converter, the generated

temporal.stub.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ public function pollActivityTask(): ?string {}
8282
/** Report an activity task completion (serialized coresdk completion). */
8383
public function completeActivityTask(string $completion): void {}
8484

85+
/**
86+
* Record an activity heartbeat (serialized coresdk ActivityHeartbeat).
87+
* Synchronous — never parks the coroutine: the core stores the heartbeat in
88+
* memory and throttles/sends it to the server on its own threads. A pending
89+
* cancellation is delivered separately, as a cancel-variant task from
90+
* pollActivityTask().
91+
*/
92+
public function recordActivityHeartbeat(string $heartbeat): void {}
93+
8594
/**
8695
* Poll for the next workflow activation. Parks the coroutine until an
8796
* activation is ready; returns the serialized coresdk WorkflowActivation, or

temporal_arginfo.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit temporal.stub.php instead.
2-
* Stub hash: e3c06eb5024b0783e98d6eb0477faa44017b1e0c */
2+
* Stub hash: 9f5736251d4657a08384ad984b98be8a08100cb8 */
33

44
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_TrueAsync_Temporal_Core_Connection___construct, 0, 0, 1)
55
ZEND_ARG_TYPE_INFO(0, address, IS_STRING, 0)
@@ -34,6 +34,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_TrueAsync_Temporal_Core_Wo
3434
ZEND_ARG_TYPE_INFO(0, completion, IS_STRING, 0)
3535
ZEND_END_ARG_INFO()
3636

37+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_TrueAsync_Temporal_Core_Worker_recordActivityHeartbeat, 0, 1, IS_VOID, 0)
38+
ZEND_ARG_TYPE_INFO(0, heartbeat, IS_STRING, 0)
39+
ZEND_END_ARG_INFO()
40+
3741
#define arginfo_class_TrueAsync_Temporal_Core_Worker_pollWorkflowActivation arginfo_class_TrueAsync_Temporal_Core_Worker_pollActivityTask
3842

3943
#define arginfo_class_TrueAsync_Temporal_Core_Worker_completeWorkflowActivation arginfo_class_TrueAsync_Temporal_Core_Worker_completeActivityTask
@@ -48,6 +52,7 @@ ZEND_METHOD(TrueAsync_Temporal_Core_Connection, rpcCall);
4852
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, __construct);
4953
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, pollActivityTask);
5054
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, completeActivityTask);
55+
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, recordActivityHeartbeat);
5156
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, pollWorkflowActivation);
5257
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, completeWorkflowActivation);
5358
ZEND_METHOD(TrueAsync_Temporal_Core_Worker, initiateShutdown);
@@ -63,6 +68,7 @@ static const zend_function_entry class_TrueAsync_Temporal_Core_Worker_methods[]
6368
ZEND_ME(TrueAsync_Temporal_Core_Worker, __construct, arginfo_class_TrueAsync_Temporal_Core_Worker___construct, ZEND_ACC_PUBLIC)
6469
ZEND_ME(TrueAsync_Temporal_Core_Worker, pollActivityTask, arginfo_class_TrueAsync_Temporal_Core_Worker_pollActivityTask, ZEND_ACC_PUBLIC)
6570
ZEND_ME(TrueAsync_Temporal_Core_Worker, completeActivityTask, arginfo_class_TrueAsync_Temporal_Core_Worker_completeActivityTask, ZEND_ACC_PUBLIC)
71+
ZEND_ME(TrueAsync_Temporal_Core_Worker, recordActivityHeartbeat, arginfo_class_TrueAsync_Temporal_Core_Worker_recordActivityHeartbeat, ZEND_ACC_PUBLIC)
6672
ZEND_ME(TrueAsync_Temporal_Core_Worker, pollWorkflowActivation, arginfo_class_TrueAsync_Temporal_Core_Worker_pollWorkflowActivation, ZEND_ACC_PUBLIC)
6773
ZEND_ME(TrueAsync_Temporal_Core_Worker, completeWorkflowActivation, arginfo_class_TrueAsync_Temporal_Core_Worker_completeWorkflowActivation, ZEND_ACC_PUBLIC)
6874
ZEND_ME(TrueAsync_Temporal_Core_Worker, initiateShutdown, arginfo_class_TrueAsync_Temporal_Core_Worker_initiateShutdown, ZEND_ACC_PUBLIC)

temporal_client.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,34 @@ PHP_METHOD(TrueAsync_Temporal_Core_Worker, completeWorkflowActivation)
914914
}
915915
}
916916

917+
PHP_METHOD(TrueAsync_Temporal_Core_Worker, recordActivityHeartbeat)
918+
{
919+
zend_string *heartbeat;
920+
921+
ZEND_PARSE_PARAMETERS_START(1, 1)
922+
Z_PARAM_STR(heartbeat)
923+
ZEND_PARSE_PARAMETERS_END();
924+
925+
temporal_worker_obj *self = temporal_worker_from_obj(Z_OBJ_P(ZEND_THIS));
926+
927+
/* No coroutine requirement: recording is synchronous in the core (in-memory
928+
* store; throttling and the server RPC run on the core's own threads). */
929+
if (self->worker == NULL || self->finalized) {
930+
zend_throw_error(NULL, "recordActivityHeartbeat must be called on a live worker");
931+
RETURN_THROWS();
932+
}
933+
934+
char *fail = tphp_worker_record_heartbeat(((tphp_handle_t *) self->worker)->box,
935+
(const uint8_t *) ZSTR_VAL(heartbeat),
936+
ZSTR_LEN(heartbeat));
937+
938+
if (fail != NULL) {
939+
zend_throw_exception_ex(temporal_ce_service_exception, 0, "%s", fail);
940+
free(fail);
941+
RETURN_THROWS();
942+
}
943+
}
944+
917945
PHP_METHOD(TrueAsync_Temporal_Core_Worker, initiateShutdown)
918946
{
919947
ZEND_PARSE_PARAMETERS_NONE();

temporal_core.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,16 @@ void tphp_worker_finalize_shutdown(void *worker, void *user_data, tphp_worker_do
533533

534534
temporal_core_worker_finalize_shutdown((TemporalCoreWorker *) worker, c, tphp_on_worker_done);
535535
}
536+
537+
char *tphp_worker_record_heartbeat(void *worker, const uint8_t *heartbeat, size_t len)
538+
{
539+
TemporalCoreByteArrayRef ref;
540+
ref.data = heartbeat;
541+
ref.size = len;
542+
543+
const TemporalCoreByteArray *fail =
544+
temporal_core_worker_record_activity_heartbeat((TemporalCoreWorker *) worker, ref);
545+
546+
size_t fail_len = 0;
547+
return tphp_copy_str(tphp_g_runtime, fail, &fail_len);
548+
}

temporal_core.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ void tphp_response_free(void *response_owner);
105105

106106
/* --- Worker ------------------------------------------------------------ */
107107

108-
/* Create an activity-only worker on a task queue (fixed-size activity slots =
109-
* max_concurrent_activities). Returns an opaque handle, or NULL with *err_out
110-
* (heap, caller frees). worker_new is synchronous in the core. */
108+
/* Create a worker on a task queue handling both workflow and activity tasks
109+
* (fixed-size activity slots = max_concurrent_activities). Returns an opaque
110+
* handle, or NULL with *err_out (heap, caller frees). worker_new is synchronous
111+
* in the core. */
111112
void *tphp_worker_new(void *connection, const char *ns, const char *task_queue,
112113
uint32_t max_concurrent_activities, char **err_out);
113114
void tphp_worker_free(void *worker);
@@ -134,4 +135,10 @@ void tphp_worker_complete_workflow(void *worker, const uint8_t *completion, size
134135
void *user_data, tphp_worker_done_cb done);
135136
void tphp_worker_finalize_shutdown(void *worker, void *user_data, tphp_worker_done_cb done);
136137

138+
/* Record an activity heartbeat (serialized coresdk ActivityHeartbeat).
139+
* Synchronous in the core: it only stores the heartbeat in memory; throttling
140+
* and the server RPC happen later on the core's own threads. Returns NULL on
141+
* success, or a heap error message (decode failure) the caller frees. */
142+
char *tphp_worker_record_heartbeat(void *worker, const uint8_t *heartbeat, size_t len);
143+
137144
#endif /* PHP_TEMPORAL_CORE_H */
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
Core\Worker recordActivityHeartbeat: accepts a heartbeat, rejects garbage and a finalized worker
3+
--EXTENSIONS--
4+
temporal
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/../inc/temporal.inc';
8+
temporal_skip_if_no_server();
9+
?>
10+
--FILE--
11+
<?php
12+
require __DIR__ . '/../inc/temporal.inc';
13+
14+
use TrueAsync\Temporal\Core\Connection;
15+
use TrueAsync\Temporal\Core\Worker;
16+
use TrueAsync\Temporal\ServiceException;
17+
use function Async\spawn;
18+
use function Async\await;
19+
20+
await(spawn(function () {
21+
$conn = new Connection(temporal_test_address());
22+
$worker = new Worker($conn, 'hb-' . bin2hex(random_bytes(3)), temporal_test_namespace(), 4);
23+
24+
// A hand-encoded coresdk ActivityHeartbeat: field 1 (task_token) = "abc".
25+
// The token matches no running activity; the core records and ignores it.
26+
$worker->recordActivityHeartbeat("\x0a\x03abc");
27+
var_dump('recorded');
28+
29+
// Bytes that are not a valid protobuf message: the core reports a decode
30+
// failure, surfaced as a ServiceException.
31+
try {
32+
$worker->recordActivityHeartbeat("\xff");
33+
} catch (ServiceException $e) {
34+
var_dump(str_contains($e->getMessage(), 'decode'));
35+
}
36+
37+
$worker->initiateShutdown();
38+
$worker->pollWorkflowActivation();
39+
$worker->pollActivityTask();
40+
$worker->finalizeShutdown();
41+
42+
// The finalized guard covers heartbeats too (the core would abort otherwise).
43+
try {
44+
$worker->recordActivityHeartbeat("\x0a\x03abc");
45+
} catch (Error $e) {
46+
var_dump($e->getMessage());
47+
}
48+
}));
49+
?>
50+
--EXPECT--
51+
string(8) "recorded"
52+
bool(true)
53+
string(55) "recordActivityHeartbeat must be called on a live worker"

0 commit comments

Comments
 (0)