Failures¶
Any call may fail, wherever the callee runs. Myxo makes failures look the same in every layout, so that code handling them doesn't depend on where the services run.
The errors of a call¶
| Error | Status | Meaning | What the caller does |
|---|---|---|---|
NotFound |
404 | what was asked for does not exist | handles it as an answer |
Invalid |
422 | the request is wrong, a malformed message included | handles it as an answer |
Unavailable |
503 | the callee is not running, cannot be reached, or did not answer within call_timeout |
retries, if the operation allows it |
RemoteError |
500 | the callee failed with an error it did not declare, or answered with a message the caller cannot decode | treats it as a bug |
All four derive from myxo.ServiceError, which gives each a status. A service raises the first three on purpose. The runtime raises RemoteError for any other exception, and logs the traceback where it happened. One handler in the guestbook's web service turns every error into an HTTP answer:
def error(_request: Request, exc: ServiceError) -> Response:
body = {"error": type(exc).__name__, "detail": str(exc)}
return Response(body, status_code=exc.status)
Retrying¶
The runtime can't know which operations are safe to repeat, so it never retries a call by itself. You choose where:
retry makes the call again with a backoff while it raises Unavailable. It tries 5 times by default, with jitter, and delays that double from 0.5 s up to 10 s. With attempts=None, it retries until cancelled.
Inside a process: services restart¶
Each service, consumer, worker and listener runs under a supervisor, which restarts it on any exception, with a fresh instance of the service. The delay starts at 0.5 s and doubles to 30 s. A run longer than 60 s resets it. After more than 5 failures within 60 s, the process gives up and exits with status 1, for whatever runs it to restart. The [supervisor] section of the layout sets these numbers.
A restart is one for one: the others keep running, and calls to the restarting service raise Unavailable meanwhile.
Around a process: the platform restarts it¶
| Runs under | Restarts a dead process | Measured, kill to work resumed |
|---|---|---|
myxo up |
with the layout's supervisor policy | |
| systemd | Restart=on-failure, after 2 s |
3.6 s |
| Kubernetes | the deployment | 4.4 s |
| Hop3 | the Hop3 process manager | 1.3 s |
The measurements come from E10: a killed worker, and the time until it finished a new job. Each is a single run.
All of them stop a process with SIGTERM. The process ends long polls and streams, stops the services, and exits with status 0.
Readiness¶
Every process with a url serves two paths on the listener:
/healthzanswers 200 while the process listens./readyzanswers 200 once the process's services run and pass theircheck(), or 503 with the list of what is wrong. An empty list means ready.
A service's check() covers what it owns, such as a database. The processes it calls are left out on purpose: behind services that route only to ready pods, two processes that call each other would wait for each other forever. A caller handles an unavailable callee as it handles any failed call.
Events and jobs¶
Work that outlives a call has separate guarantees:
- A consumer of events sees each event at least once.
Unavailablebrings the event back after a backoff. Any other error stops the consumer, which the supervisor restarts. - A job runs at least once. A job that raises is retried with a backoff, up to the
attemptslimit, then kept and marked failed, with the error. - A schedule runs at most once per period. A period no process led is skipped. A failed run is not retried.
So a handler or a job must be harmless to repeat, and work that must happen goes in a job.