mirror of
https://github.com/dunglas/frankenphp.git
synced 2026-04-23 00:37:20 +08:00
f592e0f47b
* Decouple workers. * Moves code to separate file. * Cleans up the exponential backoff. * Initial working implementation. * Refactors php threads to take callbacks. * Cleanup. * Cleanup. * Cleanup. * Cleanup. * Adjusts watcher logic. * Adjusts the watcher logic. * Fix opcache_reset race condition. * Fixing merge conflicts and formatting. * Prevents overlapping of TSRM reservation and script execution. * Adjustments as suggested by @dunglas. * Adds error assertions. * Adds comments. * Removes logs and explicitly compares to C.false. * Resets check. * Adds cast for safety. * Fixes waitgroup overflow. * Resolves waitgroup race condition on startup. * Moves worker request logic to worker.go. * Removes defer. * Removes call from go to c. * Fixes merge conflict. * Adds fibers test back in. * Refactors new thread loop approach. * Removes redundant check. * Adds compareAndSwap. * Refactor: removes global waitgroups and uses a 'thread state' abstraction instead. * Removes unnecessary method. * Updates comment. * Removes unnecessary booleans. * test * First state machine steps. * Splits threads. * Minimal working implementation with broken tests. * Fixes tests. * Refactoring. * Fixes merge conflicts. * Formatting * C formatting. * More cleanup. * Allows for clean state transitions. * Adds state tests. * Adds support for thread transitioning. * Fixes the testdata path. * Formatting. * Allows transitioning back to inactive state. * Fixes go linting. * Formatting. * Removes duplication. * Applies suggestions by @dunglas * Removes redundant check. * Locks the handler on restart. * Removes unnecessary log. * Changes Unpin() logic as suggested by @withinboredom * Adds suggestions by @dunglas and resolves TODO. * Makes restarts fully safe. * Will make the initial startup fail even if the watcher is enabled (as is currently the case) * Also adds compareAndSwap to the test. * Adds comment. * Prevents panic on initial watcher startup.
162 lines
4.3 KiB
Go
162 lines
4.3 KiB
Go
package frankenphp
|
|
|
|
import (
|
|
"io"
|
|
"math/rand/v2"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var testDataPath, _ = filepath.Abs("./testdata")
|
|
|
|
func TestStartAndStopTheMainThreadWithOneInactiveThread(t *testing.T) {
|
|
logger = zap.NewNop() // the logger needs to not be nil
|
|
assert.NoError(t, initPHPThreads(1)) // reserve 1 thread
|
|
|
|
assert.Len(t, phpThreads, 1)
|
|
assert.Equal(t, 0, phpThreads[0].threadIndex)
|
|
assert.True(t, phpThreads[0].state.is(stateInactive))
|
|
|
|
drainPHPThreads()
|
|
assert.Nil(t, phpThreads)
|
|
}
|
|
|
|
func TestTransitionRegularThreadToWorkerThread(t *testing.T) {
|
|
logger = zap.NewNop()
|
|
assert.NoError(t, initPHPThreads(1))
|
|
|
|
// transition to regular thread
|
|
convertToRegularThread(phpThreads[0])
|
|
assert.IsType(t, ®ularThread{}, phpThreads[0].handler)
|
|
|
|
// transition to worker thread
|
|
worker := getDummyWorker("transition-worker-1.php")
|
|
convertToWorkerThread(phpThreads[0], worker)
|
|
assert.IsType(t, &workerThread{}, phpThreads[0].handler)
|
|
assert.Len(t, worker.threads, 1)
|
|
|
|
// transition back to inactive thread
|
|
convertToInactiveThread(phpThreads[0])
|
|
assert.IsType(t, &inactiveThread{}, phpThreads[0].handler)
|
|
assert.Len(t, worker.threads, 0)
|
|
|
|
drainPHPThreads()
|
|
assert.Nil(t, phpThreads)
|
|
}
|
|
|
|
func TestTransitionAThreadBetween2DifferentWorkers(t *testing.T) {
|
|
logger = zap.NewNop()
|
|
assert.NoError(t, initPHPThreads(1))
|
|
firstWorker := getDummyWorker("transition-worker-1.php")
|
|
secondWorker := getDummyWorker("transition-worker-2.php")
|
|
|
|
// convert to first worker thread
|
|
convertToWorkerThread(phpThreads[0], firstWorker)
|
|
firstHandler := phpThreads[0].handler.(*workerThread)
|
|
assert.Same(t, firstWorker, firstHandler.worker)
|
|
assert.Len(t, firstWorker.threads, 1)
|
|
assert.Len(t, secondWorker.threads, 0)
|
|
|
|
// convert to second worker thread
|
|
convertToWorkerThread(phpThreads[0], secondWorker)
|
|
secondHandler := phpThreads[0].handler.(*workerThread)
|
|
assert.Same(t, secondWorker, secondHandler.worker)
|
|
assert.Len(t, firstWorker.threads, 0)
|
|
assert.Len(t, secondWorker.threads, 1)
|
|
|
|
drainPHPThreads()
|
|
assert.Nil(t, phpThreads)
|
|
}
|
|
|
|
func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
|
|
numThreads := 10
|
|
numRequestsPerThread := 100
|
|
isRunning := atomic.Bool{}
|
|
isRunning.Store(true)
|
|
wg := sync.WaitGroup{}
|
|
worker1Path := testDataPath + "/transition-worker-1.php"
|
|
worker2Path := testDataPath + "/transition-worker-2.php"
|
|
|
|
assert.NoError(t, Init(
|
|
WithNumThreads(numThreads),
|
|
WithWorkers(worker1Path, 1, map[string]string{"ENV1": "foo"}, []string{}),
|
|
WithWorkers(worker2Path, 1, map[string]string{"ENV1": "foo"}, []string{}),
|
|
WithLogger(zap.NewNop()),
|
|
))
|
|
|
|
// randomly transition threads between regular, inactive and 2 worker threads
|
|
go func() {
|
|
for {
|
|
for i := 0; i < numThreads; i++ {
|
|
switch rand.IntN(4) {
|
|
case 0:
|
|
convertToRegularThread(phpThreads[i])
|
|
case 1:
|
|
convertToWorkerThread(phpThreads[i], workers[worker1Path])
|
|
case 2:
|
|
convertToWorkerThread(phpThreads[i], workers[worker2Path])
|
|
case 3:
|
|
convertToInactiveThread(phpThreads[i])
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
if !isRunning.Load() {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// randomly do requests to the 3 endpoints
|
|
wg.Add(numThreads)
|
|
for i := 0; i < numThreads; i++ {
|
|
go func(i int) {
|
|
for j := 0; j < numRequestsPerThread; j++ {
|
|
switch rand.IntN(3) {
|
|
case 0:
|
|
assertRequestBody(t, "http://localhost/transition-worker-1.php", "Hello from worker 1")
|
|
case 1:
|
|
assertRequestBody(t, "http://localhost/transition-worker-2.php", "Hello from worker 2")
|
|
case 2:
|
|
assertRequestBody(t, "http://localhost/transition-regular.php", "Hello from regular thread")
|
|
}
|
|
}
|
|
wg.Done()
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
isRunning.Store(false)
|
|
Shutdown()
|
|
}
|
|
|
|
func getDummyWorker(fileName string) *worker {
|
|
if workers == nil {
|
|
workers = make(map[string]*worker)
|
|
}
|
|
worker, _ := newWorker(workerOpt{
|
|
fileName: testDataPath + "/" + fileName,
|
|
num: 1,
|
|
})
|
|
return worker
|
|
}
|
|
|
|
func assertRequestBody(t *testing.T, url string, expected string) {
|
|
r := httptest.NewRequest("GET", url, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
req, err := NewRequestWithContext(r, WithRequestDocumentRoot(testDataPath, false))
|
|
assert.NoError(t, err)
|
|
err = ServeHTTP(w, req)
|
|
assert.NoError(t, err)
|
|
resp := w.Result()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
assert.Equal(t, expected, string(body))
|
|
}
|