1111from __future__ import annotations
1212
1313import threading
14- from collections .abc import Callable , Iterable , Iterator , Sequence
14+ from collections .abc import Callable , Iterator
1515from contextvars import ContextVar
1616from itertools import chain , count
17- from typing import Any , Generic , SupportsIndex , TypeVar , overload
18- from weakref import WeakKeyDictionary
19-
20- from typing_extensions import TypeAliasType
17+ from typing import Any , Generic , TypeVar
2118
2219from logbook .helpers import get_iterator_next_method
2320
2421_missing = object ()
25- _MAX_CONTEXT_OBJECT_CACHE = 256
2622
27- T_co = TypeVar ("T_co" , covariant = True )
2823T = TypeVar ("T" )
2924
3025
@@ -105,57 +100,36 @@ def applicationbound(self):
105100 return ApplicationBound (self )
106101
107102
108- class FrozenSequence (Sequence [T_co ]):
109- __slots__ = ("__weakref__" , "_hash" , "_items" )
110-
111- def __init__ (self , iterable : Iterable [T_co ] = ()) -> None :
112- self ._items = tuple (iterable )
113- self ._hash : int | None = None
114-
115- @overload
116- def __getitem__ (self , index : SupportsIndex ) -> T_co : ...
117-
118- @overload
119- def __getitem__ (self , index : slice ) -> FrozenSequence [T_co ]: ...
120-
121- def __getitem__ (self , index : SupportsIndex | slice ) -> T_co | FrozenSequence [T_co ]:
122- if isinstance (index , slice ):
123- return FrozenSequence (self ._items [index ])
124- return self ._items [index ]
125-
126- def __len__ (self ) -> int :
127- return len (self ._items )
103+ class _StackState (Generic [T ]):
104+ """A state of the persistent context stack.
128105
129- def __iter__ (self ) -> Iterator [T_co ]:
130- return iter (self ._items )
131-
132- def __reversed__ (self ) -> Iterator [T_co ]:
133- return reversed (self ._items )
134-
135- def __contains__ (self , item : object ) -> bool :
136- return item in self ._items
137-
138- def __eq__ (self , other : object ) -> bool :
139- if isinstance (other , FrozenSequence ):
140- return self ._items == other ._items
141- return NotImplemented
106+ Each state is a node in a parent-linked chain: pushing creates a child
107+ node and popping restores the parent, so a stack state keeps its
108+ identity (and its ``merged`` memo) across push/pop cycles. The root
109+ sentinel has ``item = parent = None``.
110+ """
142111
143- def __hash__ (self ) -> int :
144- if self ._hash is None :
145- self ._hash = hash (self ._items )
146- return self ._hash
112+ __slots__ = ("item" , "merged" , "parent" , "size" )
113+
114+ def __init__ (
115+ self , item : tuple [int , T ] | None , parent : _StackState [T ] | None
116+ ) -> None :
117+ self .item = item
118+ self .parent = parent
119+ self .size = 0 if parent is None else parent .size + 1
120+ self .merged : tuple [tuple [tuple [int , T ], ...], tuple [T , ...]] | None = None
121+
122+ def __iter__ (self ) -> Iterator [tuple [int , T ]]:
123+ items = []
124+ node = self
125+ while node .parent is not None :
126+ assert node .item is not None
127+ items .append (node .item )
128+ node = node .parent
129+ return iter (reversed (items ))
147130
148131 def __repr__ (self ) -> str :
149- if self ._items :
150- items = repr (self ._items )
151- else :
152- items = ""
153- return f"{ self .__class__ .__name__ } ({ items } )"
154-
155-
156- FrozenStack = TypeAliasType (
157- "FrozenStack" , FrozenSequence [tuple [int , T ]], type_params = (T ,)
158- )
132+ return f"<_StackState len={ self .size } >"
159133
160134
161135class ContextStackManager (Generic [T ]):
@@ -164,57 +138,82 @@ class ContextStackManager(Generic[T]):
164138 """
165139
166140 def __init__ (self ) -> None :
167- self ._global : list [T ] = []
168- self ._context_stack : ContextVar [FrozenStack [T ]] = ContextVar (
169- "stack" , default = FrozenSequence ()
141+ self ._write_lock = threading .Lock ()
142+ # () is a singleton, so empty globals from different eras are
143+ # identity-equal; harmless for the memo check because equal contents
144+ # produce equal merges.
145+ self ._global : tuple [tuple [int , T ], ...] = ()
146+ # All contexts share the root node; safe because nodes are immutable
147+ # apart from the atomically-published memo slot.
148+ self ._root : _StackState [T ] = _StackState (None , None )
149+ self ._context_stack : ContextVar [_StackState [T ]] = ContextVar (
150+ "stack" , default = self ._root
170151 )
171- self ._cache : WeakKeyDictionary [FrozenStack [T ], list [T ]] = WeakKeyDictionary ()
172152 self ._stackop : Callable [[], int ] = get_iterator_next_method (count ())
173- self ._lock = threading .Lock ()
174153
175154 def iter_context_objects (self ) -> Iterator [T ]:
176155 """Returns an iterator over all objects for the combined
177156 application and context cache.
178157 """
158+ node = self ._context_stack .get ()
159+ current_global = self ._global
160+
161+ memo = node .merged
162+ # The memo holds a strong reference to the global it was computed
163+ # against, so the identity check cannot be fooled by id reuse.
164+ if memo is not None and memo [0 ] is current_global :
165+ return iter (memo [1 ])
166+
167+ stack_objects = sorted (chain (current_global , node ), reverse = True )
168+ objects = tuple (x [1 ] for x in stack_objects )
169+ # Nodes are shared across threads via copy_context(); the tuple is
170+ # built in full before this single store, so concurrent readers see
171+ # either None or a complete memo.
172+ node .merged = (current_global , objects )
173+
174+ # A live chain deliberately keeps a memo per node — each is the warm
175+ # cache for a depth the context may pop back to — but memos computed
176+ # against an older application stack would pin its popped handlers
177+ # until then; drop those now. Clearing races benignly with
178+ # concurrent stores.
179+ ancestor = node .parent
180+ while ancestor is not None :
181+ memo = ancestor .merged
182+ if memo is not None and memo [0 ] is not current_global :
183+ ancestor .merged = None
184+ ancestor = ancestor .parent
179185
180- with self ._lock :
181- stack = self ._context_stack .get ()
182- objects = self ._cache .get (stack )
183- if objects is None :
184- if len (self ._cache ) >= _MAX_CONTEXT_OBJECT_CACHE :
185- self ._cache .clear ()
186- stack_objects = sorted (
187- chain (
188- self ._global ,
189- stack ,
190- ),
191- reverse = True ,
192- )
193- objects = [x [1 ] for x in stack_objects ]
194- self ._cache [stack ] = objects
195186 return iter (objects )
196187
197188 def push_context (self , obj : T ) -> None :
198- item = (self ._stackop (), obj )
199- stack = self ._context_stack .get ()
200- self ._context_stack .set (FrozenSequence ((* stack , item )))
189+ node = self ._context_stack .get ()
190+ self ._context_stack .set (_StackState ((self ._stackop (), obj ), node ))
201191
202192 def pop_context (self ) -> T :
203- stack = self ._context_stack .get ()
204- assert stack , "no objects on stack"
205- * remaining , poppped = stack
206- self ._context_stack .set (FrozenSequence (remaining ))
207- return poppped [1 ]
193+ node = self ._context_stack .get ()
194+ if node .parent is None :
195+ raise AssertionError ("no objects on stack" )
196+ assert node .item is not None
197+ self ._context_stack .set (node .parent )
198+ return node .item [1 ]
208199
209200 def push_application (self , obj : T ) -> None :
210- item = (self ._stackop (), obj )
211- with self ._lock :
212- self ._global .append (item )
213- self ._cache .clear ()
201+ with self ._write_lock :
202+ item = (self ._stackop (), obj )
203+ self ._global = (* self ._global , item )
204+ # Best-effort lifetime hygiene, not needed for correctness: the
205+ # root memo may reference the just-replaced global, and the root
206+ # never dies, so drop the memo now rather than waiting for a
207+ # future empty-stack iteration to replace it.
208+ self ._root .merged = None
214209
215210 def pop_application (self ) -> T :
216- with self ._lock :
217- assert self ._global , "no objects on application stack"
218- popped = self ._global .pop ()
219- self ._cache .clear ()
220- return popped [1 ]
211+ with self ._write_lock :
212+ current = self ._global
213+ if not current :
214+ raise AssertionError ("no objects on application stack" )
215+ self ._global = current [:- 1 ]
216+ # See push_application: keep the immortal root memo from pinning
217+ # the popped handler.
218+ self ._root .merged = None
219+ return current [- 1 ][1 ]
0 commit comments