Diviner Phabricator User Docs Command Line Exit Codes

Command Line Exit Codes
Phabricator User Documentation (Field Manuals)

Explains the use of exit codes in Phabricator command line scripts.

Overview

When you run a command from the command line, it exits with an exit code. This code is normally not shown on the CLI, but you can examine the exit code of the last command you ran by looking at $? in your shell:

$ ls
...
$ echo $?
0

Programs which run commands can operate on exit codes, and shell constructs like cmdx && cmdy operate on exit codes.

The code 0 means success. Other codes signal some sort of error or status condition, depending on the system and command.

With rare exception, Phabricator uses all other codes to signal catastrophic failure.

This is an explicit architectural decision and one we are unlikely to deviate from: generally, we will not accept patches which give a command a nonzero exit code to indicate an expected state, an application status, or a minor abnormal condition.

Generally, this decision reflects a philosophical belief that attaching application semantics to exit codes is a relic of a simpler time, and that they are not appropriate for communicating application state in a modern operational environment. This document explains the reasoning behind our use of exit codes in more detail.

In particular, this approach is informed by a focus on operating Phabricator clusters at scale. This is not a common deployment scenario, but we consider it the most important one. Our use of exit codes makes it easier to deploy and operate a Phabricator cluster at larger scales. It makes it slightly harder to deploy and operate a small cluster or single host by gluing together bash scripts. We are willingly trading the small scale away for advantages at larger scales.

Problems With Exit Codes

We do not use exit codes to communicate application state because doing so makes it harder to write correct scripts, and the primary benefit is that it makes it easier to write incorrect ones.

This is somewhat at odds with the philosophy of "worse is better", but a modern operations environment faces different forces than the interactive shell did in the 1970s, particularly at scale.

We consider correctness to be very important to modern operations environments. In particular, we manage a Phabricator cluster (Phacility) and believe that having reliable, repeatable processes for provisioning, configuration and deployment is critical to maintaining and scaling our operations. Our use of exit codes makes it easier to implement processes that are correct and reliable on top of Phabricator management scripts.

Exit codes as signals for application state are problematic because they are ambiguous: you can't use them to distinguish between dissimilar failure states which should prompt very different operational responses.

Exit codes primarily make writing things like bash scripts easier, but we think you shouldn't be writing bash scripts in a modern operational environment if you care very much about your software working.

Software environments which are powerful enough to handle errors properly are also powerful enough to parse command output to unambiguously read and react to complex state. Communicating application state through exit codes almost exclusively makes it easier to handle errors in a haphazard way which is often incorrect.

Exit Codes are Ambiguous

In many cases, exit codes carry very little information and many different conditions can produce the same exit code, including conditions which should prompt very different responses.

The command line tool grep searches for text. For example, you might run a command like this:

$ grep zebra corpus.txt

This searches for the text zebra in the file corpus.txt. If the text is not found, grep exits with a nonzero exit code (specifically, 1).

Suppose you run grep zebra corpus.txt and observe a nonzero exit code. What does that mean? These are some of the possible conditions which are consistent with your observation:

  • The text zebra was not found in corpus.txt.
  • corpus.txt does not exist.
  • You do not have permission to read corpus.txt.
  • grep is not installed.
  • You do not have permission to run grep.
  • There is a bug in grep.
  • Your grep binary is corrupt.
  • grep was killed by a signal.

If you're running this command interactively on a single machine, it's probably OK for all of these conditions to be conflated. You aren't going to examine the exit code anyway (it isn't even visible to you by default), and grep likely printed useful information to stderr if you hit one of the less common issues.

If you're running this command from operational software (like deployment, configuration or monitoring scripts) and you care about the correctness and repeatability of your process, we believe conflating these conditions is not OK. The operational response to text not being present in a file should almost always differ substantially from the response to the file not being present or grep being broken.

In a particularly bad case, a broken grep might cause a careless deployment script to continue down an inappropriate path and cascade into a more serious failure.

Even in a less severe case, unexpected conditions should be detected and raised to operations staff. grep being broken or a file that is expected to exist not existing are both detectable, unexpected, and likely severe conditions, but they can not be differentiated and handled by examining the exit code of grep. It is much better to detect and raise these problems immediately than discover them after a lengthy root cause analysis.

