Files
frankenphp/phpmainthread_test.go
T
Alliballibaba2 f592e0f47b refactor: decouple worker threads from non-worker threads (#1137)
* 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.
2024-12-17 11:28:51 +01:00

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, &regularThread{}, 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))
}