Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions src/http_server_class.c
Original file line number Diff line number Diff line change
Expand Up @@ -6105,6 +6105,53 @@ static zend_object *room_mint(http_server_object *server, zend_string *topic)
return obj;
}

/* Carries a room to another thread. The transit shell owns its own hub reference
* and a persistent copy of the topic, so neither outlives the source thread's
* allocator; LOAD takes a second reference and a thread-local copy. */
static zend_object *room_transfer_obj(
zend_object *object,
zend_async_thread_transfer_ctx_t *ctx,
zend_object_transfer_kind_t kind,
zend_object_transfer_default_fn default_fn)
{
room_object *src = room_from_obj(object);

if (kind == ZEND_OBJECT_TRANSFER_RELEASE) {
topic_hub_release(src->hub);
src->hub = NULL;

if (src->topic != NULL) {
zend_string_release(src->topic);
src->topic = NULL;
}

return NULL;
}

if (UNEXPECTED(src->hub == NULL)) {
return NULL; /* unminted; nothing to carry */
}

const bool persistent = (kind == ZEND_OBJECT_TRANSFER);

/* 0 lets the default size the allocation from the handler offset and the
* property count; a literal sizeof() would stop covering declared properties. */
zend_object *dst = default_fn(object, ctx, 0);

if (UNEXPECTED(dst == NULL)) {
return NULL;
}

room_object *room = room_from_obj(dst);

room->hub = src->hub;
topic_hub_addref(room->hub);
room->topic = zend_string_init(ZSTR_VAL(src->topic), ZSTR_LEN(src->topic), persistent);
room->retry = src->retry;

return dst;
}

/* Reflection can build a Room past the private constructor: hub and topic NULL. */
static bool room_is_minted(const room_object *room)
{
Expand Down Expand Up @@ -6746,6 +6793,12 @@ static zend_object *http_server_transfer_obj(
zend_object_transfer_kind_t kind,
zend_object_transfer_default_fn default_fn)
{
if (kind == ZEND_OBJECT_TRANSFER_RELEASE) {
/* The server owns its shells and frees them in
* http_server_release_worker_shell; releasing here would double-free. */
return NULL;
}

if (kind == ZEND_OBJECT_TRANSFER) {
http_server_object *src = http_server_from_obj(object);

Expand Down Expand Up @@ -7014,9 +7067,13 @@ void http_server_class_register(void)
room_ce->create_object = room_create;

memcpy(&room_handlers, &std_object_handlers, sizeof(zend_object_handlers));
room_handlers.offset = offsetof(room_object, std);
room_handlers.free_obj = room_free;
room_handlers.clone_obj = NULL;
room_handlers.offset = offsetof(room_object, std);
room_handlers.free_obj = room_free;
room_handlers.clone_obj = NULL;
room_handlers.transfer_obj = room_transfer_obj;

/* LOAD has no live source object and resolves the handler by class name. */
room_ce->default_object_handlers = &room_handlers;
#endif
}
/* }}} */
9 changes: 9 additions & 0 deletions src/http_server_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -3755,6 +3755,15 @@ static zend_object *http_server_config_transfer_obj(
zend_object_transfer_kind_t kind,
zend_object_transfer_default_fn default_fn)
{
if (kind == ZEND_OBJECT_TRANSFER_RELEASE) {
http_server_config_t *shell = http_server_config_from_obj(object);

http_server_shared_config_release(shell->frozen);
shell->frozen = NULL;

return NULL;
}

if (kind == ZEND_OBJECT_TRANSFER) {
http_server_config_t *src = http_server_config_from_obj(object);

Expand Down
4 changes: 4 additions & 0 deletions stubs/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ public function send(string $message, ?int $timeoutMs = null): int {}
* Suspends the calling coroutine until every worker answers or $timeoutMs
* elapses. Must run on a worker thread (a request/WebSocket handler or a
* spawned run coroutine); on the pool parent it returns the local count.
*
* A thread that never attached to the hub — a ThreadPool task the room was
* transferred into — gets 0, which reads the same as a room nobody joined.
* {@see trySend()} and {@see send()} report that thread honestly; this does not.
*/
public function subscriberCount(int $timeoutMs = 1000): int {}

Expand Down
77 changes: 77 additions & 0 deletions tests/phpt/websocket/069-room-transfers-to-pool.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--TEST--
Rooms: a Room captured by a ThreadPool task publishes from that thread
--SKIPIF--
<?php
if (!PHP_ZTS) die('skip ZTS required');
if (!class_exists('Async\ThreadPool')) die('skip ThreadPool not available');
?>
--EXTENSIONS--
true_async_server
true_async
--FILE--
<?php
/* A room transfers into a pool thread: the task gets its own handle on the same
* hub, and its publish reaches a WebSocket client owned by the server thread.
* Without the transfer handler the captured room arrives uninitialized. */
require_once __DIR__ . '/../server/_free_port.inc';
require_once __DIR__ . '/_ws_client.inc';

use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\WebSocket;
use TrueAsync\HttpRequest;
use Async\ThreadPool;
use function Async\spawn;
use function Async\await;
use function Async\delay;

$port = tas_free_port();
$server = new HttpServer(
(new HttpServerConfig())
->addListener('127.0.0.1', $port)
->setReadTimeout(10)
->setWriteTimeout(10)
->setWsPingIntervalMs(0)
);
$server->enableRooms();

$server->addWebSocketHandler(function (WebSocket $ws, HttpRequest $req) {
$ws->subscribe('projects/demo');

foreach ($ws as $msg) {
/* read-only watcher */
}
});

$server->addHttpHandler(function ($req, $res) { $res->setStatusCode(404)->end(); });

spawn(function () use ($port, $server) {
delay(1500);

$room = $server->room('projects/demo');

$fp = ws_open($port);
if ($fp === null) { echo "handshake failed\n"; $server->stop(); return; }

delay(400);

$pool = new ThreadPool(1);
$task = $pool->submit(function () use ($room) {
return $room->name() . ':' . $room->publish('from-pool')['posted'];
});

echo 'task: ', await($task), "\n";

delay(500);
echo 'client got: ', ws_read_pending($fp) ?? '(nothing)', "\n";

$pool->close();
fclose($fp);
$server->stop();
});

$server->start();
?>
--EXPECTF--
task: projects/demo:1
client got: from-pool%A
Loading