Skip to content

Commit 1e279bc

Browse files
refactor: simplify exponential backoff and refactor env (#1185)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
1 parent 449a0e7 commit 1e279bc

5 files changed

Lines changed: 178 additions & 130 deletions

File tree

backoff.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package frankenphp
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type exponentialBackoff struct {
9+
backoff time.Duration
10+
failureCount int
11+
mu sync.RWMutex
12+
maxBackoff time.Duration
13+
minBackoff time.Duration
14+
maxConsecutiveFailures int
15+
}
16+
17+
// recordSuccess resets the backoff and failureCount
18+
func (e *exponentialBackoff) recordSuccess() {
19+
e.mu.Lock()
20+
e.failureCount = 0
21+
e.backoff = e.minBackoff
22+
e.mu.Unlock()
23+
}
24+
25+
// recordFailure increments the failure count and increases the backoff, it returns true if maxConsecutiveFailures has been reached
26+
func (e *exponentialBackoff) recordFailure() bool {
27+
e.mu.Lock()
28+
e.failureCount += 1
29+
if e.backoff < e.minBackoff {
30+
e.backoff = e.minBackoff
31+
}
32+
33+
e.backoff = min(e.backoff*2, e.maxBackoff)
34+
35+
e.mu.Unlock()
36+
return e.failureCount >= e.maxConsecutiveFailures
37+
}
38+
39+
// wait sleeps for the backoff duration if failureCount is non-zero.
40+
// NOTE: this is not tested and should be kept 'obviously correct' (i.e., simple)
41+
func (e *exponentialBackoff) wait() {
42+
e.mu.RLock()
43+
if e.failureCount == 0 {
44+
e.mu.RUnlock()
45+
46+
return
47+
}
48+
e.mu.RUnlock()
49+
50+
time.Sleep(e.backoff)
51+
}

backoff_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package frankenphp
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestExponentialBackoff_Reset(t *testing.T) {
10+
e := &exponentialBackoff{
11+
maxBackoff: 5 * time.Second,
12+
minBackoff: 500 * time.Millisecond,
13+
maxConsecutiveFailures: 3,
14+
}
15+
16+
assert.False(t, e.recordFailure())
17+
assert.False(t, e.recordFailure())
18+
e.recordSuccess()
19+
20+
e.mu.RLock()
21+
defer e.mu.RUnlock()
22+
assert.Equal(t, 0, e.failureCount, "expected failureCount to be reset to 0")
23+
assert.Equal(t, e.backoff, e.minBackoff, "expected backoff to be reset to minBackoff")
24+
}
25+
26+
func TestExponentialBackoff_Trigger(t *testing.T) {
27+
e := &exponentialBackoff{
28+
maxBackoff: 500 * 3 * time.Millisecond,
29+
minBackoff: 500 * time.Millisecond,
30+
maxConsecutiveFailures: 3,
31+
}
32+
33+
assert.False(t, e.recordFailure())
34+
assert.False(t, e.recordFailure())
35+
assert.True(t, e.recordFailure())
36+
37+
e.mu.RLock()
38+
defer e.mu.RUnlock()
39+
assert.Equal(t, e.failureCount, e.maxConsecutiveFailures, "expected failureCount to be maxConsecutiveFailures")
40+
assert.Equal(t, e.backoff, e.maxBackoff, "expected backoff to be maxBackoff")
41+
}

env.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package frankenphp
2+
3+
// #include "frankenphp.h"
4+
import "C"
5+
import (
6+
"os"
7+
"strings"
8+
"unsafe"
9+
)
10+
11+
//export go_putenv
12+
func go_putenv(str *C.char, length C.int) C.bool {
13+
envString := C.GoStringN(str, length)
14+
15+
// Check if '=' is present in the string
16+
if key, val, found := strings.Cut(envString, "="); found {
17+
return os.Setenv(key, val) == nil
18+
}
19+
20+
// No '=', unset the environment variable
21+
return os.Unsetenv(envString) == nil
22+
}
23+
24+
//export go_getfullenv
25+
func go_getfullenv(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
26+
thread := phpThreads[threadIndex]
27+
28+
env := os.Environ()
29+
goStrings := make([]C.go_string, len(env)*2)
30+
31+
for i, envVar := range env {
32+
key, val, _ := strings.Cut(envVar, "=")
33+
goStrings[i*2] = C.go_string{C.size_t(len(key)), thread.pinString(key)}
34+
goStrings[i*2+1] = C.go_string{C.size_t(len(val)), thread.pinString(val)}
35+
}
36+
37+
value := unsafe.SliceData(goStrings)
38+
thread.Pin(value)
39+
40+
return value, C.size_t(len(env))
41+
}
42+
43+
//export go_getenv
44+
func go_getenv(threadIndex C.uintptr_t, name *C.go_string) (C.bool, *C.go_string) {
45+
thread := phpThreads[threadIndex]
46+
47+
// Create a byte slice from C string with a specified length
48+
envName := C.GoStringN(name.data, C.int(name.len))
49+
50+
// Get the environment variable value
51+
envValue, exists := os.LookupEnv(envName)
52+
if !exists {
53+
// Environment variable does not exist
54+
return false, nil // Return 0 to indicate failure
55+
}
56+
57+
// Convert Go string to C string
58+
value := &C.go_string{C.size_t(len(envValue)), thread.pinString(envValue)}
59+
thread.Pin(value)
60+
61+
return true, value // Return 1 to indicate success
62+
}
63+
64+
//export go_sapi_getenv
65+
func go_sapi_getenv(threadIndex C.uintptr_t, name *C.go_string) *C.char {
66+
envName := C.GoStringN(name.data, C.int(name.len))
67+
68+
envValue, exists := os.LookupEnv(envName)
69+
if !exists {
70+
return nil
71+
}
72+
73+
return phpThreads[threadIndex].pinCString(envValue)
74+
}

frankenphp.go

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -505,88 +505,6 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
505505
return nil
506506
}
507507

