Skip to content

Commit 5ed4a31

Browse files
Patch Django autoreload to not terminate on boot error (#47)
1 parent 9fffec1 commit 5ed4a31

File tree

1 file changed

+60
-15
lines changed

1 file changed

+60
-15
lines changed

src/tidewave/django/__init__.py

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
import io
66
import logging
7+
import subprocess
8+
import sys
79
import threading
10+
import time
811
import traceback
912
from typing import Any, Callable
1013

14+
import django.utils.autoreload
1115
from django.conf import settings
1216
from django.http import HttpResponse
1317

@@ -31,33 +35,74 @@ def add_threading_except_hook():
3135

3236
original_threading_excepthook = threading.excepthook
3337

34-
3538
def tidewave_excepthook(args):
3639
if args.thread is not None and args.thread.name == "django-main-thread":
3740
formatted = "".join(
3841
traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)
3942
)
40-
message = "Django terminated with exception:\n" + formatted
41-
record = logging.LogRecord(
42-
name="tidewave.exceptionhook",
43-
level=logging.ERROR,
44-
pathname="",
45-
lineno=0,
46-
msg=message,
47-
args=(),
48-
exc_info=None,
49-
)
50-
51-
file_handler.emit(record)
52-
file_handler.flush()
43+
emit_error_log("Django terminated with exception:\n" + formatted)
5344

5445
if original_threading_excepthook is not None:
5546
original_threading_excepthook(args)
5647

57-
5848
threading.excepthook = tidewave_excepthook
5949

50+
51+
def patch_django_autoreload():
52+
original_restart_with_reloader = django.utils.autoreload.restart_with_reloader
53+
54+
def new_restart_with_reloader():
55+
while True:
56+
# With autoreload enabled, Django has a top-level process,
57+
# which then starts a child server process. The child
58+
# process also runs a watcher and calls sys.exit(3) whenever
59+
# files change. The top-level process then tries to start
60+
# a new child. This loop is defined in restart_with_reloader [1].
61+
# If booting the child fails (for example, exception in
62+
# settings.py), the loop stops and all processes terminate.
63+
# We want to keep the process running, so we patch the
64+
# loop, such that on boot failure, we wait 1 second and
65+
# try booting again. We want to enable the LLM to see the
66+
# exception, so we run "python manage.py shell", which
67+
# should fail with the exception in stderr, and then we
68+
# write that exception to our log file.
69+
#
70+
# [1]: https://github.com/django/django/blob/5.2.6/django/utils/autoreload.py#L269-L275
71+
original_restart_with_reloader()
72+
73+
manage_py_path = sys.argv[0]
74+
process = subprocess.run(
75+
[sys.executable, manage_py_path, "shell"], capture_output=True, text=True, input=""
76+
)
77+
78+
if process.returncode != 0:
79+
print("[Tidewave] Django failed to boot, retrying in 2 seconds")
80+
emit_error_log(
81+
"Django failed to boot, retrying in 2 seconds. Error:" + process.stderr
82+
)
83+
time.sleep(2)
84+
85+
django.utils.autoreload.restart_with_reloader = new_restart_with_reloader
86+
87+
88+
def emit_error_log(message: str):
89+
record = logging.LogRecord(
90+
name="tidewave",
91+
level=logging.ERROR,
92+
pathname="",
93+
lineno=0,
94+
msg=message,
95+
args=(),
96+
exc_info=None,
97+
)
98+
99+
file_handler.emit(record)
100+
file_handler.flush()
101+
102+
60103
add_threading_except_hook()
104+
patch_django_autoreload()
105+
61106

62107
class Middleware:
63108
"""Django middleware for Tidewave MCP integration

0 commit comments

Comments
 (0)