FastAPI — Types Are the Contract
Close resources even when requests fail
Goal
Separate the startup, shutdown, and exception paths, and actually run the FastAPI lifespan.
Why it matters
The tests passed, but connections were left over when production restarted. The startup and shutdown code had not run because TestClient was used without a context manager. A test that looks at one normal response cannot tell you when the app opens and closes its resources. Here, instead of an external connection, we observe the lifecycle with a small resource that records events.
Steps
- In
/root/work/fa-resource-lifecycle-lab/service.py, new_resource() is a new dictionary {open:False, events:[]}, and separate calls do not share events.
Prepare this once at the start. Existing files are not overwritten.
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
-
In
/root/work/fa-resource-lifecycle-lab/service.py, start(resource) raises ValueError if it is already open; otherwise it sets open=True and appends 'open' to events. -
In
/root/work/fa-resource-lifecycle-lab/service.py, stop(resource) sets open=False and appends 'close' to events only when it is open. If it is already closed, it leaves things as they are. -
In
/root/work/fa-resource-lifecycle-lab/service.py, read(resource) raises RuntimeError if it is closed, and returns {ready:True} if it is open. -
In
/root/work/fa-resource-lifecycle-lab/service.py, scope(resource) is a contextmanager. On entry it calls start, inside the block it yields resource, and on both success and failure of the block it closes with stop. An exception in the block is propagated. -
In
/root/work/fa-resource-lifecycle-lab/service.py, lifespan_for(resource) returns an asynccontextmanager function lifespan(app). Inside scope(resource), it sets app.state.resource and yields. -
In
/root/work/fa-resource-lifecycle-lab/service.py, create_app(resource) uses lifespan_for. GET /ready returns the result of read on app.state.resource. The resource must be closed when the context ends. -
In
/root/work/fa-resource-lifecycle-lab/service.py, exercise(resource, fail=False) calls GET /ready inside with TestClient(create_app(resource)). If fail=True, it raises RuntimeError inside, and otherwise it returns the response JSON. In both cases the resource must be closed.
Notes
- You work in the existing lab-dev environment with no internet and no package installation.
- Each step runs within a 45-second grading budget. Do not add real sleeps or network calls.
- The grader loads the submitted module fresh and checks it with independent inputs and a temporary DB. Implement the contract instead of returning the expected values as constants.
- FastAPI official documentation · pytest official documentation · Python sqlite3
- Limitation: the teaching resource dictionary is an observation device that stands in for a real DB connection pool. In production you also have to design for partial initialization failure, concurrency of the connection pool, cancellation handling, and a shutdown time limit. Instead of writing event strings in a report, you check the state of objects that the learner's code changed as it ran.
Make the resource state independent
In /root/work/fa-resource-lifecycle-lab/service.py, new_resource() is a new dictionary {open:False, events:[]}, and separate calls do not share events.
Prepare this once at the start. Existing files are not overwritten.
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
Do not share a mutable list through a global or a default argument.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/01-contract.sh.
Reject a duplicate start
In /root/work/fa-resource-lifecycle-lab/service.py, start(resource) raises ValueError if it is already open; otherwise it sets open=True and appends 'open' to events.
Reject the behavior of starting twice and losing one resource.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/02-contract.sh.
Make shutdown idempotent
In /root/work/fa-resource-lifecycle-lab/service.py, stop(resource) sets open=False and appends 'close' to events only when it is open. If it is already closed, it leaves things as they are.
Even when several cleanup paths overlap, a duplicate close event must not appear.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/03-contract.sh.
Block the use of a closed resource
In /root/work/fa-resource-lifecycle-lab/service.py, read(resource) raises RuntimeError if it is closed, and returns {ready:True} if it is open.
The ready state and the existence of the object are different things. The object can exist and still be closed.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/04-contract.sh.
Put finally on the exception path
In /root/work/fa-resource-lifecycle-lab/service.py, scope(resource) is a contextmanager. On entry it calls start, inside the block it yields resource, and on both success and failure of the block it closes with stop. An exception in the block is propagated.
If you write the close only after the yield, that line is never reached when an exception occurs.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/05-contract.sh.
Connect the app lifespan to the resource
In /root/work/fa-resource-lifecycle-lab/service.py, lifespan_for(resource) returns an asynccontextmanager function lifespan(app). Inside scope(resource), it sets app.state.resource and yields.
You do not call the lifespan function itself; you pass it to the FastAPI constructor.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/06-contract.sh.
Read the ready state through a real request
In /root/work/fa-resource-lifecycle-lab/service.py, create_app(resource) uses lifespan_for. GET /ready returns the result of read on app.state.resource. The resource must be closed when the context ends.
You have to use with TestClient for both the lifespan startup and shutdown to run.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/07-contract.sh.
Clean up after a failure following the request
In /root/work/fa-resource-lifecycle-lab/service.py, exercise(resource, fail=False) calls GET /ready inside with TestClient(create_app(resource)). If fail=True, it raises RuntimeError inside, and otherwise it returns the response JSON. In both cases the resource must be closed.
If you bind the normal path and the exception path into the same cleanup structure, you reduce the number of shutdown paths you miss.
After saving, check with bash /opt/lab/checks/fa-resource-lifecycle-lab/08-contract.sh.