Skip to content

Commit c845c54

Browse files
authored
Closed database connections.
eb16ed0 removed (among a lot of other things) a call to close_old_connections(), mostly because I hadn't understood what that was doing exactly. Since then I've been kept awake at night with the sense of dread that old database connections were piling up unclosed on Django's server. So I wrote this wsgi middleware that should hopefully restore balance in the universe, or at least close db connections.
1 parent 1e87f0f commit c845c54

File tree

4 files changed

+87
-1
lines changed

4 files changed

+87
-1
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
uses: actions/checkout@v4
3535
- name: Install requirements
3636
run: python -m pip install -r requirements.txt
37+
- name: Install backport of unittest.mock
38+
run: python -m pip install mock
3739
- name: Run tests
3840
run: python -m django test tracdjangoplugin.tests
3941
env:
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.core.signals import request_finished, request_started
2+
3+
4+
class DjangoDBManagementMiddleware:
5+
"""
6+
A simple WSGI middleware that manually manages opening/closing db connections.
7+
8+
Django normally does that as part of its own middleware chain, but we're using Trac's middleware
9+
so we must do this by hand.
10+
This hopefully prevents open connections from piling up.
11+
"""
12+
13+
def __init__(self, application):
14+
self.application = application
15+
16+
def __call__(self, environ, start_response):
17+
request_started.send(sender=self.__class__)
18+
try:
19+
for data in self.application(environ, start_response):
20+
yield data
21+
finally:
22+
request_finished.send(sender=self.__class__)

DjangoPlugin/tracdjangoplugin/tests.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
from functools import partial
22

3+
try:
4+
from unittest.mock import Mock
5+
except ImportError:
6+
from mock import Mock
7+
8+
from django.core.signals import request_finished, request_started
39
from django.contrib.auth.forms import AuthenticationForm
410
from django.contrib.auth.models import User
5-
from django.test import TestCase
11+
from django.test import SimpleTestCase, TestCase
612

713
from trac.test import EnvironmentStub, MockRequest
814
from trac.web.api import RequestDone
915
from trac.web.main import RequestDispatcher
1016

17+
from tracdjangoplugin.middlewares import DjangoDBManagementMiddleware
1118
from tracdjangoplugin.plugins import PlainLoginComponent
1219

1320

@@ -127,3 +134,53 @@ def test_login_invalid_username_uppercased(self):
127134
def test_login_invalid_inactive_user(self):
128135
User.objects.create_user(username="test", password="test", is_active=False)
129136
self.assertLoginFails(username="test", password="test")
137+
138+
139+
class DjangoDBManagementMiddlewareTestCase(SimpleTestCase):
140+
@classmethod
141+
def setUpClass(cls):
142+
# Remove receivers from the request_started and request_finished signals,
143+
# replacing them with a mock object so we can still check if they were called.
144+
super(DjangoDBManagementMiddlewareTestCase, cls).setUpClass()
145+
cls._original_signal_receivers = {}
146+
cls.signals = {}
147+
for signal in [request_started, request_finished]:
148+
cls.signals[signal] = Mock()
149+
cls._original_signal_receivers[signal] = signal.receivers
150+
signal.receivers = []
151+
signal.connect(cls.signals[signal])
152+
153+
@classmethod
154+
def tearDownClass(cls):
155+
# Restore the signals we modified in setUpClass() to what they were before
156+
super(DjangoDBManagementMiddlewareTestCase, cls).tearDownClass()
157+
for signal, original_receivers in cls._original_signal_receivers.items():
158+
# messing about with receivers directly is not an official API, so we need to
159+
# call some undocumented methods to make sure caches and such are taken care of.
160+
with signal.lock:
161+
signal.receivers = original_receivers
162+
signal._clear_dead_receivers()
163+
signal.sender_receivers_cache.clear()
164+
165+
def setUp(self):
166+
super(DjangoDBManagementMiddlewareTestCase, self).setUp()
167+
for mockobj in self.signals.values():
168+
mockobj.reset_mock()
169+
170+
def test_request_start_fired(self):
171+
app = DjangoDBManagementMiddleware(lambda environ, start_response: [b"test"])
172+
output = b"".join(app(None, None))
173+
self.assertEqual(output, b"test")
174+
self.signals[request_started].assert_called_once()
175+
176+
def test_request_finished_fired(self):
177+
app = DjangoDBManagementMiddleware(lambda environ, start_response: [b"test"])
178+
output = b"".join(app(None, None))
179+
self.assertEqual(output, b"test")
180+
self.signals[request_finished].assert_called_once()
181+
182+
def test_request_finished_fired_even_with_error(self):
183+
app = DjangoDBManagementMiddleware(lambda environ, start_response: [1 / 0])
184+
with self.assertRaises(ZeroDivisionError):
185+
list(app(None, None))
186+
self.signals[request_finished].assert_called_once()

DjangoPlugin/tracdjangoplugin/wsgi.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
# Python 3 would perform better here, but we are still on 2.7 for Trac, so leak fds for now.
1313
from tracopt.versioncontrol.git import PyGIT
1414

15+
from .middlewares import DjangoDBManagementMiddleware
16+
17+
18+
application = DjangoDBManagementMiddleware(application)
19+
1520
PyGIT.close_fds = False
1621

1722
trac_dsn = os.getenv("SENTRY_DSN")

0 commit comments

Comments
 (0)