Skip to content

Commit 8698d87

Browse files
Replace should_stop by get_signaling_stream
1 parent 0a767dc commit 8698d87

18 files changed

+185
-53
lines changed

docs/sidekicks.md

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,29 @@ Each call **replaces** the entire snapshot atomically.
5353
- Throws `RuntimeException` if not called from a sidekick context
5454
- Throws `ValueError` if keys or values are not strings
5555

56-
### `frankenphp_sidekick_should_stop(): bool`
56+
### `frankenphp_sidekick_get_signaling_stream(): resource`
5757

58-
Returns `true` when FrankenPHP is shutting down.
58+
Returns a readable stream that becomes ready when FrankenPHP is shutting down or restarting workers.
59+
Use `stream_select()` instead of `sleep()` or `usleep()` to wait between iterations:
60+
61+
```php
62+
function sidekick_should_stop(float $timeout = 0): bool
63+
{
64+
static $signalingStream;
65+
$signalingStream ??= frankenphp_sidekick_get_signaling_stream();
66+
$s = (int) $timeout;
67+
68+
return 0 !== stream_select(...[[$signalingStream], [], [], $s, (int) (($timeout - $s) * 1e6)]);
69+
};
70+
71+
do {
72+
// ... do work, call set_vars() ...
73+
} while (!sidekick_should_stop(5));
74+
```
75+
76+
**Why not `sleep()`?** `sleep()` and `usleep()` block at the C level and cannot be interrupted.
77+
A sidekick using `sleep(60)` would delay shutdown or worker restart by up to 60 seconds.
78+
`stream_select()` with the signaling stream wakes up immediately when FrankenPHP needs the thread to stop.
5979

6080
- Throws `RuntimeException` if not called from a sidekick context
6181

@@ -72,25 +92,29 @@ require __DIR__.'/../vendor/autoload.php';
7292
$command = $_SERVER['FRANKENPHP_SIDEKICK_NAME'] ?? '';
7393

7494
match ($command) {
75-
'redis-watcher' => runRedisWatcher(),
95+
'redis-watcher' => run_redis_watcher(),
7696
default => throw new \RuntimeException("Unknown sidekick: $command"),
7797
};
7898

