15467: Adding KeepServices WIP
[arvados.git] / services / crunch-dispatch-slurm / crunch-dispatch-slurm.go
index f77023697e0f54ccaa12e2e7bc1bf3dd39f71509..300e923640bdd0d4cb2e6bb8ecf7a5c83cfa0024 100644 (file)
@@ -7,6 +7,7 @@ package main
 // Dispatcher service for Crunch that submits containers to the slurm queue.
 
 import (
+       "bytes"
        "context"
        "flag"
        "fmt"
@@ -17,49 +18,49 @@ import (
        "strings"
        "time"
 
+       "git.curoverse.com/arvados.git/lib/config"
        "git.curoverse.com/arvados.git/lib/dispatchcloud"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
-       "git.curoverse.com/arvados.git/sdk/go/config"
        "git.curoverse.com/arvados.git/sdk/go/dispatch"
        "github.com/coreos/go-systemd/daemon"
+       "github.com/ghodss/yaml"
+       "github.com/sirupsen/logrus"
 )
 
+type logger interface {
+       dispatch.Logger
+       Fatalf(string, ...interface{})
+}
+
+const initialNiceValue int64 = 10000
+
 var (
-       version           = "dev"
-       defaultConfigPath = "/etc/arvados/crunch-dispatch-slurm/crunch-dispatch-slurm.yml"
+       version = "dev"
 )
 
 type Dispatcher struct {
        *dispatch.Dispatcher
+       logger  logrus.FieldLogger
        cluster *arvados.Cluster
        sqCheck *SqueueChecker
        slurm   Slurm
 
        Client arvados.Client
-
-       SbatchArguments []string
-       PollPeriod      arvados.Duration
-
-       // crunch-run command to invoke. The container UUID will be
-       // appended. If nil, []string{"crunch-run"} will be used.
-       //
-       // Example: []string{"crunch-run", "--cgroup-parent-subsystem=memory"}
-       CrunchRunCommand []string
-
-       // Extra RAM to reserve (in Bytes) for SLURM job, in addition
-       // to the amount specified in the container's RuntimeConstraints
-       ReserveExtraRAM int64
-
-       // Minimum time between two attempts to run the same container
-       MinRetryPeriod arvados.Duration
 }
 
 func main() {
-       disp := &Dispatcher{}
+       logger := logrus.StandardLogger()
+       if os.Getenv("DEBUG") != "" {
+               logger.SetLevel(logrus.DebugLevel)
+       }
+       logger.Formatter = &logrus.JSONFormatter{
+               TimestampFormat: "2006-01-02T15:04:05.000000000Z07:00",
+       }
+       disp := &Dispatcher{logger: logger}
        err := disp.Run(os.Args[0], os.Args[1:])
        if err != nil {
-               log.Fatal(err)
+               logrus.Fatalf("%s", err)
        }
 }
 
@@ -73,13 +74,15 @@ func (disp *Dispatcher) Run(prog string, args []string) error {
 
 // configure() loads config files. Tests skip this.
 func (disp *Dispatcher) configure(prog string, args []string) error {
+       if disp.logger == nil {
+               disp.logger = logrus.StandardLogger()
+       }
        flags := flag.NewFlagSet(prog, flag.ExitOnError)
        flags.Usage = func() { usage(flags) }
 
-       configPath := flags.String(
-               "config",
-               defaultConfigPath,
-               "`path` to JSON or YAML configuration file")
+       loader := config.NewLoader(nil, disp.logger)
+       loader.SetupFlags(flags)
+
        dumpConfig := flag.Bool(
                "dump-config",
                false,
@@ -88,8 +91,15 @@ func (disp *Dispatcher) configure(prog string, args []string) error {
                "version",
                false,
                "Print version information and exit.")
+
+       args = loader.MungeLegacyConfigArgs(logrus.StandardLogger(), args, "-legacy-crunch-dispatch-slurm-config")
+
        // Parse args; omit the first arg which is the command name
-       flags.Parse(args)
+       err := flags.Parse(args)
+
+       if err == flag.ErrHelp {
+               return nil
+       }
 
        // Print version information if requested
        if *getVersion {
@@ -97,20 +107,20 @@ func (disp *Dispatcher) configure(prog string, args []string) error {
                return nil
        }
 
-       log.Printf("crunch-dispatch-slurm %s started", version)
+       disp.logger.Printf("crunch-dispatch-slurm %s started", version)
 
-       err := disp.readConfig(*configPath)
+       cfg, err := loader.Load()
        if err != nil {
                return err
        }
 
-       if disp.CrunchRunCommand == nil {
-               disp.CrunchRunCommand = []string{"crunch-run"}
+       if disp.cluster, err = cfg.GetCluster(""); err != nil {
+               return fmt.Errorf("config error: %s", err)
        }
 
-       if disp.PollPeriod == 0 {
-               disp.PollPeriod = arvados.Duration(10 * time.Second)
-       }
+       disp.Client.APIHost = disp.cluster.Services.Controller.ExternalURL.Host
+       disp.Client.AuthToken = disp.cluster.SystemRootToken
+       disp.Client.Insecure = disp.cluster.TLS.Insecure
 
        if disp.Client.APIHost != "" || disp.Client.AuthToken != "" {
                // Copy real configs into env vars so [a]
@@ -122,23 +132,30 @@ func (disp *Dispatcher) configure(prog string, args []string) error {
                if disp.Client.Insecure {
                        os.Setenv("ARVADOS_API_HOST_INSECURE", "1")
                }
-               os.Setenv("ARVADOS_KEEP_SERVICES", strings.Join(disp.Client.KeepServiceURIs, " "))
+               ks := ""
+               if length(disp.cluster.Containers.SLURM.KeepServices) > 0 {
+                       for _, svc := range disp.cluster.Containers.SLURM.KeepServices {
+                               for k, _ := range svc.InternalURLs {
+                                       ks += k
+                                       ks += " "
+                               }
+                       }
+               }
+               os.Setenv("ARVADOS_KEEP_SERVICES", ks)
                os.Setenv("ARVADOS_EXTERNAL_CLIENT", "")
        } else {
-               log.Printf("warning: Client credentials missing from config, so falling back on environment variables (deprecated).")
+               disp.logger.Warnf("Client credentials missing from config, so falling back on environment variables (deprecated).")
        }
 
        if *dumpConfig {
-               return config.DumpAndExit(disp)
-       }
-
-       siteConfig, err := arvados.GetConfig(arvados.DefaultConfigFile)
-       if os.IsNotExist(err) {
-               log.Printf("warning: no cluster config (%s), proceeding with no node types defined", err)
-       } else if err != nil {
-               return fmt.Errorf("error loading config: %s", err)
-       } else if disp.cluster, err = siteConfig.GetCluster(""); err != nil {
-               return fmt.Errorf("config error: %s", err)
+               out, err := yaml.Marshal(cfg)
+               if err != nil {
+                       return err
+               }
+               _, err = os.Stdout.Write(out)
+               if err != nil {
+                       return err
+               }
        }
 
        return nil
@@ -148,20 +165,24 @@ func (disp *Dispatcher) configure(prog string, args []string) error {
 func (disp *Dispatcher) setup() {
        arv, err := arvadosclient.MakeArvadosClient()
        if err != nil {
-               log.Fatalf("Error making Arvados client: %v", err)
+               disp.logger.Fatalf("Error making Arvados client: %v", err)
        }
        arv.Retries = 25
 
-       disp.slurm = &slurmCLI{}
+       disp.slurm = NewSlurmCLI()
        disp.sqCheck = &SqueueChecker{
-               Period: time.Duration(disp.PollPeriod),
-               Slurm:  disp.slurm,
+               Logger:         disp.logger,
+               Period:         time.Duration(disp.cluster.Containers.CloudVMs.PollInterval),
+               PrioritySpread: disp.cluster.Containers.SLURM.PrioritySpread,
+               Slurm:          disp.slurm,
        }
        disp.Dispatcher = &dispatch.Dispatcher{
                Arv:            arv,
+               Logger:         disp.logger,
+               BatchSize:      disp.cluster.API.MaxItemsPerResponse,
                RunContainer:   disp.runContainer,
-               PollPeriod:     time.Duration(disp.PollPeriod),
-               MinRetryPeriod: time.Duration(disp.MinRetryPeriod),
+               PollPeriod:     time.Duration(disp.cluster.Containers.CloudVMs.PollInterval),
+               MinRetryPeriod: time.Duration(disp.cluster.Containers.MinRetryPeriod),
        }
 }
 
@@ -169,7 +190,7 @@ func (disp *Dispatcher) run() error {
        defer disp.sqCheck.Stop()
 
        if disp.cluster != nil && len(disp.cluster.InstanceTypes) > 0 {
-               go dispatchcloud.SlurmNodeTypeFeatureKludge(disp.cluster)
+               go SlurmNodeTypeFeatureKludge(disp.cluster)
        }
 
        if _, err := daemon.SdNotify(false, "READY=1"); err != nil {
@@ -198,50 +219,43 @@ func (disp *Dispatcher) checkSqueueForOrphans() {
        }
 }
 
-func (disp *Dispatcher) niceness(priority int) int {
-       if priority > 1000 {
-               priority = 1000
-       }
-       if priority < 0 {
-               priority = 0
+func (disp *Dispatcher) slurmConstraintArgs(container arvados.Container) []string {
+       mem := int64(math.Ceil(float64(container.RuntimeConstraints.RAM+
+               container.RuntimeConstraints.KeepCacheRAM+
+               int64(disp.cluster.Containers.ReserveExtraRAM)) / float64(1048576)))
+
+       disk := dispatchcloud.EstimateScratchSpace(&container)
+       disk = int64(math.Ceil(float64(disk) / float64(1048576)))
+       return []string{
+               fmt.Sprintf("--mem=%d", mem),
+               fmt.Sprintf("--cpus-per-task=%d", container.RuntimeConstraints.VCPUs),
+               fmt.Sprintf("--tmp=%d", disk),
        }
-       // Niceness range 1-10000
-       return (1000 - priority) * 10
 }
 
 func (disp *Dispatcher) sbatchArgs(container arvados.Container) ([]string, error) {
-       mem := int64(math.Ceil(float64(container.RuntimeConstraints.RAM+container.RuntimeConstraints.KeepCacheRAM+disp.ReserveExtraRAM) / float64(1048576)))
-
-       var disk int64
-       for _, m := range container.Mounts {
-               if m.Kind == "tmp" {
-                       disk += m.Capacity
-               }
-       }
-       disk = int64(math.Ceil(float64(disk) / float64(1048576)))
-
-       var sbatchArgs []string
-       sbatchArgs = append(sbatchArgs, disp.SbatchArguments...)
-       sbatchArgs = append(sbatchArgs, fmt.Sprintf("--job-name=%s", container.UUID))
-       sbatchArgs = append(sbatchArgs, fmt.Sprintf("--mem=%d", mem))
-       sbatchArgs = append(sbatchArgs, fmt.Sprintf("--cpus-per-task=%d", container.RuntimeConstraints.VCPUs))
-       sbatchArgs = append(sbatchArgs, fmt.Sprintf("--tmp=%d", disk))
-       sbatchArgs = append(sbatchArgs, fmt.Sprintf("--nice=%d", disp.niceness(container.Priority)))
-       if len(container.SchedulingParameters.Partitions) > 0 {
-               sbatchArgs = append(sbatchArgs, fmt.Sprintf("--partition=%s", strings.Join(container.SchedulingParameters.Partitions, ",")))
-       }
+       var args []string
+       args = append(args, disp.cluster.Containers.SLURM.SbatchArgumentsList...)
+       args = append(args, "--job-name="+container.UUID, fmt.Sprintf("--nice=%d", initialNiceValue), "--no-requeue")
 
        if disp.cluster == nil {
                // no instance types configured
+               args = append(args, disp.slurmConstraintArgs(container)...)
        } else if it, err := dispatchcloud.ChooseInstanceType(disp.cluster, &container); err == dispatchcloud.ErrInstanceTypesNotConfigured {
                // ditto
+               args = append(args, disp.slurmConstraintArgs(container)...)
        } else if err != nil {
                return nil, err
        } else {
-               sbatchArgs = append(sbatchArgs, "--constraint=instancetype="+it.Name)
+               // use instancetype constraint instead of slurm mem/cpu/tmp specs
+               args = append(args, "--constraint=instancetype="+it.Name)
        }
 
-       return sbatchArgs, nil
+       if len(container.SchedulingParameters.Partitions) > 0 {
+               args = append(args, "--partition="+strings.Join(container.SchedulingParameters.Partitions, ","))
+       }
+
+       return args, nil
 }
 
 func (disp *Dispatcher) submit(container arvados.Container, crunchRunCommand []string) error {
@@ -251,9 +265,6 @@ func (disp *Dispatcher) submit(container arvados.Container, crunchRunCommand []s
        crArgs = append(crArgs, container.UUID)
        crScript := strings.NewReader(execScript(crArgs))
 
-       disp.sqCheck.L.Lock()
-       defer disp.sqCheck.L.Unlock()
-
        sbArgs, err := disp.sbatchArgs(container)
        if err != nil {
                return err
@@ -272,10 +283,25 @@ func (disp *Dispatcher) runContainer(_ *dispatch.Dispatcher, ctr arvados.Contain
 
        if ctr.State == dispatch.Locked && !disp.sqCheck.HasUUID(ctr.UUID) {
                log.Printf("Submitting container %s to slurm", ctr.UUID)
-               if err := disp.submit(ctr, disp.CrunchRunCommand); err != nil {
+               cmd := []string{disp.cluster.Containers.CrunchRunCommand}
+               cmd = append(cmd, disp.cluster.Containers.CrunchRunArgumentsList...)
+               if err := disp.submit(ctr, cmd); err != nil {
                        var text string
-                       if err == dispatchcloud.ErrConstraintsNotSatisfiable {
-                               text = fmt.Sprintf("cannot run container %s: %s", ctr.UUID, err)
+                       if err, ok := err.(dispatchcloud.ConstraintsNotSatisfiableError); ok {
+                               var logBuf bytes.Buffer
+                               fmt.Fprintf(&logBuf, "cannot run container %s: %s\n", ctr.UUID, err)
+                               if len(err.AvailableTypes) == 0 {
+                                       fmt.Fprint(&logBuf, "No instance types are configured.\n")
+                               } else {
+                                       fmt.Fprint(&logBuf, "Available instance types:\n")
+                                       for _, t := range err.AvailableTypes {
+                                               fmt.Fprintf(&logBuf,
+                                                       "Type %q: %d VCPUs, %d RAM, %d Scratch, %f Price\n",
+                                                       t.Name, t.VCPUs, t.RAM, t.Scratch, t.Price,
+                                               )
+                                       }
+                               }
+                               text = logBuf.String()
                                disp.UpdateState(ctr.UUID, dispatch.Cancelled)
                        } else {
                                text = fmt.Sprintf("Error submitting container %s to slurm: %s", ctr.UUID, err)
@@ -310,7 +336,7 @@ func (disp *Dispatcher) runContainer(_ *dispatch.Dispatcher, ctr arvados.Contain
                case <-ctx.Done():
                        // Disappeared from squeue
                        if err := disp.Arv.Get("containers", ctr.UUID, nil, &ctr); err != nil {
-                               log.Printf("Error getting final container state for %s: %s", ctr.UUID, err)
+                               log.Printf("error getting final container state for %s: %s", ctr.UUID, err)
                        }
                        switch ctr.State {
                        case dispatch.Running:
@@ -327,17 +353,21 @@ func (disp *Dispatcher) runContainer(_ *dispatch.Dispatcher, ctr arvados.Contain
                                log.Printf("container %s has state %q, priority %d: cancel slurm job", ctr.UUID, updated.State, updated.Priority)
                                disp.scancel(ctr)
                        } else {
-                               disp.renice(updated)
+                               p := int64(updated.Priority)
+                               if p <= 1000 {
+                                       // API is providing
+                                       // user-assigned priority. If
+                                       // ctrs have equal priority,
+                                       // run the older one first.
+                                       p = int64(p)<<50 - (updated.CreatedAt.UnixNano() >> 14)
+                               }
+                               disp.sqCheck.SetPriority(ctr.UUID, p)
                        }
                }
        }
 }
-
 func (disp *Dispatcher) scancel(ctr arvados.Container) {
-       disp.sqCheck.L.Lock()
        err := disp.slurm.Cancel(ctr.UUID)
-       disp.sqCheck.L.Unlock()
-
        if err != nil {
                log.Printf("scancel: %s", err)
                time.Sleep(time.Second)
@@ -346,34 +376,3 @@ func (disp *Dispatcher) scancel(ctr arvados.Container) {
                time.Sleep(time.Second)
        }
 }
-
-func (disp *Dispatcher) renice(ctr arvados.Container) {
-       nice := disp.niceness(ctr.Priority)
-       oldnice := disp.sqCheck.GetNiceness(ctr.UUID)
-       if nice == oldnice || oldnice == -1 {
-               return
-       }
-       log.Printf("updating slurm nice value to %d (was %d)", nice, oldnice)
-       disp.sqCheck.L.Lock()
-       err := disp.slurm.Renice(ctr.UUID, nice)
-       disp.sqCheck.L.Unlock()
-
-       if err != nil {
-               log.Printf("renice: %s", err)
-               time.Sleep(time.Second)
-               return
-       }
-       if disp.sqCheck.HasUUID(ctr.UUID) {
-               log.Printf("container %s has arvados priority %d, slurm nice %d",
-                       ctr.UUID, ctr.Priority, disp.sqCheck.GetNiceness(ctr.UUID))
-       }
-}
-
-func (disp *Dispatcher) readConfig(path string) error {
-       err := config.LoadFile(disp, path)
-       if err != nil && os.IsNotExist(err) && path == defaultConfigPath {
-               log.Printf("Config not specified. Continue with default configuration.")
-               err = nil
-       }
-       return err
-}