|
| 1 | +import asyncio |
1 | 2 | import threading |
2 | 3 | import typing |
3 | 4 | from concurrent.futures import ThreadPoolExecutor |
|
6 | 7 |
|
7 | 8 | from modern_di import Container, Group, Scope |
8 | 9 | from modern_di.cache import CacheItem, close_async |
| 10 | +from modern_di.exceptions import FinalizerError |
9 | 11 | from modern_di.providers import CacheSettings, Factory |
10 | 12 | from modern_di.types import UNSET |
11 | 13 | from tests.helpers import cache_item |
@@ -160,3 +162,84 @@ async def test_close_async_runs_only_owed_finalizers_and_empties_the_order() -> |
160 | 162 | assert persistent.cache == "persistent" |
161 | 163 | assert with_finalizer.cache is UNSET |
162 | 164 | assert creation_order == [] |
| 165 | + |
| 166 | + |
| 167 | +def _close_with_a_build_finishing_mid_close( |
| 168 | + close: typing.Callable[[Container], typing.Any], *, x_finalizer: typing.Callable[[object], typing.Any] |
| 169 | +) -> tuple[Container, list[str]]: |
| 170 | + """Close a container while another thread's cached build finishes during that close. |
| 171 | +
|
| 172 | + ``Y`` is cached before the close starts. ``X``'s creator blocks until ``Y``'s finalizer |
| 173 | + releases it, and that finalizer waits for the build to finish, so ``X`` is cached and queued |
| 174 | + while the close is still running. |
| 175 | + """ |
| 176 | + log: list[str] = [] |
| 177 | + started = threading.Event() |
| 178 | + go = threading.Event() |
| 179 | + |
| 180 | + class X: |
| 181 | + def __init__(self) -> None: |
| 182 | + started.set() |
| 183 | + go.wait(timeout=5) |
| 184 | + |
| 185 | + class Y: ... |
| 186 | + |
| 187 | + def finalize_y(_: Y) -> None: |
| 188 | + log.append("Y") |
| 189 | + go.set() |
| 190 | + builder.join(timeout=5) |
| 191 | + |
| 192 | + class G(Group): |
| 193 | + x = Factory(creator=X, cache=CacheSettings(finalizer=x_finalizer)) |
| 194 | + y = Factory(creator=Y, cache=CacheSettings(finalizer=finalize_y)) |
| 195 | + |
| 196 | + container = Container(groups=[G]) |
| 197 | + container.resolve(Y) |
| 198 | + builder = threading.Thread(target=container.resolve, args=(X,)) |
| 199 | + builder.start() |
| 200 | + assert started.wait(timeout=5) |
| 201 | + close(container) |
| 202 | + assert not builder.is_alive() |
| 203 | + return container, log |
| 204 | + |
| 205 | + |
| 206 | +def test_a_build_finishing_during_close_sync_is_finalized_by_the_next_close() -> None: |
| 207 | + log: list[str] = [] |
| 208 | + container, closed = _close_with_a_build_finishing_mid_close( |
| 209 | + Container.close_sync, x_finalizer=lambda _: log.append("X") |
| 210 | + ) |
| 211 | + assert (closed, log) == (["Y"], []) |
| 212 | + |
| 213 | + container.close_sync() |
| 214 | + |
| 215 | + assert log == ["X"] |
| 216 | + |
| 217 | + |
| 218 | +def _close_async(container: Container) -> None: |
| 219 | + asyncio.run(container.close_async()) |
| 220 | + |
| 221 | + |
| 222 | +def test_a_build_finishing_during_close_async_is_finalized_by_the_next_close() -> None: |
| 223 | + log: list[str] = [] |
| 224 | + container, closed = _close_with_a_build_finishing_mid_close(_close_async, x_finalizer=lambda _: log.append("X")) |
| 225 | + assert (closed, log) == (["Y"], []) |
| 226 | + |
| 227 | + _close_async(container) |
| 228 | + |
| 229 | + assert log == ["X"] |
| 230 | + |
| 231 | + |
| 232 | +def test_a_build_finishing_during_close_sync_keeps_an_async_finalizer_pending() -> None: |
| 233 | + log: list[str] = [] |
| 234 | + |
| 235 | + async def finalize_x(_: object) -> None: |
| 236 | + log.append("X") |
| 237 | + |
| 238 | + container, _ = _close_with_a_build_finishing_mid_close(Container.close_sync, x_finalizer=finalize_x) |
| 239 | + with pytest.raises(FinalizerError): |
| 240 | + container.close_sync() |
| 241 | + assert log == [] |
| 242 | + |
| 243 | + _close_async(container) |
| 244 | + |
| 245 | + assert log == ["X"] |
0 commit comments