17840: Deduplicate flag-parsing code.
[arvados.git] / services / crunch-dispatch-local / crunch-dispatch-local.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 // Dispatcher service for Crunch that runs containers locally.
8
9 import (
10         "context"
11         "flag"
12         "fmt"
13         "os"
14         "os/exec"
15         "os/signal"
16         "sync"
17         "syscall"
18         "time"
19
20         "git.arvados.org/arvados.git/lib/cmd"
21         "git.arvados.org/arvados.git/lib/config"
22         "git.arvados.org/arvados.git/sdk/go/arvados"
23         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
24         "git.arvados.org/arvados.git/sdk/go/dispatch"
25         "github.com/sirupsen/logrus"
26 )
27
28 var version = "dev"
29
30 var (
31         runningCmds      map[string]*exec.Cmd
32         runningCmdsMutex sync.Mutex
33         waitGroup        sync.WaitGroup
34         crunchRunCommand *string
35 )
36
37 func main() {
38         logger := logrus.StandardLogger()
39         if os.Getenv("DEBUG") != "" {
40                 logger.SetLevel(logrus.DebugLevel)
41         }
42         logger.Formatter = &logrus.JSONFormatter{
43                 TimestampFormat: "2006-01-02T15:04:05.000000000Z07:00",
44         }
45
46         flags := flag.NewFlagSet("crunch-dispatch-local", flag.ExitOnError)
47
48         pollInterval := flags.Int(
49                 "poll-interval",
50                 10,
51                 "Interval in seconds to poll for queued containers")
52
53         crunchRunCommand = flags.String(
54                 "crunch-run-command",
55                 "/usr/bin/crunch-run",
56                 "Crunch command to run container")
57
58         getVersion := flags.Bool(
59                 "version",
60                 false,
61                 "Print version information and exit.")
62
63         if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "", os.Stderr); !ok {
64                 os.Exit(code)
65         }
66
67         // Print version information if requested
68         if *getVersion {
69                 fmt.Printf("crunch-dispatch-local %s\n", version)
70                 return
71         }
72
73         loader := config.NewLoader(nil, logger)
74         cfg, err := loader.Load()
75         cluster, err := cfg.GetCluster("")
76         if err != nil {
77                 fmt.Fprintf(os.Stderr, "config error: %s\n", err)
78                 os.Exit(1)
79         }
80
81         logger.Printf("crunch-dispatch-local %s started", version)
82
83         runningCmds = make(map[string]*exec.Cmd)
84
85         var client arvados.Client
86         client.APIHost = cluster.Services.Controller.ExternalURL.Host
87         client.AuthToken = cluster.SystemRootToken
88         client.Insecure = cluster.TLS.Insecure
89
90         if client.APIHost != "" || client.AuthToken != "" {
91                 // Copy real configs into env vars so [a]
92                 // MakeArvadosClient() uses them, and [b] they get
93                 // propagated to crunch-run via SLURM.
94                 os.Setenv("ARVADOS_API_HOST", client.APIHost)
95                 os.Setenv("ARVADOS_API_TOKEN", client.AuthToken)
96                 os.Setenv("ARVADOS_API_HOST_INSECURE", "")
97                 if client.Insecure {
98                         os.Setenv("ARVADOS_API_HOST_INSECURE", "1")
99                 }
100                 os.Setenv("ARVADOS_EXTERNAL_CLIENT", "")
101         } else {
102                 logger.Warnf("Client credentials missing from config, so falling back on environment variables (deprecated).")
103         }
104
105         arv, err := arvadosclient.MakeArvadosClient()
106         if err != nil {
107                 logger.Errorf("error making Arvados client: %v", err)
108                 os.Exit(1)
109         }
110         arv.Retries = 25
111
112         ctx, cancel := context.WithCancel(context.Background())
113
114         dispatcher := dispatch.Dispatcher{
115                 Logger:       logger,
116                 Arv:          arv,
117                 RunContainer: (&LocalRun{startFunc, make(chan bool, 8), ctx, cluster}).run,
118                 PollPeriod:   time.Duration(*pollInterval) * time.Second,
119         }
120
121         err = dispatcher.Run(ctx)
122         if err != nil {
123                 logger.Error(err)
124                 return
125         }
126
127         c := make(chan os.Signal, 1)
128         signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
129         sig := <-c
130         logger.Printf("Received %s, shutting down", sig)
131         signal.Stop(c)
132
133         cancel()
134
135         runningCmdsMutex.Lock()
136         // Finished dispatching; interrupt any crunch jobs that are still running
137         for _, cmd := range runningCmds {
138                 cmd.Process.Signal(os.Interrupt)
139         }
140         runningCmdsMutex.Unlock()
141
142         // Wait for all running crunch jobs to complete / terminate
143         waitGroup.Wait()
144 }
145
146 func startFunc(container arvados.Container, cmd *exec.Cmd) error {
147         return cmd.Start()
148 }
149
150 type LocalRun struct {
151         startCmd         func(container arvados.Container, cmd *exec.Cmd) error
152         concurrencyLimit chan bool
153         ctx              context.Context
154         cluster          *arvados.Cluster
155 }
156
157 // Run a container.
158 //
159 // If the container is Locked, start a new crunch-run process and wait until
160 // crunch-run completes.  If the priority is set to zero, set an interrupt
161 // signal to the crunch-run process.
162 //
163 // If the container is in any other state, or is not Complete/Cancelled after
164 // crunch-run terminates, mark the container as Cancelled.
165 func (lr *LocalRun) run(dispatcher *dispatch.Dispatcher,
166         container arvados.Container,
167         status <-chan arvados.Container) error {
168
169         uuid := container.UUID
170
171         if container.State == dispatch.Locked {
172
173                 select {
174                 case lr.concurrencyLimit <- true:
175                         break
176                 case <-lr.ctx.Done():
177                         return lr.ctx.Err()
178                 }
179
180                 defer func() { <-lr.concurrencyLimit }()
181
182                 select {
183                 case c := <-status:
184                         // Check for state updates after possibly
185                         // waiting to be ready-to-run
186                         if c.Priority == 0 {
187                                 goto Finish
188                         }
189                 default:
190                         break
191                 }
192
193                 waitGroup.Add(1)
194                 defer waitGroup.Done()
195
196                 cmd := exec.Command(*crunchRunCommand, "--runtime-engine="+lr.cluster.Containers.RuntimeEngine, uuid)
197                 cmd.Stdin = nil
198                 cmd.Stderr = os.Stderr
199                 cmd.Stdout = os.Stderr
200
201                 dispatcher.Logger.Printf("starting container %v", uuid)
202
203                 // Add this crunch job to the list of runningCmds only if we
204                 // succeed in starting crunch-run.
205
206                 runningCmdsMutex.Lock()
207                 if err := lr.startCmd(container, cmd); err != nil {
208                         runningCmdsMutex.Unlock()
209                         dispatcher.Logger.Warnf("error starting %q for %s: %s", *crunchRunCommand, uuid, err)
210                         dispatcher.UpdateState(uuid, dispatch.Cancelled)
211                 } else {
212                         runningCmds[uuid] = cmd
213                         runningCmdsMutex.Unlock()
214
215                         // Need to wait for crunch-run to exit
216                         done := make(chan struct{})
217
218                         go func() {
219                                 if _, err := cmd.Process.Wait(); err != nil {
220                                         dispatcher.Logger.Warnf("error while waiting for crunch job to finish for %v: %q", uuid, err)
221                                 }
222                                 dispatcher.Logger.Debugf("sending done")
223                                 done <- struct{}{}
224                         }()
225
226                 Loop:
227                         for {
228                                 select {
229                                 case <-done:
230                                         break Loop
231                                 case c := <-status:
232                                         // Interrupt the child process if priority changes to 0
233                                         if (c.State == dispatch.Locked || c.State == dispatch.Running) && c.Priority == 0 {
234                                                 dispatcher.Logger.Printf("sending SIGINT to pid %d to cancel container %v", cmd.Process.Pid, uuid)
235                                                 cmd.Process.Signal(os.Interrupt)
236                                         }
237                                 }
238                         }
239                         close(done)
240
241                         dispatcher.Logger.Printf("finished container run for %v", uuid)
242
243                         // Remove the crunch job from runningCmds
244                         runningCmdsMutex.Lock()
245                         delete(runningCmds, uuid)
246                         runningCmdsMutex.Unlock()
247                 }
248         }
249
250 Finish:
251
252         // If the container is not finalized, then change it to "Cancelled".
253         err := dispatcher.Arv.Get("containers", uuid, nil, &container)
254         if err != nil {
255                 dispatcher.Logger.Warnf("error getting final container state: %v", err)
256         }
257         if container.State == dispatch.Locked || container.State == dispatch.Running {
258                 dispatcher.Logger.Warnf("after %q process termination, container state for %v is %q; updating it to %q",
259                         *crunchRunCommand, uuid, container.State, dispatch.Cancelled)
260                 dispatcher.UpdateState(uuid, dispatch.Cancelled)
261         }
262
263         // drain any subsequent status changes
264         for range status {
265         }
266
267         dispatcher.Logger.Printf("finalized container %v", uuid)
268         return nil
269 }