Link Search Menu Expand Document

Requests

Requests are specified in YAML files. On startup, the Request Manager (RM) reads and checks all specs: all the .yaml files in specs.dir. All files in and under the specs directory ending with .yaml (case-insensitive) will be assumed to be specs files.

All spec files have the same syntax. The RM combines specs from multiple files to complete a request. One sequence per file and descriptively named files help keep all the specs oranized and easy to find by humans.

Sequence Spec

The root of every spec file is a sequence spec:

---
sequences:
  stop-container:
    request: true
    args:
      required:
        - name: containerName
          desc: "Container name to stop"
      optional:
        - name: restart
          desc: "Restart the container if \"yes\""
          default: ""
      static:
        - name: slackChan
          default: "#dba"
    acl:
      - role: eng
        ops: admin
      - role: ba
        ops: ["start","stop"]
    nodes:
      NODE_SPECS

This defines one or more sequences under sequences:. Although multiple sequence can be defined in a single file, we suggest one sequence per file.

The example above defines one sequence called “stop-container”. (We would name this file stop-container.yaml.) If request: true, the sequence is a request that callers can make. This also makes spinc (ran without any command line options) list the request. Set request: true only for top-level sequences that you want to expose to users as requests. All requests are sequences, but not all sequences are requests. To distinguish:

  • request: a sequence with request: true
  • non-request sequence (NRS): a sequence with request: false (usually omitted)
  • sequence: any and all sequences, generally speaking

args:

Sequences have three types of arguments (args): required, optional, and static.

  • required: args are, unsurprisingly, required. For requests, required args are provided by the caller, so they should be kept to a minimum—require the user to provide only what is necessary and sufficient to start the request, then figure out other args in jobs. For non-request sequences (NRS), required args are provided by the parent node (nodes are discussed in the next section).
  • optional: args are optional. If not explicitly given, the default value in the spec is used. In the example above, arg “restart” defaults to an empty string unless the user provides a value.
  • static: args are fixed values. Static arg “slackChan” has value “#dba”. Static args are useful when the value is known but differs in different sequences. For example, another request might set slackChan=#yourTeam to get Slack notifications at #yourTeam instead of #dba. This could also be solved by making slackChan a required or optional arg.

In job args, there are no distinctions. jobArgs["slackChan"] is the same as jobArgs["containerName"], and jobs can change its value.

Node Specs

A sequence is one or more node (vertex in the graph) defined under nodes:. There are three types of node specs. Shared fields (e.g. retry:) are only described once.

Job Node

A job node specifies a job to run. (If this was a tree data structure, these would be leaf nodes.) Every sequence eventually leads to job nodes. This is where work happens:

      expand-cluster:
        category: job
        type: etre/expand-cluster
        args:
          - expected: cluster
            given: cluster
        sets:
          - arg: app   # string
            as: clusterApp
          - arg: env   # string
          - arg: nodes # []string
        retry: 2
        retryWait: 3s
        deps: []

All node specs begin with a node name: “expand-cluster”, in this case. Node names must be unique within the sequence. (Spin Cycle makes nodes unique within a request by assigning them an internal job ID.) category: job makes this node a job node. type: specifies the job type: “etre/expand-cluster”. The jobs.Factory in your jobs repo must be able to make a job of this type.

args: lists all job args that the job requires. expected: is the job arg name that the job expects, and given: is the job arg name in the specs to use. In other words, jobArgs[expected] = jobArgs[given]. This is useful because it is nearly impossible to make all job args in specs match all job args in jobs. For example, a spec might use “host” for a server’s hostname, but a job uses “hostname”. In this case,

- expected: hostname
  given: host

makes Spin Cycle do jobArgs["hostname"] = jobArgs["host"] before passing jobArgs to the job.

If expected == given, given: may be omitted.

Only job args listed under args: are passed to the job. If a job needs arg “foo” but “foo” is not listed, then jobArgs["foo"] will be nil in the job. This requirement is strict and somewhat tedious, but it makes specs complete self-describing and easy to follow because there are no “hidden” args.

If a job has optional args, they must be listed so they are passed to the job, in case they exist. The job is responsible for using the optional args or not. (Note: “optional” here is not the same as sequence-level optional args.)

sets: specifies the job args that the job sets. The RM checks this. arg: and as: are to sets: as expected: and given: are to args: above: arg: is the job arg name set by the job, and as: is the job arg name in the specs to use.

If arg == as, as: may be omitted.

In the example above, the job sets “app” (remapped to “clusterApp” by the RM), “env”, and “node” in jobArgs. After calling the job’s Create method, the RM checks that all three are set in jobArgs (with any value, including nil). Like args:, this is strict but makes it possible to follow every arg through different sequences. It also makes it explicit which jobs set which args.

retry: and retryWait: specify how many times the JR should retry the job if Run does not return proto.STATE_COMPLETE. The job is always ran once, so total runs is 1 + retry. retryWait is the wait time between tries. It is a time.Duration string like “3s” or “500ms”. If not specified, the default is no wait between tries.

deps: is a list of node names that this node depends on. For nodes A and B, if B depends on A, the graph is A -> B. The JR runs B only after A completes successfully. A node can depend on many nodes, creating fan-out and fan-in points:

      B ->
    /      \
