mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 12:42:16 +01:00
fix(supervisor): if process is already restarting, requeue a task (#754)
This commit is contained in:
@@ -2,12 +2,17 @@ package gotenberg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrProcessAlreadyRestarting happens if the [ProcessSupervisor] is trying
|
||||||
|
// to restart an already restarting [Process].
|
||||||
|
var ErrProcessAlreadyRestarting = errors.New("process already restarting")
|
||||||
|
|
||||||
// Process is an interface that represents an abstract process
|
// Process is an interface that represents an abstract process
|
||||||
// and provides methods for starting, stopping, and checking the health of the
|
// and provides methods for starting, stopping, and checking the health of the
|
||||||
// process.
|
// process.
|
||||||
@@ -122,7 +127,7 @@ func (s *processSupervisor) restart() error {
|
|||||||
if s.isRestarting.Load() {
|
if s.isRestarting.Load() {
|
||||||
s.logger.Debug("process already restarting, skip restart")
|
s.logger.Debug("process already restarting, skip restart")
|
||||||
|
|
||||||
return nil
|
return ErrProcessAlreadyRestarting
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Debug("restart process")
|
s.logger.Debug("restart process")
|
||||||
@@ -164,53 +169,66 @@ func (s *processSupervisor) Healthy() bool {
|
|||||||
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
s.reqQueueSize.Add(1)
|
s.reqQueueSize.Add(1)
|
||||||
|
|
||||||
select {
|
for {
|
||||||
case s.mutexChan <- struct{}{}:
|
err := func() error {
|
||||||
logger.Debug("process lock acquired")
|
select {
|
||||||
s.reqQueueSize.Add(-1)
|
case s.mutexChan <- struct{}{}:
|
||||||
s.reqCounter.Add(1)
|
logger.Debug("process lock acquired")
|
||||||
|
s.reqQueueSize.Add(-1)
|
||||||
|
s.reqCounter.Add(1)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debug("process lock released")
|
logger.Debug("process lock released")
|
||||||
<-s.mutexChan
|
<-s.mutexChan
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !s.firstStart.Load() {
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.Launch()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process first start: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.Healthy() {
|
||||||
|
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.restart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process restart before task: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
||||||
|
s.logger.Debug("max request limit reached, restarting...")
|
||||||
|
err := s.runWithDeadline(ctx, func() error {
|
||||||
|
return s.restart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("process restart before task: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||||
|
return s.runWithDeadline(ctx, task)
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug("failed to acquire process lock before deadline")
|
||||||
|
s.reqQueueSize.Add(-1)
|
||||||
|
|
||||||
|
return fmt.Errorf("acquire process lock: %w", ctx.Err())
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !s.firstStart.Load() {
|
if errors.Is(err, ErrProcessAlreadyRestarting) {
|
||||||
err := s.runWithDeadline(ctx, func() error {
|
logger.Debug("process is already restarting, trying to acquire process lock again...")
|
||||||
return s.Launch()
|
s.reqQueueSize.Add(1)
|
||||||
})
|
continue
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process first start: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.Healthy() {
|
// Note: no error wrapping because it leaks on Chromium console exceptions output.
|
||||||
s.logger.Debug("process is unhealthy, cannot handle task, restarting...")
|
return err
|
||||||
err := s.runWithDeadline(ctx, func() error {
|
|
||||||
return s.restart()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process restart before task: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.maxReqLimit > 0 && s.reqCounter.Load() >= s.maxReqLimit {
|
|
||||||
s.logger.Debug("max request limit reached, restarting...")
|
|
||||||
err := s.runWithDeadline(ctx, func() error {
|
|
||||||
return s.restart()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("process restart before task: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: no error wrapping because it leaks on Chromium console exceptions output.
|
|
||||||
return s.runWithDeadline(ctx, task)
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Debug("failed to acquire process lock before deadline")
|
|
||||||
s.reqQueueSize.Add(-1)
|
|
||||||
|
|
||||||
return fmt.Errorf("acquire process lock: %w", ctx.Err())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,13 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
|||||||
startError error
|
startError error
|
||||||
stopError error
|
stopError error
|
||||||
expectError bool
|
expectError bool
|
||||||
|
expectedError error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "already restarting",
|
scenario: "already restarting",
|
||||||
initiallyRestarting: true,
|
initiallyRestarting: true,
|
||||||
expectError: false,
|
expectError: true,
|
||||||
|
expectedError: ErrProcessAlreadyRestarting,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "successful restart",
|
scenario: "successful restart",
|
||||||
@@ -166,6 +168,10 @@ func TestProcessSupervisor_restart(t *testing.T) {
|
|||||||
if tc.expectError && err == nil {
|
if tc.expectError && err == nil {
|
||||||
t.Fatal("expected error but got none")
|
t.Fatal("expected error but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
|
||||||
|
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,12 +238,14 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
initiallyStarted bool
|
initiallyStarted bool
|
||||||
|
isRestarting bool
|
||||||
startError error
|
startError error
|
||||||
processHealthy bool
|
processHealthy bool
|
||||||
maxReqLimit int64
|
maxReqLimit int64
|
||||||
tasksToRun int
|
tasksToRun int
|
||||||
taskError error
|
taskError error
|
||||||
expectError bool
|
expectError bool
|
||||||
|
skipCallsCheck bool
|
||||||
expectedStartCalls int64
|
expectedStartCalls int64
|
||||||
expectedHealthyCalls int64
|
expectedHealthyCalls int64
|
||||||
expectedStopCalls int64
|
expectedStopCalls int64
|
||||||
@@ -245,6 +253,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "successfully run task on non-started process",
|
scenario: "successfully run task on non-started process",
|
||||||
initiallyStarted: false,
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -256,6 +265,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "cannot launch non-started process",
|
scenario: "cannot launch non-started process",
|
||||||
initiallyStarted: false,
|
initiallyStarted: false,
|
||||||
|
isRestarting: false,
|
||||||
startError: errors.New("launch error"),
|
startError: errors.New("launch error"),
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
@@ -268,6 +278,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "run task with unhealthy process causing restart",
|
scenario: "run task with unhealthy process causing restart",
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: false,
|
processHealthy: false,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -280,6 +291,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
scenario: "cannot restart unhealthy process",
|
scenario: "cannot restart unhealthy process",
|
||||||
startError: errors.New("start error"),
|
startError: errors.New("start error"),
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: false,
|
processHealthy: false,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -288,9 +300,20 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
expectedHealthyCalls: 1,
|
expectedHealthyCalls: 1,
|
||||||
expectedStopCalls: 1,
|
expectedStopCalls: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrProcessAlreadyRestarting",
|
||||||
|
initiallyStarted: true,
|
||||||
|
isRestarting: true,
|
||||||
|
processHealthy: false,
|
||||||
|
maxReqLimit: 1,
|
||||||
|
tasksToRun: 1,
|
||||||
|
expectError: true,
|
||||||
|
skipCallsCheck: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "run tasks reaching max request limit causing restart",
|
scenario: "run tasks reaching max request limit causing restart",
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 3,
|
tasksToRun: 3,
|
||||||
@@ -303,6 +326,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
scenario: "cannot restart after reaching max request limit",
|
scenario: "cannot restart after reaching max request limit",
|
||||||
startError: errors.New("start error"),
|
startError: errors.New("start error"),
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 2,
|
maxReqLimit: 2,
|
||||||
tasksToRun: 2,
|
tasksToRun: 2,
|
||||||
@@ -314,6 +338,7 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
{
|
{
|
||||||
scenario: "task error",
|
scenario: "task error",
|
||||||
initiallyStarted: true,
|
initiallyStarted: true,
|
||||||
|
isRestarting: false,
|
||||||
processHealthy: true,
|
processHealthy: true,
|
||||||
maxReqLimit: 0,
|
maxReqLimit: 0,
|
||||||
tasksToRun: 1,
|
tasksToRun: 1,
|
||||||
@@ -351,6 +376,9 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
if tc.initiallyStarted {
|
if tc.initiallyStarted {
|
||||||
ps.firstStart.Store(true)
|
ps.firstStart.Store(true)
|
||||||
}
|
}
|
||||||
|
if tc.isRestarting {
|
||||||
|
ps.isRestarting.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
task := func() error {
|
task := func() error {
|
||||||
return tc.taskError
|
return tc.taskError
|
||||||
@@ -386,6 +414,10 @@ func TestProcessSupervisor_Run(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.skipCallsCheck {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if startCalls.Load() != tc.expectedStartCalls {
|
if startCalls.Load() != tc.expectedStartCalls {
|
||||||
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
t.Errorf("expected %d process.Start calls, got %d", tc.expectedStartCalls, startCalls.Load())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user