Skip to content

← Memory, Copies and Slow Code step 11 of 38

Medium Primitives

Fix a Nested Context Manager That Hides Errors

PooledResource borrows a named resource from a pool for the length of a with block. The starter has two bugs. Both pass mypy, and both show up only when something goes wrong:

  • __exit__ returns True, so an error raised in the block disappears.
  • Entering the same wrapper twice checks out two resources, and the inner exit gives a resource back while the outer block still uses it.

Fix PooledResource. Keep ResourcePool and solve as they are.

What to write

  • Creating a PooledResource borrows nothing. Do not use __del__ for cleanup.
  • __enter__ returns the wrapper itself, annotated -> Self. The first entry checks out one resource.
  • An entry while the wrapper is already entered uses the resource it holds. Only the outermost exit gives the resource back.
  • On the outermost exit, __exit__ gives the resource back, whether the block finished or raised. It returns None, so it never hides the block's exception. Keep its three parameters, and change its return annotation from bool to None.
  • The name property returns the held name while the wrapper is entered. At any other time it raises RuntimeError("not checked out").
  • After the outermost exit, you can enter the wrapper again. If a checkout fails, the wrapper stays unentered, so a later attempt can succeed once a resource is free.

The tests use balanced with blocks in one thread. You do not need to handle threads, or a call to __exit__ without a matching __enter__.

The pool

ResourcePool(n) starts with the free names r0 to r<n-1>. checkout() takes the first free name, logs out:<name> and returns the name. With no free name, it raises RuntimeError("pool exhausted"). checkin(name) adds the name to the end of the free list and logs in:<name>. available is the number of free names.

The driver

solve(pool_size, actions) creates the pool and runs the actions in order. pool_size is 0 or more.

Action What it does
"ok" Enters a new wrapper, logs use:<name>, and exits normally.
"raise" Enters a new wrapper, logs use:<name>, raises ValueError("body failed"), and catches it outside the block.
"nested" Enters one wrapper, then enters it again inside the first block. Logs use:<name> in the inner block and still:<name> after the inner block ends.
"orphan" Creates a wrapper without entering it, and logs orphan.

It returns {"log": pool.log, "available": pool.available, "escaped": escaped}. escaped holds one "ValueError:body failed" for each "raise" action whose error got out of the block. Any other error reaches the caller of solve, such as the RuntimeError from an empty pool. An unknown action raises ValueError.

Each action gives its resource back before the next one starts, so the free list keeps its order. A nested action needs only one free resource. With a pool of size 0, an empty action list or a list of orphan actions still completes.