X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/0b0b4c7b23e96a6efb3cfd88b0ba7224158e9544..40f551004ab4e5f1d8ab02ddb55dca225ee8f6ac:/lib/crunchrun/crunchrun.go diff --git a/lib/crunchrun/crunchrun.go b/lib/crunchrun/crunchrun.go index 5638e81e4d..a5e69387ec 100644 --- a/lib/crunchrun/crunchrun.go +++ b/lib/crunchrun/crunchrun.go @@ -6,6 +6,7 @@ package crunchrun import ( "bytes" + "context" "encoding/json" "errors" "flag" @@ -13,6 +14,8 @@ import ( "io" "io/ioutil" "log" + "net" + "net/http" "os" "os/exec" "os/signal" @@ -33,13 +36,20 @@ import ( "git.arvados.org/arvados.git/sdk/go/arvadosclient" "git.arvados.org/arvados.git/sdk/go/keepclient" "git.arvados.org/arvados.git/sdk/go/manifest" - "golang.org/x/net/context" ) type command struct{} var Command = command{} +// ConfigData contains environment variables and (when needed) cluster +// configuration, passed from dispatchcloud to crunch-run on stdin. +type ConfigData struct { + Env map[string]string + KeepBuffers int + Cluster *arvados.Cluster +} + // IArvadosClient is the minimal Arvados API methods used by crunch-run. type IArvadosClient interface { Create(resourceType string, parameters arvadosclient.Dict, output interface{}) error @@ -55,17 +65,18 @@ var ErrCancelled = errors.New("Cancelled") // IKeepClient is the minimal Keep API methods used by crunch-run. type IKeepClient interface { - PutB(buf []byte) (string, int, error) + BlockWrite(context.Context, arvados.BlockWriteOptions) (arvados.BlockWriteResponse, error) ReadAt(locator string, p []byte, off int) (int, error) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) LocalLocator(locator string) (string, error) ClearBlockCache() + SetStorageClasses(sc []string) } // NewLogWriter is a factory function to create a new log writer. type NewLogWriter func(name string) (io.WriteCloser, error) -type RunArvMount func(args []string, tok string) (*exec.Cmd, error) +type RunArvMount func(cmdline []string, tok string) (*exec.Cmd, error) type MkTempDir func(string, string) (string, error) @@ -76,7 +87,10 @@ type PsProcess interface { // ContainerRunner is the main stateful struct used for a single execution of a // container. type ContainerRunner struct { - executor containerExecutor + executor containerExecutor + executorStdin io.Closer + executorStdout io.Closer + executorStderr io.Closer // Dispatcher client is initialized with the Dispatcher token. // This is a privileged token used to manage container status @@ -105,8 +119,6 @@ type ContainerRunner struct { ExitCode *int NewLogWriter NewLogWriter CrunchLog *ThrottledLogger - Stdout io.WriteCloser - Stderr io.WriteCloser logUUID string logMtx sync.Mutex LogCollection arvados.CollectionFileSystem @@ -125,6 +137,8 @@ type ContainerRunner struct { finalState string parentTemp string + keepstoreLogger io.WriteCloser + keepstoreLogbuf *bufThenWrite statLogger io.WriteCloser statReporter *crunchstat.Reporter hoststatLogger io.WriteCloser @@ -258,23 +272,21 @@ func (runner *ContainerRunner) LoadImage() (string, error) { return "", fmt.Errorf("cannot choose from multiple tar files in image collection: %v", tarfiles) } imageID := tarfiles[0][:len(tarfiles[0])-4] - imageFile := runner.ArvMountPoint + "/by_id/" + runner.Container.ContainerImage + "/" + tarfiles[0] + imageTarballPath := runner.ArvMountPoint + "/by_id/" + runner.Container.ContainerImage + "/" + imageID + ".tar" runner.CrunchLog.Printf("Using Docker image id %q", imageID) - if !runner.executor.ImageLoaded(imageID) { - runner.CrunchLog.Print("Loading Docker image from keep") - err = runner.executor.LoadImage(imageFile) - if err != nil { - return "", err - } - } else { - runner.CrunchLog.Print("Docker image is available") + runner.CrunchLog.Print("Loading Docker image from keep") + err = runner.executor.LoadImage(imageID, imageTarballPath, runner.Container, runner.ArvMountPoint, + runner.containerClient) + if err != nil { + return "", err } + return imageID, nil } -func (runner *ContainerRunner) ArvMountCmd(arvMountCmd []string, token string) (c *exec.Cmd, err error) { - c = exec.Command("arv-mount", arvMountCmd...) +func (runner *ContainerRunner) ArvMountCmd(cmdline []string, token string) (c *exec.Cmd, err error) { + c = exec.Command(cmdline[0], cmdline[1:]...) // Copy our environment, but override ARVADOS_API_TOKEN with // the container auth token. @@ -291,8 +303,16 @@ func (runner *ContainerRunner) ArvMountCmd(arvMountCmd []string, token string) ( return nil, err } runner.arvMountLog = NewThrottledLogger(w) + scanner := logScanner{ + Patterns: []string{ + "Keep write error", + "Block not found error", + "Unhandled exception during FUSE operation", + }, + ReportFunc: runner.reportArvMountWarning, + } c.Stdout = runner.arvMountLog - c.Stderr = io.MultiWriter(runner.arvMountLog, os.Stderr) + c.Stderr = io.MultiWriter(runner.arvMountLog, os.Stderr, &scanner) runner.CrunchLog.Printf("Running %v", c.Args) @@ -392,11 +412,16 @@ func (runner *ContainerRunner) SetupMounts() (map[string]bindmount, error) { pdhOnly := true tmpcount := 0 arvMountCmd := []string{ + "arv-mount", "--foreground", - "--allow-other", "--read-write", + "--storage-classes", strings.Join(runner.Container.OutputStorageClasses, ","), fmt.Sprintf("--crunchstat-interval=%v", runner.statInterval.Seconds())} + if runner.executor.Runtime() == "docker" { + arvMountCmd = append(arvMountCmd, "--allow-other") + } + if runner.Container.RuntimeConstraints.KeepCacheRAM > 0 { arvMountCmd = append(arvMountCmd, "--file-cache", fmt.Sprintf("%d", runner.Container.RuntimeConstraints.KeepCacheRAM)) } @@ -596,6 +621,7 @@ func (runner *ContainerRunner) SetupMounts() (map[string]bindmount, error) { } else { arvMountCmd = append(arvMountCmd, "--mount-by-id", "by_id") } + arvMountCmd = append(arvMountCmd, "--mount-by-id", "by_uuid") arvMountCmd = append(arvMountCmd, runner.ArvMountPoint) runner.ArvMount, err = runner.RunArvMount(arvMountCmd, token) @@ -875,7 +901,7 @@ func (runner *ContainerRunner) getStdoutFile(mntPath string) (*os.File, error) { // CreateContainer creates the docker container. func (runner *ContainerRunner) CreateContainer(imageID string, bindmounts map[string]bindmount) error { - var stdin io.ReadCloser + var stdin io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil)) if mnt, ok := runner.Container.Mounts["stdin"]; ok { switch mnt.Kind { case "collection": @@ -952,6 +978,9 @@ func (runner *ContainerRunner) CreateContainer(imageID string, bindmounts map[st if !runner.enableMemoryLimit { ram = 0 } + runner.executorStdin = stdin + runner.executorStdout = stdout + runner.executorStderr = stderr return runner.executor.Create(containerSpec{ Image: imageID, VCPUs: runner.Container.RuntimeConstraints.VCPUs, @@ -1016,6 +1045,27 @@ func (runner *ContainerRunner) WaitFinish() error { } runner.ExitCode = &exitcode + var returnErr error + if err = runner.executorStdin.Close(); err != nil { + err = fmt.Errorf("error closing container stdin: %s", err) + runner.CrunchLog.Printf("%s", err) + returnErr = err + } + if err = runner.executorStdout.Close(); err != nil { + err = fmt.Errorf("error closing container stdout: %s", err) + runner.CrunchLog.Printf("%s", err) + if returnErr == nil { + returnErr = err + } + } + if err = runner.executorStderr.Close(); err != nil { + err = fmt.Errorf("error closing container stderr: %s", err) + runner.CrunchLog.Printf("%s", err) + if returnErr == nil { + returnErr = err + } + } + if runner.statReporter != nil { runner.statReporter.Stop() err = runner.statLogger.Close() @@ -1023,7 +1073,7 @@ func (runner *ContainerRunner) WaitFinish() error { runner.CrunchLog.Printf("error closing crunchstat logs: %v", err) } } - return nil + return returnErr } func (runner *ContainerRunner) updateLogs() { @@ -1074,6 +1124,21 @@ func (runner *ContainerRunner) updateLogs() { } } +func (runner *ContainerRunner) reportArvMountWarning(pattern, text string) { + var updated arvados.Container + err := runner.DispatcherArvClient.Update("containers", runner.Container.UUID, arvadosclient.Dict{ + "container": arvadosclient.Dict{ + "runtime_status": arvadosclient.Dict{ + "warning": "arv-mount: " + pattern, + "warningDetail": text, + }, + }, + }, &updated) + if err != nil { + runner.CrunchLog.Printf("error updating container runtime_status: %s", err) + } +} + // CaptureOutput saves data from the container's output directory if // needed, and updates the container output accordingly. func (runner *ContainerRunner) CaptureOutput(bindmounts map[string]bindmount) error { @@ -1144,6 +1209,7 @@ func (runner *ContainerRunner) CleanupDirs() { if umnterr != nil { runner.CrunchLog.Printf("Error unmounting: %v", umnterr) + runner.ArvMount.Process.Kill() } else { // If arv-mount --unmount gets stuck for any reason, we // don't want to wait for it forever. Do Wait() in a goroutine @@ -1174,12 +1240,14 @@ func (runner *ContainerRunner) CleanupDirs() { } } } + runner.ArvMount = nil } if runner.ArvMountPoint != "" { if rmerr := os.Remove(runner.ArvMountPoint); rmerr != nil { runner.CrunchLog.Printf("While cleaning up arv-mount directory %s: %v", runner.ArvMountPoint, rmerr) } + runner.ArvMountPoint = "" } if rmerr := os.RemoveAll(runner.parentTemp); rmerr != nil { @@ -1214,6 +1282,16 @@ func (runner *ContainerRunner) CommitLogs() error { runner.CrunchLog.Immediate = log.New(os.Stderr, runner.Container.UUID+" ", 0) }() + if runner.keepstoreLogger != nil { + // Flush any buffered logs from our local keepstore + // process. Discard anything logged after this point + // -- it won't end up in the log collection, so + // there's no point writing it to the collectionfs. + runner.keepstoreLogbuf.SetWriter(io.Discard) + runner.keepstoreLogger.Close() + runner.keepstoreLogger = nil + } + if runner.LogsPDH != nil { // If we have already assigned something to LogsPDH, // we must be closing the re-opened log, which won't @@ -1222,6 +1300,7 @@ func (runner *ContainerRunner) CommitLogs() error { // -- it exists only to send logs to other channels. return nil } + saved, err := runner.saveLogCollection(true) if err != nil { return fmt.Errorf("error saving log collection: %s", err) @@ -1348,7 +1427,7 @@ func (runner *ContainerRunner) NewArvLogWriter(name string) (io.WriteCloser, err // Run the full container lifecycle. func (runner *ContainerRunner) Run() (err error) { runner.CrunchLog.Printf("crunch-run %s started", cmd.Version.String()) - runner.CrunchLog.Printf("Executing container '%s'", runner.Container.UUID) + runner.CrunchLog.Printf("Executing container '%s' using %s runtime", runner.Container.UUID, runner.executor.Runtime()) hostname, hosterr := os.Hostname() if hosterr != nil { @@ -1414,6 +1493,7 @@ func (runner *ContainerRunner) Run() (err error) { } checkErr("stopHoststat", runner.stopHoststat()) checkErr("CommitLogs", runner.CommitLogs()) + runner.CleanupDirs() checkErr("UpdateContainerFinal", runner.UpdateContainerFinal()) }() @@ -1519,6 +1599,9 @@ func (runner *ContainerRunner) fetchContainerRecord() error { return fmt.Errorf("error creating container API client: %v", err) } + runner.ContainerKeepClient.SetStorageClasses(runner.Container.OutputStorageClasses) + runner.DispatcherKeepClient.SetStorageClasses(runner.Container.OutputStorageClasses) + err = runner.ContainerArvClient.Call("GET", "containers", runner.Container.UUID, "secret_mounts", nil, &sm) if err != nil { if apierr, ok := err.(arvadosclient.APIServerError); !ok || apierr.HttpStatusCode != 404 { @@ -1580,6 +1663,7 @@ func NewContainerRunner(dispatcherClient *arvados.Client, } func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + log := log.New(stderr, "", 0) flags := flag.NewFlagSet(prog, flag.ContinueOnError) statInterval := flags.Duration("crunchstat-interval", 10*time.Second, "sampling period for periodic resource usage reporting") cgroupRoot := flags.String("cgroup-root", "/sys/fs/cgroup", "path to sysfs cgroup tree") @@ -1587,7 +1671,7 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s cgroupParentSubsystem := flags.String("cgroup-parent-subsystem", "", "use current cgroup for given subsystem as parent cgroup for container") caCertsPath := flags.String("ca-certs", "", "Path to TLS root certificates") detach := flags.Bool("detach", false, "Detach from parent process and run in the background") - stdinEnv := flags.Bool("stdin-env", false, "Load environment variables from JSON message on stdin") + stdinConfig := flags.Bool("stdin-config", false, "Load config and environment variables from JSON message on stdin") sleep := flags.Duration("sleep", 0, "Delay before starting (testing use only)") kill := flags.Int("kill", -1, "Send signal to an existing crunch-run process for given UUID") list := flags.Bool("list", false, "List UUIDs of existing crunch-run processes") @@ -1615,35 +1699,51 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s } else if err != nil { log.Print(err) return 1 - } - - if *stdinEnv && !ignoreDetachFlag { - // Load env vars on stdin if asked (but not in a - // detached child process, in which case stdin is - // /dev/null). - err := loadEnv(os.Stdin) - if err != nil { - log.Print(err) - return 1 - } + } else if flags.NArg() != 1 { + fmt.Fprintf(flags.Output(), "Usage: %s [options] containerUUID\n\nOptions:\n", prog) + flags.PrintDefaults() + return 2 } containerUUID := flags.Arg(0) switch { case *detach && !ignoreDetachFlag: - return Detach(containerUUID, prog, args, os.Stdout, os.Stderr) + return Detach(containerUUID, prog, args, os.Stdin, os.Stdout, os.Stderr) case *kill >= 0: return KillProcess(containerUUID, syscall.Signal(*kill), os.Stdout, os.Stderr) case *list: return ListProcesses(os.Stdout, os.Stderr) } - if containerUUID == "" { + if len(containerUUID) != 27 { log.Printf("usage: %s [options] UUID", prog) return 1 } + var conf ConfigData + if *stdinConfig { + err := json.NewDecoder(stdin).Decode(&conf) + if err != nil { + log.Printf("decode stdin: %s", err) + return 1 + } + for k, v := range conf.Env { + err = os.Setenv(k, v) + if err != nil { + log.Printf("setenv(%q): %s", k, err) + return 1 + } + } + if conf.Cluster != nil { + // ClusterID is missing from the JSON + // representation, but we need it to generate + // a valid config file for keepstore, so we + // fill it using the container UUID prefix. + conf.Cluster.ClusterID = containerUUID[:5] + } + } + log.Printf("crunch-run %s started", cmd.Version.String()) time.Sleep(*sleep) @@ -1651,6 +1751,16 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s arvadosclient.CertFiles = []string{*caCertsPath} } + var keepstoreLogbuf bufThenWrite + keepstore, err := startLocalKeepstore(conf, io.MultiWriter(&keepstoreLogbuf, stderr)) + if err != nil { + log.Print(err) + return 1 + } + if keepstore != nil { + defer keepstore.Process.Kill() + } + api, err := arvadosclient.MakeArvadosClient() if err != nil { log.Printf("%s: %v", containerUUID, err) @@ -1658,9 +1768,9 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s } api.Retries = 8 - kc, kcerr := keepclient.MakeKeepClient(api) - if kcerr != nil { - log.Printf("%s: %v", containerUUID, kcerr) + kc, err := keepclient.MakeKeepClient(api) + if err != nil { + log.Printf("%s: %v", containerUUID, err) return 1 } kc.BlockCache = &keepclient.BlockCache{MaxBlocks: 2} @@ -1672,6 +1782,43 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s return 1 } + if keepstore == nil { + // Log explanation (if any) for why we're not running + // a local keepstore. + var buf bytes.Buffer + keepstoreLogbuf.SetWriter(&buf) + if buf.Len() > 0 { + cr.CrunchLog.Printf("%s", strings.TrimSpace(buf.String())) + } + } else if logWhat := conf.Cluster.Containers.LocalKeepLogsToContainerLog; logWhat == "none" { + cr.CrunchLog.Printf("using local keepstore process (pid %d) at %s", keepstore.Process.Pid, os.Getenv("ARVADOS_KEEP_SERVICES")) + keepstoreLogbuf.SetWriter(io.Discard) + } else { + cr.CrunchLog.Printf("using local keepstore process (pid %d) at %s, writing logs to keepstore.txt in log collection", keepstore.Process.Pid, os.Getenv("ARVADOS_KEEP_SERVICES")) + logwriter, err := cr.NewLogWriter("keepstore") + if err != nil { + log.Print(err) + return 1 + } + cr.keepstoreLogger = NewThrottledLogger(logwriter) + + var writer io.WriteCloser = cr.keepstoreLogger + if logWhat == "errors" { + writer = &filterKeepstoreErrorsOnly{WriteCloser: writer} + } else if logWhat != "all" { + // should have been caught earlier by + // dispatcher's config loader + log.Printf("invalid value for Containers.LocalKeepLogsToContainerLog: %q", logWhat) + return 1 + } + err = keepstoreLogbuf.SetWriter(writer) + if err != nil { + log.Print(err) + return 1 + } + cr.keepstoreLogbuf = &keepstoreLogbuf + } + switch *runtimeEngine { case "docker": cr.executor, err = newDockerExecutor(containerUUID, cr.CrunchLog.Printf, cr.containerWatchdogInterval) @@ -1759,21 +1906,98 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s return 0 } -func loadEnv(rdr io.Reader) error { - buf, err := ioutil.ReadAll(rdr) +func startLocalKeepstore(configData ConfigData, logbuf io.Writer) (*exec.Cmd, error) { + if configData.Cluster == nil || configData.KeepBuffers < 1 { + return nil, nil + } + for uuid, vol := range configData.Cluster.Volumes { + if len(vol.AccessViaHosts) > 0 { + fmt.Fprintf(logbuf, "not starting a local keepstore process because a volume (%s) uses AccessViaHosts\n", uuid) + return nil, nil + } + if !vol.ReadOnly && vol.Replication < configData.Cluster.Collections.DefaultReplication { + fmt.Fprintf(logbuf, "not starting a local keepstore process because a writable volume (%s) has replication less than Collections.DefaultReplication (%d < %d)\n", uuid, vol.Replication, configData.Cluster.Collections.DefaultReplication) + return nil, nil + } + } + + // Rather than have an alternate way to tell keepstore how + // many buffers to use when starting it this way, we just + // modify the cluster configuration that we feed it on stdin. + configData.Cluster.API.MaxKeepBlobBuffers = configData.KeepBuffers + + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, err + } + _, port, err := net.SplitHostPort(ln.Addr().String()) if err != nil { - return fmt.Errorf("read stdin: %s", err) + ln.Close() + return nil, err } - var env map[string]string - err = json.Unmarshal(buf, &env) + ln.Close() + url := "http://localhost:" + port + + fmt.Fprintf(logbuf, "starting keepstore on %s\n", url) + + var confJSON bytes.Buffer + err = json.NewEncoder(&confJSON).Encode(arvados.Config{ + Clusters: map[string]arvados.Cluster{ + configData.Cluster.ClusterID: *configData.Cluster, + }, + }) if err != nil { - return fmt.Errorf("decode stdin: %s", err) + return nil, err } - for k, v := range env { - err = os.Setenv(k, v) + cmd := exec.Command("/proc/self/exe", "keepstore", "-config=-") + if target, err := os.Readlink(cmd.Path); err == nil && strings.HasSuffix(target, ".test") { + // If we're a 'go test' process, running + // /proc/self/exe would start the test suite in a + // child process, which is not what we want. + cmd.Path, _ = exec.LookPath("go") + cmd.Args = append([]string{"go", "run", "../../cmd/arvados-server"}, cmd.Args[1:]...) + cmd.Env = os.Environ() + } + cmd.Stdin = &confJSON + cmd.Stdout = logbuf + cmd.Stderr = logbuf + cmd.Env = append(cmd.Env, + "GOGC=10", + "ARVADOS_SERVICE_INTERNAL_URL="+url) + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("error starting keepstore process: %w", err) + } + cmdExited := false + go func() { + cmd.Wait() + cmdExited = true + }() + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*10)) + defer cancel() + poll := time.NewTicker(time.Second / 10) + defer poll.Stop() + client := http.Client{} + for range poll.C { + testReq, err := http.NewRequestWithContext(ctx, "GET", url+"/_health/ping", nil) + testReq.Header.Set("Authorization", "Bearer "+configData.Cluster.ManagementToken) if err != nil { - return fmt.Errorf("setenv(%q): %s", k, err) + return nil, err + } + resp, err := client.Do(testReq) + if err == nil { + resp.Body.Close() + if resp.StatusCode == http.StatusOK { + break + } + } + if cmdExited { + return nil, fmt.Errorf("keepstore child process exited") + } + if ctx.Err() != nil { + return nil, fmt.Errorf("timed out waiting for new keepstore process to report healthy") } } - return nil + os.Setenv("ARVADOS_KEEP_SERVICES", url) + return cmd, nil }