A ->        +-> E
    \      /
      C ->

Node E has deps: [B,C]. Nodes B and C have deps: [A]. Node A has deps: [].

deps: determines the order of nodes, not the order of node specs in the file. Every sequence must have a node with deps: [] (the first node in the sequence). Cycles are not allowed.

Sequence Node

All node specs begin with a node name: “notify-app-owners”, in this case. category: sequence makes this node a sequence node. type: specifies the sequence name: “notify-app-owners”. A node and sequence can have the same name. Whereas a job node runs a job, a sequence node imports another sequence.

      notify-app-owners:
        category: sequence
        type: notify-app-owners
        args:
          - expected: appName
            given: app
          - expected: env
            given: env
        sets:
          - arg: messageSent
            as: message
        deps: [expand-cluster]
        retry: 9
        retryWait: 5000ms

When the RM encounters this sequence node, it looks for a sequence called “notify-app-owners”. (We would put that sequence in a file named notify-app-owners.yaml.) It replaces the sequence node with all the nodes in the target sequence. Since sequences can have required args, the sequence node must specify the args: to pass to the sequence (as if the sequence was a request). The same rules about args:, expected:, and given: apply. The only difference is that the job args are passed to a sequence instead of a job.

sets: also applies to sequences - it specifies the job args that the jobs within the sequence set. In this example, the “notify-app-owners” sequence sets the job “messageSent”, which is renamed to “message” for use in this spec. One of the jobs within the “notify-app-owners” sequence must therefore set the arg “messageSent”.

The same rules about deps: apply (described above). In this example, the “notify-app-owners” sequence is not called until the “expand-cluster” node is complete and successful. Likewise, if another node deps: [notify-app-owners], it is not called until the entire sequence is complete and successful. The sequence node, at this point in the spec, acts like a single node—it just happens to contain/run other nodes and sequences.

retry: and retryWait: apply to sequences, too. If any job in the sequence fails, the entire sequence is retried from its beginning.

Sequences of Sequences

Sequences “calling” sequences are how large requests are built. Like a job, a sequence is a unit of work—a bigger unit of work. The “notify-app-owners” sequence, for example, might have several jobs which detremine who the app owners are, what their notification preferences are, and then notify them accordingly. That is one unit of work: notifying app owners. It is also a reusable unit of work.

Conditional Node

category: conditional makes this node a conditional node. if: specifies the job arg to use, and eq: operates like a switch cases on the if: job arg value.

      restart-vttablet:
        category: conditional
        if: vitess
        eq:
          yes: restart-vttablet
          default: noop
        args:
          - expected: node
            given: name
          - expected: host
            given: physicalHost
        deps: [bgp-peer-node]

In the example above, if jobArgs["vitess"] == "yes", then sequence “restart-vttablet” is called. Else, the default is a special, built-in sequence called “noop” which does nothing. In the case that jobArgs["vitess"] == "yes" and sequence “restart-vttablet” is called, the node acts exactly like a sequence node.

All values and comparison are expected to be strings. The if: job arg must be set with a string value, and the values listed under eq: are string values, except “default” which is a special case.

Conditional nodes can be used to switch between alternatives or, like the example above, do nothing in one part of a spec (but do everything else before and after).

Conditional nodes can also set job args using sets:, just as in job or sequence nodes. In order for a conditional node to set an arg, every sequence that the conditional may call must set that arg.

Sequence Expansion

Sequence expansion is possible in sequence and conditional nodes with each::

      decomm-nodes:
        category: sequence
        type: decomm-node
        each:
          - nodeHostname:node
          - hosts:host
        parallel: maxParallel
        args: []
          - expected: archiveData
            given: archiveData
        deps: []

each: takes a list of job arg names and “expands” the sequence, “decomm-node”, in parallel for each job arg value. Expanded sequences are ran in parallel.

parallel: takes a positive integer. At most maxParallel expanded sequences will run in parallel at a given time.

The job args must be type []string of equal lengths. In this example, the job args could be:

nodeHostname := []string{"node1", "node2"}
hosts := []string{"host1", "host2"}

The syntax is list:element where each jobArg[element] is initialized from the next value in list. The target sequence should require element.

The args: are passed to each expanded sequence as-is, i.e. each “decomm-node” sequence receives jobArgs[archiveData].

A conditional node with sequence expansion expands the sequence that matches if: and eq:.

The same rules about deps: apply (described above).

Linter

The RM checks all spec files on startup. This includes static checks, most of which can be performed by looking at a single node or sequence, and graph checks, which necessarily involve building graphs that describe the request specs. If some (less important) checks fail, the RM logs warnings; if others fail, the RM logs those errors and fails.

Some examples of static checks: ensuring that the category field is one of job, conditional, or sequence; checking that a sequence has at least one node; making sure a sequence node calls an actual sequence.

Some examples of graph checks: catching circular dependencies; making sure all job args for a node has been set by previous nodes, or by the sequence.

spinc-linter CLI

spinc-linter is a CLI into a local build of the linter (and only the linter). It runs exactly the same checks that the RM does on startup and logs all errors to stdout. Any errors thrown by linter should be addressed, because they will cause the RM to fail. Warnings should be ignored with caution; they indicate likely typos or mistakes in the specs.