launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #04861
[Merge] lp:~stub/launchpad/librarian-disco into lp:launchpad
Stuart Bishop has proposed merging lp:~stub/launchpad/librarian-disco into lp:launchpad with lp:~stub/launchpad/branch-rewrite as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #840098 in Launchpad itself: "Librarian should return 503 pages during database outages"
https://bugs.launchpad.net/launchpad/+bug/840098
For more details, see:
https://code.launchpad.net/~stub/launchpad/librarian-disco/+merge/73934
= Summary =
Tests that confirm the Librarian survives database outages, and make it emit the correct response code while we are there.
== Proposed fix ==
== Pre-implementation notes ==
== Implementation details ==
== Tests ==
== Demo and Q/A ==
= Launchpad lint =
Just lint from dependant branches. Cleaned up web.py.
--
https://code.launchpad.net/~stub/launchpad/librarian-disco/+merge/73934
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stub/launchpad/librarian-disco into lp:launchpad.
=== added file 'lib/canonical/librarian/tests/test_db_outage.py'
--- lib/canonical/librarian/tests/test_db_outage.py 1970-01-01 00:00:00 +0000
+++ lib/canonical/librarian/tests/test_db_outage.py 2011-09-03 05:17:27 +0000
@@ -0,0 +1,109 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test behavior of the Librarian during a database outage.
+
+Database outages happen by accident and during fastdowntime deployments."""
+
+__metaclass__ = type
+
+from cStringIO import StringIO
+import urllib2
+
+from fixtures import Fixture
+
+from canonical.librarian.client import LibrarianClient
+from canonical.librarian.testing.server import LibrarianServerFixture
+from canonical.testing.layers import (
+ BaseLayer,
+ DatabaseFunctionalLayer,
+ )
+from lp.testing import TestCase
+from lp.testing.fixture import PGBouncerFixture
+
+
+class PGBouncerLibrarianLayer(DatabaseFunctionalLayer):
+ """Custom layer for TestLibrarianDBOutage.
+
+ We are using a custom layer instead of standard setUp/tearDown to
+ avoid the lengthy Librarian startup time, and to cope with undoing
+ changes made to BaseLayer.config_fixture to allow access to the
+ Librarian we just started up.
+ """
+ pgbouncer_fixture = None
+ librarian_fixture = None
+
+ @classmethod
+ def setUp(cls):
+ # Fixture to hold other fixtures.
+ cls._fixture = Fixture()
+ cls._fixture.setUp()
+
+ cls.pgbouncer_fixture = PGBouncerFixture()
+ # Install the PGBouncer fixture so we shut it down to
+ # create database outages.
+ cls._fixture.useFixture(cls.pgbouncer_fixture)
+
+ # Bring up the Librarian, which will be connecting via
+ # pgbouncer.
+ cls.librarian_fixture = LibrarianServerFixture(
+ BaseLayer.config_fixture)
+ cls._fixture.useFixture(cls.librarian_fixture)
+
+ @classmethod
+ def tearDown(cls):
+ cls.pgbouncer_fixture = None
+ cls.librarian_fixture = None
+ cls._fixture.cleanUp()
+
+ @classmethod
+ def testSetUp(cls):
+ cls.pgbouncer_fixture.start()
+
+ @classmethod
+ def testTearDown(cls):
+ cls.pgbouncer_fixture.start()
+
+
+class TestLibrarianDBOutage(TestCase):
+ layer = PGBouncerLibrarianLayer
+
+ def setUp(self):
+ super(TestLibrarianDBOutage, self).setUp()
+ self.pgbouncer = PGBouncerLibrarianLayer.pgbouncer_fixture
+ self.client = LibrarianClient()
+
+ # Add a file to the Librarian so we can download it.
+ data = 'whatever'
+ self.url = self.client.remoteAddFile(
+ 'foo.txt', len(data), StringIO(data), 'text/plain')
+
+ def get_error_code(self):
+ # We need to talk to every Librarian thread to ensure all the
+ # Librarian database connections are in a known state.
+ # XXX StuartBishop 2011-09-01 bug=840046: 20 might be overkill
+ # for the test run, but we have no real way of knowing how many
+ # connections are in use.
+ num_librarian_threads = 20
+ codes = set()
+ for count in range(num_librarian_threads):
+ try:
+ urllib2.urlopen(self.url).read()
+ codes.add(200)
+ except urllib2.HTTPError, error:
+ codes.add(error.code)
+ self.assertTrue(len(codes) == 1, 'Mixed responses: %s' % str(codes))
+ return codes.pop()
+
+ def test_outage(self):
+ # Everything should be working fine to start with.
+ self.assertEqual(self.get_error_code(), 200)
+
+ # When the outage kicks in, we start getting 503 responses
+ # instead of 200 and 404s.
+ self.pgbouncer.stop()
+ self.assertEqual(self.get_error_code(), 503)
+
+ # When the outage is over, things are back to normal.
+ self.pgbouncer.start()
+ self.assertEqual(self.get_error_code(), 200)
=== modified file 'lib/canonical/librarian/web.py'
--- lib/canonical/librarian/web.py 2011-03-23 16:28:51 +0000
+++ lib/canonical/librarian/web.py 2011-09-03 05:17:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -7,6 +7,7 @@
import time
from urlparse import urlparse
+from storm.exceptions import DisconnectionError
from twisted.python import log
from twisted.web import resource, static, util, server, proxy
from twisted.internet.threads import deferToThread
@@ -26,7 +27,8 @@
<h1>Launchpad Librarian</h1>
<p>
http://librarian.launchpad.net/ is a
- file repository used by <a href="https://launchpad.net/">Launchpad</a>.
+ file repository used by
+ <a href="https://launchpad.net/">Launchpad</a>.
</p>
<p><small>Copyright 2004-2009 Canonical Ltd.</small></p>
<!-- kthxbye. -->
@@ -107,7 +109,8 @@
token = request.args.get('token', [None])[0]
path = request.path
- deferred = deferToThread(self._getFileAlias, self.aliasID, token, path)
+ deferred = deferToThread(
+ self._getFileAlias, self.aliasID, token, path)
deferred.addCallback(
self._cb_getFileAlias, filename, request
)
@@ -125,11 +128,19 @@
raise NotFound
def _eb_getFileAlias(self, failure):
- failure.trap(NotFound)
- return fourOhFour
+ err = failure.trap(NotFound, DisconnectionError)
+ if err == DisconnectionError:
+ return resource.ErrorPage(
+ 503, 'Database unavailable',
+ 'A required database is unavailable.\n'
+ 'See http://identi.ca/launchpadstatus '
+ 'for maintenance and outage notifications.')
+ else:
+ return fourOhFour
def _cb_getFileAlias(
- self, (dbcontentID, dbfilename, mimetype, date_created, restricted),
+ self,
+ (dbcontentID, dbfilename, mimetype, date_created, restricted),
filename, request
):
# Return a 404 if the filename in the URL is incorrect. This offers
@@ -167,6 +178,7 @@
class File(static.File):
isLeaf = True
+
def __init__(
self, contentType, encoding, modification_time, *args, **kwargs):
# Have to convert the UTC datetime to POSIX timestamp (localtime)
=== modified file 'lib/canonical/testing/layers.py'
--- lib/canonical/testing/layers.py 2011-08-19 13:58:57 +0000
+++ lib/canonical/testing/layers.py 2011-09-03 05:17:27 +0000
@@ -1502,6 +1502,7 @@
class LaunchpadTestSetup(PgTestSetup):
template = 'launchpad_ftest_template'
dbuser = 'launchpad'
+ host = 'localhost'
class LaunchpadZopelessLayer(LaunchpadScriptLayer):
=== modified file 'lib/lp/testing/fixture.py'
--- lib/lp/testing/fixture.py 2011-09-03 05:17:26 +0000
+++ lib/lp/testing/fixture.py 2011-09-03 05:17:27 +0000
@@ -85,6 +85,10 @@
self.users[section_name] = 'trusted'
self.users[section_name + '_ro'] = 'trusted'
self.users[os.environ['USER']] = 'trusted'
+ self.users['pgbouncer'] = 'trusted'
+
+ # Administrative access is useful for debugging.
+ self.admin_users = ['launchpad', 'pgbouncer', os.environ['USER']]
def setUp(self):
super(PGBouncerFixture, self).setUp()