508-
//export go_putenv
509-
func go_putenv(str *C.char, length C.int) C.bool {
510-
// Create a byte slice from C string with a specified length
511-
s := C.GoBytes(unsafe.Pointer(str), length)
512-
513-
// Convert byte slice to string
514-
envString := string(s)
515-
516-
// Check if '=' is present in the string
517-
if key, val, found := strings.Cut(envString, "="); found {
518-
if os.Setenv(key, val) != nil {
519-
return false // Failure
520-
}
521-
} else {
522-
// No '=', unset the environment variable
523-
if os.Unsetenv(envString) != nil {
524-
return false // Failure
525-
}
526-
}
527-
528-
return true // Success
529-
}
530-
531-
//export go_getfullenv
532-
func go_getfullenv(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
533-
thread := phpThreads[threadIndex]
534-
535-
env := os.Environ()
536-
goStrings := make([]C.go_string, len(env)*2)
537-
538-
for i, envVar := range env {
539-
key, val, _ := strings.Cut(envVar, "=")
540-
k := unsafe.StringData(key)
541-
v := unsafe.StringData(val)
542-
thread.Pin(k)
543-
thread.Pin(v)
544-
545-
goStrings[i*2] = C.go_string{C.size_t(len(key)), (*C.char)(unsafe.Pointer(k))}
546-
goStrings[i*2+1] = C.go_string{C.size_t(len(val)), (*C.char)(unsafe.Pointer(v))}
547-
}
548-
549-
value := unsafe.SliceData(goStrings)
550-
thread.Pin(value)
551-
552-
return value, C.size_t(len(env))
553-
}
554-
555-
//export go_getenv
556-
func go_getenv(threadIndex C.uintptr_t, name *C.go_string) (C.bool, *C.go_string) {
557-
thread := phpThreads[threadIndex]
558-
559-
// Create a byte slice from C string with a specified length
560-
envName := C.GoStringN(name.data, C.int(name.len))
561-
562-
// Get the environment variable value
563-
envValue, exists := os.LookupEnv(envName)
564-
if !exists {
565-
// Environment variable does not exist
566-
return false, nil // Return 0 to indicate failure
567-
}
568-
569-
// Convert Go string to C string
570-
val := unsafe.StringData(envValue)
571-
thread.Pin(val)
572-
value := &C.go_string{C.size_t(len(envValue)), (*C.char)(unsafe.Pointer(val))}
573-
thread.Pin(value)
574-
575-
return true, value // Return 1 to indicate success
576-
}
577-
578-
//export go_sapi_getenv
579-
func go_sapi_getenv(threadIndex C.uintptr_t, name *C.go_string) *C.char {
580-
envName := C.GoStringN(name.data, C.int(name.len))
581-
582-
envValue, exists := os.LookupEnv(envName)
583-
if !exists {
584-
return nil
585-
}
586-
587-
return phpThreads[threadIndex].pinCString(envValue)
588-
}
589-
590508
//export go_handle_request
591509
func go_handle_request(threadIndex C.uintptr_t) bool {
592510
select {

worker.go

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ type worker struct {
2626
ready chan struct{}
2727
}
2828

29-
const maxWorkerErrorBackoff = 1 * time.Second
30-
const minWorkerErrorBackoff = 100 * time.Millisecond
31-
const maxWorkerConsecutiveFailures = 6
32-
3329
var (
3430
watcherIsEnabled bool
3531
workerShutdownWG sync.WaitGroup
@@ -97,33 +93,15 @@ func newWorker(o workerOpt) (*worker, error) {
9793
func (worker *worker) startNewWorkerThread() {
9894
workerShutdownWG.Add(1)
9995
defer workerShutdownWG.Done()
100-
101-
backoff := minWorkerErrorBackoff
102-
failureCount := 0
103-
backingOffLock := sync.RWMutex{}
96+
backoff := &exponentialBackoff{
97+
maxBackoff: 1 * time.Second,
98+
minBackoff: 100 * time.Millisecond,
99+
maxConsecutiveFailures: 6,
100+
}
104101

105102
for {
106103
// if the worker can stay up longer than backoff*2, it is probably an application error
107-
upFunc := sync.Once{}
108-
go func() {
109-
backingOffLock.RLock()
110-
wait := backoff * 2
111-
backingOffLock.RUnlock()
112-
time.Sleep(wait)
113-
upFunc.Do(func() {
114-
backingOffLock.Lock()
115-
defer backingOffLock.Unlock()
116-
// if we come back to a stable state, reset the failure count
117-
if backoff == minWorkerErrorBackoff {
118-
failureCount = 0
119-
}
120-
121-
// earn back the backoff over time
122-
if failureCount > 0 {
123-
backoff = max(backoff/2, 100*time.Millisecond)
124-
}
125-
})
126-
}()
104+
backoff.wait()
127105

128106
metrics.StartWorker(worker.fileName)
129107

@@ -176,31 +154,17 @@ func (worker *worker) startNewWorkerThread() {
176154
c.Write(zap.String("worker", worker.fileName))
177155
}
178156
metrics.StopWorker(worker.fileName, StopReasonRestart)
157+
backoff.recordSuccess()
179158
continue
180159
}
181160

182161
// on exit status 1 we log the error and apply an exponential backoff when restarting
183-
upFunc.Do(func() {
184-
backingOffLock.Lock()
185-
defer backingOffLock.Unlock()
186-
// if we end up here, the worker has not been up for backoff*2
187-
// this is probably due to a syntax error or another fatal error
188-
if failureCount >= maxWorkerConsecutiveFailures {
189-
if !watcherIsEnabled {
190-
panic(fmt.Errorf("workers %q: too many consecutive failures", worker.fileName))
191-
}
192-
logger.Warn("many consecutive worker failures", zap.String("worker", worker.fileName), zap.Int("failures", failureCount))
162+
if backoff.recordFailure() {
163+
if !watcherIsEnabled {
164+
panic(fmt.Errorf("workers %q: too many consecutive failures", worker.fileName))
193165
}
194-
failureCount += 1
195-
})
196-
backingOffLock.RLock()
197-
wait := backoff
198-
backingOffLock.RUnlock()
199-
time.Sleep(wait)
200-
backingOffLock.Lock()
201-
backoff *= 2
202-
backoff = min(backoff, maxWorkerErrorBackoff)
203-
backingOffLock.Unlock()
166+
logger.Warn("many consecutive worker failures", zap.String("worker", worker.fileName), zap.Int("failures", backoff.failureCount))
167+
}
204168
metrics.StopWorker(worker.fileName, StopReasonCrash)
205169
}
206170

0 commit comments

Comments
 (0)