Skip to content

Commit a1e5ef0

Browse files
Add sample for scheduling over multiple executors (#112)
1 parent 5caea6a commit a1e5ef0

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"time"
9+
10+
"github.com/microsoft/durabletask-go/api"
11+
"github.com/microsoft/durabletask-go/backend"
12+
"github.com/microsoft/durabletask-go/backend/sqlite"
13+
"github.com/microsoft/durabletask-go/client"
14+
"github.com/microsoft/durabletask-go/task"
15+
"google.golang.org/grpc"
16+
"google.golang.org/grpc/credentials/insecure"
17+
)
18+
19+
const (
20+
doubleActivityName = "double"
21+
tripleActivityName = "triple"
22+
orchestratorName = "sixTimes"
23+
24+
localExecutorName = "local"
25+
grpcExecutorName = "grpc"
26+
)
27+
28+
// Config defines the routing configuration for tasks.
29+
// It maps task names to executor names and specifies a default executor.
30+
type Config struct {
31+
Routes map[string]string
32+
DefaultExecutor string
33+
}
34+
35+
// Resolve returns the executor name for a given task name.
36+
// If no specific route is found, it returns the default executor name.
37+
func (c *Config) Resolve(taskName string) string {
38+
executorName, ok := c.Routes[taskName]
39+
if !ok {
40+
executorName = c.DefaultExecutor
41+
}
42+
return executorName
43+
}
44+
45+
// RoutingExecutor is a backend.Executor implementation that Routes tasks to different Executors
46+
// based on a configuration. This allows for heterogeneous execution environments where
47+
// different tasks (orchestrators or activities) can be executed by different Executors
48+
// (e.g., local vs gRPC).
49+
type RoutingExecutor struct {
50+
Config Config
51+
Executors map[string]backend.Executor
52+
}
53+
54+
// getExecutor retrieves the appropriate backend.Executor for a given task name
55+
// based on the routing configuration.
56+
func (e *RoutingExecutor) getExecutor(taskName string) (backend.Executor, error) {
57+
executorName := e.Config.Resolve(taskName)
58+
executor, ok := e.Executors[executorName]
59+
if !ok {
60+
return nil, fmt.Errorf("executor %s for task %s not found", executorName, taskName)
61+
}
62+
return executor, nil
63+
}
64+
65+
// getOrchestratorName extracts the orchestrator name from the provided history events.
66+
// It iterates through both old and new events to find the ExecutionStarted event,
67+
// which contains the orchestrator's name. This is crucial for routing orchestrations
68+
// to the correct executor based on their name.
69+
func (e *RoutingExecutor) getOrchestratorName(oldEvents, newEvents []*backend.HistoryEvent) string {
70+
71+
for _, event := range oldEvents {
72+
if x := event.GetExecutionStarted(); x != nil {
73+
return x.Name
74+
}
75+
}
76+
for _, event := range newEvents {
77+
if x := event.GetExecutionStarted(); x != nil {
78+
return x.Name
79+
}
80+
}
81+
return ""
82+
}
83+
84+
// ExecuteOrchestrator Routes the orchestration execution request to the appropriate executor.
85+
// It determines the orchestrator name from history events and uses it to Resolve the executor.
86+
func (e *RoutingExecutor) ExecuteOrchestrator(ctx context.Context, id api.InstanceID, oldEvents []*backend.HistoryEvent, newEvents []*backend.HistoryEvent) (*backend.ExecutionResults, error) {
87+
name := e.getOrchestratorName(oldEvents, newEvents)
88+
89+
executor, err := e.getExecutor(name)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
return executor.ExecuteOrchestrator(ctx, id, oldEvents, newEvents)
95+
}
96+
97+
// ExecuteActivity Routes the activity execution request to the appropriate executor.
98+
// It extracts the activity name from the task scheduled event and uses it to Resolve the executor.
99+
func (e *RoutingExecutor) ExecuteActivity(ctx context.Context, id api.InstanceID, event *backend.HistoryEvent) (*backend.HistoryEvent, error) {
100+
name := event.GetTaskScheduled().GetName()
101+
executor, err := e.getExecutor(name)
102+
if err != nil {
103+
return nil, err
104+
}
105+
return executor.ExecuteActivity(ctx, id, event)
106+
}
107+
108+
// Shutdown shuts down all registered Executors in parallel.
109+
// It returns a joined error if any of the Executors fail to shut down.
110+
func (e *RoutingExecutor) Shutdown(ctx context.Context) error {
111+
var errs []error
112+
for _, executor := range e.Executors {
113+
err := executor.Shutdown(ctx)
114+
if err != nil {
115+
errs = append(errs, err)
116+
}
117+
}
118+
119+
if len(errs) == 0 {
120+
return nil
121+
}
122+
123+
return errors.Join(errs...)
124+
}
125+
126+
func main() {
127+
128+
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
129+
defer cancelFunc()
130+
131+
logger := backend.DefaultLogger()
132+
be := sqlite.NewSqliteBackend(sqlite.NewSqliteOptions(""), logger)
133+
134+
executor, err := initExecutor(ctx, be, logger)
135+
if err != nil {
136+
panic(err)
137+
}
138+
139+
workflowWorker := backend.NewOrchestrationWorker(be, executor, logger)
140+
activityWorker := backend.NewActivityTaskWorker(be, executor, logger)
141+
taskHubWorker := backend.NewTaskHubWorker(be, workflowWorker, activityWorker, logger)
142+
143+
if err := taskHubWorker.Start(ctx); err != nil {
144+
panic(err)
145+
}
146+
defer taskHubWorker.Shutdown(ctx)
147+
148+
taskHubClient := backend.NewTaskHubClient(be)
149+
id, err := taskHubClient.ScheduleNewOrchestration(ctx, orchestratorName, api.WithInput(1))
150+
if err != nil {
151+
panic(err)
152+
}
153+
metadata, err := taskHubClient.WaitForOrchestrationCompletion(ctx, id)
154+
if err != nil {
155+
panic(err)
156+
}
157+
fmt.Println(metadata.SerializedOutput)
158+
}
159+
160+
func initExecutor(ctx context.Context, be backend.Backend, logger backend.Logger) (backend.Executor, error) {
161+
localExecutor, err := setupLocalExecutor()
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
grpcExecutor, err := setupGrpcExecutor(ctx, be, logger)
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
return &RoutingExecutor{
172+
Config: Config{
173+
Routes: map[string]string{
174+
doubleActivityName: localExecutorName,
175+
tripleActivityName: localExecutorName,
176+
},
177+
DefaultExecutor: grpcExecutorName,
178+
},
179+
Executors: map[string]backend.Executor{
180+
localExecutorName: localExecutor,
181+
grpcExecutorName: grpcExecutor,
182+
},
183+
}, nil
184+
}
185+
186+
func setupLocalExecutor() (backend.Executor, error) {
187+
timesActivity := func(times int) task.Activity {
188+
return func(ctx task.ActivityContext) (any, error) {
189+
var input int
190+
if err := ctx.GetInput(&input); err != nil {
191+
return nil, err
192+
}
193+
return input * times, nil
194+
}
195+
}
196+
doubleActivity := timesActivity(2)
197+
tripleActivity := timesActivity(3)
198+
199+
r := task.NewTaskRegistry()
200+
if err := r.AddActivityN(doubleActivityName, doubleActivity); err != nil {
201+
return nil, err
202+
}
203+
204+
if err := r.AddActivityN(tripleActivityName, tripleActivity); err != nil {
205+
return nil, err
206+
}
207+
208+
return task.NewTaskExecutor(r), nil
209+
}
210+
211+
func setupGrpcExecutor(ctx context.Context, be backend.Backend, logger backend.Logger) (backend.Executor, error) {
212+
address := "localhost:0"
213+
grpcServer := grpc.NewServer()
214+
executor, registerFn := backend.NewGrpcExecutor(be, logger)
215+
registerFn(grpcServer)
216+
217+
lis, err := net.Listen("tcp", address)
218+
if err != nil {
219+
return nil, err
220+
}
221+
go func() {
222+
if err := grpcServer.Serve(lis); err != nil {
223+
panic(err)
224+
}
225+
}()
226+
227+
go func() {
228+
<-ctx.Done()
229+
grpcServer.GracefulStop()
230+
_ = lis.Close()
231+
}()
232+
233+
// Create a worker that connects to the gRPC server.
234+
// establish a gRPC connection, blocking until the server is ready or the timeout expires
235+
conn, err := grpc.DialContext(
236+
ctx,
237+
lis.Addr().String(),
238+
grpc.WithTransportCredentials(insecure.NewCredentials()),
239+
grpc.WithBlock(),
240+
)
241+
if err != nil {
242+
return nil, err
243+
}
244+
245+
go func() {
246+
<-ctx.Done()
247+
_ = conn.Close()
248+
}()
249+
250+
orchestrator := func(ctx *task.OrchestrationContext) (any, error) {
251+
var input int
252+
if err := ctx.GetInput(&input); err != nil {
253+
return nil, err
254+
}
255+
256+
var intermediateResult int
257+
if err := ctx.CallActivity(doubleActivityName, task.WithActivityInput(input)).Await(&intermediateResult); err != nil {
258+
return nil, err
259+
}
260+
261+
var finalResult int
262+
if err := ctx.CallActivity(tripleActivityName, task.WithActivityInput(intermediateResult)).Await(&finalResult); err != nil {
263+
return nil, err
264+
}
265+
return finalResult, nil
266+
}
267+
268+
workerClient := client.NewTaskHubGrpcClient(conn, logger)
269+
r := task.NewTaskRegistry()
270+
if err := r.AddOrchestratorN(orchestratorName, orchestrator); err != nil {
271+
return nil, err
272+
}
273+
274+
// StartWorkItemListener is not blocking
275+
if err := workerClient.StartWorkItemListener(ctx, r); err != nil {
276+
return nil, err
277+
}
278+
279+
return executor, nil
280+
}

0 commit comments

Comments
 (0)