---
myst:
html_meta:
description: >-
Lease reusable Python objects from a Melder managed spell pool: elastic, limited
and queue modes, release, reset methods, refusals, inspection and teardown.
---
# Managed spell pools: lease, release, reset
Prerequisites: [Choose an instance lifetime](../beginner/lifetimes.md),
[Cleanup and memory ownership](../beginner/cleanup.md) and
[Spell spaces, lineage, and child scopes](scopes.md).
Some objects cost more to build than to lend: connections, sessions, buffers, licence
seats. Bind such a class with the `managed_spell_quantity` existence and Melder keeps
its objects in a pool. A meld *leases* an object - an idle one when the pool has one,
a new one when it does not - and the holder gives it back when it is done. Nothing
goes back on its own: returning an object is the holder's decision.
## Bind a managed spell
```python
import melder as md
book = md.Spellbook()
book.bind(
spell=DatabaseConnection,
existence=md.Existence.managed_spell_quantity,
managed_spell_mode="limited_quantity", # elastic (default) | limited_quantity | queue
managed_spell_limit=8, # at most 8 connections alive at once
managed_spell_reset_methods=["rollback"], # run on every release, in order
disposal_method_names=["close"], # run only when the pool destroys one
)
conduit = book.conjure()
```
The fluent form stages the same options:
```python
spell_id = (
md.SpellBinder(book)
.bind(DatabaseConnection)
.as_managed_spell_quantity("queue", limit=1, timeout_seconds=5.0, reset_methods=["rollback"])
.finalize()
)
```
Each managed spell gets one pool in the store of the root conduit that owns it - at
conjure, or at the bind itself when you bind on a live root. The pool starts empty and
is registered with `md.ReleaseManagedSpell()`, the process-wide registry that objects
go back through.
| Option | Meaning |
| --- | --- |
| `managed_spell_mode` | `"elastic"` (default), `"limited_quantity"` or `"queue"`; a `md.ManagedSpellMode` member works too |
| `managed_spell_limit` | elastic: the most idle objects the pool keeps (default 256); limited_quantity and queue: the most objects alive at once, idle plus out (required) |
| `managed_spell_timeout_seconds` | queue only, and required there: how long a meld waits for a release |
| `managed_spell_reset_methods` | names of zero-argument methods run, in order, on every release |
Bind refuses a malformed managed binding before anything is registered: a managed
option on another existence, a function or other non-class target, an unknown mode,
a limit below 1, a limited_quantity or queue binding without a limit, a timeout on an
elastic binding, a queue binding without one, and reset names that are not a list or
tuple of method names of the class. A managed spell may not depend on a shorter-lived
`unique_per_conduit` spell: conjure refuses that as a scope-ordering violation.
## Choose a pool mode
| Mode | When every object is out | Typical use |
| --- | --- | --- |
| `elastic` | Never refuses: a meld builds another object. A release that would keep more idle objects than the limit destroys the oldest idle one, and its disposal methods run. | Objects that must always be available; the limit caps what stays parked |
| `limited_quantity` | A meld raises `ManagedSpellExhaustedError` before anything is built | A hard cap, such as licence seats or a connection quota |
| `queue` | A meld waits up to `managed_spell_timeout_seconds`; a release hands the waiting meld the released object; past the timeout the meld raises `ManagedSpellExhaustedError` | A scarce resource where waiting beats failing |
`ManagedSpellExhaustedError` is a `MeldExecutionError`. It carries `mode`, `limit`
and `timeout_seconds` (`None` outside queue mode), and its message names the way out:
release an object, or bind a higher limit - or, for a queue, a longer timeout.
```python
try:
conduit.meld("LicenseSeat")
except md.ManagedSpellExhaustedError as refused:
print(refused.mode, refused.limit, refused.timeout_seconds) # limited_quantity 2 None
```
Run [Managed spell elastic pool](../examples/intermediate/42-managed-spell-elastic-pool.md),
[Managed spell limited quantity](../examples/intermediate/43-managed-spell-limited-quantity.md)
and [Managed spell queue](../examples/intermediate/44-managed-spell-queue.md) to see each
mode at its limit.
## Lease objects
- `conduit.meld("DatabaseConnection")` leases one object. The pool hands out the most
recently released idle object first and builds a new one only when none is idle.
- Constructor injection leases too. Annotate a parameter with the managed class: every
injected parameter is its own lease, so two consumers hold two objects.
- `conduit.managed_spell_meld(DatabaseConnection)` melds the spell and returns a
`md.ManagedSpellLease` whose `with` block gives the object back. It refuses a spell
that is not managed with `ValueError`, before melding anything.
## Give objects back
```python
release = md.ReleaseManagedSpell().release
connection = conduit.meld("DatabaseConnection")
release(connection) # resets run, then the object is idle again
with conduit.managed_spell_meld(DatabaseConnection) as connection:
... # given back when the block ends, even if it raised
held = conduit.meld("DatabaseConnection")
with md.ReleaseManagedSpell().managed(held) as same: # wrap an object you already hold
...
```
`release(obj)` finds the pool by the object's class. When more than one pool manages
that class, pass `spell_id=` or `conduit=` to pick one. A class that no live root
pools, a filter that no pool matches and a filter of the wrong type are refused.
`release_all(objects)` gives several objects back at once.
A lease releases exactly once: a second `release()` does nothing, and `lease.object`
raises `RuntimeError` once the object went back, because the pool may already have
leased it to someone else. A `with` block never swallows the block's own exception.
Run [Managed spell leases](../examples/intermediate/46-managed-spell-leases.md) for
every lease form.
## Reset objects between holders
The pool calls the named reset methods, in the order given, on every release and
before the object is idle again, so the next holder receives a clean object.
A reset that raises means the object cannot be trusted. The pool destroys it - its
disposal methods run and its place is freed - and the reset's error propagates from
`release`. The next meld builds a fresh object. Run
[Managed spell reset methods](../examples/intermediate/45-managed-spell-reset-methods.md)
to watch both outcomes.
## Inspect a pool
```python
(snapshot,) = md.ReleaseManagedSpell().describe(DatabaseConnection)
print(snapshot["count"], snapshot["idle_count"], snapshot["out_count"]) # alive, idle, out
print(snapshot["mode"], snapshot["limit"], snapshot["timeout_seconds"])
```
`describe(cls)` returns one snapshot per pool of the class, and an empty tuple when no
live root pools it. Each snapshot also names the spell (`spell_id`, `spell_name`), the
root that holds the pool (`holding_conduit_id`) and the reset and disposal method
names.
## Request scopes and root teardown
The pool belongs to the root conduit and outlives every lesser conduit made from it.
A request-scoped handler can return its connection in its own disposal method:
leaving the request scope disposes the handler, and the connection goes back to the
root's pool for the next request.
```python
class RequestHandler:
def __init__(self, connection: PooledConnection) -> None:
self.connection = connection
def finish(self) -> None:
md.ReleaseManagedSpell().release(self.connection)
book.bind(spell=RequestHandler, existence=md.Existence.many, disposal_method_names=["finish"])
root = book.conjure()
with root.enter_lesser_conduit() as request:
handler = request.meld("RequestHandler") # its connection is leased from the root's pool
# the scope ended: finish() ran and the connection is idle in the root's pool
```
When the root is torn down, its pools are cleaned first: idle objects are disposed,
and an object that is still out stays its holder's to dispose. The pools are then
unregistered, so a later `ReleaseManagedSpell().release(obj)` raises `ValueError` and
a lease's `release()` raises `RuntimeError`.
Because the pools are cleaned before the root's other disposals run, a consumer
melded on the root itself that releases its object in its own disposal method meets
a pool that is already gone, and the root's teardown raises an `ExceptionGroup`.
Release such objects before the root ends, or keep their holders in a lesser conduit
as above. [Managed spell injection and request scopes](../examples/intermediate/47-managed-spell-injection-and-request-scopes.md)
runs this pattern end to end.
A purge does not empty a pool: `conduit.purge` returns `0` for a managed spell, and a
spell space refuses to purge one (see [spell spaces and scopes](scopes.md)).
## Change settings or move a pool
A managed spell's mode, limit, queue timeout and reset methods are settings, not part
of its spell_id. In a [dynamic world](dynamic-linking.md), change them with
`cleanup_spell` and a new `bind`: the spell_id stays the same and so does the pool,
and every object alive stays alive and counted under the new settings.
```python
spell_id = root.bind(spell=ExportWorker, existence=md.Existence.managed_spell_quantity,
managed_spell_mode="limited_quantity", managed_spell_limit=1)
root.cleanup_spell(spell=root.get_spell_by_spell_id(spell_id))
rebound = root.bind(spell=ExportWorker, existence=md.Existence.managed_spell_quantity,
managed_spell_mode="limited_quantity", managed_spell_limit=3)
assert rebound == spell_id # the same spell and the same pool, now with limit 3
```
`transfer_spell_ownership` moves the spell to another root, and its pool goes with it,
idle objects included. The new owner leases from it, releases still find it by class,
and the old owner can no longer meld the spell. Read
[Named conduits, transfer, and sever](ownership.md) before moving spells, then run
[Managed spell settings and transfer](../examples/intermediate/48-managed-spell-settings-and-transfer.md).
## The workflow in code
These are the core steps from the saved example. Use its complete linked script for all class definitions and setup.
```{literalinclude} ../downloads/02_intermediate/42_managed_spell_elastic_pool.py
:language: python
:pyobject: main
:linenos:
```
## Runnable examples
- [Managed spell elastic pool](../examples/intermediate/42-managed-spell-elastic-pool.md) — Intermediate 42
- [Managed spell limited quantity](../examples/intermediate/43-managed-spell-limited-quantity.md) — Intermediate 43
- [Managed spell queue](../examples/intermediate/44-managed-spell-queue.md) — Intermediate 44
- [Managed spell reset methods](../examples/intermediate/45-managed-spell-reset-methods.md) — Intermediate 45
- [Managed spell leases](../examples/intermediate/46-managed-spell-leases.md) — Intermediate 46
- [Managed spell injection and request scopes](../examples/intermediate/47-managed-spell-injection-and-request-scopes.md) — Intermediate 47
- [Managed spell settings and transfer](../examples/intermediate/48-managed-spell-settings-and-transfer.md) — Intermediate 48
[All intermediate examples](../examples/intermediate/index.md) · [Level contents](index.md) · [Full contents](../contents.md)
[Canonical page source](https://github.com/Synaptic724/melder/blob/90d52e0a12cb0743d62568d5a19ce954a54443bc/docs/intermediate/managed-spells.md)