Skip to content

Commit 22456ff

Browse files
committed
Add an optional timeout to dmypy_server
This allows it ot shut down after a period of inactivity to avoid hogging memory. Also don't crash the server on an error communicating with the client.
1 parent da71cde commit 22456ff

File tree

2 files changed

+24
-5
lines changed

2 files changed

+24
-5
lines changed

mypy/dmypy.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@
3030
start_parser = p = subparsers.add_parser('start', help="Start daemon")
3131
p.add_argument('--log-file', metavar='FILE', type=str,
3232
help="Direct daemon stdout/stderr to FILE")
33+
p.add_argument('--timeout', metavar='TIMEOUT', type=int,
34+
help="Server shutdown timeout (in seconds)")
3335
p.add_argument('flags', metavar='FLAG', nargs='*', type=str,
3436
help="Regular mypy flags (precede with --)")
3537

3638
restart_parser = p = subparsers.add_parser('restart',
3739
help="Restart daemon (stop or kill followed by start)")
3840
p.add_argument('--log-file', metavar='FILE', type=str,
3941
help="Direct daemon stdout/stderr to FILE")
42+
p.add_argument('--timeout', metavar='TIMEOUT', type=int,
43+
help="Server shutdown timeout (in seconds)")
4044
p.add_argument('flags', metavar='FLAG', nargs='*', type=str,
4145
help="Regular mypy flags (precede with --)")
4246

@@ -63,6 +67,8 @@
6367
hang_parser = p = subparsers.add_parser('hang', help="Hang for 100 seconds")
6468

6569
daemon_parser = p = subparsers.add_parser('daemon', help="Run daemon in foreground")
70+
p.add_argument('--timeout', metavar='TIMEOUT', type=int,
71+
help="Server shutdown timeout (in seconds)")
6672
p.add_argument('flags', metavar='FLAG', nargs='*', type=str,
6773
help="Regular mypy flags (precede with --)")
6874

@@ -148,7 +154,8 @@ def start_server(args: argparse.Namespace) -> None:
148154
"""Start the server from command arguments and wait for it."""
149155
# Lazy import so this import doesn't slow down other commands.
150156
from mypy.dmypy_server import daemonize, Server, process_start_options
151-
if daemonize(Server(process_start_options(args.flags)).serve, args.log_file) != 0:
157+
if daemonize(Server(process_start_options(args.flags), timeout=args.timeout).serve,
158+
args.log_file) != 0:
152159
sys.exit(1)
153160
wait_for_server()
154161

@@ -284,7 +291,7 @@ def do_daemon(args: argparse.Namespace) -> None:
284291
"""Serve requests in the foreground."""
285292
# Lazy import so this import doesn't slow down other commands.
286293
from mypy.dmypy_server import Server, process_start_options
287-
Server(process_start_options(args.flags)).serve()
294+
Server(process_start_options(args.flags), timeout=args.timeout).serve()
288295

289296

290297
@action(help_parser)

mypy/dmypy_server.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,14 @@ class Server:
109109
# NOTE: the instance is constructed in the parent process but
110110
# serve() is called in the grandchild (by daemonize()).
111111

112-
def __init__(self, options: Options, alt_lib_path: Optional[str] = None) -> None:
112+
def __init__(self, options: Options,
113+
timeout: Optional[int] = None,
114+
alt_lib_path: Optional[str] = None) -> None:
113115
"""Initialize the server with the desired mypy flags."""
114116
self.saved_cache = {} # type: mypy.build.SavedCache
115117
self.fine_grained = options.fine_grained_incremental
116118
self.options = options
119+
self.timeout = timeout
117120
self.alt_lib_path = alt_lib_path
118121
self.fine_grained_manager = None # type: Optional[FineGrainedBuildManager]
119122

@@ -134,13 +137,19 @@ def serve(self) -> None:
134137
"""Serve requests, synchronously (no thread or fork)."""
135138
try:
136139
sock = self.create_listening_socket()
140+
if self.timeout is not None:
141+
sock.settimeout(self.timeout)
137142
try:
138143
with open(STATUS_FILE, 'w') as f:
139144
json.dump({'pid': os.getpid(), 'sockname': sock.getsockname()}, f)
140145
f.write('\n') # I like my JSON with trailing newline
141146
while True:
142147
conn, addr = sock.accept()
143-
data = receive(conn)
148+
try:
149+
data = receive(conn)
150+
except OSError as err:
151+
conn.close() # Maybe the client hung up
152+
continue
144153
resp = {} # type: Dict[str, Any]
145154
if 'command' not in data:
146155
resp = {'error': "No command found in request"}
@@ -159,12 +168,15 @@ def serve(self) -> None:
159168
if command == 'stop':
160169
sock.close()
161170
sys.exit(0)
171+
except socket.timeout:
172+
print("Exiting due to inactivity.")
173+
sys.exit(0)
162174
finally:
163175
os.unlink(STATUS_FILE)
164176
finally:
165177
os.unlink(self.sockname)
166178
exc_info = sys.exc_info()
167-
if exc_info[0]:
179+
if exc_info[0] and exc_info[0] is not SystemExit:
168180
traceback.print_exception(*exc_info) # type: ignore
169181

170182
def create_listening_socket(self) -> socket.socket:

0 commit comments

Comments
 (0)