Skip to content

test: pin finalizer ownership and close ordering, mark the threaded cache test (#616) - #621

Merged
lesnik512 merged 1 commit into
mainfrom
issue-616
Oct 9, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
issue-616

Conversation

@lesnik512

@lesnik512 lesnik512 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Closes #616.

Summary

  • Item 1 (concurrent close_async() finalizes twice) is won't-fix by maintainer decision. Closing one container concurrently is a usage error. design-decisions.md now says so in the thread-safety boundary list: never overlap two close_async() calls on one container, or call close_sync() while a close_async() is still running. No library code changes.
  • New tests pin three guarantees that mutation testing showed were unprotected (items 2 to 4 of the issue).
  • tests/test_cache.py::test_concurrent_fetches_of_one_provider_share_one_item gets the thread_race marker, and just test-race selects -m thread_race over the whole suite instead of listing files. The free-threaded CI step (just test-race --count=50) now runs 6 tests instead of 5.

Design decisions

  • Item 4: the issue's suggested test (clear_cache=False, close_async, reopen, close_async) does not catch the mutation. A completed close_async empties the creation order, so the second close never visits the item. The finalized flag on the async path only matters when an item is visited twice, which happens after a cancelled close: main keeps the whole creation order queued then, as docs/providers/lifecycle.md documents. The test uses that path. A clear_cache=False instance finalized before the cancellation must not be finalized again by the follow-up close.

Test plan

  • Item 2 mutation (target._creation_order to container._creation_order in resolver_compiler.py): test_app_singleton_resolved_through_a_child_is_finalized_by_the_app_container fails; reverted.
  • Item 3 mutation (drop remaining.reverse() in cache.close_sync): test_async_finalizers_left_by_close_sync_run_newest_first fails; reverted.
  • Item 4 mutation (drop cache_item.finalized = True in cache.close_async): test_close_async_after_a_cancelled_close_does_not_refinalize_a_clear_cache_false_instance fails; reverted.
  • just test-race selects 6 tests.
  • just lint, just lint-ci, just test-ci (992 passed, 100% coverage), mkdocs build --strict.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: e471a16 Previous: f6e343d Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 2952941.4234773433 iter/sec (stddev: 1.2434187032995931e-8) 2921718.256656569 iter/sec (stddev: 1.675866103278825e-8) 0.99
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 3013346.0191137115 iter/sec (stddev: 8.984557829129306e-9) 2939203.314248642 iter/sec (stddev: 1.204015671986658e-8) 0.98
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 21774.330610173107 iter/sec (stddev: 0.000042810952544833285) 21762.967229655445 iter/sec (stddev: 0.00004094890871011781) 1.00
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 16555.038708692267 iter/sec (stddev: 0.00011945612921760568) 16433.6760366631 iter/sec (stddev: 0.00010812780919654217) 0.99
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 436.18487056878615 iter/sec (stddev: 0.00006788493241210529) 461.45135894055323 iter/sec (stddev: 0.00003990784243928985) 1.06
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 439.0770028986512 iter/sec (stddev: 0.00005657361314767956) 451.0026376153234 iter/sec (stddev: 0.000054903965874733085) 1.03
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 419.0395736734624 iter/sec (stddev: 0.00006634597450916865) 425.7675844707922 iter/sec (stddev: 0.00008308038620630768) 1.02
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 12401.889427729511 iter/sec (stddev: 0.00004337578425906983) 12751.290749215299 iter/sec (stddev: 0.00000819216963265991) 1.03
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 6048.459044214393 iter/sec (stddev: 0.000011022346142561617) 6401.705755814795 iter/sec (stddev: 0.000022867724842553285) 1.06
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 3373.1996811693725 iter/sec (stddev: 0.00004379832739125957) 3423.3858792657397 iter/sec (stddev: 0.000040939280354384506) 1.01
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 8202.204574905045 iter/sec (stddev: 0.00001553803990359062) 7612.8302325882405 iter/sec (stddev: 0.000010256134066683693) 0.93
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 3626.0292332914532 iter/sec (stddev: 0.000022107410563777) 3618.370685087365 iter/sec (stddev: 0.00002517012025414019) 1.00
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 1695.3233530497214 iter/sec (stddev: 0.00004378189138546375) 1719.2318706952121 iter/sec (stddev: 0.00004854649644947838) 1.01
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 40816.93739566888 iter/sec (stddev: 0.000004411791066466826) 32996.42153964083 iter/sec (stddev: 0.000006880551983878211) 0.81
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 13698.730218982657 iter/sec (stddev: 0.000013267147383752662) 14659.535942872064 iter/sec (stddev: 0.000012750288363749525) 1.07
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5593.578944269585 iter/sec (stddev: 0.00003343425109580368) 5849.76259472311 iter/sec (stddev: 0.0000335570564798276) 1.05
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 1632202.8397246404 iter/sec (stddev: 2.8647705275202995e-8) 1585121.4791400717 iter/sec (stddev: 3.581802492697611e-8) 0.97
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 1689189.3318649065 iter/sec (stddev: 1.9877316606395375e-8) 1634928.8822412132 iter/sec (stddev: 2.2739949474734155e-8) 0.97
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2798.228983090822 iter/sec (stddev: 0.000020150297508410128) 2851.9403996074443 iter/sec (stddev: 0.00001246850076439463) 1.02
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 56916.39609076696 iter/sec (stddev: 0.000003028333588507266) 58007.21870000694 iter/sec (stddev: 0.00000208313451377107) 1.02
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 338019.84896459634 iter/sec (stddev: 1.2248012802379184e-7) 332497.14734137605 iter/sec (stddev: 7.44393106694809e-8) 0.98
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 46943.30048627541 iter/sec (stddev: 0.000003684042772025018) 48889.69298560324 iter/sec (stddev: 0.000001820471012608131) 1.04
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 477.51685644210085 iter/sec (stddev: 0.001070361804376077) 472.1940049135932 iter/sec (stddev: 0.001026790425194055) 0.99
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2040327.3766613095 iter/sec (stddev: 5.3084234398660585e-8) 2178750.936263875 iter/sec (stddev: 2.6353327029762944e-8) 1.07
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 3336700.731696267 iter/sec (stddev: 1.7022216043700234e-8) 3311376.0403177035 iter/sec (stddev: 2.072399608841274e-8) 0.99
benchmarks/test_guard_resolve.py::test_g3_deep_chain 692618.4568908721 iter/sec (stddev: 5.790976903267621e-8) 678836.554249412 iter/sec (stddev: 4.6008684988825406e-8) 0.98
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 429336.43863643747 iter/sec (stddev: 3.881059582854595e-7) 436782.78901172994 iter/sec (stddev: 3.7702362683787223e-7) 1.02
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1782308.088077468 iter/sec (stddev: 2.6820474769231562e-8) 1876457.2449674914 iter/sec (stddev: 3.702446327477425e-8) 1.05
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1343173.4669237074 iter/sec (stddev: 1.5704764663403646e-7) 1310469.1843290261 iter/sec (stddev: 1.5759486009534166e-7) 0.98
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 682081.9692696182 iter/sec (stddev: 5.057881460027932e-8) 715658.0140887419 iter/sec (stddev: 4.497059959957813e-8) 1.05
benchmarks/test_guard_resolve.py::test_g18_alias_hop 2811636.994377848 iter/sec (stddev: 8.857325945366564e-9) 2903717.0685449257 iter/sec (stddev: 8.479702107212049e-9) 1.03
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 28380.765776117198 iter/sec (stddev: 0.00002091480109838877) 27290.39780542258 iter/sec (stddev: 0.00002305556618279706) 0.96
benchmarks/test_guard_validate.py::test_g11_validate_wide 16512.080477507283 iter/sec (stddev: 0.000026040573568393852) 16384.134733354367 iter/sec (stddev: 0.000027709419502202343) 0.99

This comment was automatically generated by workflow using github-action-benchmark.

…ache test (#616)

Adds tests for finalizer ownership by the resolving container,
newest-first order of async finalizers left by close_sync, and no
re-finalize of a clear_cache=False instance after a cancelled
close_async. Documents concurrent close of one container as
unsupported. test-race now selects the thread_race marker over the
whole suite.
@lesnik512 lesnik512 changed the title fix: finalize each cached item once under concurrent close_async (#616) test: pin finalizer ownership and close ordering, mark the threaded cache test (#616) Oct 7, 2026
@lesnik512
lesnik512 merged commit 68d4685 into main Oct 9, 2026
11 checks passed
@lesnik512
lesnik512 deleted the issue-616 branch October 9, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Close path: concurrent close_async finalizes twice, and three untested guarantees

1 participant