mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
refactor: extract the state module and make the backoff error instead of panic
This PR: - moves state.go to its own module - moves the phpheaders test the phpheaders module - simplifies backoff.go - makes the backoff error instead of panic (so it can be tested) - removes some unused C structs
This commit is contained in:
committed by
GitHub
parent
16e2bbb969
commit
98573ed7c0
51
backoff.go
51
backoff.go
@@ -1,51 +0,0 @@
|
|||||||
package frankenphp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type exponentialBackoff struct {
|
|
||||||
backoff time.Duration
|
|
||||||
failureCount int
|
|
||||||
mu sync.RWMutex
|
|
||||||
maxBackoff time.Duration
|
|
||||||
minBackoff time.Duration
|
|
||||||
maxConsecutiveFailures int
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordSuccess resets the backoff and failureCount
|
|
||||||
func (e *exponentialBackoff) recordSuccess() {
|
|
||||||
e.mu.Lock()
|
|
||||||
e.failureCount = 0
|
|
||||||
e.backoff = e.minBackoff
|
|
||||||
e.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordFailure increments the failure count and increases the backoff, it returns true if maxConsecutiveFailures has been reached
|
|
||||||
func (e *exponentialBackoff) recordFailure() bool {
|
|
||||||
e.mu.Lock()
|
|
||||||
e.failureCount += 1
|
|
||||||
if e.backoff < e.minBackoff {
|
|
||||||
e.backoff = e.minBackoff
|
|
||||||
}
|
|
||||||
|
|
||||||
e.backoff = min(e.backoff*2, e.maxBackoff)
|
|
||||||
|
|
||||||
e.mu.Unlock()
|
|
||||||
return e.maxConsecutiveFailures != -1 && e.failureCount >= e.maxConsecutiveFailures
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait sleeps for the backoff duration if failureCount is non-zero.
|
|
||||||
// NOTE: this is not tested and should be kept 'obviously correct' (i.e., simple)
|
|
||||||
func (e *exponentialBackoff) wait() {
|
|
||||||
e.mu.RLock()
|
|
||||||
if e.failureCount == 0 {
|
|
||||||
e.mu.RUnlock()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.mu.RUnlock()
|
|
||||||
|
|
||||||
time.Sleep(e.backoff)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package frankenphp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestExponentialBackoff_Reset(t *testing.T) {
|
|
||||||
e := &exponentialBackoff{
|
|
||||||
maxBackoff: 5 * time.Second,
|
|
||||||
minBackoff: 500 * time.Millisecond,
|
|
||||||
maxConsecutiveFailures: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.False(t, e.recordFailure())
|
|
||||||
assert.False(t, e.recordFailure())
|
|
||||||
e.recordSuccess()
|
|
||||||
|
|
||||||
e.mu.RLock()
|
|
||||||
defer e.mu.RUnlock()
|
|
||||||
assert.Equal(t, 0, e.failureCount, "expected failureCount to be reset to 0")
|
|
||||||
assert.Equal(t, e.backoff, e.minBackoff, "expected backoff to be reset to minBackoff")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExponentialBackoff_Trigger(t *testing.T) {
|
|
||||||
e := &exponentialBackoff{
|
|
||||||
maxBackoff: 500 * 3 * time.Millisecond,
|
|
||||||
minBackoff: 500 * time.Millisecond,
|
|
||||||
maxConsecutiveFailures: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.False(t, e.recordFailure())
|
|
||||||
assert.False(t, e.recordFailure())
|
|
||||||
assert.True(t, e.recordFailure())
|
|
||||||
|
|
||||||
e.mu.RLock()
|
|
||||||
defer e.mu.RUnlock()
|
|
||||||
assert.Equal(t, e.failureCount, e.maxConsecutiveFailures, "expected failureCount to be maxConsecutiveFailures")
|
|
||||||
assert.Equal(t, e.backoff, e.maxBackoff, "expected backoff to be maxBackoff")
|
|
||||||
}
|
|
||||||
6
cgi.go
6
cgi.go
@@ -277,13 +277,13 @@ func splitPos(path string, splitPath []string) int {
|
|||||||
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
|
// See: https://github.com/php/php-src/blob/345e04b619c3bc11ea17ee02cdecad6ae8ce5891/main/SAPI.h#L72
|
||||||
//
|
//
|
||||||
//export go_update_request_info
|
//export go_update_request_info
|
||||||
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) C.bool {
|
func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) {
|
||||||
thread := phpThreads[threadIndex]
|
thread := phpThreads[threadIndex]
|
||||||
fc := thread.frankenPHPContext()
|
fc := thread.frankenPHPContext()
|
||||||
request := fc.request
|
request := fc.request
|
||||||
|
|
||||||
if request == nil {
|
if request == nil {
|
||||||
return C.bool(fc.worker != nil)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authUser, authPassword, ok := request.BasicAuth()
|
authUser, authPassword, ok := request.BasicAuth()
|
||||||
@@ -311,8 +311,6 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
|
|||||||
info.request_uri = thread.pinCString(request.URL.RequestURI())
|
info.request_uri = thread.pinCString(request.URL.RequestURI())
|
||||||
|
|
||||||
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
|
info.proto_num = C.int(request.ProtoMajor*1000 + request.ProtoMinor)
|
||||||
|
|
||||||
return C.bool(fc.worker != nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
|
// SanitizedPathJoin performs filepath.Join(root, reqPath) that
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package frankenphp
|
package frankenphp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
|
// EXPERIMENTAL: ThreadDebugState prints the state of a single PHP thread - debugging purposes only
|
||||||
type ThreadDebugState struct {
|
type ThreadDebugState struct {
|
||||||
Index int
|
Index int
|
||||||
@@ -23,7 +27,7 @@ func DebugState() FrankenPHPDebugState {
|
|||||||
ReservedThreadCount: 0,
|
ReservedThreadCount: 0,
|
||||||
}
|
}
|
||||||
for _, thread := range phpThreads {
|
for _, thread := range phpThreads {
|
||||||
if thread.state.is(stateReserved) {
|
if thread.state.Is(state.Reserved) {
|
||||||
fullState.ReservedThreadCount++
|
fullState.ReservedThreadCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -38,9 +42,9 @@ func threadDebugState(thread *phpThread) ThreadDebugState {
|
|||||||
return ThreadDebugState{
|
return ThreadDebugState{
|
||||||
Index: thread.threadIndex,
|
Index: thread.threadIndex,
|
||||||
Name: thread.name(),
|
Name: thread.name(),
|
||||||
State: thread.state.name(),
|
State: thread.state.Name(),
|
||||||
IsWaiting: thread.state.isInWaitingState(),
|
IsWaiting: thread.state.IsInWaitingState(),
|
||||||
IsBusy: !thread.state.isInWaitingState(),
|
IsBusy: !thread.state.IsInWaitingState(),
|
||||||
WaitingSinceMilliseconds: thread.state.waitTime(),
|
WaitingSinceMilliseconds: thread.state.WaitTime(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
env.go
5
env.go
@@ -1,10 +1,9 @@
|
|||||||
package frankenphp
|
package frankenphp
|
||||||
|
|
||||||
// #cgo nocallback frankenphp_init_persistent_string
|
// #cgo nocallback frankenphp_init_persistent_string
|
||||||
// #cgo nocallback frankenphp_add_assoc_str_ex
|
|
||||||
// #cgo noescape frankenphp_init_persistent_string
|
// #cgo noescape frankenphp_init_persistent_string
|
||||||
// #cgo noescape frankenphp_add_assoc_str_ex
|
|
||||||
// #include "frankenphp.h"
|
// #include "frankenphp.h"
|
||||||
|
// #include <Zend/zend_API.h>
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -98,7 +97,7 @@ func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
|
|||||||
env := getSandboxedEnv(thread)
|
env := getSandboxedEnv(thread)
|
||||||
|
|
||||||
for key, val := range env {
|
for key, val := range env {
|
||||||
C.frankenphp_add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
|
C.add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
frankenphp.c
17
frankenphp.c
@@ -51,7 +51,6 @@ frankenphp_version frankenphp_get_version() {
|
|||||||
|
|
||||||
frankenphp_config frankenphp_get_config() {
|
frankenphp_config frankenphp_get_config() {
|
||||||
return (frankenphp_config){
|
return (frankenphp_config){
|
||||||
frankenphp_get_version(),
|
|
||||||
#ifdef ZTS
|
#ifdef ZTS
|
||||||
true,
|
true,
|
||||||
#else
|
#else
|
||||||
@@ -75,6 +74,10 @@ __thread uintptr_t thread_index;
|
|||||||
__thread bool is_worker_thread = false;
|
__thread bool is_worker_thread = false;
|
||||||
__thread zval *os_environment = NULL;
|
__thread zval *os_environment = NULL;
|
||||||
|
|
||||||
|
void frankenphp_update_local_thread_context(bool is_worker) {
|
||||||
|
is_worker_thread = is_worker;
|
||||||
|
}
|
||||||
|
|
||||||
static void frankenphp_update_request_context() {
|
static void frankenphp_update_request_context() {
|
||||||
/* the server context is stored on the go side, still SG(server_context) needs
|
/* the server context is stored on the go side, still SG(server_context) needs
|
||||||
* to not be NULL */
|
* to not be NULL */
|
||||||
@@ -82,7 +85,7 @@ static void frankenphp_update_request_context() {
|
|||||||
/* status It is not reset by zend engine, set it to 200. */
|
/* status It is not reset by zend engine, set it to 200. */
|
||||||
SG(sapi_headers).http_response_code = 200;
|
SG(sapi_headers).http_response_code = 200;
|
||||||
|
|
||||||
is_worker_thread = go_update_request_info(thread_index, &SG(request_info));
|
go_update_request_info(thread_index, &SG(request_info));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void frankenphp_free_request_context() {
|
static void frankenphp_free_request_context() {
|
||||||
@@ -206,11 +209,6 @@ PHPAPI void get_full_env(zval *track_vars_array) {
|
|||||||
go_getfullenv(thread_index, track_vars_array);
|
go_getfullenv(thread_index, track_vars_array);
|
||||||
}
|
}
|
||||||
|
|
||||||
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
|
|
||||||
size_t keylen, zend_string *val) {
|
|
||||||
add_assoc_str_ex(track_vars_array, key, keylen, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Adapted from php_request_startup() */
|
/* Adapted from php_request_startup() */
|
||||||
static int frankenphp_worker_request_startup() {
|
static int frankenphp_worker_request_startup() {
|
||||||
int retval = SUCCESS;
|
int retval = SUCCESS;
|
||||||
@@ -652,8 +650,9 @@ static char *frankenphp_read_cookies(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* all variables with well defined keys can safely be registered like this */
|
/* all variables with well defined keys can safely be registered like this */
|
||||||
void frankenphp_register_trusted_var(zend_string *z_key, char *value,
|
static inline void frankenphp_register_trusted_var(zend_string *z_key,
|
||||||
size_t val_len, HashTable *ht) {
|
char *value, size_t val_len,
|
||||||
|
HashTable *ht) {
|
||||||
if (value == NULL) {
|
if (value == NULL) {
|
||||||
zval empty;
|
zval empty;
|
||||||
ZVAL_EMPTY_STRING(&empty);
|
ZVAL_EMPTY_STRING(&empty);
|
||||||
|
|||||||
10
frankenphp.h
10
frankenphp.h
@@ -23,12 +23,6 @@ typedef struct ht_key_value_pair {
|
|||||||
size_t val_len;
|
size_t val_len;
|
||||||
} ht_key_value_pair;
|
} ht_key_value_pair;
|
||||||
|
|
||||||
typedef struct php_variable {
|
|
||||||
const char *var;
|
|
||||||
size_t data_len;
|
|
||||||
char *data;
|
|
||||||
} php_variable;
|
|
||||||
|
|
||||||
typedef struct frankenphp_version {
|
typedef struct frankenphp_version {
|
||||||
unsigned char major_version;
|
unsigned char major_version;
|
||||||
unsigned char minor_version;
|
unsigned char minor_version;
|
||||||
@@ -40,7 +34,6 @@ typedef struct frankenphp_version {
|
|||||||
frankenphp_version frankenphp_get_version();
|
frankenphp_version frankenphp_get_version();
|
||||||
|
|
||||||
typedef struct frankenphp_config {
|
typedef struct frankenphp_config {
|
||||||
frankenphp_version version;
|
|
||||||
bool zts;
|
bool zts;
|
||||||
bool zend_signals;
|
bool zend_signals;
|
||||||
bool zend_max_execution_timers;
|
bool zend_max_execution_timers;
|
||||||
@@ -52,6 +45,7 @@ bool frankenphp_new_php_thread(uintptr_t thread_index);
|
|||||||
|
|
||||||
bool frankenphp_shutdown_dummy_request(void);
|
bool frankenphp_shutdown_dummy_request(void);
|
||||||
int frankenphp_execute_script(char *file_name);
|
int frankenphp_execute_script(char *file_name);
|
||||||
|
void frankenphp_update_local_thread_context(bool is_worker);
|
||||||
|
|
||||||
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
|
int frankenphp_execute_script_cli(char *script, int argc, char **argv,
|
||||||
bool eval);
|
bool eval);
|
||||||
@@ -65,8 +59,6 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
|
|||||||
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
|
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
|
||||||
int frankenphp_reset_opcache(void);
|
int frankenphp_reset_opcache(void);
|
||||||
int frankenphp_get_current_memory_limit();
|
int frankenphp_get_current_memory_limit();
|
||||||
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
|
|
||||||
size_t keylen, zend_string *val);
|
|
||||||
|
|
||||||
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
|
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
|
||||||
zval *track_vars_array);
|
zval *track_vars_array);
|
||||||
|
|||||||
@@ -618,10 +618,12 @@ func testRequestHeaders(t *testing.T, opts *testOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFailingWorker(t *testing.T) {
|
func TestFailingWorker(t *testing.T) {
|
||||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
err := frankenphp.Init(
|
||||||
body, _ := testGet("http://example.com/failing-worker.php", handler, t)
|
frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
|
||||||
assert.Contains(t, body, "ok")
|
frankenphp.WithWorkers("failing worker", "testdata/failing-worker.php", 4, frankenphp.WithWorkerMaxFailures(1)),
|
||||||
}, &testOptions{workerScript: "failing-worker.php"})
|
frankenphp.WithNumThreads(5),
|
||||||
|
)
|
||||||
|
assert.Error(t, err, "should return an immediate error if workers fail on startup")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnv(t *testing.T) {
|
func TestEnv(t *testing.T) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package phpheaders
|
package phpheaders
|
||||||
|
|
||||||
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|||||||
22
internal/phpheaders/phpheaders_test.go
Normal file
22
internal/phpheaders/phpheaders_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package phpheaders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAllCommonHeadersAreCorrect(t *testing.T) {
|
||||||
|
fakeRequest := httptest.NewRequest("GET", "http://localhost", nil)
|
||||||
|
|
||||||
|
for header, phpHeader := range CommonRequestHeaders {
|
||||||
|
// verify that common and uncommon headers return the same result
|
||||||
|
expectedPHPHeader := GetUnCommonHeader(t.Context(), header)
|
||||||
|
assert.Equal(t, phpHeader+"\x00", expectedPHPHeader, "header is not well formed: "+phpHeader)
|
||||||
|
|
||||||
|
// net/http will capitalize lowercase headers, verify that headers are capitalized
|
||||||
|
fakeRequest.Header.Add(header, "foo")
|
||||||
|
assert.Contains(t, fakeRequest.Header, header, "header is not correctly capitalized: "+header)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +1,38 @@
|
|||||||
package frankenphp
|
package state
|
||||||
|
|
||||||
|
import "C"
|
||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stateID uint8
|
type State string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// livecycle states of a thread
|
// livecycle States of a thread
|
||||||
stateReserved stateID = iota
|
Reserved State = "reserved"
|
||||||
stateBooting
|
Booting State = "booting"
|
||||||
stateBootRequested
|
BootRequested State = "boot requested"
|
||||||
stateShuttingDown
|
ShuttingDown State = "shutting down"
|
||||||
stateDone
|
Done State = "done"
|
||||||
|
|
||||||
// these states are 'stable' and safe to transition from at any time
|
// these States are 'stable' and safe to transition from at any time
|
||||||
stateInactive
|
Inactive State = "inactive"
|
||||||
stateReady
|
Ready State = "ready"
|
||||||
|
|
||||||
// states necessary for restarting workers
|
// States necessary for restarting workers
|
||||||
stateRestarting
|
Restarting State = "restarting"
|
||||||
stateYielding
|
Yielding State = "yielding"
|
||||||
|
|
||||||
// states necessary for transitioning between different handlers
|
// States necessary for transitioning between different handlers
|
||||||
stateTransitionRequested
|
TransitionRequested State = "transition requested"
|
||||||
stateTransitionInProgress
|
TransitionInProgress State = "transition in progress"
|
||||||
stateTransitionComplete
|
TransitionComplete State = "transition complete"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stateNames = map[stateID]string{
|
type ThreadState struct {
|
||||||
stateReserved: "reserved",
|
currentState State
|
||||||
stateBooting: "booting",
|
|
||||||
stateInactive: "inactive",
|
|
||||||
stateReady: "ready",
|
|
||||||
stateShuttingDown: "shutting down",
|
|
||||||
stateDone: "done",
|
|
||||||
stateRestarting: "restarting",
|
|
||||||
stateYielding: "yielding",
|
|
||||||
stateTransitionRequested: "transition requested",
|
|
||||||
stateTransitionInProgress: "transition in progress",
|
|
||||||
stateTransitionComplete: "transition complete",
|
|
||||||
}
|
|
||||||
|
|
||||||
type threadState struct {
|
|
||||||
currentState stateID
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
subscribers []stateSubscriber
|
subscribers []stateSubscriber
|
||||||
// how long threads have been waiting in stable states
|
// how long threads have been waiting in stable states
|
||||||
@@ -54,19 +41,19 @@ type threadState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type stateSubscriber struct {
|
type stateSubscriber struct {
|
||||||
states []stateID
|
states []State
|
||||||
ch chan struct{}
|
ch chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newThreadState() *threadState {
|
func NewThreadState() *ThreadState {
|
||||||
return &threadState{
|
return &ThreadState{
|
||||||
currentState: stateReserved,
|
currentState: Reserved,
|
||||||
subscribers: []stateSubscriber{},
|
subscribers: []stateSubscriber{},
|
||||||
mu: sync.RWMutex{},
|
mu: sync.RWMutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *threadState) is(state stateID) bool {
|
func (ts *ThreadState) Is(state State) bool {
|
||||||
ts.mu.RLock()
|
ts.mu.RLock()
|
||||||
ok := ts.currentState == state
|
ok := ts.currentState == state
|
||||||
ts.mu.RUnlock()
|
ts.mu.RUnlock()
|
||||||
@@ -74,7 +61,7 @@ func (ts *threadState) is(state stateID) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *threadState) compareAndSwap(compareTo stateID, swapTo stateID) bool {
|
func (ts *ThreadState) CompareAndSwap(compareTo State, swapTo State) bool {
|
||||||
ts.mu.Lock()
|
ts.mu.Lock()
|
||||||
ok := ts.currentState == compareTo
|
ok := ts.currentState == compareTo
|
||||||
if ok {
|
if ok {
|
||||||
@@ -86,11 +73,11 @@ func (ts *threadState) compareAndSwap(compareTo stateID, swapTo stateID) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *threadState) name() string {
|
func (ts *ThreadState) Name() string {
|
||||||
return stateNames[ts.get()]
|
return string(ts.Get())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *threadState) get() stateID {
|
func (ts *ThreadState) Get() State {
|
||||||
ts.mu.RLock()
|
ts.mu.RLock()
|
||||||
id := ts.currentState
|
id := ts.currentState
|
||||||
ts.mu.RUnlock()
|
ts.mu.RUnlock()
|
||||||
@@ -98,14 +85,14 @@ func (ts *threadState) get() stateID {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *threadState) set(nextState stateID) {
|
func (ts *ThreadState) Set(nextState State) {
|
||||||
ts.mu.Lock()
|
ts.mu.Lock()
|
||||||
ts.currentState = nextState
|
ts.currentState = nextState
|
||||||
ts.notifySubscribers(nextState)
|
ts.notifySubscribers(nextState)
|
||||||
ts.mu.Unlock()
|
ts.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *threadState) notifySubscribers(nextState stateID) {
|
func (ts *ThreadState) notifySubscribers(nextState State) {
|
||||||
if len(ts.subscribers) == 0 {
|
if len(ts.subscribers) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -122,7 +109,7 @@ func (ts *threadState) notifySubscribers(nextState stateID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// block until the thread reaches a certain state
|
// block until the thread reaches a certain state
|
||||||
func (ts *threadState) waitFor(states ...stateID) {
|
func (ts *ThreadState) WaitFor(states ...State) {
|
||||||
ts.mu.Lock()
|
ts.mu.Lock()
|
||||||
if slices.Contains(states, ts.currentState) {
|
if slices.Contains(states, ts.currentState) {
|
||||||
ts.mu.Unlock()
|
ts.mu.Unlock()
|
||||||
@@ -138,15 +125,15 @@ func (ts *threadState) waitFor(states ...stateID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// safely request a state change from a different goroutine
|
// safely request a state change from a different goroutine
|
||||||
func (ts *threadState) requestSafeStateChange(nextState stateID) bool {
|
func (ts *ThreadState) RequestSafeStateChange(nextState State) bool {
|
||||||
ts.mu.Lock()
|
ts.mu.Lock()
|
||||||
switch ts.currentState {
|
switch ts.currentState {
|
||||||
// disallow state changes if shutting down or done
|
// disallow state changes if shutting down or done
|
||||||
case stateShuttingDown, stateDone, stateReserved:
|
case ShuttingDown, Done, Reserved:
|
||||||
ts.mu.Unlock()
|
ts.mu.Unlock()
|
||||||
return false
|
return false
|
||||||
// ready and inactive are safe states to transition from
|
// ready and inactive are safe states to transition from
|
||||||
case stateReady, stateInactive:
|
case Ready, Inactive:
|
||||||
ts.currentState = nextState
|
ts.currentState = nextState
|
||||||
ts.notifySubscribers(nextState)
|
ts.notifySubscribers(nextState)
|
||||||
ts.mu.Unlock()
|
ts.mu.Unlock()
|
||||||
@@ -155,12 +142,12 @@ func (ts *threadState) requestSafeStateChange(nextState stateID) bool {
|
|||||||
ts.mu.Unlock()
|
ts.mu.Unlock()
|
||||||
|
|
||||||
// wait for the state to change to a safe state
|
// wait for the state to change to a safe state
|
||||||
ts.waitFor(stateReady, stateInactive, stateShuttingDown)
|
ts.WaitFor(Ready, Inactive, ShuttingDown)
|
||||||
return ts.requestSafeStateChange(nextState)
|
return ts.RequestSafeStateChange(nextState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// markAsWaiting hints that the thread reached a stable state and is waiting for requests or shutdown
|
// markAsWaiting hints that the thread reached a stable state and is waiting for requests or shutdown
|
||||||
func (ts *threadState) markAsWaiting(isWaiting bool) {
|
func (ts *ThreadState) MarkAsWaiting(isWaiting bool) {
|
||||||
ts.mu.Lock()
|
ts.mu.Lock()
|
||||||
if isWaiting {
|
if isWaiting {
|
||||||
ts.isWaiting = true
|
ts.isWaiting = true
|
||||||
@@ -172,7 +159,7 @@ func (ts *threadState) markAsWaiting(isWaiting bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isWaitingState returns true if a thread is waiting for a request or shutdown
|
// isWaitingState returns true if a thread is waiting for a request or shutdown
|
||||||
func (ts *threadState) isInWaitingState() bool {
|
func (ts *ThreadState) IsInWaitingState() bool {
|
||||||
ts.mu.RLock()
|
ts.mu.RLock()
|
||||||
isWaiting := ts.isWaiting
|
isWaiting := ts.isWaiting
|
||||||
ts.mu.RUnlock()
|
ts.mu.RUnlock()
|
||||||
@@ -180,7 +167,7 @@ func (ts *threadState) isInWaitingState() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// waitTime returns the time since the thread is waiting in a stable state in ms
|
// waitTime returns the time since the thread is waiting in a stable state in ms
|
||||||
func (ts *threadState) waitTime() int64 {
|
func (ts *ThreadState) WaitTime() int64 {
|
||||||
ts.mu.RLock()
|
ts.mu.RLock()
|
||||||
waitTime := int64(0)
|
waitTime := int64(0)
|
||||||
if ts.isWaiting {
|
if ts.isWaiting {
|
||||||
@@ -189,3 +176,9 @@ func (ts *threadState) waitTime() int64 {
|
|||||||
ts.mu.RUnlock()
|
ts.mu.RUnlock()
|
||||||
return waitTime
|
return waitTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *ThreadState) SetWaitTime(t time.Time) {
|
||||||
|
ts.mu.Lock()
|
||||||
|
ts.waitingSince = t
|
||||||
|
ts.mu.Unlock()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package frankenphp
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -8,37 +8,38 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Test2GoroutinesYieldToEachOtherViaStates(t *testing.T) {
|
func Test2GoroutinesYieldToEachOtherViaStates(t *testing.T) {
|
||||||
threadState := &threadState{currentState: stateBooting}
|
threadState := &ThreadState{currentState: Booting}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
threadState.waitFor(stateInactive)
|
threadState.WaitFor(Inactive)
|
||||||
assert.True(t, threadState.is(stateInactive))
|
assert.True(t, threadState.Is(Inactive))
|
||||||
threadState.set(stateReady)
|
threadState.Set(Ready)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
threadState.set(stateInactive)
|
threadState.Set(Inactive)
|
||||||
threadState.waitFor(stateReady)
|
threadState.WaitFor(Ready)
|
||||||
assert.True(t, threadState.is(stateReady))
|
assert.True(t, threadState.Is(Ready))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStateShouldHaveCorrectAmountOfSubscribers(t *testing.T) {
|
func TestStateShouldHaveCorrectAmountOfSubscribers(t *testing.T) {
|
||||||
threadState := &threadState{currentState: stateBooting}
|
threadState := &ThreadState{currentState: Booting}
|
||||||
|
|
||||||
// 3 subscribers waiting for different states
|
// 3 subscribers waiting for different states
|
||||||
go threadState.waitFor(stateInactive)
|
go threadState.WaitFor(Inactive)
|
||||||
go threadState.waitFor(stateInactive, stateShuttingDown)
|
go threadState.WaitFor(Inactive, ShuttingDown)
|
||||||
go threadState.waitFor(stateShuttingDown)
|
go threadState.WaitFor(ShuttingDown)
|
||||||
|
|
||||||
assertNumberOfSubscribers(t, threadState, 3)
|
assertNumberOfSubscribers(t, threadState, 3)
|
||||||
|
|
||||||
threadState.set(stateInactive)
|
threadState.Set(Inactive)
|
||||||
assertNumberOfSubscribers(t, threadState, 1)
|
assertNumberOfSubscribers(t, threadState, 1)
|
||||||
|
|
||||||
assert.True(t, threadState.compareAndSwap(stateInactive, stateShuttingDown))
|
assert.True(t, threadState.CompareAndSwap(Inactive, ShuttingDown))
|
||||||
assertNumberOfSubscribers(t, threadState, 0)
|
assertNumberOfSubscribers(t, threadState, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertNumberOfSubscribers(t *testing.T, threadState *threadState, expected int) {
|
func assertNumberOfSubscribers(t *testing.T, threadState *ThreadState, expected int) {
|
||||||
|
t.Helper()
|
||||||
for range 10_000 { // wait for 1 second max
|
for range 10_000 { // wait for 1 second max
|
||||||
time.Sleep(100 * time.Microsecond)
|
time.Sleep(100 * time.Microsecond)
|
||||||
threadState.mu.RLock()
|
threadState.mu.RLock()
|
||||||
@@ -14,12 +14,13 @@ import (
|
|||||||
|
|
||||||
"github.com/dunglas/frankenphp/internal/memory"
|
"github.com/dunglas/frankenphp/internal/memory"
|
||||||
"github.com/dunglas/frankenphp/internal/phpheaders"
|
"github.com/dunglas/frankenphp/internal/phpheaders"
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// represents the main PHP thread
|
// represents the main PHP thread
|
||||||
// the thread needs to keep running as long as all other threads are running
|
// the thread needs to keep running as long as all other threads are running
|
||||||
type phpMainThread struct {
|
type phpMainThread struct {
|
||||||
state *threadState
|
state *state.ThreadState
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
numThreads int
|
numThreads int
|
||||||
maxThreads int
|
maxThreads int
|
||||||
@@ -39,7 +40,7 @@ var (
|
|||||||
// and reserves a fixed number of possible PHP threads
|
// and reserves a fixed number of possible PHP threads
|
||||||
func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) (*phpMainThread, error) {
|
func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) (*phpMainThread, error) {
|
||||||
mainThread = &phpMainThread{
|
mainThread = &phpMainThread{
|
||||||
state: newThreadState(),
|
state: state.NewThreadState(),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
numThreads: numThreads,
|
numThreads: numThreads,
|
||||||
maxThreads: numMaxThreads,
|
maxThreads: numMaxThreads,
|
||||||
@@ -80,11 +81,11 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string)
|
|||||||
func drainPHPThreads() {
|
func drainPHPThreads() {
|
||||||
doneWG := sync.WaitGroup{}
|
doneWG := sync.WaitGroup{}
|
||||||
doneWG.Add(len(phpThreads))
|
doneWG.Add(len(phpThreads))
|
||||||
mainThread.state.set(stateShuttingDown)
|
mainThread.state.Set(state.ShuttingDown)
|
||||||
close(mainThread.done)
|
close(mainThread.done)
|
||||||
for _, thread := range phpThreads {
|
for _, thread := range phpThreads {
|
||||||
// shut down all reserved threads
|
// shut down all reserved threads
|
||||||
if thread.state.compareAndSwap(stateReserved, stateDone) {
|
if thread.state.CompareAndSwap(state.Reserved, state.Done) {
|
||||||
doneWG.Done()
|
doneWG.Done()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -96,8 +97,8 @@ func drainPHPThreads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
doneWG.Wait()
|
doneWG.Wait()
|
||||||
mainThread.state.set(stateDone)
|
mainThread.state.Set(state.Done)
|
||||||
mainThread.state.waitFor(stateReserved)
|
mainThread.state.WaitFor(state.Reserved)
|
||||||
phpThreads = nil
|
phpThreads = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ func (mainThread *phpMainThread) start() error {
|
|||||||
return ErrMainThreadCreation
|
return ErrMainThreadCreation
|
||||||
}
|
}
|
||||||
|
|
||||||
mainThread.state.waitFor(stateReady)
|
mainThread.state.WaitFor(state.Ready)
|
||||||
|
|
||||||
// cache common request headers as zend_strings (HTTP_ACCEPT, HTTP_USER_AGENT, etc.)
|
// cache common request headers as zend_strings (HTTP_ACCEPT, HTTP_USER_AGENT, etc.)
|
||||||
mainThread.commonHeaders = make(map[string]*C.zend_string, len(phpheaders.CommonRequestHeaders))
|
mainThread.commonHeaders = make(map[string]*C.zend_string, len(phpheaders.CommonRequestHeaders))
|
||||||
@@ -125,13 +126,13 @@ func (mainThread *phpMainThread) start() error {
|
|||||||
|
|
||||||
func getInactivePHPThread() *phpThread {
|
func getInactivePHPThread() *phpThread {
|
||||||
for _, thread := range phpThreads {
|
for _, thread := range phpThreads {
|
||||||
if thread.state.is(stateInactive) {
|
if thread.state.Is(state.Inactive) {
|
||||||
return thread
|
return thread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, thread := range phpThreads {
|
for _, thread := range phpThreads {
|
||||||
if thread.state.compareAndSwap(stateReserved, stateBootRequested) {
|
if thread.state.CompareAndSwap(state.Reserved, state.BootRequested) {
|
||||||
thread.boot()
|
thread.boot()
|
||||||
return thread
|
return thread
|
||||||
}
|
}
|
||||||
@@ -147,8 +148,8 @@ func go_frankenphp_main_thread_is_ready() {
|
|||||||
mainThread.maxThreads = mainThread.numThreads
|
mainThread.maxThreads = mainThread.numThreads
|
||||||
}
|
}
|
||||||
|
|
||||||
mainThread.state.set(stateReady)
|
mainThread.state.Set(state.Ready)
|
||||||
mainThread.state.waitFor(stateDone)
|
mainThread.state.WaitFor(state.Done)
|
||||||
}
|
}
|
||||||
|
|
||||||
// max_threads = auto
|
// max_threads = auto
|
||||||
@@ -174,7 +175,7 @@ func (mainThread *phpMainThread) setAutomaticMaxThreads() {
|
|||||||
|
|
||||||
//export go_frankenphp_shutdown_main_thread
|
//export go_frankenphp_shutdown_main_thread
|
||||||
func go_frankenphp_shutdown_main_thread() {
|
func go_frankenphp_shutdown_main_thread() {
|
||||||
mainThread.state.set(stateReserved)
|
mainThread.state.Set(state.Reserved)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export go_get_custom_php_ini
|
//export go_get_custom_php_ini
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dunglas/frankenphp/internal/phpheaders"
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func TestStartAndStopTheMainThreadWithOneInactiveThread(t *testing.T) {
|
|||||||
|
|
||||||
assert.Len(t, phpThreads, 1)
|
assert.Len(t, phpThreads, 1)
|
||||||
assert.Equal(t, 0, phpThreads[0].threadIndex)
|
assert.Equal(t, 0, phpThreads[0].threadIndex)
|
||||||
assert.True(t, phpThreads[0].state.is(stateInactive))
|
assert.True(t, phpThreads[0].state.Is(state.Inactive))
|
||||||
|
|
||||||
drainPHPThreads()
|
drainPHPThreads()
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ func TestFinishBootingAWorkerScript(t *testing.T) {
|
|||||||
// boot the worker
|
// boot the worker
|
||||||
worker := getDummyWorker(t, "transition-worker-1.php")
|
worker := getDummyWorker(t, "transition-worker-1.php")
|
||||||
convertToWorkerThread(phpThreads[0], worker)
|
convertToWorkerThread(phpThreads[0], worker)
|
||||||
phpThreads[0].state.waitFor(stateReady)
|
phpThreads[0].state.WaitFor(state.Ready)
|
||||||
|
|
||||||
assert.NotNil(t, phpThreads[0].handler.(*workerThread).dummyContext)
|
assert.NotNil(t, phpThreads[0].handler.(*workerThread).dummyContext)
|
||||||
assert.Nil(t, phpThreads[0].handler.(*workerThread).workerContext)
|
assert.Nil(t, phpThreads[0].handler.(*workerThread).workerContext)
|
||||||
@@ -209,9 +209,8 @@ func getDummyWorker(t *testing.T, fileName string) *worker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
worker, _ := newWorker(workerOpt{
|
worker, _ := newWorker(workerOpt{
|
||||||
fileName: testDataPath + "/" + fileName,
|
fileName: testDataPath + "/" + fileName,
|
||||||
num: 1,
|
num: 1,
|
||||||
maxConsecutiveFailures: defaultMaxConsecutiveFailures,
|
|
||||||
})
|
})
|
||||||
workers = append(workers, worker)
|
workers = append(workers, worker)
|
||||||
|
|
||||||
@@ -237,7 +236,7 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT
|
|||||||
convertToRegularThread,
|
convertToRegularThread,
|
||||||
func(thread *phpThread) { thread.shutdown() },
|
func(thread *phpThread) { thread.shutdown() },
|
||||||
func(thread *phpThread) {
|
func(thread *phpThread) {
|
||||||
if thread.state.is(stateReserved) {
|
if thread.state.Is(state.Reserved) {
|
||||||
thread.boot()
|
thread.boot()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -248,20 +247,6 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllCommonHeadersAreCorrect(t *testing.T) {
|
|
||||||
fakeRequest := httptest.NewRequest("GET", "http://localhost", nil)
|
|
||||||
|
|
||||||
for header, phpHeader := range phpheaders.CommonRequestHeaders {
|
|
||||||
// verify that common and uncommon headers return the same result
|
|
||||||
expectedPHPHeader := phpheaders.GetUnCommonHeader(t.Context(), header)
|
|
||||||
assert.Equal(t, phpHeader+"\x00", expectedPHPHeader, "header is not well formed: "+phpHeader)
|
|
||||||
|
|
||||||
// net/http will capitalize lowercase headers, verify that headers are capitalized
|
|
||||||
fakeRequest.Header.Add(header, "foo")
|
|
||||||
assert.Contains(t, fakeRequest.Header, header, "header is not correctly capitalized: "+header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCorrectThreadCalculation(t *testing.T) {
|
func TestCorrectThreadCalculation(t *testing.T) {
|
||||||
maxProcs := runtime.GOMAXPROCS(0) * 2
|
maxProcs := runtime.GOMAXPROCS(0) * 2
|
||||||
oneWorkerThread := []workerOpt{{num: 1}}
|
oneWorkerThread := []workerOpt{{num: 1}}
|
||||||
|
|||||||
37
phpthread.go
37
phpthread.go
@@ -8,6 +8,8 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// representation of the actual underlying PHP thread
|
// representation of the actual underlying PHP thread
|
||||||
@@ -19,7 +21,7 @@ type phpThread struct {
|
|||||||
drainChan chan struct{}
|
drainChan chan struct{}
|
||||||
handlerMu sync.Mutex
|
handlerMu sync.Mutex
|
||||||
handler threadHandler
|
handler threadHandler
|
||||||
state *threadState
|
state *state.ThreadState
|
||||||
sandboxedEnv map[string]*C.zend_string
|
sandboxedEnv map[string]*C.zend_string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +38,15 @@ func newPHPThread(threadIndex int) *phpThread {
|
|||||||
return &phpThread{
|
return &phpThread{
|
||||||
threadIndex: threadIndex,
|
threadIndex: threadIndex,
|
||||||
requestChan: make(chan contextHolder),
|
requestChan: make(chan contextHolder),
|
||||||
state: newThreadState(),
|
state: state.NewThreadState(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// boot starts the underlying PHP thread
|
// boot starts the underlying PHP thread
|
||||||
func (thread *phpThread) boot() {
|
func (thread *phpThread) boot() {
|
||||||
// thread must be in reserved state to boot
|
// thread must be in reserved state to boot
|
||||||
if !thread.state.compareAndSwap(stateReserved, stateBooting) && !thread.state.compareAndSwap(stateBootRequested, stateBooting) {
|
if !thread.state.CompareAndSwap(state.Reserved, state.Booting) && !thread.state.CompareAndSwap(state.BootRequested, state.Booting) {
|
||||||
panic("thread is not in reserved state: " + thread.state.name())
|
panic("thread is not in reserved state: " + thread.state.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
// boot threads as inactive
|
// boot threads as inactive
|
||||||
@@ -58,22 +60,22 @@ func (thread *phpThread) boot() {
|
|||||||
panic("unable to create thread")
|
panic("unable to create thread")
|
||||||
}
|
}
|
||||||
|
|
||||||
thread.state.waitFor(stateInactive)
|
thread.state.WaitFor(state.Inactive)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shutdown the underlying PHP thread
|
// shutdown the underlying PHP thread
|
||||||
func (thread *phpThread) shutdown() {
|
func (thread *phpThread) shutdown() {
|
||||||
if !thread.state.requestSafeStateChange(stateShuttingDown) {
|
if !thread.state.RequestSafeStateChange(state.ShuttingDown) {
|
||||||
// already shutting down or done
|
// already shutting down or done
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
close(thread.drainChan)
|
close(thread.drainChan)
|
||||||
thread.state.waitFor(stateDone)
|
thread.state.WaitFor(state.Done)
|
||||||
thread.drainChan = make(chan struct{})
|
thread.drainChan = make(chan struct{})
|
||||||
|
|
||||||
// threads go back to the reserved state from which they can be booted again
|
// threads go back to the reserved state from which they can be booted again
|
||||||
if mainThread.state.is(stateReady) {
|
if mainThread.state.Is(state.Ready) {
|
||||||
thread.state.set(stateReserved)
|
thread.state.Set(state.Reserved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,24 +84,23 @@ func (thread *phpThread) shutdown() {
|
|||||||
func (thread *phpThread) setHandler(handler threadHandler) {
|
func (thread *phpThread) setHandler(handler threadHandler) {
|
||||||
thread.handlerMu.Lock()
|
thread.handlerMu.Lock()
|
||||||
defer thread.handlerMu.Unlock()
|
defer thread.handlerMu.Unlock()
|
||||||
|
if !thread.state.RequestSafeStateChange(state.TransitionRequested) {
|
||||||
if !thread.state.requestSafeStateChange(stateTransitionRequested) {
|
|
||||||
// no state change allowed == shutdown or done
|
// no state change allowed == shutdown or done
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
close(thread.drainChan)
|
close(thread.drainChan)
|
||||||
thread.state.waitFor(stateTransitionInProgress)
|
thread.state.WaitFor(state.TransitionInProgress)
|
||||||
thread.handler = handler
|
thread.handler = handler
|
||||||
thread.drainChan = make(chan struct{})
|
thread.drainChan = make(chan struct{})
|
||||||
thread.state.set(stateTransitionComplete)
|
thread.state.Set(state.TransitionComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
// transition to a new handler safely
|
// transition to a new handler safely
|
||||||
// is triggered by setHandler and executed on the PHP thread
|
// is triggered by setHandler and executed on the PHP thread
|
||||||
func (thread *phpThread) transitionToNewHandler() string {
|
func (thread *phpThread) transitionToNewHandler() string {
|
||||||
thread.state.set(stateTransitionInProgress)
|
thread.state.Set(state.TransitionInProgress)
|
||||||
thread.state.waitFor(stateTransitionComplete)
|
thread.state.WaitFor(state.TransitionComplete)
|
||||||
|
|
||||||
// execute beforeScriptExecution of the new handler
|
// execute beforeScriptExecution of the new handler
|
||||||
return thread.handler.beforeScriptExecution()
|
return thread.handler.beforeScriptExecution()
|
||||||
@@ -142,6 +143,10 @@ func (thread *phpThread) pinCString(s string) *C.char {
|
|||||||
return thread.pinString(s + "\x00")
|
return thread.pinString(s + "\x00")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*phpThread) updateContext(isWorker bool) {
|
||||||
|
C.frankenphp_update_local_thread_context(C.bool(isWorker))
|
||||||
|
}
|
||||||
|
|
||||||
//export go_frankenphp_before_script_execution
|
//export go_frankenphp_before_script_execution
|
||||||
func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {
|
func go_frankenphp_before_script_execution(threadIndex C.uintptr_t) *C.char {
|
||||||
thread := phpThreads[threadIndex]
|
thread := phpThreads[threadIndex]
|
||||||
@@ -172,5 +177,5 @@ func go_frankenphp_after_script_execution(threadIndex C.uintptr_t, exitStatus C.
|
|||||||
func go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {
|
func go_frankenphp_on_thread_shutdown(threadIndex C.uintptr_t) {
|
||||||
thread := phpThreads[threadIndex]
|
thread := phpThreads[threadIndex]
|
||||||
thread.Unpin()
|
thread.Unpin()
|
||||||
thread.state.set(stateDone)
|
thread.state.Set(state.Done)
|
||||||
}
|
}
|
||||||
|
|||||||
17
scaling.go
17
scaling.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dunglas/frankenphp/internal/cpu"
|
"github.com/dunglas/frankenphp/internal/cpu"
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -67,7 +68,7 @@ func addRegularThread() (*phpThread, error) {
|
|||||||
return nil, ErrMaxThreadsReached
|
return nil, ErrMaxThreadsReached
|
||||||
}
|
}
|
||||||
convertToRegularThread(thread)
|
convertToRegularThread(thread)
|
||||||
thread.state.waitFor(stateReady, stateShuttingDown, stateReserved)
|
thread.state.WaitFor(state.Ready, state.ShuttingDown, state.Reserved)
|
||||||
return thread, nil
|
return thread, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ func addWorkerThread(worker *worker) (*phpThread, error) {
|
|||||||
return nil, ErrMaxThreadsReached
|
return nil, ErrMaxThreadsReached
|
||||||
}
|
}
|
||||||
convertToWorkerThread(thread, worker)
|
convertToWorkerThread(thread, worker)
|
||||||
thread.state.waitFor(stateReady, stateShuttingDown, stateReserved)
|
thread.state.WaitFor(state.Ready, state.ShuttingDown, state.Reserved)
|
||||||
return thread, nil
|
return thread, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ func scaleWorkerThread(worker *worker) {
|
|||||||
scalingMu.Lock()
|
scalingMu.Lock()
|
||||||
defer scalingMu.Unlock()
|
defer scalingMu.Unlock()
|
||||||
|
|
||||||
if !mainThread.state.is(stateReady) {
|
if !mainThread.state.Is(state.Ready) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ func scaleRegularThread() {
|
|||||||
scalingMu.Lock()
|
scalingMu.Lock()
|
||||||
defer scalingMu.Unlock()
|
defer scalingMu.Unlock()
|
||||||
|
|
||||||
if !mainThread.state.is(stateReady) {
|
if !mainThread.state.Is(state.Ready) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,18 +213,18 @@ func deactivateThreads() {
|
|||||||
thread := autoScaledThreads[i]
|
thread := autoScaledThreads[i]
|
||||||
|
|
||||||
// the thread might have been stopped otherwise, remove it
|
// the thread might have been stopped otherwise, remove it
|
||||||
if thread.state.is(stateReserved) {
|
if thread.state.Is(state.Reserved) {
|
||||||
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
waitTime := thread.state.waitTime()
|
waitTime := thread.state.WaitTime()
|
||||||
if stoppedThreadCount > maxTerminationCount || waitTime == 0 {
|
if stoppedThreadCount > maxTerminationCount || waitTime == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert threads to inactive if they have been idle for too long
|
// convert threads to inactive if they have been idle for too long
|
||||||
if thread.state.is(stateReady) && waitTime > maxThreadIdleTime.Milliseconds() {
|
if thread.state.Is(state.Ready) && waitTime > maxThreadIdleTime.Milliseconds() {
|
||||||
convertToInactiveThread(thread)
|
convertToInactiveThread(thread)
|
||||||
stoppedThreadCount++
|
stoppedThreadCount++
|
||||||
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
||||||
@@ -238,7 +239,7 @@ func deactivateThreads() {
|
|||||||
// TODO: Completely stopping threads is more memory efficient
|
// TODO: Completely stopping threads is more memory efficient
|
||||||
// Some PECL extensions like #1296 will prevent threads from fully stopping (they leak memory)
|
// Some PECL extensions like #1296 will prevent threads from fully stopping (they leak memory)
|
||||||
// Reactivate this if there is a better solution or workaround
|
// Reactivate this if there is a better solution or workaround
|
||||||
// if thread.state.is(stateInactive) && waitTime > maxThreadIdleTime.Milliseconds() {
|
// if thread.state.Is(state.Inactive) && waitTime > maxThreadIdleTime.Milliseconds() {
|
||||||
// logger.LogAttrs(nil, slog.LevelDebug, "auto-stopping thread", slog.Int("thread", thread.threadIndex))
|
// logger.LogAttrs(nil, slog.LevelDebug, "auto-stopping thread", slog.Int("thread", thread.threadIndex))
|
||||||
// thread.shutdown()
|
// thread.shutdown()
|
||||||
// stoppedThreadCount++
|
// stoppedThreadCount++
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ func TestScaleARegularThreadUpAndDown(t *testing.T) {
|
|||||||
|
|
||||||
// scale up
|
// scale up
|
||||||
scaleRegularThread()
|
scaleRegularThread()
|
||||||
assert.Equal(t, stateReady, autoScaledThread.state.get())
|
assert.Equal(t, state.Ready, autoScaledThread.state.Get())
|
||||||
assert.IsType(t, ®ularThread{}, autoScaledThread.handler)
|
assert.IsType(t, ®ularThread{}, autoScaledThread.handler)
|
||||||
|
|
||||||
// on down-scale, the thread will be marked as inactive
|
// on down-scale, the thread will be marked as inactive
|
||||||
@@ -49,7 +50,7 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
|||||||
|
|
||||||
// scale up
|
// scale up
|
||||||
scaleWorkerThread(getWorkerByPath(workerPath))
|
scaleWorkerThread(getWorkerByPath(workerPath))
|
||||||
assert.Equal(t, stateReady, autoScaledThread.state.get())
|
assert.Equal(t, state.Ready, autoScaledThread.state.Get())
|
||||||
|
|
||||||
// on down-scale, the thread will be marked as inactive
|
// on down-scale, the thread will be marked as inactive
|
||||||
setLongWaitTime(autoScaledThread)
|
setLongWaitTime(autoScaledThread)
|
||||||
@@ -60,7 +61,5 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setLongWaitTime(thread *phpThread) {
|
func setLongWaitTime(thread *phpThread) {
|
||||||
thread.state.mu.Lock()
|
thread.state.SetWaitTime(time.Now().Add(-time.Hour))
|
||||||
thread.state.waitingSince = time.Now().Add(-time.Hour)
|
|
||||||
thread.state.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
testdata/failing-worker.php
vendored
17
testdata/failing-worker.php
vendored
@@ -1,18 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
$fail = random_int(1, 100) < 10;
|
if (rand(1, 100) <= 50) {
|
||||||
$wait = random_int(1000 * 100, 1000 * 500); // wait 100ms - 500ms
|
throw new Exception('this exception is expected to fail the worker');
|
||||||
|
|
||||||
usleep($wait);
|
|
||||||
if ($fail) {
|
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while (frankenphp_handle_request(function () {
|
// frankenphp_handle_request() has not been reached (also a failure)
|
||||||
echo "ok";
|
|
||||||
})) {
|
|
||||||
$fail = random_int(1, 100) < 10;
|
|
||||||
if ($fail) {
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package frankenphp
|
package frankenphp
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
// representation of a thread with no work assigned to it
|
// representation of a thread with no work assigned to it
|
||||||
// implements the threadHandler interface
|
// implements the threadHandler interface
|
||||||
@@ -17,26 +21,26 @@ func convertToInactiveThread(thread *phpThread) {
|
|||||||
func (handler *inactiveThread) beforeScriptExecution() string {
|
func (handler *inactiveThread) beforeScriptExecution() string {
|
||||||
thread := handler.thread
|
thread := handler.thread
|
||||||
|
|
||||||
switch thread.state.get() {
|
switch thread.state.Get() {
|
||||||
case stateTransitionRequested:
|
case state.TransitionRequested:
|
||||||
return thread.transitionToNewHandler()
|
return thread.transitionToNewHandler()
|
||||||
|
|
||||||
case stateBooting, stateTransitionComplete:
|
case state.Booting, state.TransitionComplete:
|
||||||
thread.state.set(stateInactive)
|
thread.state.Set(state.Inactive)
|
||||||
|
|
||||||
// wait for external signal to start or shut down
|
// wait for external signal to start or shut down
|
||||||
thread.state.markAsWaiting(true)
|
thread.state.MarkAsWaiting(true)
|
||||||
thread.state.waitFor(stateTransitionRequested, stateShuttingDown)
|
thread.state.WaitFor(state.TransitionRequested, state.ShuttingDown)
|
||||||
thread.state.markAsWaiting(false)
|
thread.state.MarkAsWaiting(false)
|
||||||
|
|
||||||
return handler.beforeScriptExecution()
|
return handler.beforeScriptExecution()
|
||||||
|
|
||||||
case stateShuttingDown:
|
case state.ShuttingDown:
|
||||||
// signal to stop
|
// signal to stop
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unexpected state: " + thread.state.name())
|
panic("unexpected state: " + thread.state.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *inactiveThread) afterScriptExecution(int) {
|
func (handler *inactiveThread) afterScriptExecution(int) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// representation of a non-worker PHP thread
|
// representation of a non-worker PHP thread
|
||||||
@@ -13,7 +15,7 @@ import (
|
|||||||
type regularThread struct {
|
type regularThread struct {
|
||||||
contextHolder
|
contextHolder
|
||||||
|
|
||||||
state *threadState
|
state *state.ThreadState
|
||||||
thread *phpThread
|
thread *phpThread
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,25 +36,27 @@ func convertToRegularThread(thread *phpThread) {
|
|||||||
|
|
||||||
// beforeScriptExecution returns the name of the script or an empty string on shutdown
|
// beforeScriptExecution returns the name of the script or an empty string on shutdown
|
||||||
func (handler *regularThread) beforeScriptExecution() string {
|
func (handler *regularThread) beforeScriptExecution() string {
|
||||||
switch handler.state.get() {
|
switch handler.state.Get() {
|
||||||
case stateTransitionRequested:
|
case state.TransitionRequested:
|
||||||
detachRegularThread(handler.thread)
|
detachRegularThread(handler.thread)
|
||||||
return handler.thread.transitionToNewHandler()
|
return handler.thread.transitionToNewHandler()
|
||||||
|
|
||||||
case stateTransitionComplete:
|
case state.TransitionComplete:
|
||||||
handler.state.set(stateReady)
|
handler.thread.updateContext(false)
|
||||||
|
handler.state.Set(state.Ready)
|
||||||
|
|
||||||
return handler.waitForRequest()
|
return handler.waitForRequest()
|
||||||
|
|
||||||
case stateReady:
|
case state.Ready:
|
||||||
return handler.waitForRequest()
|
return handler.waitForRequest()
|
||||||
|
|
||||||
case stateShuttingDown:
|
case state.ShuttingDown:
|
||||||
detachRegularThread(handler.thread)
|
detachRegularThread(handler.thread)
|
||||||
// signal to stop
|
// signal to stop
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unexpected state: " + handler.state.name())
|
panic("unexpected state: " + handler.state.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *regularThread) afterScriptExecution(_ int) {
|
func (handler *regularThread) afterScriptExecution(_ int) {
|
||||||
@@ -75,7 +79,7 @@ func (handler *regularThread) waitForRequest() string {
|
|||||||
// clear any previously sandboxed env
|
// clear any previously sandboxed env
|
||||||
clearSandboxedEnv(handler.thread)
|
clearSandboxedEnv(handler.thread)
|
||||||
|
|
||||||
handler.state.markAsWaiting(true)
|
handler.state.MarkAsWaiting(true)
|
||||||
|
|
||||||
var ch contextHolder
|
var ch contextHolder
|
||||||
|
|
||||||
@@ -89,7 +93,7 @@ func (handler *regularThread) waitForRequest() string {
|
|||||||
|
|
||||||
handler.ctx = ch.ctx
|
handler.ctx = ch.ctx
|
||||||
handler.contextHolder.frankenPHPContext = ch.frankenPHPContext
|
handler.contextHolder.frankenPHPContext = ch.frankenPHPContext
|
||||||
handler.state.markAsWaiting(false)
|
handler.state.MarkAsWaiting(false)
|
||||||
|
|
||||||
// set the scriptFilename that should be executed
|
// set the scriptFilename that should be executed
|
||||||
return handler.contextHolder.frankenPHPContext.scriptFilename
|
return handler.contextHolder.frankenPHPContext.scriptFilename
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package frankenphp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// representation of a thread that handles tasks directly assigned by go
|
// representation of a thread that handles tasks directly assigned by go
|
||||||
@@ -42,23 +44,23 @@ func convertToTaskThread(thread *phpThread) *taskThread {
|
|||||||
func (handler *taskThread) beforeScriptExecution() string {
|
func (handler *taskThread) beforeScriptExecution() string {
|
||||||
thread := handler.thread
|
thread := handler.thread
|
||||||
|
|
||||||
switch thread.state.get() {
|
switch thread.state.Get() {
|
||||||
case stateTransitionRequested:
|
case state.TransitionRequested:
|
||||||
return thread.transitionToNewHandler()
|
return thread.transitionToNewHandler()
|
||||||
case stateBooting, stateTransitionComplete:
|
case state.Booting, state.TransitionComplete:
|
||||||
thread.state.set(stateReady)
|
thread.state.Set(state.Ready)
|
||||||
handler.waitForTasks()
|
handler.waitForTasks()
|
||||||
|
|
||||||
return handler.beforeScriptExecution()
|
return handler.beforeScriptExecution()
|
||||||
case stateReady:
|
case state.Ready:
|
||||||
handler.waitForTasks()
|
handler.waitForTasks()
|
||||||
|
|
||||||
return handler.beforeScriptExecution()
|
return handler.beforeScriptExecution()
|
||||||
case stateShuttingDown:
|
case state.ShuttingDown:
|
||||||
// signal to stop
|
// signal to stop
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
panic("unexpected state: " + thread.state.name())
|
panic("unexpected state: " + thread.state.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *taskThread) afterScriptExecution(_ int) {
|
func (handler *taskThread) afterScriptExecution(_ int) {
|
||||||
|
|||||||
@@ -4,25 +4,28 @@ package frankenphp
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// representation of a thread assigned to a worker script
|
// representation of a thread assigned to a worker script
|
||||||
// executes the PHP worker script in a loop
|
// executes the PHP worker script in a loop
|
||||||
// implements the threadHandler interface
|
// implements the threadHandler interface
|
||||||
type workerThread struct {
|
type workerThread struct {
|
||||||
state *threadState
|
state *state.ThreadState
|
||||||
thread *phpThread
|
thread *phpThread
|
||||||
worker *worker
|
worker *worker
|
||||||
dummyFrankenPHPContext *frankenPHPContext
|
dummyFrankenPHPContext *frankenPHPContext
|
||||||
dummyContext context.Context
|
dummyContext context.Context
|
||||||
workerFrankenPHPContext *frankenPHPContext
|
workerFrankenPHPContext *frankenPHPContext
|
||||||
workerContext context.Context
|
workerContext context.Context
|
||||||
backoff *exponentialBackoff
|
|
||||||
isBootingScript bool // true if the worker has not reached frankenphp_handle_request yet
|
isBootingScript bool // true if the worker has not reached frankenphp_handle_request yet
|
||||||
|
failureCount int // number of consecutive startup failures
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToWorkerThread(thread *phpThread, worker *worker) {
|
func convertToWorkerThread(thread *phpThread, worker *worker) {
|
||||||
@@ -30,32 +33,28 @@ func convertToWorkerThread(thread *phpThread, worker *worker) {
|
|||||||
state: thread.state,
|
state: thread.state,
|
||||||
thread: thread,
|
thread: thread,
|
||||||
worker: worker,
|
worker: worker,
|
||||||
backoff: &exponentialBackoff{
|
|
||||||
maxBackoff: 1 * time.Second,
|
|
||||||
minBackoff: 100 * time.Millisecond,
|
|
||||||
maxConsecutiveFailures: worker.maxConsecutiveFailures,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
worker.attachThread(thread)
|
worker.attachThread(thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
// beforeScriptExecution returns the name of the script or an empty string on shutdown
|
// beforeScriptExecution returns the name of the script or an empty string on shutdown
|
||||||
func (handler *workerThread) beforeScriptExecution() string {
|
func (handler *workerThread) beforeScriptExecution() string {
|
||||||
switch handler.state.get() {
|
switch handler.state.Get() {
|
||||||
case stateTransitionRequested:
|
case state.TransitionRequested:
|
||||||
if handler.worker.onThreadShutdown != nil {
|
if handler.worker.onThreadShutdown != nil {
|
||||||
handler.worker.onThreadShutdown(handler.thread.threadIndex)
|
handler.worker.onThreadShutdown(handler.thread.threadIndex)
|
||||||
}
|
}
|
||||||
handler.worker.detachThread(handler.thread)
|
handler.worker.detachThread(handler.thread)
|
||||||
return handler.thread.transitionToNewHandler()
|
return handler.thread.transitionToNewHandler()
|
||||||
case stateRestarting:
|
case state.Restarting:
|
||||||
if handler.worker.onThreadShutdown != nil {
|
if handler.worker.onThreadShutdown != nil {
|
||||||
handler.worker.onThreadShutdown(handler.thread.threadIndex)
|
handler.worker.onThreadShutdown(handler.thread.threadIndex)
|
||||||
}
|
}
|
||||||
handler.state.set(stateYielding)
|
handler.state.Set(state.Yielding)
|
||||||
handler.state.waitFor(stateReady, stateShuttingDown)
|
handler.state.WaitFor(state.Ready, state.ShuttingDown)
|
||||||
return handler.beforeScriptExecution()
|
return handler.beforeScriptExecution()
|
||||||
case stateReady, stateTransitionComplete:
|
case state.Ready, state.TransitionComplete:
|
||||||
|
handler.thread.updateContext(true)
|
||||||
if handler.worker.onThreadReady != nil {
|
if handler.worker.onThreadReady != nil {
|
||||||
handler.worker.onThreadReady(handler.thread.threadIndex)
|
handler.worker.onThreadReady(handler.thread.threadIndex)
|
||||||
}
|
}
|
||||||
@@ -63,7 +62,7 @@ func (handler *workerThread) beforeScriptExecution() string {
|
|||||||
setupWorkerScript(handler, handler.worker)
|
setupWorkerScript(handler, handler.worker)
|
||||||
|
|
||||||
return handler.worker.fileName
|
return handler.worker.fileName
|
||||||
case stateShuttingDown:
|
case state.ShuttingDown:
|
||||||
if handler.worker.onThreadShutdown != nil {
|
if handler.worker.onThreadShutdown != nil {
|
||||||
handler.worker.onThreadShutdown(handler.thread.threadIndex)
|
handler.worker.onThreadShutdown(handler.thread.threadIndex)
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ func (handler *workerThread) beforeScriptExecution() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unexpected state: " + handler.state.name())
|
panic("unexpected state: " + handler.state.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *workerThread) afterScriptExecution(exitStatus int) {
|
func (handler *workerThread) afterScriptExecution(exitStatus int) {
|
||||||
@@ -100,10 +99,9 @@ func (handler *workerThread) name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupWorkerScript(handler *workerThread, worker *worker) {
|
func setupWorkerScript(handler *workerThread, worker *worker) {
|
||||||
handler.backoff.wait()
|
|
||||||
metrics.StartWorker(worker.name)
|
metrics.StartWorker(worker.name)
|
||||||
|
|
||||||
if handler.state.is(stateReady) {
|
if handler.state.Is(state.Ready) {
|
||||||
metrics.ReadyWorker(handler.worker.name)
|
metrics.ReadyWorker(handler.worker.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +143,6 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) {
|
|||||||
// on exit status 0 we just run the worker script again
|
// on exit status 0 we just run the worker script again
|
||||||
if exitStatus == 0 && !handler.isBootingScript {
|
if exitStatus == 0 && !handler.isBootingScript {
|
||||||
metrics.StopWorker(worker.name, StopReasonRestart)
|
metrics.StopWorker(worker.name, StopReasonRestart)
|
||||||
handler.backoff.recordSuccess()
|
|
||||||
|
|
||||||
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
|
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
|
||||||
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "restarting", slog.String("worker", worker.name), slog.Int("thread", handler.thread.threadIndex), slog.Int("exit_status", exitStatus))
|
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "restarting", slog.String("worker", worker.name), slog.Int("thread", handler.thread.threadIndex), slog.Int("exit_status", exitStatus))
|
||||||
@@ -166,20 +163,32 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalLogger.Enabled(globalCtx, slog.LevelError) {
|
if worker.maxConsecutiveFailures >= 0 && startupFailChan != nil && !watcherIsEnabled && handler.failureCount >= worker.maxConsecutiveFailures {
|
||||||
globalLogger.LogAttrs(globalCtx, slog.LevelError, "worker script has not reached frankenphp_handle_request()", slog.String("worker", worker.name), slog.Int("thread", handler.thread.threadIndex))
|
startupFailChan <- fmt.Errorf("too many consecutive failures: worker %s has not reached frankenphp_handle_request()", worker.fileName)
|
||||||
|
handler.thread.state.Set(state.ShuttingDown)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// panic after exponential backoff if the worker has never reached frankenphp_handle_request
|
if watcherIsEnabled {
|
||||||
if handler.backoff.recordFailure() {
|
// worker script has probably failed due to script changes while watcher is enabled
|
||||||
if !watcherIsEnabled && !handler.state.is(stateReady) {
|
if globalLogger.Enabled(globalCtx, slog.LevelError) {
|
||||||
panic("too many consecutive worker failures")
|
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "(watcher enabled) worker script has not reached frankenphp_handle_request()", slog.String("worker", worker.name), slog.Int("thread", handler.thread.threadIndex))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// rare case where worker script has failed on a restart during normal operation
|
||||||
|
// this can happen if startup success depends on external resources
|
||||||
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
|
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
|
||||||
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "many consecutive worker failures", slog.String("worker", worker.name), slog.Int("thread", handler.thread.threadIndex), slog.Int("failures", handler.backoff.failureCount))
|
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "worker script has failed on restart", slog.String("worker", worker.name), slog.Int("thread", handler.thread.threadIndex), slog.Int("failures", handler.failureCount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait a bit and try again (exponential backoff)
|
||||||
|
backoffDuration := time.Duration(handler.failureCount*handler.failureCount*100) * time.Millisecond
|
||||||
|
if backoffDuration > time.Second {
|
||||||
|
backoffDuration = time.Second
|
||||||
|
}
|
||||||
|
handler.failureCount++
|
||||||
|
time.Sleep(backoffDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForWorkerRequest is called during frankenphp_handle_request in the php worker script.
|
// waitForWorkerRequest is called during frankenphp_handle_request in the php worker script.
|
||||||
@@ -194,20 +203,21 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
|
|||||||
// Clear the first dummy request created to initialize the worker
|
// Clear the first dummy request created to initialize the worker
|
||||||
if handler.isBootingScript {
|
if handler.isBootingScript {
|
||||||
handler.isBootingScript = false
|
handler.isBootingScript = false
|
||||||
|
handler.failureCount = 0
|
||||||
if !C.frankenphp_shutdown_dummy_request() {
|
if !C.frankenphp_shutdown_dummy_request() {
|
||||||
panic("Not in CGI context")
|
panic("Not in CGI context")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// worker threads are 'ready' after they first reach frankenphp_handle_request()
|
// worker threads are 'ready' after they first reach frankenphp_handle_request()
|
||||||
// 'stateTransitionComplete' is only true on the first boot of the worker script,
|
// 'state.TransitionComplete' is only true on the first boot of the worker script,
|
||||||
// while 'isBootingScript' is true on every boot of the worker script
|
// while 'isBootingScript' is true on every boot of the worker script
|
||||||
if handler.state.is(stateTransitionComplete) {
|
if handler.state.Is(state.TransitionComplete) {
|
||||||
metrics.ReadyWorker(handler.worker.name)
|
metrics.ReadyWorker(handler.worker.name)
|
||||||
handler.state.set(stateReady)
|
handler.state.Set(state.Ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.state.markAsWaiting(true)
|
handler.state.MarkAsWaiting(true)
|
||||||
|
|
||||||
var requestCH contextHolder
|
var requestCH contextHolder
|
||||||
select {
|
select {
|
||||||
@@ -218,7 +228,7 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
|
|||||||
|
|
||||||
// flush the opcache when restarting due to watcher or admin api
|
// flush the opcache when restarting due to watcher or admin api
|
||||||
// note: this is done right before frankenphp_handle_request() returns 'false'
|
// note: this is done right before frankenphp_handle_request() returns 'false'
|
||||||
if handler.state.is(stateRestarting) {
|
if handler.state.Is(state.Restarting) {
|
||||||
C.frankenphp_reset_opcache()
|
C.frankenphp_reset_opcache()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +239,7 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
|
|||||||
|
|
||||||
handler.workerContext = requestCH.ctx
|
handler.workerContext = requestCH.ctx
|
||||||
handler.workerFrankenPHPContext = requestCH.frankenPHPContext
|
handler.workerFrankenPHPContext = requestCH.frankenPHPContext
|
||||||
handler.state.markAsWaiting(false)
|
handler.state.MarkAsWaiting(false)
|
||||||
|
|
||||||
if globalLogger.Enabled(requestCH.ctx, slog.LevelDebug) {
|
if globalLogger.Enabled(requestCH.ctx, slog.LevelDebug) {
|
||||||
if handler.workerFrankenPHPContext.request == nil {
|
if handler.workerFrankenPHPContext.request == nil {
|
||||||
|
|||||||
24
worker.go
24
worker.go
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||||
|
"github.com/dunglas/frankenphp/internal/state"
|
||||||
"github.com/dunglas/frankenphp/internal/watcher"
|
"github.com/dunglas/frankenphp/internal/watcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,22 +37,26 @@ type worker struct {
|
|||||||
var (
|
var (
|
||||||
workers []*worker
|
workers []*worker
|
||||||
watcherIsEnabled bool
|
watcherIsEnabled bool
|
||||||
|
startupFailChan chan (error)
|
||||||
)
|
)
|
||||||
|
|
||||||
func initWorkers(opt []workerOpt) error {
|
func initWorkers(opt []workerOpt) error {
|
||||||
workers = make([]*worker, 0, len(opt))
|
workers = make([]*worker, 0, len(opt))
|
||||||
directoriesToWatch := getDirectoriesToWatch(opt)
|
directoriesToWatch := getDirectoriesToWatch(opt)
|
||||||
watcherIsEnabled = len(directoriesToWatch) > 0
|
watcherIsEnabled = len(directoriesToWatch) > 0
|
||||||
|
totalThreadsToStart := 0
|
||||||
|
|
||||||
for _, o := range opt {
|
for _, o := range opt {
|
||||||
w, err := newWorker(o)
|
w, err := newWorker(o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
totalThreadsToStart += w.num
|
||||||
workers = append(workers, w)
|
workers = append(workers, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
var workersReady sync.WaitGroup
|
var workersReady sync.WaitGroup
|
||||||
|
startupFailChan = make(chan error, totalThreadsToStart)
|
||||||
|
|
||||||
for _, w := range workers {
|
for _, w := range workers {
|
||||||
for i := 0; i < w.num; i++ {
|
for i := 0; i < w.num; i++ {
|
||||||
@@ -59,18 +64,27 @@ func initWorkers(opt []workerOpt) error {
|
|||||||
convertToWorkerThread(thread, w)
|
convertToWorkerThread(thread, w)
|
||||||
|
|
||||||
workersReady.Go(func() {
|
workersReady.Go(func() {
|
||||||
thread.state.waitFor(stateReady)
|
thread.state.WaitFor(state.Ready, state.ShuttingDown, state.Done)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workersReady.Wait()
|
workersReady.Wait()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-startupFailChan:
|
||||||
|
// at least 1 worker has failed, shut down and return an error
|
||||||
|
Shutdown()
|
||||||
|
return fmt.Errorf("failed to initialize workers: %w", err)
|
||||||
|
default:
|
||||||
|
// all workers started successfully
|
||||||
|
startupFailChan = nil
|
||||||
|
}
|
||||||
|
|
||||||
if !watcherIsEnabled {
|
if !watcherIsEnabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherIsEnabled = true
|
|
||||||
if err := watcher.InitWatcher(globalCtx, directoriesToWatch, RestartWorkers, globalLogger); err != nil {
|
if err := watcher.InitWatcher(globalCtx, directoriesToWatch, RestartWorkers, globalLogger); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -167,7 +181,7 @@ func drainWorkerThreads() []*phpThread {
|
|||||||
worker.threadMutex.RLock()
|
worker.threadMutex.RLock()
|
||||||
ready.Add(len(worker.threads))
|
ready.Add(len(worker.threads))
|
||||||
for _, thread := range worker.threads {
|
for _, thread := range worker.threads {
|
||||||
if !thread.state.requestSafeStateChange(stateRestarting) {
|
if !thread.state.RequestSafeStateChange(state.Restarting) {
|
||||||
ready.Done()
|
ready.Done()
|
||||||
// no state change allowed == thread is shutting down
|
// no state change allowed == thread is shutting down
|
||||||
// we'll proceed to restart all other threads anyways
|
// we'll proceed to restart all other threads anyways
|
||||||
@@ -176,7 +190,7 @@ func drainWorkerThreads() []*phpThread {
|
|||||||
close(thread.drainChan)
|
close(thread.drainChan)
|
||||||
drainedThreads = append(drainedThreads, thread)
|
drainedThreads = append(drainedThreads, thread)
|
||||||
go func(thread *phpThread) {
|
go func(thread *phpThread) {
|
||||||
thread.state.waitFor(stateYielding)
|
thread.state.WaitFor(state.Yielding)
|
||||||
ready.Done()
|
ready.Done()
|
||||||
}(thread)
|
}(thread)
|
||||||
}
|
}
|
||||||
@@ -203,7 +217,7 @@ func RestartWorkers() {
|
|||||||
|
|
||||||
for _, thread := range threadsToRestart {
|
for _, thread := range threadsToRestart {
|
||||||
thread.drainChan = make(chan struct{})
|
thread.drainChan = make(chan struct{})
|
||||||
thread.state.set(stateReady)
|
thread.state.Set(state.Ready)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user