79-
function runRedisWatcher(): void
99+
function sidekick_should_stop(float $timeout = 0): bool
100+
{
101+
static $signalingStream;
102+
$signalingStream ??= frankenphp_sidekick_get_signaling_stream();
103+
$s = (int) $timeout;
104+
105+
return 0 !== stream_select(...[[$signalingStream], [], [], $s, (int) (($timeout - $s) * 1e6)]);
106+
};
107+
108+
function run_redis_watcher(): void
80109
{
81-
frankenphp_sidekick_set_vars([
82-
'MASTER_HOST' => '10.0.0.1',
83-
'MASTER_PORT' => '6379',
84-
]);
85110

86-
while (!frankenphp_sidekick_should_stop()) {
87-
$master = discoverRedisMaster();
111+
do {
112+
$master = discover_redis_master();
88113
frankenphp_sidekick_set_vars([
89114
'MASTER_HOST' => $master['host'],
90115
'MASTER_PORT' => (string) $master['port'],
91116
]);
92-
usleep(100_000);
93-
}
117+
} while (!sidekick_should_stop(0.1));
94118
}
95119
```
96120

@@ -132,5 +156,5 @@ if (function_exists('frankenphp_sidekick_get_vars')) {
132156
- `SCRIPT_FILENAME` is set to the entrypoint's full path
133157
- `$_SERVER['FRANKENPHP_SIDEKICK_NAME']` and `$_SERVER['argv'][1]` contain the sidekick name
134158
- Crash recovery: automatic restart with exponential backoff
135-
- Graceful shutdown via `frankenphp_sidekick_should_stop()`
159+
- Graceful shutdown via `frankenphp_sidekick_get_signaling_stream()` and `stream_select()`
136160
- Use `error_log()` or `frankenphp_log()` for logging — avoid `echo`

frankenphp.c

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ HashTable *main_thread_env = NULL;
8686
__thread uintptr_t thread_index;
8787
__thread bool is_worker_thread = false;
8888
__thread char *sidekick_name = NULL;
89+
__thread int sidekick_stop_fds[2] = {-1, -1};
8990
__thread HashTable *sandboxed_env = NULL;
9091

9192
void frankenphp_update_local_thread_context(bool is_worker) {
@@ -98,8 +99,16 @@ void frankenphp_update_local_thread_context(bool is_worker) {
9899
void frankenphp_set_sidekick_name(char *name) {
99100
sidekick_name = name;
100101
zend_unset_timeout();
102+
103+
/* Create a pipe for stop signaling */
104+
if (pipe(sidekick_stop_fds) != 0) {
105+
sidekick_stop_fds[0] = -1;
106+
sidekick_stop_fds[1] = -1;
107+
}
101108
}
102109

110+
int frankenphp_sidekick_get_stop_fd_write(void) { return sidekick_stop_fds[1]; }
111+
103112
static void frankenphp_update_request_context() {
104113
/* the server context is stored on the go side, still SG(server_context) needs
105114
* to not be NULL */
@@ -806,21 +815,41 @@ PHP_FUNCTION(frankenphp_sidekick_get_vars) {
806815
efree(vars_ptrs);
807816
}
808817

809-
PHP_FUNCTION(frankenphp_sidekick_should_stop) {
818+
PHP_FUNCTION(frankenphp_sidekick_get_signaling_stream) {
810819
ZEND_PARSE_PARAMETERS_NONE();
811820

812-
int result = go_frankenphp_sidekick_should_stop(thread_index);
813-
if (result < 0) {
814-
zend_throw_exception(
815-
spl_ce_RuntimeException,
816-
"frankenphp_sidekick_should_stop() can only be called from a sidekick",
817-
0);
821+
if (sidekick_name == NULL) {
822+
zend_throw_exception(spl_ce_RuntimeException,
823+
"frankenphp_sidekick_get_signaling_stream() can only "
824+
"be called from a sidekick",
825+
0);
818826
RETURN_THROWS();
819827
}
820-
if (result > 0) {
821-
RETURN_TRUE;
828+
829+
if (sidekick_stop_fds[0] < 0) {
830+
zend_throw_exception(spl_ce_RuntimeException,
831+
"failed to create sidekick stop pipe", 0);
832+
RETURN_THROWS();
822833
}
823-
RETURN_FALSE;
834+
835+
/* Use php_stream_fopen_from_fd with dup'd fd so PHP can manage its own copy
836+
*/
837+
int fd = dup(sidekick_stop_fds[0]);
838+
if (fd < 0) {
839+
zend_throw_exception(spl_ce_RuntimeException,
840+
"failed to dup sidekick stop fd", 0);
841+
RETURN_THROWS();
842+
}
843+
844+
php_stream *stream = php_stream_fopen_from_fd(fd, "rb", NULL);
845+
if (!stream) {
846+
close(fd);
847+
zend_throw_exception(spl_ce_RuntimeException,
848+
"failed to create stream from stop fd", 0);
849+
RETURN_THROWS();
850+
}
851+
852+
php_stream_to_zval(stream, return_value);
824853
}
825854

826855
PHP_FUNCTION(headers_send) {

frankenphp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ bool frankenphp_shutdown_dummy_request(void);
172172
int frankenphp_execute_script(char *file_name);
173173
void frankenphp_update_local_thread_context(bool is_worker);
174174
void frankenphp_set_sidekick_name(char *name);
175+
int frankenphp_sidekick_get_stop_fd_write(void);
175176

176177
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
177178
bool eval);

frankenphp.stub.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ function frankenphp_sidekick_set_vars(array $vars): void {}
2020

2121
function frankenphp_sidekick_get_vars(string|array $name, float $timeout = 30.0): array {}
2222

23-
function frankenphp_sidekick_should_stop(): bool {}
23+
/** @return resource */
24+
function frankenphp_sidekick_get_signaling_stream() {}
2425

2526
function headers_send(int $status = 200): int {}
2627

frankenphp_arginfo.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_sidekick_get_vars, 0,
1414
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout, IS_DOUBLE, 0, "30.0")
1515
ZEND_END_ARG_INFO()
1616

17-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_sidekick_should_stop, 0, 0, _IS_BOOL, 0)
17+
ZEND_BEGIN_ARG_INFO_EX(arginfo_frankenphp_sidekick_get_signaling_stream, 0, 0, 0)
1818
ZEND_END_ARG_INFO()
1919

2020
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
@@ -57,7 +57,7 @@ ZEND_END_ARG_INFO()
5757
ZEND_FUNCTION(frankenphp_handle_request);
5858
ZEND_FUNCTION(frankenphp_sidekick_set_vars);
5959
ZEND_FUNCTION(frankenphp_sidekick_get_vars);
60-
ZEND_FUNCTION(frankenphp_sidekick_should_stop);
60+
ZEND_FUNCTION(frankenphp_sidekick_get_signaling_stream);
6161
ZEND_FUNCTION(headers_send);
6262
ZEND_FUNCTION(frankenphp_finish_request);
6363
ZEND_FUNCTION(frankenphp_request_headers);
@@ -70,7 +70,7 @@ static const zend_function_entry ext_functions[] = {
7070
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
7171
ZEND_FE(frankenphp_sidekick_set_vars, arginfo_frankenphp_sidekick_set_vars)
7272
ZEND_FE(frankenphp_sidekick_get_vars, arginfo_frankenphp_sidekick_get_vars)
73-
ZEND_FE(frankenphp_sidekick_should_stop, arginfo_frankenphp_sidekick_should_stop)
73+
ZEND_FE(frankenphp_sidekick_get_signaling_stream, arginfo_frankenphp_sidekick_get_signaling_stream)
7474
ZEND_FE(headers_send, arginfo_headers_send)
7575
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
7676
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)

frankenphp_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,35 @@ func TestSidekickCrashRestart(t *testing.T) {
926926
})
927927
}
928928

929+
func TestSidekickSignalingStream(t *testing.T) {
930+
cwd, _ := os.Getwd()
931+
entrypoint := cwd + "/testdata/sidekick-stop-fd-entrypoint.php"
932+
933+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
934+
body, _ := testGet("http://example.com/sidekick-stop-fd.php", handler, t)
935+
assert.Equal(t, "stream", body)
936+
}, &testOptions{
937+
workerScript: "sidekick-stop-fd.php",
938+
nbWorkers: 1,
939+
nbParallelRequests: 1,
940+
initOpts: []frankenphp.Option{frankenphp.WithMaxThreads(50)},
941+
workerOpts: []frankenphp.WorkerOption{
942+
frankenphp.WithWorkerSidekickRegistry(frankenphp.NewSidekickRegistry(entrypoint)),
943+
},
944+
})
945+
}
946+
947+
func TestSidekickSignalingStreamNonSidekick(t *testing.T) {
948+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
949+
body, _ := testGet("http://example.com/sidekick-stop-fd-non-sidekick.php", handler, t)
950+
assert.Equal(t, "thrown", body)
951+
}, &testOptions{
952+
workerScript: "sidekick-stop-fd-non-sidekick.php",
953+
nbWorkers: 1,
954+
nbParallelRequests: 1,
955+
})
956+
}
957+
929958
func ExampleServeHTTP() {
930959
if err := frankenphp.Init(); err != nil {
931960
panic(err)

phpthread.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"runtime"
99
"sync"
10+
"syscall"
1011
"unsafe"
1112

1213
"github.com/dunglas/frankenphp/internal/state"
@@ -71,6 +72,11 @@ func (thread *phpThread) shutdown() {
7172
return
7273
}
7374

75+
// Signal sidekick stop pipe to unblock stream_select/sleep
76+
if handler, ok := thread.handler.(*workerThread); ok && !handler.worker.httpEnabled && handler.worker.sidekickStopFdWrite >= 0 {
77+
syscall.Write(handler.worker.sidekickStopFdWrite, []byte{1})
78+
}
79+
7480
close(thread.drainChan)
7581
thread.state.WaitFor(state.Done)
7682
thread.drainChan = make(chan struct{})
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<?php
22

3+
require __DIR__ . '/sidekick-helper.php';
4+
35
frankenphp_sidekick_set_vars([
46
'BINARY_TEST' => "hello\x00world",
57
'UTF8_TEST' => "héllo wörld 🚀",
68
'EMPTY_VAL' => "",
79
]);
810

9-
while (!frankenphp_sidekick_should_stop()) {
10-
usleep(10_000);
11+
while (!sidekick_should_stop(30)) {
1112
}

testdata/sidekick-crash.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
require __DIR__ . '/sidekick-helper.php';
4+
35
$marker = sys_get_temp_dir() . '/sidekick-crash-' . getmypid();
46
$restarted = file_exists($marker);
57

@@ -10,8 +12,7 @@
1012

1113
frankenphp_sidekick_set_vars(['SIDEKICK_STATUS' => 'restarted']);
1214

13-
while (!frankenphp_sidekick_should_stop()) {
14-
usleep(10_000);
15+
while (!sidekick_should_stop(30)) {
1516
}
1617

1718
@unlink($marker);

testdata/sidekick-dedup.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?php
22

3+
require __DIR__ . '/sidekick-helper.php';
4+
35
$name = $_SERVER['FRANKENPHP_SIDEKICK_NAME'] ?? $_SERVER['argv'][1] ?? 'unknown';
46

57
frankenphp_sidekick_set_vars(['SIDEKICK_NAME' => $name]);
68

7-
while (!frankenphp_sidekick_should_stop()) {
8-
usleep(10_000);
9+
while (!sidekick_should_stop(30)) {
910
}

0 commit comments

Comments
 (0)