@@ -47,6 +47,8 @@ class PoolExec:
4747
4848 #: whether _run_worker must hand this strategy the process main thread
4949 needs_primary_thread = False
50+ #: whether each concurrent exec occupies a thread of the worker's budget
51+ exec_costs_a_thread = True
5052
5153 def __init__ (self , gateway : WorkerGateway ) -> None :
5254 self .gateway = gateway
@@ -69,6 +71,28 @@ def trigger_shutdown(self) -> None:
6971 pass
7072
7173
74+ def _thread_signal () -> tuple [trio .Event , Callable [[], None ]]:
75+ """A loop-side event plus the callable that sets it from a foreign thread.
76+
77+ Waiting for an exec that runs *elsewhere* -- the main thread, a greenlet
78+ -- must not park a pool thread on a ``threading.Event``: that thread is
79+ part of the budget exec placement is rationed against, so an exec that
80+ costs no thread would still spend one waiting for itself.
81+
82+ Must be built on the loop (it captures the trio token).
83+ """
84+ done = trio .Event ()
85+ token = trio .lowlevel .current_trio_token ()
86+
87+ def signal () -> None :
88+ # posted callbacks must not raise, and a loop that ended while the
89+ # exec ran leaves nobody to wake
90+ with suppress (trio .RunFinishedError ):
91+ token .run_sync_soon (done .set )
92+
93+ return done , signal
94+
95+
7296class PrimaryThreadPump :
7397 """Runs exec requests handed to it on the process main thread.
7498
@@ -79,10 +103,14 @@ class PrimaryThreadPump:
79103
80104 #: whether _run_worker must hand this strategy the process main thread
81105 needs_primary_thread = True
106+ #: whether each concurrent exec occupies a thread of the worker's budget
107+ #: (see TrioWorkerExec.capacity): the primary one does not, but the
108+ #: overflow HybridExec sends to the pool does
109+ exec_costs_a_thread = True
82110
83111 def __init__ (self , gateway : WorkerGateway ) -> None :
84112 self .gateway = gateway
85- self ._primary : Mailbox [tuple [Channel , ExecItem , threading . Event ] | None ] = (
113+ self ._primary : Mailbox [tuple [Channel , ExecItem , Callable [[], None ] ] | None ] = (
86114 Mailbox ()
87115 )
88116
@@ -91,9 +119,9 @@ async def admit(self, channel: Channel, item: ExecItem) -> bool:
91119 return True
92120
93121 async def run (self , channel : Channel , item : ExecItem ) -> None :
94- done = threading . Event ()
95- self ._primary .put ((channel , item , done ))
96- await trio . to_thread . run_sync ( done .wait , abandon_on_cancel = True )
122+ done , signal = _thread_signal ()
123+ self ._primary .put ((channel , item , signal ))
124+ await done .wait ( )
97125
98126 def released (self ) -> None :
99127 """Hook: the main thread is free again (called on it, before done)."""
@@ -104,14 +132,14 @@ def integrate_as_primary_thread(self) -> None:
104132 task = self ._primary .get ()
105133 if task is None :
106134 break
107- channel , item , done = task
135+ channel , item , signal = task
108136 try :
109137 self .gateway .executetask ((channel , item ))
110138 finally :
111139 # Release before signalling: the next request should see the
112140 # main thread free as early as we can make it.
113141 self .released ()
114- done . set ()
142+ signal ()
115143
116144 def trigger_shutdown (self ) -> None :
117145 self ._primary .put (None )
@@ -186,34 +214,39 @@ class GreenletExec:
186214 """
187215
188216 needs_primary_thread = True
217+ #: greenlets, not threads: concurrent execs here cost the thread budget
218+ #: nothing, so they are not rationed against it
219+ exec_costs_a_thread = False
189220
190221 def __init__ (self , gateway : WorkerGateway ) -> None :
191222 from ._boundary import make_wakener
192223
193224 self .gateway = gateway
194225 # The integrate loop blocks in get() on the hub thread: a gevent
195226 # wakener parks only its root greenlet, letting exec greenlets run.
196- self ._primary : Mailbox [tuple [Channel , ExecItem , threading . Event ] | None ] = (
227+ self ._primary : Mailbox [tuple [Channel , ExecItem , Callable [[], None ] ] | None ] = (
197228 Mailbox (make_wakener ("gevent" ))
198229 )
199230
200231 async def admit (self , channel : Channel , item : ExecItem ) -> bool :
201232 return True
202233
203234 async def run (self , channel : Channel , item : ExecItem ) -> None :
204- done = threading . Event ()
205- self ._primary .put ((channel , item , done ))
206- await trio . to_thread . run_sync ( done .wait , abandon_on_cancel = True )
235+ done , signal = _thread_signal ()
236+ self ._primary .put ((channel , item , signal ))
237+ await done .wait ( )
207238
208239 def integrate_as_primary_thread (self ) -> None :
209240 """Run the hub on the main thread, spawning a greenlet per exec."""
210241 import gevent
211242
212- def run_exec (channel : Channel , item : ExecItem , done : threading .Event ) -> None :
243+ def run_exec (
244+ channel : Channel , item : ExecItem , signal : Callable [[], None ]
245+ ) -> None :
213246 try :
214247 self .gateway .executetask ((channel , item ))
215248 finally :
216- done . set ()
249+ signal ()
217250
218251 while True :
219252 task = self ._primary .get ()
@@ -373,14 +406,19 @@ def active_count(self) -> int:
373406 with self ._lock :
374407 return self ._running
375408
376- def capacity (self ) -> int :
377- """Concurrent execs this worker admits (loop thread only) .
409+ def capacity (self ) -> int | None :
410+ """Concurrent execs this worker admits, or None for unbounded .
378411
379- Resolved on first use rather than in ``__init__``, which runs before
380- there is a loop to read the thread limiter from. Reported by
381- ``remote_status()`` as ``execcapacity``, because a coordinator that
382- just had a request refused wants the number it hit.
412+ Only the thread-shaped strategies are rationed: a greenlet exec
413+ spends no thread, so bounding it would cap the very thing
414+ ``profile=gevent`` exists to provide. Resolved on first use rather
415+ than in ``__init__``, which runs before there is a loop to read the
416+ thread limiter from. Reported by ``remote_status()`` as
417+ ``execcapacity``, because a coordinator that just had a request
418+ refused wants the number it hit.
383419 """
420+ if not self .strategy .exec_costs_a_thread :
421+ return None
384422 if self ._capacity is None :
385423 self ._capacity = exec_capacity ()
386424 return self ._capacity
@@ -403,7 +441,7 @@ def schedule(self, channel: Channel, sourcetask: bytes) -> None:
403441 if self ._shutting_down :
404442 channel .close ("execution disallowed" )
405443 return
406- if self ._running >= capacity :
444+ if capacity is not None and self ._running >= capacity :
407445 full = True
408446 else :
409447 full = False
@@ -416,7 +454,7 @@ def schedule(self, channel: Channel, sourcetask: bytes) -> None:
416454 " concurrency limit (half its thread budget; the rest serves"
417455 " channel callbacks and protocol work). Use more gateways, or"
418456 " a profile whose execs are not threads (profile=trio,"
419- " profile=gevent)."
457+ " profile=gevent), which are not bounded this way ."
420458 )
421459 return
422460 # Already on the Trio host thread (Message handler).
0 commit comments