1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
7 // Dispatcher service for Crunch that runs containers locally.
21 "git.arvados.org/arvados.git/lib/cmd"
22 "git.arvados.org/arvados.git/lib/config"
23 "git.arvados.org/arvados.git/sdk/go/arvados"
24 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
25 "git.arvados.org/arvados.git/sdk/go/dispatch"
26 "github.com/pbnjay/memory"
27 "github.com/sirupsen/logrus"
33 runningCmds map[string]*exec.Cmd
34 runningCmdsMutex sync.Mutex
35 waitGroup sync.WaitGroup
36 crunchRunCommand string
40 baseLogger := logrus.StandardLogger()
41 if os.Getenv("DEBUG") != "" {
42 baseLogger.SetLevel(logrus.DebugLevel)
44 baseLogger.Formatter = &logrus.JSONFormatter{
45 TimestampFormat: "2006-01-02T15:04:05.000000000Z07:00",
48 flags := flag.NewFlagSet("crunch-dispatch-local", flag.ExitOnError)
50 pollInterval := flags.Int(
53 "Interval in seconds to poll for queued containers")
55 flags.StringVar(&crunchRunCommand,
58 "Crunch command to run container")
60 getVersion := flags.Bool(
63 "Print version information and exit.")
65 if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "", os.Stderr); !ok {
69 // Print version information if requested
71 fmt.Printf("crunch-dispatch-local %s\n", version)
75 loader := config.NewLoader(nil, baseLogger)
76 cfg, err := loader.Load()
78 fmt.Fprintf(os.Stderr, "error loading config: %s\n", err)
81 cluster, err := cfg.GetCluster("")
83 fmt.Fprintf(os.Stderr, "config error: %s\n", err)
87 if crunchRunCommand == "" {
88 crunchRunCommand = cluster.Containers.CrunchRunCommand
91 logger := baseLogger.WithField("ClusterID", cluster.ClusterID)
92 logger.Printf("crunch-dispatch-local %s started", version)
94 runningCmds = make(map[string]*exec.Cmd)
96 var client arvados.Client
97 client.APIHost = cluster.Services.Controller.ExternalURL.Host
98 client.AuthToken = cluster.SystemRootToken
99 client.Insecure = cluster.TLS.Insecure
101 if client.APIHost != "" || client.AuthToken != "" {
102 // Copy real configs into env vars so [a]
103 // MakeArvadosClient() uses them, and [b] they get
104 // propagated to crunch-run via SLURM.
105 os.Setenv("ARVADOS_API_HOST", client.APIHost)
106 os.Setenv("ARVADOS_API_TOKEN", client.AuthToken)
107 os.Setenv("ARVADOS_API_HOST_INSECURE", "")
109 os.Setenv("ARVADOS_API_HOST_INSECURE", "1")
112 logger.Warnf("Client credentials missing from config, so falling back on environment variables (deprecated).")
115 arv, err := arvadosclient.MakeArvadosClient()
117 logger.Errorf("error making Arvados client: %v", err)
122 ctx, cancel := context.WithCancel(context.Background())
124 localRun := LocalRun{startFunc, make(chan ResourceRequest), ctx, cluster}
126 go localRun.throttle(logger)
128 dispatcher := dispatch.Dispatcher{
131 RunContainer: localRun.run,
132 PollPeriod: time.Duration(*pollInterval) * time.Second,
135 err = dispatcher.Run(ctx)
141 c := make(chan os.Signal, 1)
142 signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
144 logger.Printf("Received %s, shutting down", sig)
149 runningCmdsMutex.Lock()
150 // Finished dispatching; interrupt any crunch jobs that are still running
151 for _, cmd := range runningCmds {
152 cmd.Process.Signal(os.Interrupt)
154 runningCmdsMutex.Unlock()
156 // Wait for all running crunch jobs to complete / terminate
160 func startFunc(container arvados.Container, cmd *exec.Cmd) error {
164 type ResourceRequest struct {
172 type LocalRun struct {
173 startCmd func(container arvados.Container, cmd *exec.Cmd) error
174 concurrencyLimit chan ResourceRequest
176 cluster *arvados.Cluster
179 func (lr *LocalRun) throttle(logger logrus.FieldLogger) {
180 maxVcpus := runtime.NumCPU()
181 var maxRam int64 = int64(memory.TotalMemory())
183 // treat all GPUs as a single resource for now.
190 pending := []ResourceRequest{}
194 rr := <-lr.concurrencyLimit
197 // allocating resources
198 pending = append(pending, rr)
200 // releasing resources (these should be
202 allocVcpus += rr.vcpus
206 logger.Infof("%v removed allocation (cpus: %v ram: %v gpus: %v); total allocated (cpus: %v ram: %v gpus: %v)",
207 rr.uuid, rr.vcpus, rr.ram, rr.gpus,
208 allocVcpus, allocRam, allocGpus)
211 for len(pending) > 0 {
213 if rr.vcpus > maxVcpus || rr.ram > maxRam || rr.gpus > maxGpus {
214 // resource request can never be fulfilled
219 if (allocVcpus+rr.vcpus) > maxVcpus || (allocRam+rr.ram) > maxRam || (allocGpus+rr.gpus) > maxGpus {
220 logger.Info("Insufficient resources to start %v, waiting for next event", rr.uuid)
221 // can't be scheduled yet, go up to
222 // the top and wait for the next event
226 allocVcpus += rr.vcpus
231 logger.Infof("%v added allocation (cpus: %v ram: %v gpus: %v); total allocated (cpus: %v ram: %v gpus: %v)",
232 rr.uuid, rr.vcpus, rr.ram, rr.gpus,
233 allocVcpus, allocRam, allocGpus)
236 for i := 0; i < len(pending)-1; i++ {
237 pending[i] = pending[i+1]
239 pending = pending[0 : len(pending)-1]
247 // If the container is Locked, start a new crunch-run process and wait until
248 // crunch-run completes. If the priority is set to zero, set an interrupt
249 // signal to the crunch-run process.
251 // If the container is in any other state, or is not Complete/Cancelled after
252 // crunch-run terminates, mark the container as Cancelled.
253 func (lr *LocalRun) run(dispatcher *dispatch.Dispatcher,
254 container arvados.Container,
255 status <-chan arvados.Container) error {
257 uuid := container.UUID
259 if container.State == dispatch.Locked {
261 resourceRequest := ResourceRequest{
262 uuid: container.UUID,
263 vcpus: container.RuntimeConstraints.VCPUs,
264 ram: (container.RuntimeConstraints.RAM +
265 container.RuntimeConstraints.KeepCacheRAM +
266 int64(lr.cluster.Containers.ReserveExtraRAM)),
267 gpus: container.RuntimeConstraints.CUDA.DeviceCount,
268 ready: make(chan bool)}
271 case lr.concurrencyLimit <- resourceRequest:
273 case <-lr.ctx.Done():
277 canRun := <-resourceRequest.ready
280 dispatcher.Logger.Warnf("Container resource request %v cannot be fulfilled.", uuid)
281 dispatcher.UpdateState(uuid, dispatch.Cancelled)
286 resourceRequest.vcpus = -resourceRequest.vcpus
287 resourceRequest.ram = -resourceRequest.ram
288 resourceRequest.gpus = -resourceRequest.gpus
289 lr.concurrencyLimit <- resourceRequest
294 // Check for state updates after possibly
295 // waiting to be ready-to-run
304 defer waitGroup.Done()
306 args := []string{"--runtime-engine=" + lr.cluster.Containers.RuntimeEngine}
307 args = append(args, lr.cluster.Containers.CrunchRunArgumentsList...)
308 args = append(args, uuid)
310 cmd := exec.Command(crunchRunCommand, args...)
312 cmd.Stderr = os.Stderr
313 cmd.Stdout = os.Stderr
315 dispatcher.Logger.Printf("starting container %v", uuid)
317 // Add this crunch job to the list of runningCmds only if we
318 // succeed in starting crunch-run.
320 runningCmdsMutex.Lock()
321 if err := lr.startCmd(container, cmd); err != nil {
322 runningCmdsMutex.Unlock()
323 dispatcher.Logger.Warnf("error starting %q for %s: %s", crunchRunCommand, uuid, err)
324 dispatcher.UpdateState(uuid, dispatch.Cancelled)
326 runningCmds[uuid] = cmd
327 runningCmdsMutex.Unlock()
329 // Need to wait for crunch-run to exit
330 done := make(chan struct{})
333 if _, err := cmd.Process.Wait(); err != nil {
334 dispatcher.Logger.Warnf("error while waiting for crunch job to finish for %v: %q", uuid, err)
336 dispatcher.Logger.Debugf("sending done")
346 // Interrupt the child process if priority changes to 0
347 if (c.State == dispatch.Locked || c.State == dispatch.Running) && c.Priority == 0 {
348 dispatcher.Logger.Printf("sending SIGINT to pid %d to cancel container %v", cmd.Process.Pid, uuid)
349 cmd.Process.Signal(os.Interrupt)
355 dispatcher.Logger.Printf("finished container run for %v", uuid)
357 // Remove the crunch job from runningCmds
358 runningCmdsMutex.Lock()
359 delete(runningCmds, uuid)
360 runningCmdsMutex.Unlock()
366 // If the container is not finalized, then change it to "Cancelled".
367 err := dispatcher.Arv.Get("containers", uuid, nil, &container)
369 dispatcher.Logger.Warnf("error getting final container state: %v", err)
371 if container.State == dispatch.Locked || container.State == dispatch.Running {
372 dispatcher.Logger.Warnf("after %q process termination, container state for %v is %q; updating it to %q",
373 crunchRunCommand, uuid, container.State, dispatch.Cancelled)
374 dispatcher.UpdateState(uuid, dispatch.Cancelled)
377 // drain any subsequent status changes
381 dispatcher.Logger.Printf("finalized container %v", uuid)