1414
1515from __future__ import annotations
1616
17+ import atexit
1718import importlib
1819import io
1920import json
2021import os
22+ import queue
2123import re
2224import signal
2325import socket
3436from .contract import CONTRACT_FILE , RECEIPT_ATTRIBUTE , Contract
3537from .protocol import (
3638 ADDRESS_VARIABLE ,
39+ CLOSE_SECONDS ,
3740 FAILURE_CODE_HEADER ,
3841 FAILURE_CODE_PATTERN ,
3942 HARNESS_CONCURRENCY ,
@@ -142,6 +145,58 @@ def drain(self, timeout: float) -> int:
142145 return self ._active
143146
144147
148+ class _Diagnostics :
149+ """The harness's own diagnostics, written so that nothing waits on stderr.
150+
151+ A placement that never reads the stream would otherwise stall whichever
152+ thread printed — and a request thread printing holds its call's slot, so
153+ every later call would be refused. Messages queue for one writer, which
154+ writes the descriptor directly and so holds no stream lock that a thread
155+ or interpreter shutdown could wait on. Past the queue's bound a message is
156+ dropped and counted rather than waited for.
157+ """
158+
159+ def __init__ (self , capacity : int = 1024 ) -> None :
160+ self ._queue : queue .Queue [str ] = queue .Queue (capacity )
161+ self ._dropped = 0
162+ self ._lock = threading .Lock ()
163+ self ._closing = threading .Event ()
164+ self ._writer = threading .Thread (target = self ._write , name = "codefly-diagnostics" , daemon = True )
165+ self ._writer .start ()
166+
167+ def say (self , message : str ) -> None :
168+ try :
169+ self ._queue .put_nowait (message )
170+ except queue .Full :
171+ with self ._lock :
172+ self ._dropped += 1
173+
174+ def _write (self ) -> None :
175+ while not (self ._closing .is_set () and self ._queue .empty ()):
176+ try :
177+ message = self ._queue .get (timeout = 0.05 )
178+ except queue .Empty :
179+ continue
180+ data = f"[codefly] { message } \n " .encode ("utf-8" , "backslashreplace" )
181+ try :
182+ while data :
183+ data = data [os .write (2 , data ):]
184+ except OSError :
185+ pass
186+
187+ def close (self , timeout : float ) -> None :
188+ """Write what is queued, for at most this long."""
189+ with self ._lock :
190+ dropped , self ._dropped = self ._dropped , 0
191+ if dropped :
192+ self .say (f"{ dropped } diagnostics were dropped while stderr was not being read" )
193+ self ._closing .set ()
194+ self ._writer .join (timeout )
195+
196+
197+ _DIAGNOSTICS = _Diagnostics ()
198+
199+
145200def main () -> int :
146201 """Serve this runnable until it is signalled. The return value is the exit code."""
147202 root = Path (__file__ ).resolve ().parent .parent
@@ -161,7 +216,8 @@ def main() -> int:
161216 calls = _Calls (contract .concurrency or HARNESS_CONCURRENCY )
162217 server = _server (address , contract , calls , handle , receipt_of )
163218 except (OSError , KeyError , ValueError , ProtocolError , AttributeError , ImportError ) as err :
164- print (f"[codefly] { err } " , file = sys .stderr )
219+ _DIAGNOSTICS .say (str (err ))
220+ _DIAGNOSTICS .close (CLOSE_SECONDS )
165221 return 1
166222
167223 stopping = threading .Event ()
@@ -190,26 +246,38 @@ def stop(*_: object) -> None:
190246 # the last one has returned; an idle connection holds nothing.
191247 remaining = calls .drain (SHUTDOWN_GRACE_SECONDS )
192248 if remaining :
193- print (
194- f"[codefly] { remaining } handlers were still running when the { SHUTDOWN_GRACE_SECONDS :g} s grace ended; "
195- "their effects are unproven" ,
196- file = sys .stderr ,
249+ _DIAGNOSTICS .say (
250+ f"{ remaining } handlers were still running when the { SHUTDOWN_GRACE_SECONDS :g} s grace ended; "
251+ "their effects are unproven"
197252 )
198- _exit (1 if remaining else 0 )
199- return 1 if remaining else 0
253+ # No thread the harness started holds a stream lock — its diagnostics are
254+ # written to the descriptor directly — so interpreter shutdown cannot
255+ # abort on one, and the process exits normally: the handler module's
256+ # atexit hooks run and its buffers are flushed.
257+ _DIAGNOSTICS .close (CLOSE_SECONDS )
258+ code = 1 if remaining else 0
259+ if remaining :
260+ _exit_now (code )
261+ return code
200262
201263
202- def _exit (code : int ) -> None :
203- """End the process with this code, whatever its connection threads are doing .
264+ def _exit_now (code : int ) -> None :
265+ """End the process with this code while threads the harness cannot stop are still running .
204266
205- Interpreter finalization with daemon threads still parked on connections —
206- or holding the lock of a stream they are writing a diagnostic to — can
207- abort instead of exiting, and a placement would read a crash where the code
208- says what happened to the calls. Streams get a bounded moment to flush.
267+ A handler past the grace is author code, and it may hold any lock —
268+ including a stream's — so interpreter shutdown beside it could abort, and a
269+ placement would read a crash where the code says what happened to the
270+ calls. The handler module's atexit hooks still run and the standard
271+ streams are flushed, for at most CLOSE_SECONDS, before the process ends.
209272 """
210- flushing = threading .Thread (target = lambda : (sys .stdout .flush (), sys .stderr .flush ()), daemon = True )
211- flushing .start ()
212- flushing .join (2.0 )
273+ def finish () -> None :
274+ atexit ._run_exitfuncs ()
275+ sys .stdout .flush ()
276+ sys .stderr .flush ()
277+
278+ finishing = threading .Thread (target = finish , daemon = True )
279+ finishing .start ()
280+ finishing .join (CLOSE_SECONDS )
213281 os ._exit (code )
214282
215283
@@ -287,6 +355,10 @@ def process_request(self, request, client_address) -> None:
287355 self .register (request )
288356 super ().process_request (request , client_address )
289357
358+ def handle_error (self , request , client_address ) -> None :
359+ """An exception a connection thread did not answer: a diagnostic, not a traceback on stderr."""
360+ _DIAGNOSTICS .say (f"a connection ended on { _describe (sys .exc_info ()[1 ])} " )
361+
290362 def register (self , request ) -> None :
291363 """Count a new connection in, idle, closing the one idle the longest if it is at the bound."""
292364 with self ._lock :
@@ -520,7 +592,8 @@ def _decode(self, body: bytes) -> Call | _Answer:
520592 def _answer (self , answer : _Answer ) -> None :
521593 # Anything but a success may have left request bytes unread, and a
522594 # stopping harness keeps no connection for a call it will refuse.
523- if answer .failure is not None and not _asserts (answer .failure ):
595+ code = _code_of (answer .failure ) if answer .failure is not None else None
596+ if answer .failure is not None and code is None :
524597 # Checked again where it is sent: a code changed after it was
525598 # constructed asserts nothing, and never reaches a header.
526599 answer = _Answer (500 , code = "internal" , message = "the handler's failure code changed after it was checked" )
@@ -530,7 +603,7 @@ def _answer(self, answer: _Answer) -> None:
530603 # The one place this header is set. A caller reads it before the
531604 # status, and without it no status proves anything.
532605 self .send_response (answer .status )
533- self .send_header (FAILURE_CODE_HEADER , answer . failure . code )
606+ self .send_header (FAILURE_CODE_HEADER , code )
534607 body = _error ("failed_precondition" , str (answer .failure ))
535608 elif answer .status == 200 :
536609 self .send_response (200 )
@@ -548,7 +621,7 @@ def _answer(self, answer: _Answer) -> None:
548621 self .end_headers ()
549622 self .wfile .write (body )
550623 if answer .message :
551- print ( f"[codefly] { answer .message } " , file = sys . stderr )
624+ _DIAGNOSTICS . say ( answer .message )
552625
553626 def log_request (self , * _ : object ) -> None :
554627 """No request log: an answer's diagnostics are printed by _answer."""
@@ -559,9 +632,17 @@ def log_message(self, *_: object) -> None:
559632 return _Server (host , port , Handler , readers = readers , connections = readers + calls .limit + IDLE_CONNECTIONS )
560633
561634
562- def _asserts (failure : HandlerFailure ) -> bool :
563- """Whether a failure's code is still one the grammar admits."""
564- return isinstance (failure .code , str ) and FAILURE_CODE_PATTERN .fullmatch (failure .code ) is not None
635+ def _code_of (failure : HandlerFailure ) -> str | None :
636+ """The failure's code, read once, or None when it is no longer one the grammar admits.
637+
638+ Read once because the value checked must be the value sent: a code that
639+ could change between the two reads would reach the header unchecked.
640+ """
641+ try :
642+ code = failure .code
643+ except Exception : # noqa: BLE001 — a code that cannot be read asserts nothing
644+ return None
645+ return code if isinstance (code , str ) and FAILURE_CODE_PATTERN .fullmatch (code ) else None
565646
566647
567648def _listen (address : str ) -> tuple [str , int ]:
0 commit comments