Python API¶
Myxo 0.x promises no stability: any name on this page may change in a minor release. The changelog lists each change, with what to do about it. The package is typed (py.typed).
myxo.Service¶
A service subclasses myxo.Service:
| Attribute | What it is |
|---|---|
name |
the service's name in layouts and entry points |
stage |
within a process, services start by stage, each once the previous stage is ready (1 by default); processes start independently, and calls between them handle Unavailable |
api |
the Protocol it implements for other services, if any |
Config |
a msgspec.Struct, filled from [services.NAME] |
migrate(config) |
a class method that brings the service's storage up to date; it must be safe to run again |
run(ready) |
serves until cancelled, calling ready() once it can take calls; by default it is ready at once |
check() |
raises Unavailable when it cannot serve, because the database is unreachable, say; part of /readyz |
The runtime gives each instance self.config and self.runtime. A service that overrides __init__ takes (config, runtime) and calls super().__init__(config, runtime). Each restart makes a new instance.
myxo.Runtime¶
A service reaches the process through self.runtime:
| Member | What it is |
|---|---|
client(API) |
a client for the service implementing API, wherever the layout places it; create it in __init__ or later |
provides(API) |
whether a service in the layout implements API, for an optional dependency |
stopping |
an asyncio.Event, set when the process begins to stop |
layout, process |
the layout, and this process's settings |
Interfaces and messages¶
An interface is a Protocol of async methods, each taking one message and returning one or None. An operation that needs no input takes an empty struct. Messages are frozen msgspec.Structs whose fields are immutable all the way down: numbers, strings, bytes, booleans, dates and times, UUIDs, decimals, enums, literals, tuples, frozensets, and other frozen structs. The runtime refuses others when it starts, and myxo check before.
The framework stores an event or a job under its type's module.qualname. Before moving or renaming such a class, give it that name with the decorator @myxo.topic("app.events.Noted"). What is stored under the name then still reaches the class. myxo migrate warns about stored jobs that no service of the layout handles.
Errors¶
From myxo. Calls raise the same errors in every layout:
| Error | Status | Meaning |
|---|---|---|
NotFound |
404 | what was asked for does not exist |
Invalid |
422 | the request is wrong, a malformed message included |
Unavailable |
503 | the callee is not running, cannot be reached, or did not answer within call_timeout |
RemoteError |
500 | the callee failed with an error it did not declare, or its answer does not decode: a bug, or two versions of a message |
All derive from ServiceError, whose status attribute gives the number.
myxo.retry¶
The helper await myxo.retry(call, arg, attempts=5, first=0.5, cap=10.0) calls again with backoff while the call raises Unavailable, for operations that are safe to repeat; attempts=None retries until cancelled.
myxo.db¶
A service keeps its own data with SQLAlchemy:
make_engine(url, **options): an engine with the asynchronous drivers, from apostgresql://orsqlite://URL, with SQLite in WAL mode and a busy timeout of 5 s. Options go to SQLAlchemy.DB_ERRORS: the exceptions a database operation raises when something is wrong. A service'scheck()catches them and raisesUnavailable.
myxo.transport¶
await serve_asgi(app, address, ready, stopping) serves any ASGI application until cancelled, on (host, port) or on a Unix socket's path. ready is called once it listens, and stopping before the graceful stop. A web service passes self.runtime.stopping.set, so that long polls and streams end first.
Entry points and extras¶
A package declares services in the myxo.services entry points, by name, and interfaces in myxo.apis:
[project.entry-points."myxo.services"]
book = "guestbook.book:Book"
[project.entry-points."myxo.apis"]
book = "guestbook.api:BookAPI"
The runtime reads entry points from the installed package: after adding one, install the package again. A process imports only the services it runs, and reaches the others through their interfaces alone, so an interface module must stay light: messages and a Protocol, and no import of the implementation. A service with heavy dependencies, such as machine-learning libraries, declares them as an optional extra of the package (pip install app[render]), so that an image for the other processes leaves them out (E5).
The framework's services¶
Each runs on SQLite and on PostgreSQL, in the layout's database, whose tables (myxo_*) myxo migrate creates. A service declares what it uses as class attributes or decorators, so that a process loads them only if the services it runs use them.
Events: myxo.events¶
events = Events(), thenawait self.events.publish(tx, event)with the service's SQLAlchemy connection or session: the event commits or rolls back with the transaction.@subscribe(EventType)on a method: it runs after the commit, at least once for the consumer group (the service's name, orgroup=), one member at a time, in the order of the commits. A new group starts from the oldest event kept.Unavailablebrings the event back after a backoff; any other error stops the consumer, which the supervisor restarts.@subscribe(EventType, in_transaction=True): the method takes(tx, event)and runs insidepublish, so that the method's writes commit with the event; it runs only in the publisher's process.await myxo.events.publish(tx, event): the same, from code outside the layout's services, such as a command that imports data; it runs noin_transactionhandler.await self.events.read(after, (EventType,), wait): the events after a position, waiting up towaitseconds; it returns at once when the process stops.
Handled events are kept an hour (RETENTION), for readers that resume, then pruned by one process.
Push: myxo.push¶
Push(bus, (EventType,), keepalive=15.0) is an ASGI app that streams events to browsers as Server-Sent Events, resuming after Last-Event-ID. bus is a service's self.events.
Jobs: myxo.jobs¶
jobs = Jobs(), thenawait self.jobs.put(job, tx=None, key=None, delay=0.0): durable; withtx, in the caller's transaction; withkey, nothing happens while a job with that key is in the queue; a done job's key is free again.@job(JobType, workers=1, attempts=10, lease=30.0, backoff=1.0)on a method: runs each job at least once. A claim lastsleaseseconds, renewed while the job runs; a job whose worker died is claimed again. A job that raises is retried afterbackoffseconds, doubling to 5 minutes (or tobackoff, if longer), then kept, marked failed, with the error.
Schedules: myxo.schedule¶
@scheduled(every=timedelta(hours=1)), or every="period" to read the period in seconds from the service's configuration: the method runs once per period, from one leader in the installation, at most once even when the leader changes. A period that no process led is skipped; for work that must happen, the method puts a job.
Caches: myxo.cache¶
LocalCache(ttl, size=10_000): in the process, for what no other process needs. Its methods are synchronous:get(key), which returnsNoneif the key is absent or expired,set(key, value)anddelete(key).cache = Cache(), thenawait self.cache.get(key, Type),set(key, value, ttl)anddelete(key): shared by every process, in an unlogged table on PostgreSQL; a crash of the database empties it.deleteis durable, so that a revocation stays revoked.
Health¶
Every process with a url serves two paths on the listener:
/healthz: 200 while it listens;/readyz: 200 once the process's services run and pass theircheck(), else 503 with what is wrong. The processes it calls are left out: behind services that route only to ready pods, two processes that call each other would wait for each other.
The wire format¶
A call to another process is POST /rpc/SERVICE/OPERATION on that process's listener, with the message as JSON. A success answers 200, with the result as JSON, or an empty body for None. A failure answers the error's status, with {"error": "Invalid", "detail": "..."}. The caller turns that back into the same exception.
Fault injection¶
MYXO_FAULTS names a JSON file that makes calls fail or services crash, for experiments. It is off by default, and re-read whenever it changes:
{"calls": {"catalog.get_item": {"raise": true}, "catalog.list_items": {"sleep": 3.0}},
"crash": {"catalog": 1}}
A call fault applies to every call of that operation, in-process or over HTTP, while the file says so. A crash makes the service's task fail once for each increment of the counter.