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