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