Skip to content

Commit e94e3bc

Browse files
committed
Tests.
1 parent e23e0c5 commit e94e3bc

9 files changed

Lines changed: 175 additions & 4 deletions

File tree

caddy/caddy_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package caddy_test
33
import (
44
"bytes"
55
"fmt"
6+
"math/rand/v2"
67
"net/http"
78
"os"
89
"path/filepath"
@@ -11,6 +12,7 @@ import (
1112
"sync"
1213
"sync/atomic"
1314
"testing"
15+
"time"
1416

1517
"github.com/caddyserver/caddy/v2"
1618
"github.com/caddyserver/caddy/v2/caddytest"
@@ -1472,3 +1474,70 @@ func TestDd(t *testing.T) {
14721474
"dump123",
14731475
)
14741476
}
1477+
1478+
// test to force the opcache segfault race condition under concurrency (~1.7s)
1479+
func TestOpcacheReset(t *testing.T) {
1480+
tester := caddytest.NewTester(t)
1481+
tester.InitServer(`
1482+
{
1483+
skip_install_trust
1484+
admin localhost:2999
1485+
http_port `+testPort+`
1486+
metrics
1487+
1488+
frankenphp {
1489+
num_threads 40
1490+
php_ini {
1491+
opcache.enable 1
1492+
zend_extension opcache.so
1493+
opcache.log_verbosity_level 4
1494+
}
1495+
}
1496+
}
1497+
1498+
localhost:`+testPort+` {
1499+
php {
1500+
root ../testdata
1501+
worker {
1502+
file sleep.php
1503+
match /sleep*
1504+
num 20
1505+
}
1506+
}
1507+
}
1508+
`, "caddyfile")
1509+
1510+
wg := sync.WaitGroup{}
1511+
numRequests := 100
1512+
wg.Add(numRequests)
1513+
for i := 0; i < numRequests; i++ {
1514+
1515+
// introduce some random delay
1516+
if rand.IntN(10) > 8 {
1517+
time.Sleep(time.Millisecond * 10)
1518+
}
1519+
1520+
go func() {
1521+
// randomly call opcache_reset
1522+
if rand.IntN(10) > 5 {
1523+
tester.AssertGetResponse(
1524+
"http://localhost:"+testPort+"/opcache_reset.php",
1525+
http.StatusOK,
1526+
"opcache reset done",
1527+
)
1528+
wg.Done()
1529+
return
1530+
}
1531+
1532+
// otherwise call sleep.php with random sleep and work values
1533+
tester.AssertGetResponse(
1534+
fmt.Sprintf("http://localhost:%s/sleep.php?sleep=%d&work=%d", testPort, i, i),
1535+
http.StatusOK,
1536+
fmt.Sprintf("slept for %d ms and worked for %d iterations", i, i),
1537+
)
1538+
wg.Done()
1539+
}()
1540+
}
1541+
1542+
wg.Wait()
1543+
}

frankenphp.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ bool should_filter_var = 0;
7373
__thread uintptr_t thread_index;
7474
__thread bool is_worker_thread = false;
7575
__thread zval *os_environment = NULL;
76+
zif_handler orig_opcache_reset;
7677

7778
void frankenphp_update_local_thread_context(bool is_worker) {
7879
is_worker_thread = is_worker;
@@ -340,6 +341,15 @@ PHP_FUNCTION(frankenphp_getenv) {
340341
}
341342
} /* }}} */
342343

344+
/* {{{ thread-safe opcache reset */
345+
PHP_FUNCTION(frankenphp_opcache_reset) {
346+
if (go_schedule_opcache_reset(thread_index)) {
347+
orig_opcache_reset(INTERNAL_FUNCTION_PARAM_PASSTHRU);
348+
}
349+
350+
RETVAL_FALSE;
351+
} /* }}} */
352+
343353
/* {{{ Fetch all HTTP request headers */
344354
PHP_FUNCTION(frankenphp_request_headers) {
345355
ZEND_PARSE_PARAMETERS_NONE();
@@ -570,6 +580,15 @@ PHP_MINIT_FUNCTION(frankenphp) {
570580
php_error(E_WARNING, "Failed to find built-in getenv function");
571581
}
572582

583+
// Override opcache_reset
584+
func = zend_hash_str_find_ptr(CG(function_table), "opcache_reset",
585+
sizeof("opcache_reset") - 1);
586+
if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {
587+
orig_opcache_reset = ((zend_internal_function *)func)->handler;
588+
((zend_internal_function *)func)->handler =
589+
ZEND_FN(frankenphp_opcache_reset);
590+
}
591+
573592
return SUCCESS;
574593
}
575594

frankenphp.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ import (
3232
"runtime"
3333
"strings"
3434
"sync"
35+
"sync/atomic"
3536
"syscall"
3637
"time"
3738
"unsafe"
3839
// debug on Linux
3940
//_ "github.com/ianlancetaylor/cgosymbolizer"
41+
42+
"github.com/dunglas/frankenphp/internal/state"
4043
)
4144

4245
type contextKeyStruct struct{}
@@ -56,8 +59,10 @@ var (
5659
contextKey = contextKeyStruct{}
5760
serverHeader = []string{"FrankenPHP"}
5861

59-
isRunning bool
60-
onServerShutdown []func()
62+
isRunning bool
63+
isOpcacheResetting atomic.Bool
64+
threadsAreRestarting atomic.Bool
65+
onServerShutdown []func()
6166

6267
// Set default values to make Shutdown() idempotent
6368
globalMu sync.Mutex
@@ -698,6 +703,61 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
698703
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
699704
}
700705

706+
//export go_schedule_opcache_reset
707+
func go_schedule_opcache_reset(threadIndex C.uintptr_t) C.bool {
708+
if isOpcacheResetting.CompareAndSwap(false, true) {
709+
restartThreadsForOpcacheReset(nil)
710+
return C.bool(true)
711+
}
712+
713+
return C.bool(phpThreads[threadIndex].state.Is(state.Restarting))
714+
}
715+
716+
// restart all threads for an opcache_reset
717+
func restartThreadsForOpcacheReset(exceptThisThread *phpThread) {
718+
if threadsAreRestarting.Load() {
719+
// ignore reloads while a restart is already ongoing
720+
return
721+
}
722+
723+
// disallow scaling threads while restarting workers
724+
scalingMu.Lock()
725+
defer scalingMu.Unlock()
726+
727+
// drain workers
728+
globalLogger.Info("Restarting all PHP threads for opcache_reset")
729+
threadsToRestart := drainWorkerThreads()
730+
731+
// drain regular threads
732+
globalLogger.Info("Draining regular PHP threads for opcache_reset")
733+
wg := sync.WaitGroup{}
734+
for _, thread := range regularThreads {
735+
if thread.state.Is(state.Ready) {
736+
threadsToRestart = append(threadsToRestart, thread)
737+
thread.state.Set(state.Restarting)
738+
close(thread.drainChan)
739+
740+
wg.Go(func() {
741+
thread.state.WaitFor(state.Yielding)
742+
})
743+
}
744+
}
745+
746+
// other threads may not parse new scripts while this thread is scheduling an opcache_reset
747+
// sleeping a bit here makes this much less likely to happen
748+
// waiting for all other threads to drain first can potentially deadlock
749+
time.Sleep(100 * time.Millisecond)
750+
751+
go func() {
752+
wg.Wait()
753+
for _, thread := range threadsToRestart {
754+
thread.drainChan = make(chan struct{})
755+
thread.state.Set(state.Ready)
756+
isOpcacheResetting.Store(false)
757+
}
758+
}()
759+
}
760+
701761
// ExecuteScriptCLI executes the PHP script passed as parameter.
702762
// It returns the exit status code of the script.
703763
func ExecuteScriptCLI(script string, args []string) int {

phpmainthread_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
9797

9898
var (
9999
isDone atomic.Bool
100-
wg sync.WaitGroup
100+
wg sync.WaitGroup
101101
)
102102

103103
numThreads := 10

testdata/opcache_reset.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
require_once __DIR__.'/_executor.php';
4+
5+
return function () {
6+
require __DIR__ .'/require.php';
7+
opcache_reset();
8+
echo "opcache reset done";
9+
};

testdata/require.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
// dummy require file for opcache_reset test
4+
return function (){
5+
echo "";
6+
};

threadregular.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package frankenphp
22

3+
// #include "frankenphp.h"
4+
import "C"
35
import (
46
"context"
57
"runtime"
@@ -46,6 +48,10 @@ func (handler *regularThread) beforeScriptExecution() string {
4648
handler.state.Set(state.Ready)
4749

4850
return handler.waitForRequest()
51+
case state.Restarting:
52+
handler.state.Set(state.Yielding)
53+
handler.state.WaitFor(state.Ready, state.ShuttingDown)
54+
return handler.beforeScriptExecution()
4955

5056
case state.Ready:
5157
return handler.waitForRequest()

watcher.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
type hotReloadOpt struct {
13-
hotReload []*watcher.PatternGroup
13+
hotReload []*watcher.PatternGroup
1414
}
1515

1616
var restartWorkers atomic.Bool

worker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ func drainWorkerThreads() []*phpThread {
213213
// RestartWorkers attempts to restart all workers gracefully
214214
// All workers must be restarted at the same time to prevent issues with opcache resetting.
215215
func RestartWorkers() {
216+
threadsAreRestarting.Store(true)
217+
defer threadsAreRestarting.Store(false)
216218
// disallow scaling threads while restarting workers
217219
scalingMu.Lock()
218220
defer scalingMu.Unlock()

0 commit comments

Comments
 (0)