Some of these conditions can be differentiated by examining the specific exit code of the command instead of acting on all nonzero exit codes. However, many failure conditions produce the same exit codes (particularly code 1) and there is no way to guarantee that a particular code signals a particular condition, especially across systems.

Realistically, it is also relatively rare for scripts to even make an effort to distinguish between exit codes, and all nonzero exit codes are often treated the same way.

Bash Scripts are not Robust

Exit codes that indicate application status make writing bash scripts (or scripts in other tools which provide a thin layer on top of what is essentially bash) a lot easier and more convenient.

For example, it is pretty tricky to parse JSON in bash or with standard command-line tools, and much easier to react to exit codes. This is sometimes used as an argument for communicating application status in exit codes.

We reject this because we don't think you should be writing bash scripts if you're doing real operations. Fundamentally, bash shell scripts are not a robust building block for creating correct, reliable operational processes.

Here is one problem with using bash scripts to perform operational tasks. Consider this command:

$ mysqldump | gzip > backup.sql.gz

Now, consider this command:

$ mysqldermp | gzip > backup.sql.gz

These commands represent a fairly standard way to accomplish a task (dumping a compressed database backup to disk) in a bash script.

Note that the second command contains a typo (dermp instead of dump) which will cause the command to exit abruptly with a nonzero exit code.

However, both these statements run successfully and exit with exit code 0 (indicating success). Both will create a backup.sql.gz file. One backs up your data; the other never backs up your data. This second command will never work and never do what the author intended, but will appear successful under casual inspection.

These behaviors are the same under set -e.

This fragile attitude toward error handling is endemic to bash scripts. The default behavior is to continue on errors, and it isn't easy to change this default. Options like set -e are unreliable and it is difficult to detect and react to errors in fundamental constructs like pipes. The tools that bash scripts employ (like grep) emit ambiguous error codes. Scripts can not help but propagate this ambiguity no matter how careful they are with error handling.

It is likely possible to implement these things safely and correctly in bash, but it is not easy or straightforward. More importantly, it is not the default: the default behavior of bash is to ignore errors and continue.

Gluing commands together in bash or something that sits on top of bash makes it easy and convenient to get a process that works fairly well most of the time at small scales, but we are not satisfied that it represents a robust foundation for operations at larger scales.

Reacting to State

Instead of communicating application state through exit codes, we generally communicate application state through machine-parseable output with a success (0) exit code. All nonzero exit codes indicate catastrophic failure which requires operational intervention.

Callers are expected to request machine-parseable output if necessary (for example, by passing a --json flag or other similar flags), verify the command exits with a 0 exit code, parse the output, then react to the state it communicates as appropriate.

In a sufficiently powerful scripting environment (e.g., one with data structures and a JSON parser), this is straightforward and makes it easy to react precisely and correctly. It also allows scripts to communicate arbitrarily complex state. Provided your environment gives you an appropriate toolset, it is much more powerful and not significantly more complex than using error codes.

Most importantly, it allows the calling environment to treat nonzero exit statuses as catastrophic failure by default.

Moving Forward

Given these concerns, we are generally unwilling to bring changes which use exit codes to communicate application state (other than catastrophic failure) into the upstream. There are some exceptions, but these are rare. In particular, ease of use in a bash environment is not a compelling motivation.

We are broadly willing to make output machine parseable or provide an explicit machine output mode (often a --json flag) if there is a reasonable use case for it. However, we operate a large production cluster of Phabricator instances with the tools available in the upstream, so the lack of machine parseable output is not sufficient to motivate adding such output on its own: we also need to understand the problem you're facing, and why it isn't a problem we face. A simpler or cleaner approach to the problem may already exist.

If you just want to write bash scripts on top of Phabricator scripts and you are unswayed by these concerns, you can often just build a composite command to get roughly the same effect that you'd get out of an exit code.

For example, you can pipe things to grep to convert output into exit codes. This should generally have failure rates that are comparable to the background failure level of relying on bash as a scripting environment.