Skip to content

Commit a0e9c9b

Browse files
authored
Adding concurrency limit to the self hosted worker config (#34)
1 parent cb0a2bc commit a0e9c9b

8 files changed

Lines changed: 140 additions & 31 deletions

File tree

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ require (
77
github.com/distribution/reference v0.6.0
88
github.com/docker/cli v29.1.3+incompatible
99
github.com/docker/docker v28.5.2+incompatible
10+
github.com/go-playground/validator/v10 v10.30.1
1011
github.com/gorilla/websocket v1.5.3
1112
github.com/rs/zerolog v1.31.0
13+
golang.org/x/sync v0.19.0
14+
gopkg.in/yaml.v3 v3.0.1
1215
)
1316

1417
require (
@@ -30,7 +33,6 @@ require (
3033
github.com/go-logr/stdr v1.2.2 // indirect
3134
github.com/go-playground/locales v0.14.1 // indirect
3235
github.com/go-playground/universal-translator v0.18.1 // indirect
33-
github.com/go-playground/validator/v10 v10.30.1 // indirect
3436
github.com/golang/protobuf v1.5.0 // indirect
3537
github.com/gorilla/mux v1.8.1 // indirect
3638
github.com/joho/godotenv v1.5.1 // indirect
@@ -61,6 +63,5 @@ require (
6163
golang.org/x/text v0.32.0 // indirect
6264
golang.org/x/time v0.14.0 // indirect
6365
google.golang.org/protobuf v1.36.10 // indirect
64-
gopkg.in/yaml.v3 v3.0.1 // indirect
6566
gotest.tools/v3 v3.5.2 // indirect
6667
)

go.sum

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
6666
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
6767
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
6868
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
69+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
70+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
6971
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
7072
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
7173
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -118,8 +120,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
118120
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
119121
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
120122
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
123+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
124+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
121125
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
122126
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
127+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
128+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
123129
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
124130
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
125131
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -176,6 +182,8 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
176182
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
177183
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
178184
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
185+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
186+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
179187
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
180188
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
181189
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -230,6 +238,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
230238
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
231239
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
232240
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
241+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
242+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
233243
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
234244
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
235245
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -250,8 +260,6 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
250260
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
251261
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
252262
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
253-
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
254-
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
255263
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
256264
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
257265
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
@@ -277,6 +285,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
277285
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
278286
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
279287
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
288+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
289+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
280290
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
281291
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
282292
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

internal/config/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import (
1414

1515
// FileConfig represents the top-level YAML configuration file.
1616
type FileConfig struct {
17-
WorkerID string `yaml:"worker_id"`
18-
Cleanup *bool `yaml:"cleanup"`
19-
Backend BackendConfig `yaml:"backend"`
17+
WorkerID string `yaml:"worker_id"`
18+
Cleanup *bool `yaml:"cleanup"`
19+
MaxConcurrentTasks *int `yaml:"max_concurrent_tasks"`
20+
Backend BackendConfig `yaml:"backend"`
2021
}
2122

2223
// BackendConfig contains the backend selection.

internal/config/config_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,35 @@ func TestLoadFileNotFound(t *testing.T) {
256256
t.Fatal("expected error for missing file")
257257
}
258258
}
259+
260+
func TestLoadMaxConcurrentTasks(t *testing.T) {
261+
t.Run("parses max_concurrent_tasks when set", func(t *testing.T) {
262+
path := writeTestConfig(t, `
263+
worker_id: "test"
264+
max_concurrent_tasks: 5
265+
`)
266+
cfg, err := Load(path)
267+
if err != nil {
268+
t.Fatalf("unexpected error: %v", err)
269+
}
270+
if cfg.MaxConcurrentTasks == nil {
271+
t.Fatal("expected max_concurrent_tasks to be set")
272+
}
273+
if *cfg.MaxConcurrentTasks != 5 {
274+
t.Errorf("max_concurrent_tasks = %d, want 5", *cfg.MaxConcurrentTasks)
275+
}
276+
})
277+
278+
t.Run("max_concurrent_tasks is nil when not set", func(t *testing.T) {
279+
path := writeTestConfig(t, `
280+
worker_id: "test"
281+
`)
282+
cfg, err := Load(path)
283+
if err != nil {
284+
t.Fatalf("unexpected error: %v", err)
285+
}
286+
if cfg.MaxConcurrentTasks != nil {
287+
t.Errorf("expected max_concurrent_tasks to be nil, got %d", *cfg.MaxConcurrentTasks)
288+
}
289+
})
290+
}

internal/types/messages.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
MessageTypeTaskAssignment MessageType = "task_assignment"
1313
MessageTypeTaskClaimed MessageType = "task_claimed"
1414
MessageTypeTaskFailed MessageType = "task_failed"
15+
MessageTypeTaskRejected MessageType = "task_rejected"
1516
MessageTypeHeartbeat MessageType = "heartbeat"
1617
)
1718

@@ -53,6 +54,13 @@ type TaskFailedMessage struct {
5354
Message string `json:"message"`
5455
}
5556

57+
// TaskRejectedMessage is sent from worker to server when the worker cannot accept the task
58+
// (e.g. at maximum concurrency). The server should keep the task queued rather than marking it failed.
59+
type TaskRejectedMessage struct {
60+
TaskID string `json:"task_id"`
61+
Reason string `json:"reason"`
62+
}
63+
5664
type TaskDefinition struct {
5765
Prompt string `json:"prompt"`
5866
}

internal/worker/direct.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,12 @@ func (b *DirectBackend) cleanup(ctx context.Context, taskID, workspaceDir string
198198
}
199199

200200
// runCommand executes a shell command with the given working directory and environment.
201-
// The command inherits only standard host vars (HOME, TMPDIR, PATH) plus env.
201+
// Setup/teardown commands inherit the full worker environment so they can access
202+
// tools and credentials (e.g. aws, docker) needed for workspace provisioning.
202203
func (b *DirectBackend) runCommand(ctx context.Context, command, dir string, env []string) error {
203204
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
204205
cmd.Dir = dir
205-
cmd.Env = mergeEnvVars(hostBaseEnv(), env)
206+
cmd.Env = mergeEnvVars(os.Environ(), env)
206207
cmd.Stdout = os.Stdout
207208
cmd.Stderr = os.Stderr
208209
return cmd.Run()

internal/worker/worker.go

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/warpdotdev/oz-agent-worker/internal/common"
1313
"github.com/warpdotdev/oz-agent-worker/internal/log"
1414
"github.com/warpdotdev/oz-agent-worker/internal/types"
15+
"golang.org/x/sync/semaphore"
1516
)
1617

1718
const (
@@ -25,12 +26,13 @@ const (
2526
)
2627

2728
type Config struct {
28-
APIKey string
29-
WorkerID string
30-
WebSocketURL string
31-
ServerRootURL string
32-
LogLevel string
33-
BackendType string // "docker" or "direct"
29+
APIKey string
30+
WorkerID string
31+
WebSocketURL string
32+
ServerRootURL string
33+
LogLevel string
34+
BackendType string // "docker" or "direct"
35+
MaxConcurrentTasks int // 0 means unlimited
3436

3537
// Backend-specific configs. Only the one matching BackendType should be set.
3638
Docker *DockerBackendConfig
@@ -49,6 +51,7 @@ type Worker struct {
4951
activeTasks map[string]context.CancelFunc
5052
tasksMutex sync.Mutex
5153
backend Backend
54+
taskSemaphore *semaphore.Weighted // nil when unlimited
5255
}
5356

5457
func New(ctx context.Context, config Config) (*Worker, error) {
@@ -79,6 +82,11 @@ func New(ctx context.Context, config Config) (*Worker, error) {
7982
return nil, err
8083
}
8184

85+
var taskSemaphore *semaphore.Weighted
86+
if config.MaxConcurrentTasks > 0 {
87+
taskSemaphore = semaphore.NewWeighted(int64(config.MaxConcurrentTasks))
88+
}
89+
8290
return &Worker{
8391
config: config,
8492
ctx: workerCtx,
@@ -87,6 +95,7 @@ func New(ctx context.Context, config Config) (*Worker, error) {
8795
sendChan: make(chan []byte, 256),
8896
activeTasks: make(map[string]context.CancelFunc),
8997
backend: backend,
98+
taskSemaphore: taskSemaphore,
9099
}, nil
91100
}
92101

@@ -298,6 +307,17 @@ func (w *Worker) handleMessage(message []byte) {
298307
func (w *Worker) handleTaskAssignment(assignment *types.TaskAssignmentMessage) {
299308
log.Infof(w.ctx, "Received task assignment: taskID=%s, title=%s", assignment.TaskID, assignment.Task.Title)
300309

310+
// Check concurrency limit before claiming the task.
311+
if w.taskSemaphore != nil {
312+
if !w.taskSemaphore.TryAcquire(1) {
313+
log.Warnf(w.ctx, "Rejecting task %s: worker at maximum concurrency (%d)", assignment.TaskID, w.config.MaxConcurrentTasks)
314+
if err := w.sendTaskRejected(assignment.TaskID, "worker at maximum concurrency"); err != nil {
315+
log.Errorf(w.ctx, "Failed to send task rejected message: %v", err)
316+
}
317+
return
318+
}
319+
}
320+
301321
// It's important to update the task state to claimed as the task lifecycle treats this as a dependency to advance to further states.
302322
if err := w.sendTaskClaimed(assignment.TaskID); err != nil {
303323
log.Errorf(w.ctx, "Failed to send task claimed message: %v", err)
@@ -379,6 +399,10 @@ func (w *Worker) executeTask(ctx context.Context, assignment *types.TaskAssignme
379399
w.tasksMutex.Lock()
380400
delete(w.activeTasks, assignment.TaskID)
381401
w.tasksMutex.Unlock()
402+
403+
if w.taskSemaphore != nil {
404+
w.taskSemaphore.Release(1)
405+
}
382406
}()
383407

384408
taskID := assignment.TaskID
@@ -420,6 +444,30 @@ func (w *Worker) sendTaskClaimed(taskID string) error {
420444
return w.sendMessage(msgBytes)
421445
}
422446

447+
func (w *Worker) sendTaskRejected(taskID, reason string) error {
448+
rejectedMsg := types.TaskRejectedMessage{
449+
TaskID: taskID,
450+
Reason: reason,
451+
}
452+
453+
data, err := json.Marshal(rejectedMsg)
454+
if err != nil {
455+
return fmt.Errorf("failed to marshal task rejected message: %w", err)
456+
}
457+
458+
msg := types.WebSocketMessage{
459+
Type: types.MessageTypeTaskRejected,
460+
Data: data,
461+
}
462+
463+
msgBytes, err := json.Marshal(msg)
464+
if err != nil {
465+
return fmt.Errorf("failed to marshal websocket message: %w", err)
466+
}
467+
468+
return w.sendMessage(msgBytes)
469+
}
470+
423471
func (w *Worker) sendTaskFailed(taskID, message string) error {
424472
failedMsg := types.TaskFailedMessage{
425473
TaskID: taskID,

main.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ import (
1515
)
1616

1717
var CLI struct {
18-
ConfigFile string `help:"Path to YAML config file" type:"path"`
19-
Backend string `help:"Backend type (docker or direct)" enum:"docker,direct," default:""`
20-
APIKey string `help:"API key for authentication" env:"WARP_API_KEY" required:""`
21-
WorkerID string `help:"Worker host identifier (required via flag or config file)"`
22-
WebSocketURL string `default:"wss://oz.warp.dev/api/v1/selfhosted/worker/ws" hidden:""`
23-
ServerRootURL string `default:"https://app.warp.dev" hidden:""`
24-
LogLevel string `help:"Log level (debug, info, warn, error)" default:"info" enum:"debug,info,warn,error"`
25-
NoCleanup bool `help:"Do not remove containers after execution (for debugging)"`
26-
Volumes []string `help:"Volume mounts for task containers (format: HOST_PATH:CONTAINER_PATH or HOST_PATH:CONTAINER_PATH:MODE)" short:"v"`
27-
Env []string `help:"Environment variables for task containers (format: KEY=VALUE or KEY to pass through from host)" short:"e"`
18+
ConfigFile string `help:"Path to YAML config file" type:"path"`
19+
Backend string `help:"Backend type (docker or direct)" enum:"docker,direct," default:""`
20+
APIKey string `help:"API key for authentication" env:"WARP_API_KEY" required:""`
21+
WorkerID string `help:"Worker host identifier (required via flag or config file)"`
22+
WebSocketURL string `default:"wss://oz.warp.dev/api/v1/selfhosted/worker/ws" hidden:""`
23+
ServerRootURL string `default:"https://app.warp.dev" hidden:""`
24+
LogLevel string `help:"Log level (debug, info, warn, error)" default:"info" enum:"debug,info,warn,error"`
25+
NoCleanup bool `help:"Do not remove containers after execution (for debugging)"`
26+
Volumes []string `help:"Volume mounts for task containers (format: HOST_PATH:CONTAINER_PATH or HOST_PATH:CONTAINER_PATH:MODE)" short:"v"`
27+
Env []string `help:"Environment variables for task containers (format: KEY=VALUE or KEY to pass through from host)" short:"e"`
28+
MaxConcurrentTasks int `help:"Maximum number of tasks to run concurrently (0 for unlimited)" default:"0"`
2829
}
2930

3031
func main() {
@@ -116,13 +117,20 @@ func mergeConfig(fileConfig *config.FileConfig) (worker.Config, error) {
116117
return worker.Config{}, err
117118
}
118119

120+
// Resolve max_concurrent_tasks: CLI (non-zero) > config file > 0 (unlimited).
121+
maxConcurrentTasks := CLI.MaxConcurrentTasks
122+
if maxConcurrentTasks == 0 && fileConfig != nil && fileConfig.MaxConcurrentTasks != nil {
123+
maxConcurrentTasks = *fileConfig.MaxConcurrentTasks
124+
}
125+
119126
wc := worker.Config{
120-
APIKey: CLI.APIKey,
121-
WorkerID: workerID,
122-
WebSocketURL: CLI.WebSocketURL,
123-
ServerRootURL: CLI.ServerRootURL,
124-
LogLevel: CLI.LogLevel,
125-
BackendType: backendType,
127+
APIKey: CLI.APIKey,
128+
WorkerID: workerID,
129+
WebSocketURL: CLI.WebSocketURL,
130+
ServerRootURL: CLI.ServerRootURL,
131+
LogLevel: CLI.LogLevel,
132+
BackendType: backendType,
133+
MaxConcurrentTasks: maxConcurrentTasks,
126134
}
127135

128136
switch backendType {

0 commit comments

Comments
 (0)