Merged
Conversation
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: e1df19a | Previous: f653f91 | Ratio |
|---|---|---|---|
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type |
6594271.674904684 iter/sec (stddev: 4.512464983434615e-9) |
2808032.8497195244 iter/sec (stddev: 9.795020193349189e-9) |
0.43 |
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry |
6308526.56663586 iter/sec (stddev: 5.347713078238282e-9) |
2784559.5726145664 iter/sec (stddev: 9.167981887280914e-9) |
0.44 |
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve |
37775.43222111646 iter/sec (stddev: 0.00005318764670639172) |
20512.454898109616 iter/sec (stddev: 0.00005257311766882013) |
0.54 |
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached |
28845.18412212416 iter/sec (stddev: 0.0001755055994743204) |
16432.75503654348 iter/sec (stddev: 0.00011159457484330999) |
0.57 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] |
852.3555032916524 iter/sec (stddev: 0.000028412255143672705) |
346.3522241650109 iter/sec (stddev: 0.00005478765485046593) |
0.41 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] |
802.43609664018 iter/sec (stddev: 0.00011685848210619385) |
322.94426905724225 iter/sec (stddev: 0.0000921166425445254) |
0.40 |
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] |
709.1859695790007 iter/sec (stddev: 0.000026774045790597278) |
302.6646249644815 iter/sec (stddev: 0.00008618395957780148) |
0.43 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] |
3362.9109761061186 iter/sec (stddev: 0.0001148809362016014) |
1891.945863483343 iter/sec (stddev: 0.00016330519623458548) |
0.56 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] |
2244.109563037999 iter/sec (stddev: 0.0003626030439924928) |
1429.8289461081597 iter/sec (stddev: 0.00022430633429209966) |
0.64 |
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] |
1796.1725000957658 iter/sec (stddev: 0.0001505734799106195) |
1035.2239521970916 iter/sec (stddev: 0.00020739484717365007) |
0.58 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] |
3432.8324563923543 iter/sec (stddev: 0.00011577046598816617) |
1869.5371831393138 iter/sec (stddev: 0.00018570697788112053) |
0.54 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] |
1720.388818192377 iter/sec (stddev: 0.001627053592405799) |
1291.7767879469964 iter/sec (stddev: 0.00021786341182087493) |
0.75 |
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] |
1376.6820846625126 iter/sec (stddev: 0.00016135805730881192) |
728.8235843524715 iter/sec (stddev: 0.0016130279229931673) |
0.53 |
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container |
1757451.6388909854 iter/sec (stddev: 2.4116016172860228e-8) |
972925.9990545122 iter/sec (stddev: 3.8841634458924165e-8) |
0.55 |
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope |
1516637.5901995546 iter/sec (stddev: 1.8184763204963783e-8) |
861585.4749432497 iter/sec (stddev: 2.559318414959247e-8) |
0.57 |
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch |
4329.317375371346 iter/sec (stddev: 0.00000675778488499393) |
2385.2748839344185 iter/sec (stddev: 0.000020601593475511862) |
0.55 |
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control |
105271.61187841432 iter/sec (stddev: 0.000003804042165865548) |
57003.747610629194 iter/sec (stddev: 0.0000020684942624501446) |
0.54 |
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync |
391100.87504623196 iter/sec (stddev: 0.0000010547359925265641) |
||
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale |
55907.14922556173 iter/sec (stddev: 0.00001671816519727452) |
42613.95750751593 iter/sec (stddev: 0.0000020692069621908607) |
0.76 |
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers |
720.7455213301145 iter/sec (stddev: 0.0014810945613538407) |
532.8806923945746 iter/sec (stddev: 0.000023541091148788333) |
0.74 |
benchmarks/test_guard_resolve.py::test_g1_transient_resolve |
3864860.8877912927 iter/sec (stddev: 2.7754623922591276e-8) |
2123955.757148316 iter/sec (stddev: 3.2169744591608866e-8) |
0.55 |
benchmarks/test_guard_resolve.py::test_g2_cached_resolve |
6462900.045415802 iter/sec (stddev: 6.435925550800141e-9) |
3022366.6617990714 iter/sec (stddev: 1.591874027620522e-8) |
0.47 |
benchmarks/test_guard_resolve.py::test_g3_deep_chain |
1324664.4641491468 iter/sec (stddev: 2.9843918707921635e-8) |
724918.7710399074 iter/sec (stddev: 5.7652116350526364e-8) |
0.55 |
benchmarks/test_guard_resolve.py::test_g4_wide_resolve |
803490.7817731618 iter/sec (stddev: 3.433818491574874e-7) |
444395.1560837804 iter/sec (stddev: 4.195349989203622e-7) |
0.55 |
benchmarks/test_guard_resolve.py::test_g5_cross_scope |
3521536.2631922797 iter/sec (stddev: 2.0395701996344087e-8) |
1899234.4992829212 iter/sec (stddev: 3.2124622489837866e-8) |
0.54 |
benchmarks/test_guard_resolve.py::test_g9_context_resolve |
2681355.3097677585 iter/sec (stddev: 1.3604131901366342e-7) |
1302325.3572747398 iter/sec (stddev: 1.6882419986561893e-7) |
0.49 |
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve |
1356910.835113653 iter/sec (stddev: 3.4698021015850224e-8) |
746349.3069998431 iter/sec (stddev: 5.358371448849914e-8) |
0.55 |
benchmarks/test_guard_resolve.py::test_g18_alias_hop |
6513531.7971865125 iter/sec (stddev: 4.549070659559068e-9) |
2925676.066686839 iter/sec (stddev: 1.3628581836897855e-8) |
0.45 |
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain |
46097.609906500664 iter/sec (stddev: 0.000015737030991330215) |
27297.053414137306 iter/sec (stddev: 0.000021567751573877215) |
0.59 |
benchmarks/test_guard_validate.py::test_g11_validate_wide |
25693.781562704717 iter/sec (stddev: 0.00029424937586414033) |
16076.279244196823 iter/sec (stddev: 0.00002850035408034272) |
0.63 |
This comment was automatically generated by workflow using github-action-benchmark.
…under that lock (#569) Replace the container tree's single RLock with one RLock per cache item, created with the item. A cold miss resolves the dependencies and calls the creator under the item's lock, so concurrent misses build the dependencies once, and a cached creator can wait on a worker thread resolving another cached type without deadlocking.
#569) Switch the cache item lock to the documented threading.RLock. Document that the runtime cycle guard covers one thread, and that concurrent cold resolves of an unvalidated cyclic graph can block. Add the G7b guard benchmark for one request cycle with a sync close.
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #569.
Summary
The container tree had one
RLock, held aroundcreateon a cache miss, with the dependencies resolved before the lock. That gave two bugs:resolve(B)for another cached type to a worker thread and waits for it deadlocked: the worker blocked on the lock the creator held. The same lock also serialised unrelated cold creations across every request in the tree.Svc(conn: Conn)together each built their ownConn, and all but one were discarded. On 3.14t, 8 threads missing together built 1.38Connper miss on average onmain(50 trials), and exactly 1 on this branch.Each
CacheItemnow owns anRLock, created with the item, andget_or_createresolves the dependencies and calls the creator under it.Container._lockis gone: nothing else used it.Design decisions
RLockper cache item, created with the item, so a child build allocates nothing. The lock is re-entrant so a same-thread cycle through cached factories re-enters each item's lock and still reaches the recursion handler asCircularDependencyError. A cross-thread deadlock is now only possible when the creators form a real cycle.get_or_create, andget_or_createchecks again before and under the lock.fetch_cache_itempublishes withdict.setdefault, which runs as one operation under the GIL and under the dict's own lock on free-threaded builds. A losing thread drops its own item and lock and gets the winner's. A barrier test checks 8 threads get the same item.threading.RLock. Calling_thread.RLockdirectly would save about 50 ns per cache item (CacheItemconstruction is 127 ns onmain, 237 ns withthreading.RLock, 183 ns with_thread.RLock), but_thread.RLockis not documented.mainboth gotCircularDependencyErrorfrom the recursion guard. A single thread still gets that error. The troubleshooting page, the errors page, design decisions and the migration entry now say the runtime guard covers one thread and recommendvalidate()at startup.get_or_create(resolve, create)loses itslockargument; the template passes the two callables positionally. The frame budget is unchanged: the build already ran insideget_or_create's frame.fix!: creators of different cached factories can now run at the same time on different threads, where 3.x ran every cached creator under one lock. The migration guide gets an entry.Tests
test_cached_creator_resolving_a_cached_type_on_another_thread_completes: the issue's deadlock;.result(timeout=5)turns a regression into a failure.test_concurrent_cache_misses_build_the_value_and_its_dependencies_once: replacestest_concurrent_cache_misses_create_once, which put a barrier in a dependency and relied on dependencies resolving outside the lock. The new test swaps in a counting lock and makes the firstConnbuild wait until the other 7 threads are blocked on the item's lock, so it is deterministic in both directions.test_unrelated_cached_items_are_created_concurrently: one creator blocked on an event, another item created meanwhile.test_cached_cycle_reenters_the_item_lock_and_raises_circular_dependency_error: runs twice on daemon threads, so a lock left held fails the second run instead of hanging.test_warm_cached_resolve_does_not_wait_for_the_item_lock(from test: 4.0 cleanup and missing coverage (#576) #595) now holds the item's lock.test_child_shares_the_root_lockis removed with the tree lock.tests/helpers.pyhas acache_item(container, provider)helper for the tests that reach a container's cache item.Benchmark
New guard scenario G7b: build a REQUEST child, first-resolve one cached REQUEST provider,
close_sync(). Every cycle creates a fresh cache item and its lock, so it is where a per-item cost shows. It is pinned at 200 rounds of 100 iterations and listed inbenchmarks/README.md.Benchmarks
Paired alternating runs,
mainat ed03e40 in its own checkout and venv against this branch, median of per-run medians. CPython 3.14.7 and 3.14.7t on an Apple M2. Base has no G7b, so R1 is the same scenario run from a file outside the repo on both sides.The G2, G6, G7, G15b and R1 rows are from the final code (
threading.RLock): 6 pairs under the GIL, 4 on 3.14t. The G15 rows are from the first round (8 pairs,_thread.RLock). G15 races on one root's items, so the lock factory's allocation cost does not change it.How to read it:
threading.RLock: +7.7% on R1/G7b and +4.6% on G7 under the GIL. G15b's +3 to +5% under the GIL is the same cost, paid by 50 cold items per child.mainand 6.4 to 9.7 ms on the branch. The G15b gains point the expected way (sibling requests no longer share a lock), but the ranges overlap, so I would not claim them.Open questions
get_or_createcall line in_CACHED, so the rebase should be mechanical.Test plan
main(deadlock and concurrent-items tests time out, the dependency-count test has no item lock to hold) and pass herejust lintandjust lint-cicleanjust test-ci: 611 passed, 100% line coverage--count=5mkdocs build --strict