-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for activity retry policies (#83)
Adds the ActivityRetryPolicy struct and updates CallActivity to execute a different code path if retries are needed. Signed-off-by: Fabian Martinez <[email protected]>
- Loading branch information
Showing
9 changed files
with
436 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"log" | ||
"math/rand" | ||
"time" | ||
|
||
"github.com/microsoft/durabletask-go/backend" | ||
"github.com/microsoft/durabletask-go/backend/sqlite" | ||
"github.com/microsoft/durabletask-go/task" | ||
) | ||
|
||
func main() { | ||
// Create a new task registry and add the orchestrator and activities | ||
r := task.NewTaskRegistry() | ||
r.AddOrchestrator(RetryActivityOrchestrator) | ||
r.AddActivity(RandomFailActivity) | ||
|
||
// Init the client | ||
ctx := context.Background() | ||
client, worker, err := Init(ctx, r) | ||
if err != nil { | ||
log.Fatalf("Failed to initialize the client: %v", err) | ||
} | ||
defer worker.Shutdown(ctx) | ||
|
||
// Start a new orchestration | ||
id, err := client.ScheduleNewOrchestration(ctx, RetryActivityOrchestrator) | ||
if err != nil { | ||
log.Fatalf("Failed to schedule new orchestration: %v", err) | ||
} | ||
|
||
// Wait for the orchestration to complete | ||
metadata, err := client.WaitForOrchestrationCompletion(ctx, id) | ||
if err != nil { | ||
log.Fatalf("Failed to wait for orchestration to complete: %v", err) | ||
} | ||
|
||
// Print the results | ||
metadataEnc, err := json.MarshalIndent(metadata, "", " ") | ||
if err != nil { | ||
log.Fatalf("Failed to encode result to JSON: %v", err) | ||
} | ||
log.Printf("Orchestration completed: %v", string(metadataEnc)) | ||
} | ||
|
||
// Init creates and initializes an in-memory client and worker pair with default configuration. | ||
func Init(ctx context.Context, r *task.TaskRegistry) (backend.TaskHubClient, backend.TaskHubWorker, error) { | ||
logger := backend.DefaultLogger() | ||
|
||
// Create an executor | ||
executor := task.NewTaskExecutor(r) | ||
|
||
// Create a new backend | ||
// Use the in-memory sqlite provider by specifying "" | ||
be := sqlite.NewSqliteBackend(sqlite.NewSqliteOptions(""), logger) | ||
orchestrationWorker := backend.NewOrchestrationWorker(be, executor, logger) | ||
activityWorker := backend.NewActivityTaskWorker(be, executor, logger) | ||
taskHubWorker := backend.NewTaskHubWorker(be, orchestrationWorker, activityWorker, logger) | ||
|
||
// Start the worker | ||
err := taskHubWorker.Start(ctx) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
// Get the client to the backend | ||
taskHubClient := backend.NewTaskHubClient(be) | ||
|
||
return taskHubClient, taskHubWorker, nil | ||
} | ||
|
||
func RetryActivityOrchestrator(ctx *task.OrchestrationContext) (any, error) { | ||
if err := ctx.CallActivity(RandomFailActivity, task.WithRetryPolicy(&task.ActivityRetryPolicy{ | ||
MaxAttempts: 10, | ||
InitialRetryInterval: 100 * time.Millisecond, | ||
BackoffCoefficient: 2, | ||
MaxRetryInterval: 3 * time.Second, | ||
})).Await(nil); err != nil { | ||
return nil, err | ||
} | ||
return nil, nil | ||
} | ||
|
||
func RandomFailActivity(ctx task.ActivityContext) (any, error) { | ||
// 70% possibility for activity failure | ||
if rand.Intn(100) <= 70 { | ||
log.Println("random activity failure") | ||
return "", errors.New("random activity failure") | ||
} | ||
return "ok", nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package task | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
) | ||
|
||
func Test_computeNextDelay(t *testing.T) { | ||
time1 := time.Now() | ||
time2 := time.Now().Add(1 * time.Minute) | ||
type args struct { | ||
currentTimeUtc time.Time | ||
policy ActivityRetryPolicy | ||
attempt int | ||
firstAttempt time.Time | ||
err error | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want time.Duration | ||
}{ | ||
{ | ||
name: "first attempt", | ||
args: args{ | ||
currentTimeUtc: time2, | ||
policy: ActivityRetryPolicy{ | ||
MaxAttempts: 3, | ||
InitialRetryInterval: 2 * time.Second, | ||
BackoffCoefficient: 2, | ||
MaxRetryInterval: 10 * time.Second, | ||
Handle: func(err error) bool { return true }, | ||
RetryTimeout: 2 * time.Minute, | ||
}, | ||
attempt: 0, | ||
firstAttempt: time1, | ||
}, | ||
want: 2 * time.Second, | ||
}, | ||
{ | ||
name: "second attempt", | ||
args: args{ | ||
currentTimeUtc: time2, | ||
policy: ActivityRetryPolicy{ | ||
MaxAttempts: 3, | ||
InitialRetryInterval: 2 * time.Second, | ||
BackoffCoefficient: 2, | ||
MaxRetryInterval: 10 * time.Second, | ||
Handle: func(err error) bool { return true }, | ||
RetryTimeout: 2 * time.Minute, | ||
}, | ||
attempt: 1, | ||
firstAttempt: time1, | ||
}, | ||
want: 4 * time.Second, | ||
}, | ||
{ | ||
name: "third attempt", | ||
args: args{ | ||
currentTimeUtc: time2, | ||
policy: ActivityRetryPolicy{ | ||
MaxAttempts: 3, | ||
InitialRetryInterval: 2 * time.Second, | ||
BackoffCoefficient: 2, | ||
MaxRetryInterval: 10 * time.Second, | ||
Handle: func(err error) bool { return true }, | ||
RetryTimeout: 2 * time.Minute, | ||
}, | ||
attempt: 2, | ||
firstAttempt: time1, | ||
}, | ||
want: 8 * time.Second, | ||
}, | ||
{ | ||
name: "fourth attempt", | ||
args: args{ | ||
currentTimeUtc: time2, | ||
policy: ActivityRetryPolicy{ | ||
MaxAttempts: 3, | ||
InitialRetryInterval: 2 * time.Second, | ||
BackoffCoefficient: 2, | ||
MaxRetryInterval: 10 * time.Second, | ||
Handle: func(err error) bool { return true }, | ||
RetryTimeout: 2 * time.Minute, | ||
}, | ||
attempt: 3, | ||
firstAttempt: time1, | ||
}, | ||
want: 10 * time.Second, | ||
}, | ||
{ | ||
name: "expired", | ||
args: args{ | ||
currentTimeUtc: time2, | ||
policy: ActivityRetryPolicy{ | ||
MaxAttempts: 3, | ||
InitialRetryInterval: 2 * time.Second, | ||
BackoffCoefficient: 2, | ||
MaxRetryInterval: 10 * time.Second, | ||
Handle: func(err error) bool { return true }, | ||
RetryTimeout: 30 * time.Second, | ||
}, | ||
attempt: 3, | ||
firstAttempt: time1, | ||
}, | ||
want: 0, | ||
}, | ||
{ | ||
name: "fourth attempt backoff 1", | ||
args: args{ | ||
currentTimeUtc: time2, | ||
policy: ActivityRetryPolicy{ | ||
MaxAttempts: 3, | ||
InitialRetryInterval: 2 * time.Second, | ||
BackoffCoefficient: 1, | ||
MaxRetryInterval: 10 * time.Second, | ||
Handle: func(err error) bool { return true }, | ||
RetryTimeout: 2 * time.Minute, | ||
}, | ||
attempt: 3, | ||
firstAttempt: time1, | ||
}, | ||
want: 2 * time.Second, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := computeNextDelay(tt.args.currentTimeUtc, tt.args.policy, tt.args.attempt, tt.args.firstAttempt, tt.args.err); got != tt.want { | ||
t.Errorf("computeNextDelay() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.