Skip to content

Commit cb3ec9e

Browse files
authored
feat: handle job panics in executor (#39)
1 parent c2f3d5b commit cb3ec9e

File tree

2 files changed

+60
-14
lines changed

2 files changed

+60
-14
lines changed

executor.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package async
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"sync"
78
"sync/atomic"
89
)
@@ -13,12 +14,12 @@ type ExecutorStatus uint32
1314
const (
1415
ExecutorStatusRunning ExecutorStatus = iota
1516
ExecutorStatusTerminating
16-
ExecutorStatusShutdown
17+
ExecutorStatusShutDown
1718
)
1819

1920
var (
2021
ErrExecutorQueueFull = errors.New("async: executor queue is full")
21-
ErrExecutorShutdown = errors.New("async: executor is shut down")
22+
ErrExecutorShutDown = errors.New("async: executor is shut down")
2223
)
2324

2425
// ExecutorService is an interface that defines a task executor.
@@ -44,6 +45,7 @@ type ExecutorConfig struct {
4445
}
4546

4647
// NewExecutorConfig returns a new [ExecutorConfig].
48+
// workerPoolSize must be positive and queueSize non-negative.
4749
func NewExecutorConfig(workerPoolSize, queueSize int) *ExecutorConfig {
4850
return &ExecutorConfig{
4951
WorkerPoolSize: workerPoolSize,
@@ -53,6 +55,7 @@ func NewExecutorConfig(workerPoolSize, queueSize int) *ExecutorConfig {
5355

5456
// Executor implements the [ExecutorService] interface.
5557
type Executor[T any] struct {
58+
mtx sync.RWMutex
5659
cancel context.CancelFunc
5760
queue chan executorJob[T]
5861
status atomic.Uint32
@@ -65,6 +68,16 @@ type executorJob[T any] struct {
6568
task func(context.Context) (T, error)
6669
}
6770

71+
// run executes the task, handling possible panics.
72+
func (job *executorJob[T]) run(ctx context.Context) (result T, err error) {
73+
defer func() {
74+
if r := recover(); r != nil {
75+
err = fmt.Errorf("recovered: %v", r)
76+
}
77+
}()
78+
return job.task(ctx)
79+
}
80+
6881
// NewExecutor returns a new [Executor].
6982
func NewExecutor[T any](ctx context.Context, config *ExecutorConfig) *Executor[T] {
7083
ctx, cancel := context.WithCancel(ctx)
@@ -97,10 +110,11 @@ func (e *Executor[T]) startWorkers(ctx context.Context, poolSize int) {
97110
go func() {
98111
defer wg.Done()
99112
loop:
113+
// check the status to break the loop even if the queue is not empty
100114
for ExecutorStatus(e.status.Load()) == ExecutorStatusRunning {
101115
select {
102116
case job := <-e.queue:
103-
result, err := job.task(ctx)
117+
result, err := job.run(ctx)
104118
if err != nil {
105119
job.promise.Failure(err)
106120
} else {
@@ -115,30 +129,39 @@ func (e *Executor[T]) startWorkers(ctx context.Context, poolSize int) {
115129

116130
// wait for all workers to exit
117131
wg.Wait()
132+
// mark the executor as terminating
133+
e.status.Store(uint32(ExecutorStatusTerminating))
134+
135+
// avoid submissions while draining the queue
136+
e.mtx.Lock()
137+
defer e.mtx.Unlock()
138+
118139
// close the queue and cancel all pending tasks
119140
close(e.queue)
120141
for job := range e.queue {
121-
job.promise.Failure(ErrExecutorShutdown)
142+
job.promise.Failure(ErrExecutorShutDown)
122143
}
123144
// mark the executor as shut down
124-
e.status.Store(uint32(ExecutorStatusShutdown))
145+
e.status.Store(uint32(ExecutorStatusShutDown))
125146
}
126147

127148
// Submit submits a function to the executor.
128149
// The function will be executed asynchronously and the result will be
129150
// available via the returned future.
130151
func (e *Executor[T]) Submit(f func(context.Context) (T, error)) (Future[T], error) {
131-
promise := NewPromise[T]()
152+
e.mtx.RLock()
153+
defer e.mtx.RUnlock()
154+
132155
if ExecutorStatus(e.status.Load()) == ExecutorStatusRunning {
156+
promise := NewPromise[T]()
133157
select {
134158
case e.queue <- executorJob[T]{promise, f}:
159+
return promise.Future(), nil
135160
default:
136161
return nil, ErrExecutorQueueFull
137162
}
138-
} else {
139-
return nil, ErrExecutorShutdown
140163
}
141-
return promise.Future(), nil
164+
return nil, ErrExecutorShutDown
142165
}
143166

144167
// Shutdown shuts down the executor.

executor_test.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,17 @@ func TestExecutor(t *testing.T) {
5050

5151
// verify that submit fails after the executor was shut down
5252
_, err = executor.Submit(job)
53-
assert.ErrorIs(t, err, ErrExecutorShutdown)
53+
assert.ErrorIs(t, err, ErrExecutorShutDown)
5454

5555
// validate the executor status
5656
assert.Equal(t, executor.Status(), ExecutorStatusTerminating)
5757
time.Sleep(10 * time.Millisecond)
58-
assert.Equal(t, executor.Status(), ExecutorStatusShutdown)
58+
assert.Equal(t, executor.Status(), ExecutorStatusShutDown)
5959

6060
assert.Equal(t, routines, runtime.NumGoroutine()+4)
6161

6262
assertFutureResult(t, 1, future1, future2, future3, future4)
63-
assertFutureError(t, ErrExecutorShutdown, future5, future6)
63+
assertFutureError(t, ErrExecutorShutDown, future5, future6)
6464
}
6565

6666
func TestExecutor_context(t *testing.T) {
@@ -80,15 +80,38 @@ func TestExecutor_context(t *testing.T) {
8080

8181
cancel()
8282
time.Sleep(5 * time.Millisecond)
83-
assert.Equal(t, executor.Status(), ExecutorStatusShutdown)
83+
84+
_, err = executor.Submit(job)
85+
assert.ErrorIs(t, err, ErrExecutorShutDown)
86+
87+
assert.Equal(t, executor.Status(), ExecutorStatusShutDown)
88+
}
89+
90+
func TestExecutor_jobPanic(t *testing.T) {
91+
ctx := context.Background()
92+
executor := NewExecutor[int](ctx, NewExecutorConfig(2, 2))
93+
94+
job := func(_ context.Context) (int, error) {
95+
var i int
96+
return 1 / i, nil
97+
}
98+
99+
future, err := executor.Submit(job)
100+
assert.IsNil(t, err)
101+
102+
result, err := future.Join()
103+
assert.Equal(t, result, 0)
104+
assert.ErrorContains(t, err, "integer divide by zero")
105+
106+
_ = executor.Shutdown()
84107
}
85108

86109
func submitJob[T any](t *testing.T, executor ExecutorService[T],
87110
f func(context.Context) (T, error)) Future[T] {
88111
future, err := executor.Submit(f)
89112
assert.IsNil(t, err)
90113

91-
time.Sleep(time.Millisecond) // switch context
114+
runtime.Gosched()
92115
return future
93116
}
94117

0 commit comments

Comments
 (0)