diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index 74fff323ed..b7d6c231eb 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -903,7 +903,7 @@ func TestPhpServerWorkerMatchPoolCount(t *testing.T) { require.NoError(t, err, "failed to read metrics") var pools []string - for _, line := range strings.Split(metrics.String(), "\n") { + for line := range strings.SplitSeq(metrics.String(), "\n") { if !strings.HasPrefix(line, "frankenphp_total_workers{worker=") { continue } @@ -1830,7 +1830,7 @@ func TestOpcacheReset(t *testing.T) { wg := sync.WaitGroup{} numRequests := 500 wg.Add(numRequests) - for i := 0; i < numRequests; i++ { + for i := range numRequests { // introduce a delay every 10 requests if i%10 == 0 { @@ -2138,7 +2138,7 @@ func TestSymlinkWorkerBehavior(t *testing.T) { `, "caddyfile") // Make multiple requests - each should increment the counter - for i := 0; i < 5; i++ { + for i := range 5 { tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, fmt.Sprintf("Request: %d\n", i)) } }) diff --git a/caddy/config_test.go b/caddy/config_test.go index 4540ece26b..607051cbd8 100644 --- a/caddy/config_test.go +++ b/caddy/config_test.go @@ -254,7 +254,7 @@ func TestCreateUniqueWorkerNames(t *testing.T) { filename := "../testdata/worker-with-env.php" absFileName, _ := filepath.Abs(filename) names := make([]string, 6) - for i := 0; i < 3; i++ { + for i := range 3 { names[i] = app.createUniqueWorkerName(workerConfig{ FileName: filename, Name: "custom-worker-name", diff --git a/caddy/module.go b/caddy/module.go index e1128a3851..20dcec9ee2 100644 --- a/caddy/module.go +++ b/caddy/module.go @@ -227,7 +227,7 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c err := f.server.ServeHTTP(w, r, opts...) - if err != nil && !errors.As(err, &frankenphp.ErrRejected{}) { + if _, rejected := errors.AsType[frankenphp.ErrRejected](err); err != nil && !rejected { return caddyhttp.Error(http.StatusInternalServerError, err) } diff --git a/cgi.go b/cgi.go index e489a6f301..99545137b7 100644 --- a/cgi.go +++ b/cgi.go @@ -253,7 +253,7 @@ func splitPos(path string, splitPath []string) int { for i := 0; i <= pathLen-splitLen; i++ { match := true - for j := 0; j < splitLen; j++ { + for j := range splitLen { c := path[i+j] if c >= utf8.RuneSelf { match = false @@ -363,12 +363,8 @@ func splitRemoteAddr(remoteAddr string) (ip, port string) { return host, p } - if idx := strings.LastIndex(remoteAddr, ":"); idx > -1 { - ip = remoteAddr[:idx] - port = remoteAddr[idx+1:] - } else { - ip = remoteAddr - } + // CutLast yields (remoteAddr, "") when there is no colon, i.e. no port. + ip, port, _ = strings.CutLast(remoteAddr, ":") if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' { ip = ip[1 : len(ip)-1] diff --git a/cli_test.go b/cli_test.go index 5a07dd8d2c..56d88a92d9 100644 --- a/cli_test.go +++ b/cli_test.go @@ -21,8 +21,7 @@ func TestExecuteScriptCLI(t *testing.T) { stdoutStderr, err := cmd.CombinedOutput() assert.Error(t, err) - var exitError *exec.ExitError - if errors.As(err, &exitError) { + if exitError, ok := errors.AsType[*exec.ExitError](err); ok { assert.Equal(t, 3, exitError.ExitCode()) } @@ -60,8 +59,7 @@ func TestExecuteCLIPHPInfo(t *testing.T) { if frankenphp.Version().VersionID < 80600 { assert.Error(t, err) - var exitError *exec.ExitError - if errors.As(err, &exitError) { + if exitError, ok := errors.AsType[*exec.ExitError](err); ok { assert.Equal(t, 1, exitError.ExitCode()) } diff --git a/context.go b/context.go index f31d3cfe40..d184965fea 100644 --- a/context.go +++ b/context.go @@ -236,8 +236,8 @@ func (fc *frankenPHPContext) reject(err error) { return } - re := &ErrRejected{} - if !errors.As(err, re) { + re, ok := errors.AsType[ErrRejected](err) + if !ok { // Should never happen panic("only instance of ErrRejected can be passed to reject") } diff --git a/frankenphp.go b/frankenphp.go index 79b135b808..8b19dd2285 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -320,7 +320,7 @@ func Init(options ...Option) error { } regularThreads = make([]*phpThread, 0, opt.numThreads-workerThreadCount) - for i := 0; i < opt.numThreads-workerThreadCount; i++ { + for range opt.numThreads - workerThreadCount { convertToRegularThread(getInactivePHPThread()) } @@ -533,7 +533,7 @@ func splitRawHeader(rawHeader *C.char, length int) (string, string) { } // anything left is the header value - valuePtr := (*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(rawHeader)) + uintptr(j))) + valuePtr := (*C.char)(unsafe.Add(unsafe.Pointer(rawHeader), j)) headerValue := C.GoStringN(valuePtr, C.int(length-j)) return headerKey, headerValue diff --git a/frankenphp_test.go b/frankenphp_test.go index 19fb3dd06f..5bfc104a38 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -87,7 +87,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), * assert.NoError(t, err) err = frankenphp.ServeHTTP(w, req) - if err != nil && !errors.As(err, &frankenphp.ErrRejected{}) { + if _, rejected := errors.AsType[frankenphp.ErrRejected](err); err != nil && !rejected { assert.Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err)) } } diff --git a/goroutineleak_test.go b/goroutineleak_test.go new file mode 100644 index 0000000000..7615c5684d --- /dev/null +++ b/goroutineleak_test.go @@ -0,0 +1,61 @@ +package frankenphp + +import ( + "bytes" + "net/http/httptest" + "runtime/pprof" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// leakedGoroutines runs a leak-detection GC cycle and returns how many goroutines +// it found blocked forever on a channel or mutex that no running goroutine can +// still reach, along with their stacks. +// +// Profile.Count only reports the result of the previous detection cycle, so the +// profile has to be written first even when only the count is wanted. +func leakedGoroutines(t *testing.T) (int, string) { + t.Helper() + + profile := pprof.Lookup("goroutineleak") + require.NotNil(t, profile, "the goroutineleak profile is only available since Go 1.27") + + var stacks bytes.Buffer + require.NoError(t, profile.WriteTo(&stacks, 1)) + + return profile.Count(), stacks.String() +} + +// TestNoGoroutinesAreLeakedByAFullServerLifecycle boots PHP threads, serves requests +// through a worker and a regular thread, then shuts everything down. +// +// Shutdown has to unblock every goroutine it started, so a goroutine still parked on +// an unreachable channel afterwards means a thread, a scaling ticker or a watcher +// outlived Shutdown with nothing left to wake it. The count is compared against a +// baseline rather than against zero: tests share a process, so earlier tests may have +// left leaks of their own behind. +func TestNoGoroutinesAreLeakedByAFullServerLifecycle(t *testing.T) { + before, _ := leakedGoroutines(t) + + require.NoError(t, Init( + WithNumThreads(2), + WithMaxThreads(4), + WithWorkers("worker", testDataPath+"/index.php", 1, WithWorkerMaxFailures(0)), + )) + + for range 5 { + r := httptest.NewRequest("GET", "http://localhost/index.php", nil) + req, err := NewRequestWithContext(r, WithRequestDocumentRoot(testDataPath, false)) + require.NoError(t, err) + require.NoError(t, ServeHTTP(httptest.NewRecorder(), req)) + } + + Shutdown() + + after, stacks := leakedGoroutines(t) + t.Logf("leaked goroutines: %d before, %d after", before, after) + + assert.LessOrEqual(t, after, before, "goroutines leaked across an Init/Shutdown cycle:\n%s", stacks) +} diff --git a/hotreload.go b/hotreload.go index 5ff1a7c61a..dde11a6cf1 100644 --- a/hotreload.go +++ b/hotreload.go @@ -30,7 +30,7 @@ func WithHotReload(topic string, hub *mercure.Hub, patterns []string) Option { if err := hub.Publish(globalCtx, &mercure.Update{ Topics: []string{topic}, - Event: mercure.Event{Data: string(data)}, + Data: string(data), Debug: globalLogger.Enabled(globalCtx, slog.LevelDebug), }); err != nil && globalLogger.Enabled(globalCtx, slog.LevelError) { globalLogger.LogAttrs(globalCtx, slog.LevelError, "error publishing hot reloading Mercure update", slog.Any("error", err)) diff --git a/internal/extgen/gofile.go b/internal/extgen/gofile.go index 044696a47f..4b5390460b 100644 --- a/internal/extgen/gofile.go +++ b/internal/extgen/gofile.go @@ -219,8 +219,8 @@ func extractGoFunctionCallParams(goFunction string) string { } var names []string - parts := strings.Split(params, ",") - for _, part := range parts { + parts := strings.SplitSeq(params, ",") + for part := range parts { part = strings.TrimSpace(part) if len(part) == 0 { continue diff --git a/internal/state/state.go b/internal/state/state.go index 37e4c2e696..839c4b5fce 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -1,6 +1,5 @@ package state -import "C" import ( "slices" "sync" diff --git a/internal/state/state_test.go b/internal/state/state_test.go index 3da5266038..767a27606e 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -2,54 +2,66 @@ package state import ( "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" ) func Test2GoroutinesYieldToEachOtherViaStates(t *testing.T) { - threadState := &ThreadState{currentState: Booting} + synctest.Test(t, func(t *testing.T) { + threadState := &ThreadState{currentState: Booting} - go func() { - threadState.WaitFor(Inactive) - assert.True(t, threadState.Is(Inactive)) - threadState.Set(Ready) - }() + go func() { + threadState.WaitFor(Inactive) + assert.True(t, threadState.Is(Inactive)) + threadState.Set(Ready) + }() - threadState.Set(Inactive) - threadState.WaitFor(Ready) - assert.True(t, threadState.Is(Ready)) + threadState.Set(Inactive) + threadState.WaitFor(Ready) + assert.True(t, threadState.Is(Ready)) + }) } func TestStateShouldHaveCorrectAmountOfSubscribers(t *testing.T) { - threadState := &ThreadState{currentState: Booting} + synctest.Test(t, func(t *testing.T) { + threadState := &ThreadState{currentState: Booting} - // 3 subscribers waiting for different states - go threadState.WaitFor(Inactive) - go threadState.WaitFor(Inactive, ShuttingDown) - go threadState.WaitFor(ShuttingDown) + // 3 subscribers waiting for different states + go threadState.WaitFor(Inactive) + go threadState.WaitFor(Inactive, ShuttingDown) + go threadState.WaitFor(ShuttingDown) - assertNumberOfSubscribers(t, threadState, 3) + assertNumberOfSubscribers(t, threadState, 3) - threadState.Set(Inactive) - assertNumberOfSubscribers(t, threadState, 1) + threadState.Set(Inactive) + assertNumberOfSubscribers(t, threadState, 1) - assert.True(t, threadState.CompareAndSwap(Inactive, ShuttingDown)) - assertNumberOfSubscribers(t, threadState, 0) + assert.True(t, threadState.CompareAndSwap(Inactive, ShuttingDown)) + assertNumberOfSubscribers(t, threadState, 0) + }) +} + +func TestWaitForStateWithTimeoutGivesUpAndDropsItsSubscriber(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + threadState := &ThreadState{currentState: Booting} + + // the fake clock makes the timeout fire instantly instead of after a real second + assert.False(t, threadState.WaitForStateWithTimeout(time.Second, Ready)) + assert.Empty(t, threadState.subscribers) + }) } func assertNumberOfSubscribers(t *testing.T, threadState *ThreadState, expected int) { t.Helper() - for range 10_000 { // wait for 1 second max - time.Sleep(100 * time.Microsecond) - threadState.mu.RLock() - if len(threadState.subscribers) == expected { - threadState.mu.RUnlock() - break - } - threadState.mu.RUnlock() - } + + // every subscriber goroutine is durably blocked on its channel once Wait returns, + // so the subscriber list has reached its final shape for this state + synctest.Wait() + threadState.mu.RLock() + defer threadState.mu.RUnlock() + assert.Len(t, threadState.subscribers, expected) - threadState.mu.RUnlock() } diff --git a/internal/watcher/pattern.go b/internal/watcher/pattern.go index be045f747d..0f0bf3603e 100644 --- a/internal/watcher/pattern.go +++ b/internal/watcher/pattern.go @@ -207,7 +207,7 @@ func expandCurlyBraces(s string) []string { } var out []string - for _, subPattern := range strings.Split(inside, ",") { + for subPattern := range strings.SplitSeq(inside, ",") { out = append(out, expandCurlyBraces(before+subPattern+after)...) } diff --git a/maxrequests_regular_test.go b/maxrequests_regular_test.go index 47a39a02d8..e3b34af2ad 100644 --- a/maxrequests_regular_test.go +++ b/maxrequests_regular_test.go @@ -22,7 +22,7 @@ func TestModuleMaxRequests(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { - for i := 0; i < totalRequests; i++ { + for range totalRequests { body, resp := testGet("http://example.com/index.php", handler, t) assert.Equal(t, 200, resp.StatusCode) assert.Contains(t, body, "I am by birth a Genevese") @@ -50,14 +50,12 @@ func TestModuleMaxRequestsConcurrent(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { var wg sync.WaitGroup - for i := 0; i < totalRequests; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range totalRequests { + wg.Go(func() { body, resp := testGet("http://example.com/index.php", handler, t) assert.Equal(t, 200, resp.StatusCode) assert.Contains(t, body, "I am by birth a Genevese") - }() + }) } wg.Wait() }, &testOptions{ diff --git a/metrics.go b/metrics.go index baab7bbc90..fc25816506 100644 --- a/metrics.go +++ b/metrics.go @@ -97,6 +97,15 @@ type PrometheusMetrics struct { mu sync.RWMutex } +// mustRegister registers c, tolerating a collector that is already registered. +func (m *PrometheusMetrics) mustRegister(c prometheus.Collector) { + if err := m.registry.Register(c); err != nil { + if _, ok := errors.AsType[prometheus.AlreadyRegisteredError](err); !ok { + panic(err) + } + } +} + func (m *PrometheusMetrics) StartWorker(name string) { m.mu.RLock() defer m.mu.RUnlock() @@ -161,10 +170,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Name: "total_workers", Help: "Total number of PHP workers for this worker", }, basicLabels) - if err := m.registry.Register(m.totalWorkers); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.totalWorkers) } if m.readyWorkers == nil { @@ -173,10 +179,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Name: "ready_workers", Help: "Running workers that have successfully called frankenphp_handle_request at least once", }, basicLabels) - if err := m.registry.Register(m.readyWorkers); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.readyWorkers) } if m.busyWorkers == nil { @@ -185,10 +188,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Name: "busy_workers", Help: "Number of busy PHP workers for this worker", }, basicLabels) - if err := m.registry.Register(m.busyWorkers); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.busyWorkers) } if m.workerCrashes == nil { @@ -198,10 +198,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Name: "crashes", Help: "Number of PHP worker crashes for this worker", }, basicLabels) - if err := m.registry.Register(m.workerCrashes); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.workerCrashes) } if m.workerRestarts == nil { @@ -211,10 +208,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Name: "restarts", Help: "Number of PHP worker restarts for this worker", }, basicLabels) - if err := m.registry.Register(m.workerRestarts); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.workerRestarts) } if m.workerRequestTime == nil { @@ -223,10 +217,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Subsystem: sub, Name: "request_time", }, basicLabels) - if err := m.registry.Register(m.workerRequestTime); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.workerRequestTime) } if m.workerRequestCount == nil { @@ -235,10 +226,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Subsystem: sub, Name: "request_count", }, basicLabels) - if err := m.registry.Register(m.workerRequestCount); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.workerRequestCount) } if m.workerQueueDepth == nil { @@ -247,10 +235,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) { Subsystem: sub, Name: "queue_depth", }, basicLabels) - if err := m.registry.Register(m.workerQueueDepth); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.workerQueueDepth) } } @@ -402,20 +387,11 @@ func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics { workerQueueDepth: nil, } - if err := m.registry.Register(m.totalThreads); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.totalThreads) - if err := m.registry.Register(m.busyThreads); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.busyThreads) - if err := m.registry.Register(m.queueDepth); err != nil && - !errors.As(err, &prometheus.AlreadyRegisteredError{}) { - panic(err) - } + m.mustRegister(m.queueDepth) return m } diff --git a/phpmainthread.go b/phpmainthread.go index 64c782319b..5e19c0fc75 100644 --- a/phpmainthread.go +++ b/phpmainthread.go @@ -75,7 +75,7 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) // start the underlying C threads var ready sync.WaitGroup - for i := 0; i < numThreads; i++ { + for i := range numThreads { ready.Go(phpThreads[i].boot) } diff --git a/requestoptions.go b/requestoptions.go index 8414fb557d..962727562f 100644 --- a/requestoptions.go +++ b/requestoptions.go @@ -102,7 +102,7 @@ func normalizeSplitPath(splitPath []string) error { for i, split := range splitPath { b.Grow(len(split)) - for j := 0; j < len(split); j++ { + for j := range len(split) { c := split[j] if c >= utf8.RuneSelf { return ErrInvalidSplitPath diff --git a/scaling.go b/scaling.go index e4edc0c846..dd21a7e37c 100644 --- a/scaling.go +++ b/scaling.go @@ -3,6 +3,7 @@ package frankenphp import ( "errors" "log/slog" + "slices" "sync" "time" @@ -205,12 +206,10 @@ func deactivateThreads() { stoppedThreadCount := 0 scalingMu.Lock() defer scalingMu.Unlock() - for i := len(autoScaledThreads) - 1; i >= 0; i-- { - thread := autoScaledThreads[i] - + for i, thread := range slices.Backward(autoScaledThreads) { // the thread might have been stopped otherwise, remove it if thread.state.Is(state.Reserved) { - autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...) + autoScaledThreads = slices.Delete(autoScaledThreads, i, i+1) continue } @@ -223,7 +222,7 @@ func deactivateThreads() { if thread.state.Is(state.Ready) && waitTime > maxIdleTime.Milliseconds() { convertToInactiveThread(thread) stoppedThreadCount++ - autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...) + autoScaledThreads = slices.Delete(autoScaledThreads, i, i+1) if globalLogger.Enabled(globalCtx, slog.LevelInfo) { globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "downscaling thread", slog.Int("thread", thread.threadIndex), slog.Int64("wait_time", waitTime), slog.Int("num_threads", len(autoScaledThreads))) @@ -239,7 +238,7 @@ func deactivateThreads() { // logger.LogAttrs(nil, slog.LevelDebug, "auto-stopping thread", slog.Int("thread", thread.threadIndex)) // thread.shutdown() // stoppedThreadCount++ - // autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...) + // autoScaledThreads = slices.Delete(autoScaledThreads, i, i+1) // continue // } } diff --git a/threadregular_test.go b/threadregular_test.go index e79123753a..7a1d0700f6 100644 --- a/threadregular_test.go +++ b/threadregular_test.go @@ -42,7 +42,7 @@ func TestRequestsQueuedBeforeThreadsAreReadyAreHandedOver(t *testing.T) { errChans[i] = errChan } - for i := 0; i < requests; i++ { + for i := range requests { select { case fc := <-regularRequestChan: fc.closeContext() diff --git a/types.go b/types.go index 96898cde33..6759e58753 100644 --- a/types.go +++ b/types.go @@ -474,8 +474,8 @@ func zendHashDestroy(p unsafe.Pointer) { } // EXPERIMENTAL: CallPHPCallable executes a PHP callable with the given parameters. -// Returns the result of the callable as a Go interface{}, or nil if the call failed. -func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} { +// Returns the result of the callable as a Go any, or nil if the call failed. +func CallPHPCallable(cb unsafe.Pointer, params []any) any { if cb == nil { return nil } @@ -494,15 +494,15 @@ func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} { if paramCount > 0 { paramStorage = (*C.zval)(C.__emalloc__(C.size_t(paramCount) * C.size_t(unsafe.Sizeof(C.zval{})))) defer func() { - for i := 0; i < paramCount; i++ { - targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{}))) + for i := range paramCount { + targetZval := (*C.zval)(unsafe.Add(unsafe.Pointer(paramStorage), uintptr(i)*unsafe.Sizeof(C.zval{}))) C.zval_ptr_dtor(targetZval) } C.__efree__(unsafe.Pointer(paramStorage)) }() for i, param := range params { - targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{}))) + targetZval := (*C.zval)(unsafe.Add(unsafe.Pointer(paramStorage), uintptr(i)*unsafe.Sizeof(C.zval{}))) sourceZval := phpValue(param) *targetZval = *sourceZval C.__efree__(unsafe.Pointer(sourceZval)) diff --git a/worker.go b/worker.go index ecbed9e392..388dfbd031 100644 --- a/worker.go +++ b/worker.go @@ -77,7 +77,7 @@ func initWorkers(opts []workerOpt) error { startupFailChan = make(chan error, totalThreadsToStart) for _, w := range workers { - for i := 0; i < w.num; i++ { + for range w.num { thread := getInactivePHPThread() convertToWorkerThread(thread, w) diff --git a/worker_test.go b/worker_test.go index dc423294f6..10c2b669ae 100644 --- a/worker_test.go +++ b/worker_test.go @@ -195,7 +195,7 @@ func TestWorkerMaxRequests(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { instanceIDs := make(map[string]int) - for i := 0; i < totalRequests; i++ { + for range totalRequests { body, resp := testGet("http://example.com/worker-counter-persistent.php", handler, t) assert.Equal(t, 200, resp.StatusCode) @@ -242,10 +242,8 @@ func TestWorkerMaxRequestsHighConcurrency(t *testing.T) { ) var wg sync.WaitGroup - for i := 0; i < totalRequests; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range totalRequests { + wg.Go(func() { body, resp := testGet("http://example.com/worker-counter-persistent.php", handler, t) assert.Equal(t, 200, resp.StatusCode) @@ -256,7 +254,7 @@ func TestWorkerMaxRequestsHighConcurrency(t *testing.T) { instanceIDs[instanceID]++ } mu.Unlock() - }() + }) } wg.Wait()