Merge branch '12690-12748-crunchstat-summary'
[arvados.git] / services / keepstore / keepstore.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "flag"
9         "fmt"
10         "net"
11         "os"
12         "os/signal"
13         "syscall"
14         "time"
15
16         "git.curoverse.com/arvados.git/sdk/go/arvados"
17         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
18         "git.curoverse.com/arvados.git/sdk/go/config"
19         "git.curoverse.com/arvados.git/sdk/go/keepclient"
20         "github.com/coreos/go-systemd/daemon"
21 )
22
23 var version = "dev"
24
25 // A Keep "block" is 64MB.
26 const BlockSize = 64 * 1024 * 1024
27
28 // A Keep volume must have at least MinFreeKilobytes available
29 // in order to permit writes.
30 const MinFreeKilobytes = BlockSize / 1024
31
32 // ProcMounts /proc/mounts
33 var ProcMounts = "/proc/mounts"
34
35 var bufs *bufferPool
36
37 // KeepError types.
38 //
39 type KeepError struct {
40         HTTPCode int
41         ErrMsg   string
42 }
43
44 var (
45         BadRequestError     = &KeepError{400, "Bad Request"}
46         UnauthorizedError   = &KeepError{401, "Unauthorized"}
47         CollisionError      = &KeepError{500, "Collision"}
48         RequestHashError    = &KeepError{422, "Hash mismatch in request"}
49         PermissionError     = &KeepError{403, "Forbidden"}
50         DiskHashError       = &KeepError{500, "Hash mismatch in stored data"}
51         ExpiredError        = &KeepError{401, "Expired permission signature"}
52         NotFoundError       = &KeepError{404, "Not Found"}
53         GenericError        = &KeepError{500, "Fail"}
54         FullError           = &KeepError{503, "Full"}
55         SizeRequiredError   = &KeepError{411, "Missing Content-Length"}
56         TooLongError        = &KeepError{413, "Block is too large"}
57         MethodDisabledError = &KeepError{405, "Method disabled"}
58         ErrNotImplemented   = &KeepError{500, "Unsupported configuration"}
59         ErrClientDisconnect = &KeepError{503, "Client disconnected"}
60 )
61
62 func (e *KeepError) Error() string {
63         return e.ErrMsg
64 }
65
66 // ========================
67 // Internal data structures
68 //
69 // These global variables are used by multiple parts of the
70 // program. They are good candidates for moving into their own
71 // packages.
72
73 // The Keep VolumeManager maintains a list of available volumes.
74 // Initialized by the --volumes flag (or by FindKeepVolumes).
75 var KeepVM VolumeManager
76
77 // The pull list manager and trash queue are threadsafe queues which
78 // support atomic update operations. The PullHandler and TrashHandler
79 // store results from Data Manager /pull and /trash requests here.
80 //
81 // See the Keep and Data Manager design documents for more details:
82 // https://arvados.org/projects/arvados/wiki/Keep_Design_Doc
83 // https://arvados.org/projects/arvados/wiki/Data_Manager_Design_Doc
84 //
85 var pullq *WorkQueue
86 var trashq *WorkQueue
87
88 func main() {
89         deprecated.beforeFlagParse(theConfig)
90
91         dumpConfig := flag.Bool("dump-config", false, "write current configuration to stdout and exit (useful for migrating from command line flags to config file)")
92         getVersion := flag.Bool("version", false, "Print version information and exit.")
93
94         defaultConfigPath := "/etc/arvados/keepstore/keepstore.yml"
95         var configPath string
96         flag.StringVar(
97                 &configPath,
98                 "config",
99                 defaultConfigPath,
100                 "YAML or JSON configuration file `path`")
101         flag.Usage = usage
102         flag.Parse()
103
104         // Print version information if requested
105         if *getVersion {
106                 fmt.Printf("keepstore %s\n", version)
107                 return
108         }
109
110         deprecated.afterFlagParse(theConfig)
111
112         err := config.LoadFile(theConfig, configPath)
113         if err != nil && (!os.IsNotExist(err) || configPath != defaultConfigPath) {
114                 log.Fatal(err)
115         }
116
117         if *dumpConfig {
118                 log.Fatal(config.DumpAndExit(theConfig))
119         }
120
121         log.Printf("keepstore %s started", version)
122
123         err = theConfig.Start()
124         if err != nil {
125                 log.Fatal(err)
126         }
127
128         if pidfile := theConfig.PIDFile; pidfile != "" {
129                 f, err := os.OpenFile(pidfile, os.O_RDWR|os.O_CREATE, 0777)
130                 if err != nil {
131                         log.Fatalf("open pidfile (%s): %s", pidfile, err)
132                 }
133                 defer f.Close()
134                 err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
135                 if err != nil {
136                         log.Fatalf("flock pidfile (%s): %s", pidfile, err)
137                 }
138                 defer os.Remove(pidfile)
139                 err = f.Truncate(0)
140                 if err != nil {
141                         log.Fatalf("truncate pidfile (%s): %s", pidfile, err)
142                 }
143                 _, err = fmt.Fprint(f, os.Getpid())
144                 if err != nil {
145                         log.Fatalf("write pidfile (%s): %s", pidfile, err)
146                 }
147                 err = f.Sync()
148                 if err != nil {
149                         log.Fatalf("sync pidfile (%s): %s", pidfile, err)
150                 }
151         }
152
153         var cluster *arvados.Cluster
154         cfg, err := arvados.GetConfig(arvados.DefaultConfigFile)
155         if err != nil && os.IsNotExist(err) {
156                 log.Warnf("DEPRECATED: proceeding without cluster configuration file %q (%s)", arvados.DefaultConfigFile, err)
157                 cluster = &arvados.Cluster{
158                         ClusterID: "xxxxx",
159                 }
160         } else if err != nil {
161                 log.Fatalf("load config %q: %s", arvados.DefaultConfigFile, err)
162         } else {
163                 cluster, err = cfg.GetCluster("")
164                 if err != nil {
165                         log.Fatalf("config error in %q: %s", arvados.DefaultConfigFile, err)
166                 }
167         }
168
169         log.Println("keepstore starting, pid", os.Getpid())
170         defer log.Println("keepstore exiting, pid", os.Getpid())
171
172         // Start a round-robin VolumeManager with the volumes we have found.
173         KeepVM = MakeRRVolumeManager(theConfig.Volumes)
174
175         // Middleware/handler stack
176         router := MakeRESTRouter(cluster)
177
178         // Set up a TCP listener.
179         listener, err := net.Listen("tcp", theConfig.Listen)
180         if err != nil {
181                 log.Fatal(err)
182         }
183
184         // Initialize keepclient for pull workers
185         keepClient := &keepclient.KeepClient{
186                 Arvados:       &arvadosclient.ArvadosClient{},
187                 Want_replicas: 1,
188         }
189
190         // Initialize the pullq and workers
191         pullq = NewWorkQueue()
192         for i := 0; i < 1 || i < theConfig.PullWorkers; i++ {
193                 go RunPullWorker(pullq, keepClient)
194         }
195
196         // Initialize the trashq and workers
197         trashq = NewWorkQueue()
198         for i := 0; i < 1 || i < theConfig.TrashWorkers; i++ {
199                 go RunTrashWorker(trashq)
200         }
201
202         // Start emptyTrash goroutine
203         doneEmptyingTrash := make(chan bool)
204         go emptyTrash(doneEmptyingTrash, theConfig.TrashCheckInterval.Duration())
205
206         // Shut down the server gracefully (by closing the listener)
207         // if SIGTERM is received.
208         term := make(chan os.Signal, 1)
209         go func(sig <-chan os.Signal) {
210                 s := <-sig
211                 log.Println("caught signal:", s)
212                 doneEmptyingTrash <- true
213                 listener.Close()
214         }(term)
215         signal.Notify(term, syscall.SIGTERM)
216         signal.Notify(term, syscall.SIGINT)
217
218         if _, err := daemon.SdNotify(false, "READY=1"); err != nil {
219                 log.Printf("Error notifying init daemon: %v", err)
220         }
221         log.Println("listening at", listener.Addr())
222         srv := &server{}
223         srv.Handler = router
224         srv.Serve(listener)
225 }
226
227 // Periodically (once per interval) invoke EmptyTrash on all volumes.
228 func emptyTrash(done <-chan bool, interval time.Duration) {
229         ticker := time.NewTicker(interval)
230
231         for {
232                 select {
233                 case <-ticker.C:
234                         for _, v := range theConfig.Volumes {
235                                 if v.Writable() {
236                                         v.EmptyTrash()
237                                 }
238                         }
239                 case <-done:
240                         ticker.Stop()
241                         return
242                 }
243         }
244 }