On this page
Managed spell settings and transfer๏
๐ก Intermediate ยท Lesson 48
Change a pool's settings and hand a pool to another root (dynamic mode). A managed spell's mode, limit, queue timeout and reset methods are SETTINGS of the spell, not part of its spell_id. To change them, cleanup_spell the spell and bind it again: the spell_id stays the same, and so does its pool - every object alive stays alive and counted, and the pool takes the new settings.
An ownership transfer moves the spell to another root, and the pool goes with it, idle objects included. The new owner leases from it; releases keep routing by class, so holders change nothing.
Before you run๏
Use the Intermediate guide for prerequisite concepts. Run from a checkout with Melder installed and Python 3.14 free-threading selected. The collection download includes the level's local helper modules.
Run the saved script๏
python UX_and_AIX_experiences/02_intermediate/48_managed_spell_settings_and_transfer.py
py -3.14t UX_and_AIX_experiences/02_intermediate/48_managed_spell_settings_and_transfer.py
Public surface๏
md.Spellbook.conjure(dynamic=True), md.Conduit.bind (managed options), md.Conduit.cleanup_spell, md.Conduit.get_spell_by_spell_id, md.Conduit.link, md.Conduit.transfer_spell_ownership, md.ManagedSpellExhaustedError, md.ReleaseManagedSpell (release, release_all, describe), md.Conduit.cleanup
Code๏
1"""
2TIER: intermediate (48)
3GOAL: Change a pool's settings and hand a pool to another root (dynamic mode).
4 A managed spell's mode, limit, queue timeout and reset methods are SETTINGS of
5 the spell, not part of its spell_id. To change them, cleanup_spell the spell and
6 bind it again: the spell_id stays the same, and so does its pool - every object
7 alive stays alive and counted, and the pool takes the new settings.
8
9 An ownership transfer moves the spell to another root, and the pool goes with it,
10 idle objects included. The new owner leases from it; releases keep routing by
11 class, so holders change nothing.
12SURFACE EXERCISED: md.Spellbook.conjure(dynamic=True), md.Conduit.bind (managed
13 options), md.Conduit.cleanup_spell, md.Conduit.get_spell_by_spell_id,
14 md.Conduit.link, md.Conduit.transfer_spell_ownership,
15 md.ManagedSpellExhaustedError, md.ReleaseManagedSpell (release,
16 release_all, describe), md.Conduit.cleanup
17"""
18import sys
19from pathlib import Path
20from typing import Any, Dict, List, Tuple
21
22sys.path.insert(0, str(Path(__file__).parent)) # local helper (see _dynamic_world)
23from _dynamic_world import dynamic_spellbook
24
25import melder as md
26
27
28class ExportWorker:
29 """A pooled export worker; `close` disposes it when its pool destroys it."""
30
31 def __init__(self) -> None:
32 """Start the worker."""
33 self.closed: bool = False
34
35 def close(self) -> None:
36 """Stop the worker for good."""
37 self.closed = True
38
39
40def pool_snapshot(cls: type) -> Dict[str, Any]:
41 """Return the one pool snapshot of a managed class (this lesson binds it once)."""
42 (snapshot,) = md.ReleaseManagedSpell().describe(cls)
43 return snapshot
44
45
46def pool_counts(cls: type) -> Tuple[int, int, int]:
47 """Return (alive, idle, out) for the one pool of a managed class."""
48 snapshot = pool_snapshot(cls)
49 return snapshot["count"], snapshot["idle_count"], snapshot["out_count"]
50
51
52def raise_the_limit(reports: md.Conduit, spell_id: str) -> List[ExportWorker]:
53 """Fill a limit-1 pool, rebind it with limit 3, lease up to the new limit.
54
55 Contract:
56 The rebind keeps the spell_id and the pool: the worker that was out stays
57 alive and counted under the new limit. Every worker is released at the end.
58 Args:
59 reports: The dynamic root that owns the managed spell.
60 spell_id: The spell_id the first bind returned.
61 Returns:
62 List[ExportWorker]: The three workers the pool now holds, all idle.
63 """
64 busy: ExportWorker = reports.meld("ExportWorker")
65 try:
66 reports.meld("ExportWorker")
67 raise AssertionError("limit 1 leased a second worker")
68 except md.ManagedSpellExhaustedError:
69 print("limit 1: a second worker is refused while one is out")
70
71 # Settings are not identity: rebinding with a higher limit keeps the spell_id and the pool.
72 reports.cleanup_spell(spell=reports.get_spell_by_spell_id(spell_id))
73 rebound_id = reports.bind(
74 spell=ExportWorker,
75 existence=md.Existence.managed_spell_quantity,
76 managed_spell_mode="limited_quantity",
77 managed_spell_limit=3,
78 disposal_method_names=["close"],
79 )
80 assert rebound_id == spell_id
81 assert pool_snapshot(ExportWorker)["limit"] == 3
82 assert pool_counts(ExportWorker) == (1, 0, 1) and not busy.closed
83 print("rebound with limit 3, same spell_id; the busy worker still counts:", pool_counts(ExportWorker))
84
85 workers = [busy, reports.meld("ExportWorker"), reports.meld("ExportWorker")]
86 assert pool_counts(ExportWorker) == (3, 0, 3)
87 md.ReleaseManagedSpell().release_all(workers)
88 assert pool_counts(ExportWorker) == (3, 3, 0)
89 print("three workers under the new limit, all back:", pool_counts(ExportWorker))
90 return workers
91
92
93def transfer_the_pool(reports: md.Conduit, archive: md.Conduit, spell_id: str, workers: List[ExportWorker]) -> None:
94 """Transfer the managed spell to another root and lease from the moved pool.
95
96 Contract:
97 After the transfer the pool lives in the archive root's store, with its idle
98 workers; the archive leases them, the old owner can no longer meld the spell.
99 Args:
100 reports: The current owner.
101 archive: The new owner, linked from `reports`.
102 spell_id: The managed spell's spell_id.
103 workers: The pool's idle workers before the transfer.
104 Returns:
105 None.
106 """
107 assert pool_snapshot(ExportWorker)["holding_conduit_id"] == reports.id
108 reports.link(archive)
109 reports.transfer_spell_ownership(spell=spell_id, target_conduit=archive)
110 assert pool_snapshot(ExportWorker)["holding_conduit_id"] == archive.id
111 assert pool_counts(ExportWorker) == (3, 3, 0)
112 moved: ExportWorker = archive.meld("ExportWorker")
113 assert any(moved is worker for worker in workers)
114 print("the archive root now holds the pool and leases its idle workers:", pool_counts(ExportWorker))
115
116 try:
117 reports.meld("ExportWorker")
118 raise AssertionError("the old owner still melds the transferred spell")
119 except KeyError as gone:
120 print("the old owner can no longer meld it:", type(gone).__name__)
121 md.ReleaseManagedSpell().release(moved)
122 assert pool_counts(ExportWorker) == (3, 3, 0)
123
124
125def main() -> None:
126 """Raise a pool's limit by rebinding, then transfer the spell and its pool.
127
128 Contract:
129 Both roots and both books are cleaned in finally blocks; the archive root's
130 teardown disposes the idle workers its moved pool holds.
131 Returns:
132 None.
133 """
134 reports_book = dynamic_spellbook()
135 try:
136 spell_id = reports_book.bind(
137 spell=ExportWorker,
138 existence=md.Existence.managed_spell_quantity,
139 managed_spell_mode="limited_quantity",
140 managed_spell_limit=1,
141 disposal_method_names=["close"],
142 )
143 reports = reports_book.conjure(dynamic=True, name="reports")
144 try:
145 archive_book = dynamic_spellbook()
146 try:
147 archive = archive_book.conjure(dynamic=True, name="archive")
148 try:
149 workers = raise_the_limit(reports, spell_id)
150 transfer_the_pool(reports, archive, spell_id, workers)
151 finally:
152 archive.cleanup()
153 # The new owner's teardown disposed the idle workers its pool held.
154 assert all(worker.closed for worker in workers)
155 print("the new owner's teardown closed the pooled workers:", len(workers))
156 finally:
157 archive_book.cleanup()
158 finally:
159 reports.cleanup()
160 finally:
161 reports_book.cleanup()
162
163
164if __name__ == "__main__":
165 main()
Check the outcome๏
The script contains its own assertions or demonstrated refusal paths. Run it to evaluate those checks against your installed version. The code above is taken directly from the saved file; no run output is invented here.