33import functools
44import os
55import signal
6- import socket
76import threading
87import time
98from collections .abc import Callable
1312
1413from uvicorn import Config
1514from uvicorn ._types import ASGIReceiveCallable , ASGISendCallable , Scope
15+ from uvicorn .server import STARTUP_FAILURE
1616from uvicorn .supervisors import Multiprocess
1717from uvicorn .supervisors .multiprocess import Process
1818
@@ -41,22 +41,32 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
4141 pass # pragma: no cover
4242
4343
44- def run (sockets : list [socket .socket ] | None ) -> None :
45- while True : # pragma: no cover
46- time .sleep (1 )
44+ async def lifespan_failure_app (scope : Scope , receive : ASGIReceiveCallable , send : ASGISendCallable ) -> None :
45+ assert scope ["type" ] == "lifespan"
46+ message = await receive ()
47+ if message ["type" ] == "lifespan.startup" :
48+ raise RuntimeError ("Startup failed" )
4749
4850
4951def test_process_ping_pong () -> None :
50- process = Process (Config (app = app ), target = lambda x : None , sockets = [])
52+ process = Process (Config (app = app ), sockets = [])
5153 threading .Thread (target = process .always_pong , daemon = True ).start ()
5254 assert process .ping ()
5355
5456
5557def test_process_ping_pong_timeout () -> None :
56- process = Process (Config (app = app ), target = lambda x : None , sockets = [])
58+ process = Process (Config (app = app ), sockets = [])
5759 assert not process .ping (0.1 )
5860
5961
62+ def test_process_ready () -> None :
63+ process = Process (Config (app = app ), sockets = [])
64+ assert not process .ready
65+ process .notify_started ()
66+ assert process .ready
67+ assert process .ready
68+
69+
6070@new_console_in_windows
6171def test_multiprocess_run () -> None :
6272 """
@@ -66,7 +76,7 @@ def test_multiprocess_run() -> None:
6676 quit immediately.
6777 """
6878 config = Config (app = app , workers = 2 )
69- supervisor = Multiprocess (config , target = run , sockets = [])
79+ supervisor = Multiprocess (config , sockets = [])
7080 threading .Thread (target = supervisor .run , daemon = True ).start ()
7181 supervisor .signal_queue .append (signal .SIGINT )
7282 supervisor .join_all ()
@@ -78,9 +88,12 @@ def test_multiprocess_health_check() -> None:
7888 Ensure that the health check works as expected.
7989 """
8090 config = Config (app = app , workers = 2 )
81- supervisor = Multiprocess (config , target = run , sockets = [])
91+ supervisor = Multiprocess (config , sockets = [])
8292 threading .Thread (target = supervisor .run , daemon = True ).start ()
83- time .sleep (1 )
93+ deadline = time .monotonic () + 10
94+ while len (supervisor .processes ) < 2 or not all (p .ready for p in supervisor .processes ): # pragma: no cover
95+ assert time .monotonic () < deadline , "Timed out waiting for processes to become ready"
96+ time .sleep (0.1 )
8497 process = supervisor .processes [0 ]
8598 process .kill ()
8699 assert not process .is_alive ()
@@ -92,13 +105,32 @@ def test_multiprocess_health_check() -> None:
92105 supervisor .join_all ()
93106
94107
108+ def test_multiprocess_stops_on_startup_failure () -> None :
109+ """A worker that dies before startup completes shuts the supervisor down instead of
110+ being restarted forever (https://github.com/encode/uvicorn/discussions/2440)."""
111+ config = Config (app = "tests.supervisors.test_multiprocess:app_does_not_exist" , workers = 2 )
112+ supervisor = Multiprocess (config , sockets = [])
113+ with pytest .raises (SystemExit ) as exc_info :
114+ supervisor .run ()
115+ assert exc_info .value .code == STARTUP_FAILURE
116+
117+
118+ def test_multiprocess_stops_on_lifespan_startup_failure () -> None :
119+ """A lifespan startup failure is a startup failure too, not a reason to restart the worker."""
120+ config = Config (app = lifespan_failure_app , lifespan = "on" , workers = 2 )
121+ supervisor = Multiprocess (config , sockets = [])
122+ with pytest .raises (SystemExit ) as exc_info :
123+ supervisor .run ()
124+ assert exc_info .value .code == STARTUP_FAILURE
125+
126+
95127@new_console_in_windows
96128def test_multiprocess_sigterm () -> None :
97129 """
98130 Ensure that the SIGTERM signal is handled as expected.
99131 """
100132 config = Config (app = app , workers = 2 )
101- supervisor = Multiprocess (config , target = run , sockets = [])
133+ supervisor = Multiprocess (config , sockets = [])
102134 threading .Thread (target = supervisor .run , daemon = True ).start ()
103135 time .sleep (1 )
104136 supervisor .signal_queue .append (signal .SIGTERM )
@@ -112,7 +144,7 @@ def test_multiprocess_sigbreak() -> None: # pragma: py-not-win32
112144 Ensure that the SIGBREAK signal is handled as expected.
113145 """
114146 config = Config (app = app , workers = 2 )
115- supervisor = Multiprocess (config , target = run , sockets = [])
147+ supervisor = Multiprocess (config , sockets = [])
116148 threading .Thread (target = supervisor .run , daemon = True ).start ()
117149 time .sleep (1 )
118150 supervisor .signal_queue .append (getattr (signal , "SIGBREAK" ))
@@ -125,7 +157,7 @@ def test_multiprocess_sighup() -> None:
125157 Ensure that the SIGHUP signal is handled as expected.
126158 """
127159 config = Config (app = app , workers = 2 )
128- supervisor = Multiprocess (config , target = run , sockets = [])
160+ supervisor = Multiprocess (config , sockets = [])
129161 threading .Thread (target = supervisor .run , daemon = True ).start ()
130162 time .sleep (1 )
131163 pids = [p .pid for p in supervisor .processes ]
@@ -148,7 +180,7 @@ def test_multiprocess_sigttin() -> None:
148180 Ensure that the SIGTTIN signal is handled as expected.
149181 """
150182 config = Config (app = app , workers = 2 )
151- supervisor = Multiprocess (config , target = run , sockets = [])
183+ supervisor = Multiprocess (config , sockets = [])
152184 threading .Thread (target = supervisor .run , daemon = True ).start ()
153185 supervisor .signal_queue .append (signal .SIGTTIN )
154186 time .sleep (1 )
@@ -163,7 +195,7 @@ def test_multiprocess_sigttou() -> None:
163195 Ensure that the SIGTTOU signal is handled as expected.
164196 """
165197 config = Config (app = app , workers = 2 )
166- supervisor = Multiprocess (config , target = run , sockets = [])
198+ supervisor = Multiprocess (config , sockets = [])
167199 threading .Thread (target = supervisor .run , daemon = True ).start ()
168200 supervisor .signal_queue .append (signal .SIGTTOU )
169201 time .sleep (1 )
0 commit comments