Skip to content

Commit 438a27f

Browse files
fix(engine): handle durable wait_for conditions without base (#5260)
1 parent 826734a commit 438a27f

4 files changed

Lines changed: 98 additions & 5 deletions

File tree

‎internal/services/dispatcher/dispatcher.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func New(fs ...DispatcherOpt) (*DispatcherImpl, error) {
346346
streamEventBufferTimeout: opts.streamEventBufferTimeout,
347347
version: opts.version,
348348
refreshTimeoutBuf: newRefreshTimeoutBuffer(),
349-
serviceV1: newDispatcherService(opts.repov1, opts.mqv1, opts.pubsub, v, opts.l, opts.dispatcherId, opts.analytics, opts.promGate),
349+
serviceV1: newDispatcherService(opts.repov1, opts.mqv1, opts.pubsub, v, opts.l, opts.dispatcherId, opts.analytics, a, opts.promGate),
350350
}, nil
351351
}
352352

‎internal/services/dispatcher/dispatcher_v1.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/hatchet-dev/hatchet/internal/services/shared/streams"
1111
"github.com/hatchet-dev/hatchet/internal/syncx"
1212
"github.com/hatchet-dev/hatchet/pkg/analytics"
13+
hatcheterrors "github.com/hatchet-dev/hatchet/pkg/errors"
1314
"github.com/hatchet-dev/hatchet/pkg/integrations/metrics/prometheus"
1415
v1 "github.com/hatchet-dev/hatchet/pkg/repository"
1516
"github.com/hatchet-dev/hatchet/pkg/validator"
@@ -28,6 +29,7 @@ type DispatcherServiceImpl struct {
2829
mq msgqueue.MessageQueue
2930
v validator.Validator
3031
analytics analytics.Analytics
32+
alerter *hatcheterrors.Wrapped
3133
triggerWriter *trigger.TriggerWriter
3234
pubBuffer *msgqueue.MQPubBuffer
3335
streamSessions *streams.Registry
@@ -44,7 +46,7 @@ func (d *DispatcherServiceImpl) CancelStreamSessions() {
4446
d.streamSessions.CancelAll()
4547
}
4648

47-
func newDispatcherService(repo v1.Repository, mq msgqueue.MessageQueue, pubsub msgqueue.PubSub, v validator.Validator, l *zerolog.Logger, dispatcherId uuid.UUID, a analytics.Analytics, promGate *prometheus.Gate) *DispatcherServiceImpl {
49+
func newDispatcherService(repo v1.Repository, mq msgqueue.MessageQueue, pubsub msgqueue.PubSub, v validator.Validator, l *zerolog.Logger, dispatcherId uuid.UUID, a analytics.Analytics, alerter *hatcheterrors.Wrapped, promGate *prometheus.Gate) *DispatcherServiceImpl {
4850
pubBuffer := msgqueue.NewMQPubBuffer(mq)
4951
tw := trigger.NewTriggerWriter(mq, pubsub, repo, l, pubBuffer, 0, promGate)
5052

@@ -57,6 +59,7 @@ func newDispatcherService(repo v1.Repository, mq msgqueue.MessageQueue, pubsub m
5759
pubBuffer: pubBuffer,
5860
dispatcherId: dispatcherId,
5961
analytics: a,
62+
alerter: alerter,
6063

6164
streamSessions: streams.NewRegistry(),
6265
}

‎internal/services/dispatcher/server_v1.go‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/hatchet-dev/hatchet/internal/msgqueue"
2121
contracts "github.com/hatchet-dev/hatchet/internal/services/shared/proto/v1"
22+
"github.com/hatchet-dev/hatchet/internal/services/shared/recoveryutils"
2223
"github.com/hatchet-dev/hatchet/internal/services/shared/rpcstream"
2324
tasktypes "github.com/hatchet-dev/hatchet/internal/services/shared/tasktypes/v1"
2425
"github.com/hatchet-dev/hatchet/pkg/analytics"
@@ -885,7 +886,15 @@ func (d *DispatcherServiceImpl) handleDurableTaskRequest(
885886
ctx context.Context,
886887
invocation *durableTaskInvocation,
887888
req *contracts.DurableTaskRequest,
888-
) error {
889+
) (err error) {
890+
// recover per request, not per goroutine, so a panic doesn't stop the worker-status runner
891+
defer func() {
892+
if r := recover(); r != nil {
893+
_ = recoveryutils.RecoverWithAlert(d.l, d.alerter, r)
894+
err = fmt.Errorf("recovered from panic handling %T: %v", req.GetMessage(), r)
895+
}
896+
}()
897+
889898
ctx, span := telemetry.NewRootSpan(ctx, "dispatcher.handle-durable-task-request")
890899
defer span.End()
891900

@@ -1270,7 +1279,7 @@ func (d *DispatcherServiceImpl) handleWaitFor(
12701279

12711280
if req.WaitForConditions != nil {
12721281
for _, condition := range req.WaitForConditions.SleepConditions {
1273-
orGroupId, parseErr := uuid.Parse(condition.Base.OrGroupId)
1282+
orGroupId, parseErr := uuid.Parse(condition.GetBase().GetOrGroupId())
12741283
if parseErr != nil {
12751284
return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("or group id is not a valid uuid: %v", parseErr))
12761285
}
@@ -1283,7 +1292,7 @@ func (d *DispatcherServiceImpl) handleWaitFor(
12831292
}
12841293

12851294
for _, condition := range req.WaitForConditions.UserEventConditions {
1286-
orGroupId, parseErr := uuid.Parse(condition.Base.OrGroupId)
1295+
orGroupId, parseErr := uuid.Parse(condition.GetBase().GetOrGroupId())
12871296
if parseErr != nil {
12881297
return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("or group id is not a valid uuid: %v", parseErr))
12891298
}

‎internal/services/dispatcher/server_v1_durable_task_test.go‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import (
77
"testing"
88
"time"
99

10+
"connectrpc.com/connect"
1011
"github.com/google/uuid"
1112
"github.com/rs/zerolog"
13+
"github.com/stretchr/testify/require"
1214

1315
contracts "github.com/hatchet-dev/hatchet/internal/services/shared/proto/v1"
1416
"github.com/hatchet-dev/hatchet/internal/services/shared/rpcstream"
1517
"github.com/hatchet-dev/hatchet/internal/services/shared/streams"
1618
"github.com/hatchet-dev/hatchet/pkg/analytics"
19+
v1 "github.com/hatchet-dev/hatchet/pkg/repository"
1720
"github.com/hatchet-dev/hatchet/pkg/repository/sqlcv1"
1821
)
1922

@@ -201,3 +204,81 @@ func TestDurableTaskReturnsNilOnEOF(t *testing.T) {
201204
t.Fatal("DurableTask did not return after EOF")
202205
}
203206
}
207+
208+
type fakeFoundTask struct {
209+
v1.Repository
210+
v1.TaskRepository
211+
}
212+
213+
func (f fakeFoundTask) Tasks() v1.TaskRepository { return f }
214+
func (fakeFoundTask) GetTaskByExternalId(context.Context, uuid.UUID, uuid.UUID, bool) (*sqlcv1.FlattenExternalIdsRow, error) {
215+
return &sqlcv1.FlattenExternalIdsRow{}, nil
216+
}
217+
218+
func TestHandleWaitForNilConditionBaseReturnsInvalidArgument(t *testing.T) {
219+
d := newTestDispatcher()
220+
d.repo = fakeFoundTask{}
221+
222+
for _, c := range []*contracts.DurableEventListenerConditions{
223+
{SleepConditions: []*contracts.SleepMatchCondition{{}}},
224+
{UserEventConditions: []*contracts.UserEventMatchCondition{{}}},
225+
} {
226+
req := &contracts.DurableTaskWaitForRequest{DurableTaskExternalId: uuid.NewString(), WaitForConditions: c}
227+
err := d.handleWaitFor(context.Background(), &durableTaskInvocation{}, req)
228+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err), err)
229+
}
230+
}
231+
232+
type panickingRepo struct {
233+
v1.Repository
234+
calls chan struct{}
235+
}
236+
237+
func (r panickingRepo) Tasks() v1.TaskRepository {
238+
r.calls <- struct{}{}
239+
panic("boom")
240+
}
241+
242+
func TestDurableTaskRecoversFromHandlerPanics(t *testing.T) {
243+
d := newTestDispatcher()
244+
calls := make(chan struct{}, 3)
245+
d.repo = panickingRepo{calls: calls}
246+
247+
taskId := uuid.NewString()
248+
memo := &contracts.DurableTaskRequest{Message: &contracts.DurableTaskRequest_Memo{
249+
Memo: &contracts.DurableTaskMemoRequest{DurableTaskExternalId: taskId},
250+
}}
251+
status := &contracts.DurableTaskRequest{Message: &contracts.DurableTaskRequest_WorkerStatus{
252+
WorkerStatus: &contracts.DurableTaskWorkerStatusRequest{
253+
WaitingEntries: []*contracts.DurableTaskAwaitedCompletedEntry{{DurableTaskExternalId: taskId}},
254+
},
255+
}}
256+
257+
reqs := []*contracts.DurableTaskRequest{memo, status, status}
258+
server := &fakeDurableTaskServer{
259+
ctx: tenantContext(),
260+
recv: func() (*contracts.DurableTaskRequest, error) {
261+
// wait for the previous request's handler to panic, so the second status needs the
262+
// worker-status runner to have survived the first
263+
if len(reqs) < 3 {
264+
<-calls
265+
}
266+
267+
if len(reqs) == 0 {
268+
return nil, io.EOF
269+
}
270+
271+
req := reqs[0]
272+
reqs = reqs[1:]
273+
274+
return req, nil
275+
},
276+
}
277+
278+
select {
279+
case err := <-runDurableTask(d, server):
280+
require.NoError(t, err)
281+
case <-time.After(5 * time.Second):
282+
t.Fatal("a request after a panicking one was never handled")
283+
}
284+
}

0 commit comments

Comments
 (0)