launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05157
[Merge] lp:~allenap/launchpad/longpoll-finish-read-only-request into lp:launchpad
Gavin Panella has proposed merging lp:~allenap/launchpad/longpoll-finish-read-only-request into lp:launchpad with lp:~allenap/launchpad/longpoll-associate-consumer-not-now as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~allenap/launchpad/longpoll-finish-read-only-request/+merge/77915
This branch does two things:
- For those publications that have finishReadOnlyRequest() methods,
this branch expands their responsibilities to also fire a newly
defined event, FinishReadOnlyRequestEvent.
- Wires up the two globally defined RabbitSession instances to these
events.
--
https://code.launchpad.net/~allenap/launchpad/longpoll-finish-read-only-request/+merge/77915
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/longpoll-finish-read-only-request into lp:launchpad.
=== modified file 'lib/canonical/launchpad/doc/webapp-publication.txt'
--- lib/canonical/launchpad/doc/webapp-publication.txt 2011-06-28 15:04:29 +0000
+++ lib/canonical/launchpad/doc/webapp-publication.txt 2011-10-03 11:47:27 +0000
@@ -192,7 +192,7 @@
>>> requestfactory, publicationfactory = factory()
>>> publicationfactory
- <class 'canonical.launchpad.webapp.publication.LaunchpadBrowserPublication'>
+ <class '...LaunchpadBrowserPublication'>
If the request comes in on one of the virtual hosts, the request
factory is wrapped in an ApplicationServerSettingRequestFactory that
@@ -200,7 +200,7 @@
host configured settings.
>>> type(requestfactory)
- <class 'canonical.launchpad.webapp.servers.ApplicationServerSettingRequestFactory'>
+ <class '...ApplicationServerSettingRequestFactory'>
>>> request = requestfactory(StringIO(''), environment)
>>> type(request)
<class 'canonical.launchpad.webapp.servers.LaunchpadBrowserRequest'>
@@ -322,7 +322,7 @@
True
>>> requestfactory, publicationfactory = factory()
>>> publicationfactory
- <class 'canonical.launchpad.webapp.publication.LaunchpadBrowserPublication'>
+ <class '...LaunchpadBrowserPublication'>
Zope Publisher integration
@@ -895,7 +895,7 @@
some string in its finishReadOnlyRequest().
>>> class MyPublication(LaunchpadBrowserPublication):
- ... def finishReadOnlyRequest(self, txn):
+ ... def finishReadOnlyRequest(self, request, ob, txn):
... print "booo!"
>>> publication = MyPublication(None)
=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
--- lib/canonical/launchpad/webapp/interfaces.py 2011-08-11 19:42:23 +0000
+++ lib/canonical/launchpad/webapp/interfaces.py 2011-10-03 11:47: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).
# pylint: disable-msg=E0211,E0213
@@ -887,6 +887,24 @@
self.request = request
+class IFinishReadOnlyRequestEvent(Interface):
+ """An event which gets sent when the publication is ended"""
+
+ object = Attribute("The object to which this request pertains.")
+
+ request = Attribute("The active request.")
+
+
+class FinishReadOnlyRequestEvent:
+ """An event which gets sent when the publication is ended"""
+
+ implements(IFinishReadOnlyRequestEvent)
+
+ def __init__(self, ob, request):
+ self.object = ob
+ self.request = request
+
+
class StormRangeFactoryError(Exception):
"""Raised when a Storm result set cannot be used for slicing by a
StormRangeFactory.
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2011-09-30 19:34:28 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2011-10-03 11:47: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
@@ -62,6 +62,7 @@
import canonical.launchpad.webapp.adapter as da
from canonical.launchpad.webapp.dbpolicy import LaunchpadDatabasePolicy
from canonical.launchpad.webapp.interfaces import (
+ FinishReadOnlyRequestEvent,
IDatabasePolicy,
ILaunchpadRoot,
INotificationResponse,
@@ -151,7 +152,7 @@
# exception was added as a result of bug 597324 (message #10 in
# particular).
return
- referrer = request.getHeader('referer') # match HTTP spec misspelling
+ referrer = request.getHeader('referer') # Match HTTP spec misspelling.
if not referrer:
raise NoReferrerError('No value for REFERER header')
# XXX: jamesh 2007-04-26 bug=98437:
@@ -531,10 +532,11 @@
# Abort the transaction on a read-only request.
# NOTHING AFTER THIS SHOULD CAUSE A RETRY.
if request.method in ['GET', 'HEAD']:
- self.finishReadOnlyRequest(txn)
+ self.finishReadOnlyRequest(request, ob, txn)
elif txn.isDoomed():
- txn.abort() # Sends an abort to the database, even though
+ # The following sends an abort to the database, even though the
# transaction is still doomed.
+ txn.abort()
else:
txn.commit()
@@ -552,12 +554,13 @@
# calling beforeTraversal or doing proper cleanup.
pass
- def finishReadOnlyRequest(self, txn):
+ def finishReadOnlyRequest(self, request, ob, txn):
"""Hook called at the end of a read-only request.
By default it abort()s the transaction, but subclasses may need to
commit it instead, so they must overwrite this.
"""
+ notify(FinishReadOnlyRequestEvent(ob, request))
txn.abort()
def callTraversalHooks(self, request, ob):
@@ -736,11 +739,11 @@
if IBrowserRequest.providedBy(request):
OpStats.stats['http requests'] += 1
status = request.response.getStatus()
- if status == 404: # Not Found
+ if status == 404: # Not Found
OpStats.stats['404s'] += 1
- elif status == 500: # Unhandled exceptions
+ elif status == 500: # Unhandled exceptions
OpStats.stats['500s'] += 1
- elif status == 503: # Timeouts
+ elif status == 503: # Timeouts
OpStats.stats['503s'] += 1
# Increment counters for status code groups.
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2011-08-31 20:49:46 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2011-10-03 11:47: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).
# pylint: disable-msg=W0231,E1002
@@ -34,6 +34,7 @@
from zope.app.server import wsgi
from zope.app.wsgi import WSGIPublisherApplication
from zope.component import getUtility
+from zope.event import notify
from zope.interface import (
alsoProvides,
implements,
@@ -81,6 +82,7 @@
)
from canonical.launchpad.webapp.errorlog import ErrorReportRequest
from canonical.launchpad.webapp.interfaces import (
+ FinishReadOnlyRequestEvent,
IAPIDocRoot,
IBasicLaunchpadRequest,
IBrowserFormNG,
@@ -1206,8 +1208,9 @@
else:
return super(WebServicePublication, self).getResource(request, ob)
- def finishReadOnlyRequest(self, txn):
+ def finishReadOnlyRequest(self, request, ob, txn):
"""Commit the transaction so that created OAuthNonces are stored."""
+ notify(FinishReadOnlyRequestEvent(ob, request))
# Transaction commits usually need to be aware of the possibility of
# a doomed transaction. We do not expect that this code will
# encounter doomed transactions. If it does, this will need to be
=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
--- lib/canonical/launchpad/webapp/tests/test_servers.py 2011-08-16 19:10:40 +0000
+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2011-10-03 11:47: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).
# pylint: disable-msg=E1002
@@ -32,19 +32,24 @@
Interface,
)
+from canonical.launchpad.webapp.interfaces import IFinishReadOnlyRequestEvent
+from canonical.launchpad.webapp.publication import LaunchpadBrowserPublication
from canonical.launchpad.webapp.servers import (
ApplicationServerSettingRequestFactory,
LaunchpadBrowserRequest,
LaunchpadTestRequest,
VHostWebServiceRequestPublicationFactory,
VirtualHostRequestPublicationFactory,
+ web_service_request_to_browser_request,
WebServiceClientRequest,
WebServicePublication,
WebServiceRequestPublicationFactory,
WebServiceTestRequest,
- web_service_request_to_browser_request,
- )
-from lp.testing import TestCase
+ )
+from lp.testing import (
+ EventRecorder,
+ TestCase,
+ )
class SetInWSGIEnvironmentTestCase(TestCase):
@@ -571,6 +576,62 @@
browser_request.get('PATH_INFO'))
+class LoggingTransaction:
+
+ def __init__(self):
+ self.log = []
+
+ def commit(self):
+ self.log.append("COMMIT")
+
+ def abort(self):
+ self.log.append("ABORT")
+
+
+class TestFinishReadOnlyRequest(TestCase):
+ # Publications that have a finishReadOnlyRequest() method are obliged to
+ # fire an IFinishReadOnlyRequestEvent.
+
+ def _test_publication(self, publication, expected_transaction_log):
+ # publication.finishReadOnlyRequest() issues an
+ # IFinishReadOnlyRequestEvent and alters the transaction.
+ fake_request = object()
+ fake_object = object()
+ fake_transaction = LoggingTransaction()
+
+ with EventRecorder() as event_recorder:
+ publication.finishReadOnlyRequest(
+ fake_request, fake_object, fake_transaction)
+
+ self.assertEqual(
+ expected_transaction_log,
+ fake_transaction.log)
+
+ finish_events = [
+ event for event in event_recorder.events
+ if IFinishReadOnlyRequestEvent.providedBy(event)]
+ self.assertEqual(
+ 1, len(finish_events), (
+ "Expected only one IFinishReadOnlyRequestEvent, but "
+ "got: %r" % finish_events))
+
+ [finish_event] = finish_events
+ self.assertIs(fake_request, finish_event.request)
+ self.assertIs(fake_object, finish_event.object)
+
+ def test_WebServicePub_fires_FinishReadOnlyRequestEvent(self):
+ # WebServicePublication.finishReadOnlyRequest() issues an
+ # IFinishReadOnlyRequestEvent and commits the transaction.
+ publication = WebServicePublication(None)
+ self._test_publication(publication, ["COMMIT"])
+
+ def test_LaunchpadBrowserPub_fires_FinishReadOnlyRequestEvent(self):
+ # LaunchpadBrowserPublication.finishReadOnlyRequest() issues an
+ # IFinishReadOnlyRequestEvent and aborts the transaction.
+ publication = LaunchpadBrowserPublication(None)
+ self._test_publication(publication, ["ABORT"])
+
+
def test_suite():
suite = unittest.TestSuite()
suite.addTest(DocTestSuite(
=== modified file 'lib/lp/services/messaging/configure.zcml'
--- lib/lp/services/messaging/configure.zcml 2011-09-23 20:20:14 +0000
+++ lib/lp/services/messaging/configure.zcml 2011-10-03 11:47:27 +0000
@@ -9,4 +9,10 @@
<utility
provides=".interfaces.IMessageSession"
component=".rabbit.unreliable_session" />
+ <subscriber
+ for="canonical.launchpad.webapp.interfaces.IFinishReadOnlyRequestEvent"
+ handler=".rabbit.session_finish_handler" />
+ <subscriber
+ for="canonical.launchpad.webapp.interfaces.IFinishReadOnlyRequestEvent"
+ handler=".rabbit.unreliable_session_finish_handler" />
</configure>
=== modified file 'lib/lp/services/messaging/rabbit.py'
--- lib/lp/services/messaging/rabbit.py 2011-10-03 11:47:27 +0000
+++ lib/lp/services/messaging/rabbit.py 2011-10-03 11:47:27 +0000
@@ -136,6 +136,8 @@
# Per-thread sessions.
session = RabbitSession()
+session_finish_handler = (
+ lambda event: session.finish())
class RabbitUnreliableSession(RabbitSession):
@@ -166,6 +168,8 @@
# Per-thread "unreliable" sessions.
unreliable_session = RabbitUnreliableSession()
+unreliable_session_finish_handler = (
+ lambda event: unreliable_session.finish())
class RabbitMessageBase:
=== modified file 'lib/lp/services/messaging/tests/test_rabbit.py'
--- lib/lp/services/messaging/tests/test_rabbit.py 2011-10-03 11:47:27 +0000
+++ lib/lp/services/messaging/tests/test_rabbit.py 2011-10-03 11:47:27 +0000
@@ -14,7 +14,9 @@
import transaction
from transaction._transaction import Status as TransactionStatus
from zope.component import getUtility
+from zope.event import notify
+from canonical.launchpad.webapp.interfaces import FinishReadOnlyRequestEvent
from canonical.testing.layers import (
LaunchpadFunctionalLayer,
RabbitMQLayer,
@@ -95,12 +97,14 @@
class TestRabbitSession(RabbitTestCase):
+ session_factory = RabbitSession
+
def test_interface(self):
- session = RabbitSession()
+ session = self.session_factory()
self.assertThat(session, Provides(IMessageSession))
def test_connect(self):
- session = RabbitSession()
+ session = self.session_factory()
self.assertFalse(session.is_connected)
connection = session.connect()
self.assertTrue(session.is_connected)
@@ -108,20 +112,20 @@
def test_connect_with_incomplete_configuration(self):
self.pushConfig("rabbitmq", host="none")
- session = RabbitSession()
+ session = self.session_factory()
with ExpectedException(
MessagingUnavailable, "Incomplete configuration"):
session.connect()
def test_disconnect(self):
- session = RabbitSession()
+ session = self.session_factory()
session.connect()
session.disconnect()
self.assertFalse(session.is_connected)
def test_is_connected(self):
# is_connected is False once a connection has been closed.
- session = RabbitSession()
+ session = self.session_factory()
session.connect()
# Close the connection without using disconnect().
session._connection.close()
@@ -129,7 +133,7 @@
def test_defer(self):
task = lambda foo, bar: None
- session = RabbitSession()
+ session = self.session_factory()
session.defer(task, "foo", bar="baz")
self.assertEqual(1, len(session._deferred))
[deferred_task] = session._deferred
@@ -142,7 +146,7 @@
# RabbitSession.flush() runs deferred tasks.
log = []
task = lambda: log.append("task")
- session = RabbitSession()
+ session = self.session_factory()
session.defer(task)
session.connect()
session.flush()
@@ -155,7 +159,7 @@
# deferred tasks.
log = []
task = lambda: log.append("task")
- session = RabbitSession()
+ session = self.session_factory()
session.defer(task)
session.connect()
session.reset()
@@ -168,7 +172,7 @@
# deferred tasks.
log = []
task = lambda: log.append("task")
- session = RabbitSession()
+ session = self.session_factory()
session.defer(task)
session.connect()
session.finish()
@@ -177,27 +181,29 @@
self.assertFalse(session.is_connected)
def test_getProducer(self):
- session = RabbitSession()
+ session = self.session_factory()
producer = session.getProducer("foo")
self.assertIsInstance(producer, RabbitRoutingKey)
self.assertIs(session, producer.session)
self.assertEqual("foo", producer.key)
def test_getConsumer(self):
- session = RabbitSession()
+ session = self.session_factory()
consumer = session.getConsumer("foo")
self.assertIsInstance(consumer, RabbitQueue)
self.assertIs(session, consumer.session)
self.assertEqual("foo", consumer.name)
-class TestRabbitUnreliableSession(RabbitTestCase):
+class TestRabbitUnreliableSession(TestRabbitSession):
+
+ session_factory = RabbitUnreliableSession
def raise_AMQPException(self):
raise amqp.AMQPException(123, "Suffin broke.", "Whut?")
def test_finish_suppresses_AMQPException(self):
- session = RabbitUnreliableSession()
+ session = self.session_factory()
session.defer(self.raise_AMQPException)
session.finish()
# Look, no exceptions!
@@ -206,7 +212,7 @@
raise MessagingException("Arm stuck in combine.")
def test_finish_suppresses_MessagingException(self):
- session = RabbitUnreliableSession()
+ session = self.session_factory()
session.defer(self.raise_MessagingException)
session.finish()
# Look, no exceptions!
@@ -215,7 +221,7 @@
raise IOError("Leg eaten by cow.")
def test_finish_suppresses_IOError(self):
- session = RabbitUnreliableSession()
+ session = self.session_factory()
session.defer(self.raise_IOError)
session.finish()
# Look, no exceptions!
@@ -224,7 +230,7 @@
raise Exception("That hent worked.")
def test_finish_does_not_suppress_other_errors(self):
- session = RabbitUnreliableSession()
+ session = self.session_factory()
session.defer(self.raise_Exception)
self.assertRaises(Exception, session.finish)
@@ -407,3 +413,23 @@
self.assertIs(
global_unreliable_session,
getUtility(IMessageSession))
+
+ def _test_session_finish_read_only_request(self, session):
+ # When a read-only request ends the session is also finished.
+ log = []
+ task = lambda: log.append("task")
+ session.defer(task)
+ session.connect()
+ notify(FinishReadOnlyRequestEvent(None, None))
+ self.assertEqual(["task"], log)
+ self.assertEqual([], list(session._deferred))
+ self.assertFalse(session.is_connected)
+
+ def test_global_session_finish_read_only_request(self):
+ # When a read-only request ends the global_session is finished too.
+ self._test_session_finish_read_only_request(global_session)
+
+ def test_global_unreliable_session_finish_read_only_request(self):
+ # When a read-only request ends the global_unreliable_session is
+ # finished too.
+ self._test_session_finish_read_only_request(global_unreliable_session)