Canary: a small idea about service lifecycles
Who this is for: Python backend developers who have started wondering how the services in their app should relate to each other. This post is about how Canary Framework grew out of a small idea.
Where it started: FastAPI is great, but I wanted one more layer
I started with FastAPI. It is well designed and restrained: it does what it should and nothing more.
But after using it for a while and comparing it with Spring in the Java world, I felt that Python has no single, unified framework. FastAPI has dependency injection, but nothing on the ORM side; every project glues its own set of libraries together.
So I thought I would design a small scaffold for myself.
A lot of complex business logic needs a layer in between, to buffer the dependencies between the API layer and the service layer. You can do that in FastAPI, but it needs a good design up front: what goes where, who depends on whom, who initializes first. To me that is a mental burden — at least that is how it feels to me.
And when writing code with AI, a structure held together only by convention is easily pulled off course by the AI’s little ideas: an extra global object here today, an extra bit of initialization there tomorrow, and the structure slowly falls apart.
Every service has a life of its own
So I started thinking about how services relate to each other. It is much like the idea of microservices: every service has a life of its own — it initializes, it starts, and eventually it stops.
1 | from canary_framework import Canary, init, start, stop |
If I know these three APIs of a service — init(), start(), stop() — I can manage it, without knowing what happens inside.
Dependencies: a beautiful recursive divide and conquer
The next problem is that services depend on each other. I can’t do too much work in one service, so one service depends on another:
1 | from canary_framework import dep |
dep() declares the dependency, and to the type checker self.database is simply a Database, so the IDE completes it.
With dependencies, a service’s dependencies must all start before it does. And starting a dependency repeats the same thing — first its own dependencies, then itself.
It is a beautiful recursive divide and conquer. I only tell the top service to start, and the whole tree comes up in order:
1 | async with UserService() as service: |
Order, cycles and concurrency: one graph
The recursion leaves three problems: dependency cycles, start order, and concurrency.
My answer is to keep a dependency subgraph when starting: from the service being started, follow dep() and collect everything it depends on. With that graph:
- cycles are found while building it, and reported before any service actually runs;
- topological order gives the sequence, so “dependencies start first, the service cleans up first” comes naturally;
- services that don’t depend on each other start at the same time; each one only waits for its own dependencies.
Stopping mirrors starting: stop the service itself, then try to stop its dependencies. A dependency may be shared by several services, so it only really stops when nothing is still using it.
Phases instead of a state machine
With concurrency, the order in which nodes run is no longer predictable. The same service can be reached along several dependency paths at once, and without care its lifecycle could run twice.
I didn’t bring in something as heavy as a state machine. Instead there is a simple phase: each phase declares the phase that must come before it. Since the lifecycle is fixed:
- if the preceding phase hasn’t run, the current one won’t — calling
startwithoutinitraises an error instead of silently skipping; - a phase that has already run is skipped, so a service runs each phase only once.
init, start and stop are three phases themselves. I made this API public too, so you can define your own phase after some point in the lifecycle:
1 | from canary_framework import Canary, Phase, enter, init, leave |
Failure handling: the most careful part
Failure handling is the most careful and most important part of any program, and even more so of a framework. I had to think through which errors should reach the user, which the framework can catch and handle itself, and how resources get released after something goes wrong.
There are a few mechanisms here that I find interesting:
- A failed service cleans up after itself. If a service raises while starting, it runs its own
stopat once and releases what it already acquired. - Without its dependencies, a service doesn’t start. Services that depend on the failed one won’t start, and release the dependencies they were holding. Other services starting at the same time are not interrupted; when they finish, they are released too if nobody uses them. So when
start()raises, everything it started has already been cleaned up, and retrying is just calling it again. - Cleanup is not interrupted. If startup is cancelled (a timeout, Ctrl-C), cleanup still runs to completion before the cancellation is passed on.
- Errors are reported as they are. What you see is the exception your own hook raised; the same failure isn’t reported twice, and the framework’s internal exceptions don’t leak out. If cleanup fails too, that error is attached to the original exception as a note instead of covering it.
To check all of this, I use randomized tests that generate all kinds of dependency graphs with combinations of failures and cancellations, and check properties like “every acquired resource is released exactly once”.
Finally
The rest was tidying up: tests, benchmarks, docs, examples and so on.
1 | pip install canary-framework |
- Docs: https://hotcocoacanary.github.io/Canary-Framework/
- Source: https://github.com/HotcocoaCanary/Canary-Framework
- Examples (a FastAPI service and a long-running daemon): https://github.com/HotcocoaCanary/Canary-Framework-Example
A lot of this project was generated by AI — code, tests and docs. I have reviewed as much of it as I could find time for, but there are surely things I missed, and corrections are very welcome. I’m just a beginner, and I’m grateful to AI for turning my idea into something real so quickly.
If it interests you, or you think something is designed wrong, please tell me in Discussions. Thank you all.