Skip to content

Commit 355b240

Browse files
committed
Do not update data source or task if task claim was lost
1 parent 889fc23 commit 355b240

6 files changed

Lines changed: 115 additions & 24 deletions

File tree

auth/service/service/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ func (c *Client) CreateProviderSession(ctx context.Context, create *auth.Provide
8888

8989
if err = prvdr.OnCreate(ctx, providerSession); err != nil {
9090
log.LoggerFromContext(ctx).WithError(err).Error("Unable to finalize creation of provider session")
91-
if err := c.deleteProviderSession(ctx, repository, providerSession); err != nil {
92-
log.LoggerFromContext(ctx).WithError(err).Warn("Unable to delete provider session")
91+
if deleteErr := c.deleteProviderSession(ctx, repository, providerSession); deleteErr != nil {
92+
log.LoggerFromContext(ctx).WithError(deleteErr).Warn("Unable to delete provider session")
9393
}
9494
return nil, err
9595
}

dexcom/fetch/runner.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,17 @@ func (t *TaskRunner) Run(ctx context.Context) {
180180
t.task.AppendError(err)
181181
}
182182

183-
// A permanently failed task is not rescheduled, unless its outcome could not be recorded on the data source, in
184-
// which case run again so a later run can record it
185-
err := t.updateDataSourceWithTaskState()
186-
if err != nil {
187-
t.task.AppendError(err)
188-
}
189-
if err != nil || !t.task.IsFailed() {
190-
t.task.RepeatAvailableAfter(pointer.Default(t.availableAfter, availableAfterDuration()))
183+
// If we didn't lose the claim, then update data source and repeat if not failed
184+
if !errors.Is(context.Cause(t.context), task.ErrClaimLost) {
185+
err := t.updateDataSourceWithTaskState()
186+
if err != nil {
187+
t.task.AppendError(err)
188+
}
189+
if err != nil || !t.task.IsFailed() {
190+
t.task.RepeatAvailableAfter(pointer.Default(t.availableAfter, availableAfterDuration()))
191+
}
192+
} else {
193+
t.logger.Warn("Skipped updating data source and task because the task claim was lost")
191194
}
192195
}
193196

dexcom/fetch/runner_test.go

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,16 @@ var _ = Describe("Runner", func() {
187187

188188
Context("with task runner and context", func() {
189189
var taskRunner *dexcomFetch.TaskRunner
190+
var logger *logTest.Logger
190191
var ctx context.Context
191192

192193
BeforeEach(func() {
193194
var err error
194195
taskRunner, err = dexcomFetch.NewTaskRunner(provider, tsk)
195196
Expect(err).ToNot(HaveOccurred())
196197
Expect(taskRunner).ToNot(BeNil())
197-
ctx = log.NewContextWithLogger(context.Background(), logTest.NewLogger())
198+
logger = logTest.NewLogger()
199+
ctx = log.NewContextWithLogger(context.Background(), logger)
198200
})
199201

200202
assertTaskState := func(state string) {
@@ -364,6 +366,21 @@ var _ = Describe("Runner", func() {
364366
assertTaskAndDataSourceError(dexcomFetch.ErrorCodeResourceFailure, "unable to get provider session")
365367
})
366368

369+
It("discards the run outcome if the task claim is lost", func() {
370+
claimContext, claimCancel := context.WithCancelCause(ctx)
371+
defer claimCancel(nil)
372+
testErr := errorsTest.RandomError()
373+
authClient.EXPECT().GetProviderSession(matchContext(), "test-provider-session-id").DoAndReturn(func(ctx context.Context, id string) (*auth.ProviderSession, error) {
374+
claimCancel(task.ErrClaimLost)
375+
return nil, testErr
376+
})
377+
taskRunner.Run(claimContext)
378+
assertTaskState(task.TaskStateRunning)
379+
Expect(dataSrc.State).To(Equal(dataSource.StateConnected))
380+
Expect(dataSrc.HasError()).To(BeFalse())
381+
logger.AssertWarn("Skipped updating data source and task because the task claim was lost")
382+
})
383+
367384
It("fails if the provider session is missing", func() {
368385
authClient.EXPECT().GetProviderSession(matchContext(), "test-provider-session-id").Return(nil, nil)
369386
dataSourceClient.EXPECT().Update(matchContext(), "test-data-source-id", matchNil(), matchNotNil()).DoAndReturn(mockDataSourceClientUpdate(dataSrc))
@@ -643,7 +660,78 @@ var _ = Describe("Runner", func() {
643660
// deviceHashes - not in data
644661
// dataSource.LatestDataTime - not nil (recent)
645662
// refresh token
646-
// data ranges multiple 30 day segments
663+
})
664+
665+
Context("with provider session and a data range spanning multiple chunks", func() {
666+
var providerSession *auth.ProviderSession
667+
var firstChunkStartTime time.Time
668+
var firstChunkEndTime time.Time
669+
var secondChunkEndTime time.Time
670+
671+
BeforeEach(func() {
672+
providerSession = &auth.ProviderSession{
673+
ID: "test-provider-session-id",
674+
UserID: "test-user-id",
675+
OAuthToken: &auth.OAuthToken{
676+
AccessToken: "test-access-token-1",
677+
TokenType: "Bearer",
678+
RefreshToken: "test-refresh-token-1",
679+
ExpirationTime: time.Now().Add(time.Minute),
680+
},
681+
}
682+
authClient.EXPECT().GetProviderSession(matchContext(), "test-provider-session-id").Return(providerSession, nil)
683+
authClient.EXPECT().UpdateProviderSession(matchContext(), "test-provider-session-id", matchNotNil()).DoAndReturn(mockAuthClientUpdateProviderSession(providerSession)).AnyTimes()
684+
firstChunkStartTime = time.Now().Add(-45 * Day)
685+
firstChunkEndTime = firstChunkStartTime.AddDate(0, 0, dexcomFetch.DataRangeDaysMaximum)
686+
secondChunkEndTime = time.Now().Add(-3 * Day)
687+
dataRangeResponse := &dexcom.DataRangesResponse{
688+
Calibrations: &dexcom.DataRange{
689+
Start: &dexcom.Moment{SystemTime: &dexcom.Time{Time: firstChunkStartTime}},
690+
End: &dexcom.Moment{SystemTime: &dexcom.Time{Time: secondChunkEndTime}},
691+
},
692+
}
693+
dexcomClient.EXPECT().GetDataRange(matchContext(), nil, matchNotNil()).DoAndReturn(mockDexcomClientGetDataRange(nil, dataRangeResponse, nil))
694+
})
695+
696+
// Expects the fetch of a single chunk, all responses empty, invoking onEvents, if any, during the
697+
// final fetch of the chunk
698+
expectFetchChunk := func(startTime time.Time, endTime time.Time, onEvents func()) {
699+
dexcomClient.EXPECT().GetAlerts(matchContext(), startTime, endTime, matchNotNil()).DoAndReturn(mockDexcomClientGetData(nil, &dexcom.AlertsResponse{Records: &dexcom.Alerts{}}, nil))
700+
dexcomClient.EXPECT().GetCalibrations(matchContext(), startTime, endTime, matchNotNil()).DoAndReturn(mockDexcomClientGetData(nil, &dexcom.CalibrationsResponse{Records: &dexcom.Calibrations{}}, nil))
701+
dexcomClient.EXPECT().GetDevices(matchContext(), startTime, endTime, matchNotNil()).DoAndReturn(mockDexcomClientGetData(nil, &dexcom.DevicesResponse{Records: &dexcom.Devices{}}, nil))
702+
dexcomClient.EXPECT().GetEGVs(matchContext(), startTime, endTime, matchNotNil()).DoAndReturn(mockDexcomClientGetData(nil, &dexcom.EGVsResponse{Records: &dexcom.EGVs{}}, nil))
703+
dexcomClient.EXPECT().GetEvents(matchContext(), startTime, endTime, matchNotNil()).DoAndReturn(func(ctx context.Context, startTime time.Time, endTime time.Time, tokenSource oauth.TokenSource) (*dexcom.EventsResponse, error) {
704+
if onEvents != nil {
705+
onEvents()
706+
}
707+
return &dexcom.EventsResponse{Records: &dexcom.Events{}}, nil
708+
})
709+
}
710+
711+
It("fetches every chunk of the data range", func() {
712+
expectFetchChunk(firstChunkStartTime, firstChunkEndTime, nil)
713+
expectFetchChunk(firstChunkEndTime, secondChunkEndTime, nil)
714+
dataSourceClient.EXPECT().Update(matchContext(), "test-data-source-id", matchNil(), matchNotNil()).DoAndReturn(mockDataSourceClientUpdate(dataSrc))
715+
taskRunner.Run(ctx)
716+
assertTaskAndDataSourceState(task.TaskStatePending)
717+
assertTaskAvailableAfterStandardDuration()
718+
assertTaskRetryCountNotPresent()
719+
assertTaskAndDataSourceErrorNotPresent()
720+
assertDataSourceLastImportTimePresent()
721+
})
722+
723+
It("discards the run outcome if the task claim is lost mid-fetch", func() {
724+
claimContext, claimCancel := context.WithCancelCause(ctx)
725+
defer claimCancel(nil)
726+
expectFetchChunk(firstChunkStartTime, firstChunkEndTime, func() { claimCancel(task.ErrClaimLost) })
727+
// The canceled context fails the next chunk, ending the run
728+
dexcomClient.EXPECT().GetAlerts(matchContext(), firstChunkEndTime, secondChunkEndTime, matchNotNil()).DoAndReturn(mockDexcomClientGetData[dexcom.AlertsResponse](nil, nil, context.Canceled))
729+
taskRunner.Run(claimContext)
730+
assertTaskState(task.TaskStateRunning)
731+
Expect(dataSrc.State).To(Equal(dataSource.StateConnected))
732+
Expect(dataSrc.HasError()).To(BeFalse())
733+
logger.AssertWarn("Skipped updating data source and task because the task claim was lost")
734+
})
647735
})
648736
})
649737
})

task/queue/queue.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const (
3434
DispatchTasksDelayDefault = 1 * time.Minute
3535

3636
// MonitorTaskDelayDefault is the default interval between checks that each in-flight task's claim is still held
37-
// (the task exists and its claim token is unchanged); a run whose claim is lost is canceled with ErrClaimLost.
37+
// (the task exists and its claim token is unchanged); a run whose claim is lost is canceled with task.ErrClaimLost.
3838
MonitorTaskDelayDefault = 1 * time.Minute
3939

4040
// RunnerWatchdogGracePeriodDefault is the extra time beyond the runner timeout that the watchdog waits before
@@ -363,7 +363,7 @@ func (q *Queue) runTask(ctx context.Context, tsk *task.Task) {
363363
return
364364
}
365365

366-
// The claim context is canceled with ErrClaimLost by the task claim monitor when the task is deleted or re-claimed
366+
// The claim context is canceled with task.ErrClaimLost by the task claim monitor when the task is deleted or re-claimed
367367
// mid-run. Claim loss is irreversible for this run (every claim gets a fresh token), so once canceled the outcome
368368
// can never be persisted.
369369
claimContext, claimCancel := context.WithCancelCause(ctx)
@@ -372,7 +372,7 @@ func (q *Queue) runTask(ctx context.Context, tsk *task.Task) {
372372
// Clearing the claim token marks the outcome as unpersistable, which completeTask discards. Done in a defer, after
373373
// the recover below, so a panicking runner is also reconciled correctly.
374374
defer func() {
375-
if errors.Is(context.Cause(claimContext), ErrClaimLost) {
375+
if errors.Is(context.Cause(claimContext), task.ErrClaimLost) {
376376
tsk.ClaimToken = nil
377377
}
378378
}()
@@ -408,7 +408,7 @@ func (q *Queue) runTask(ctx context.Context, tsk *task.Task) {
408408
if reason := q.monitorTaskForLostClaim(claimContext, id, claimToken); reason != nil {
409409
log.LoggerFromContext(claimContext).Warnf("Task %s; canceling task run", *reason)
410410
RunClaimLostTotal.WithLabelValues(typ, *reason).Inc()
411-
claimCancel(ErrClaimLost)
411+
claimCancel(task.ErrClaimLost)
412412
}
413413
}(tsk.ID, tsk.Type, *tsk.ClaimToken)
414414

@@ -427,7 +427,7 @@ func (q *Queue) runTask(ctx context.Context, tsk *task.Task) {
427427

428428
// The claim was lost mid-run; any write-back would miss the claim token, so skip state reconciliation. The outcome
429429
// is discarded during completion.
430-
if errors.Is(context.Cause(claimContext), ErrClaimLost) {
430+
if errors.Is(context.Cause(claimContext), task.ErrClaimLost) {
431431
return
432432
}
433433

task/queue/runner.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ import (
4343
// Context: canceled (with cause) after GetRunnerTimeout, on shutdown, and on claim loss - cooperative runners select on
4444
// ctx.Done() and return promptly. To tell them apart, inspect context.Cause(ctx): a normal shutdown with
4545
// context.Canceled; a runner timeout with ErrRunnerTimeoutExceeded, and a lost claim (the task was deleted mid-run, or
46-
// unstuck and its claim token replaced) with ErrClaimLost (use errors.Is in all cases). After a claim loss the run's
47-
// outcome is discarded - no write-back can land - so return promptly and skip any remaining work. Claim loss is
46+
// unstuck and its claim token replaced) with task.ErrClaimLost (use errors.Is in all cases). After a claim loss the
47+
// run's outcome is discarded - no write-back can land - so return promptly and skip any remaining work. Claim loss is
4848
// detected by a periodic check (MonitorTaskDelay, 1 minute by default), not instantly. The queue cannot preempt a
4949
// runner that ignores cancellation; it stays blocked until it returns or the process restarts, recovered by the
5050
// deadline/unstick mechanism (a periodic sweep that resets tasks still running past their deadline - see
@@ -109,8 +109,3 @@ type Runner interface {
109109
// runner distinguishes a timeout from a shutdown with errors.Is(context.Cause(ctx), ErrRunnerTimeoutExceeded); a
110110
// shutdown instead cancels with context.Canceled.
111111
var ErrRunnerTimeoutExceeded = errors.New("task runner timeout exceeded")
112-
113-
// ErrClaimLost is the cancellation cause set on the Run context when the run's claim on the task was lost mid-run: the
114-
// task was deleted, or it was unstuck and possibly re-claimed (its claim token no longer matches). A runner
115-
// distinguishes it with errors.Is(context.Cause(ctx), ErrClaimLost).
116-
var ErrClaimLost = errors.New("task claim lost")

task/task.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,8 @@ func (t Tasks) Sanitize(details request.AuthDetails) error {
348348
}
349349
return nil
350350
}
351+
352+
// ErrClaimLost is the cancellation cause set on the Run context when the run's claim on the task was lost mid-run: the
353+
// task was deleted, or it was unstuck and possibly re-claimed (its claim token no longer matches). A runner
354+
// distinguishes it with errors.Is(context.Cause(ctx), ErrClaimLost).
355+
var ErrClaimLost = errors.New("task claim lost")

0 commit comments

Comments
 (0)