[CalendarServer-changes] [9854] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 25 14:46:36 PDT 2012


Revision: 9854
          http://trac.calendarserver.org//changeset/9854
Author:   sagen at apple.com
Date:     2012-09-25 14:46:36 -0700 (Tue, 25 Sep 2012)
Log Message:
-----------
Graceful shutdown

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
    CalendarServer/trunk/twext/internet/tcp.py
    CalendarServer/trunk/twext/web2/channel/http.py
    CalendarServer/trunk/twext/web2/metafd.py
    CalendarServer/trunk/twext/web2/test/test_http.py

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2012-09-25 18:36:32 UTC (rev 9853)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2012-09-25 21:46:36 UTC (rev 9854)
@@ -41,7 +41,7 @@
 from twisted.python.logfile import LogFile
 from twisted.python.usage import Options, UsageError
 
-from twisted.internet.defer import gatherResults, Deferred
+from twisted.internet.defer import gatherResults, Deferred, inlineCallbacks
 
 from twisted.internet.process import ProcessExitedAlready
 from twisted.internet.protocol import Protocol, Factory
@@ -244,10 +244,18 @@
         MultiService.privilegedStartService(self)
         self.logObserver.start()
 
+    @inlineCallbacks
     def stopService(self):
-        d = MultiService.stopService(self)
+        """
+        Wait for outstanding requests to finish
+        @return: a Deferred which fires when all outstanding requests are complete
+        """
+        connectionService = self.getServiceNamed("ConnectionService")
+        # Note: removeService() also calls stopService()
+        yield self.removeService(connectionService)
+        # At this point, all outstanding requests have been responded to
+        yield MultiService.stopService(self)
         self.logObserver.stop()
-        return d
 
 
 class CalDAVOptions (Options, LoggingMixIn):
@@ -858,6 +866,12 @@
 
         config.addPostUpdateHooks((updateFactory,))
 
+        # Bundle the various connection services within a single MultiService
+        # that can be stopped before the others for graceful shutdown.
+        connectionService = MultiService()
+        connectionService.setName("ConnectionService")
+        connectionService.setServiceParent(service)
+
         if config.InheritFDs or config.InheritSSLFDs:
             # Inherit sockets to call accept() on them individually.
 
@@ -873,13 +887,13 @@
                             contextFactory,
                             backlog=config.ListenBacklog,
                             inherit=True
-                        ).setServiceParent(service)
+                        ).setServiceParent(connectionService)
             for fdAsStr in config.InheritFDs:
                 MaxAcceptTCPServer(
                     int(fdAsStr), httpFactory,
                     backlog=config.ListenBacklog,
                     inherit=True
-                ).setServiceParent(service)
+                ).setServiceParent(connectionService)
 
         elif config.MetaFD:
             # Inherit a single socket to receive accept()ed connections via
@@ -896,7 +910,7 @@
 
             ReportingHTTPService(
                 requestFactory, int(config.MetaFD), contextFactory
-            ).setServiceParent(service)
+            ).setServiceParent(connectionService)
 
         else: # Not inheriting, therefore we open our own:
             for bindAddress in self._allBindAddresses():
@@ -919,7 +933,7 @@
                                 backlog=config.ListenBacklog,
                                 inherit=False
                             )
-                            httpsService.setServiceParent(service)
+                            httpsService.setServiceParent(connectionService)
 
                 for port in config.BindHTTPPorts:
                     MaxAcceptTCPServer(
@@ -927,7 +941,7 @@
                         interface=bindAddress,
                         backlog=config.ListenBacklog,
                         inherit=False
-                    ).setServiceParent(service)
+                    ).setServiceParent(connectionService)
 
 
         # Change log level back to what it was before

Modified: CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/test/test_caldav.py	2012-09-25 18:36:32 UTC (rev 9853)
+++ CalendarServer/trunk/calendarserver/tap/test/test_caldav.py	2012-09-25 21:46:36 UTC (rev 9854)
@@ -542,7 +542,8 @@
         Test that the Slave service has sub services with the
         default TCP and SSL configuration
         """
-        service = self.makeService()
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         expectedSubServices = dict((
             (MaxAcceptTCPServer, self.config["HTTPPort"]),
@@ -568,7 +569,7 @@
         Test that the configuration of the SSLServer reflect the config file's
         SSL Private Key and SSL Certificate
         """
-        service = self.makeService()
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         sslService = None
         for s in service.services:
@@ -597,7 +598,7 @@
         del self.config["SSLPort"]
         self.writeConfig()
 
-        service = self.makeService()
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         self.assertNotIn(
             internet.SSLServer,
@@ -612,7 +613,7 @@
         del self.config["HTTPPort"]
         self.writeConfig()
 
-        service = self.makeService()
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         self.assertNotIn(
             internet.TCPServer,
@@ -626,7 +627,7 @@
         self.config.BindAddresses = ["127.0.0.1"]
         self.writeConfig()
 
-        service = self.makeService()
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         for s in service.services:
             if isinstance(s, (internet.TCPServer, internet.SSLServer)):
@@ -644,7 +645,7 @@
         ]
 
         self.writeConfig()
-        service = self.makeService()
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         tcpServers = []
         sslServers = []
@@ -676,7 +677,7 @@
         """
         self.config.ListenBacklog = 1024
         self.writeConfig()
-        service = self.makeService()
+        service = self.makeService().getServiceNamed("ConnectionService")
 
         for s in service.services:
             if isinstance(s, (internet.TCPServer, internet.SSLServer)):

Modified: CalendarServer/trunk/twext/internet/tcp.py
===================================================================
--- CalendarServer/trunk/twext/internet/tcp.py	2012-09-25 18:36:32 UTC (rev 9853)
+++ CalendarServer/trunk/twext/internet/tcp.py	2012-09-25 21:46:36 UTC (rev 9854)
@@ -114,7 +114,8 @@
 
     def __init__(self, *args, **kwargs):
         internet.TCPServer.__init__(self, *args, **kwargs)
-        self.args[1].myServer = self
+        self.httpFactory = self.args[1]
+        self.httpFactory.myServer = self
         self.inherit = self.kwargs.get("inherit", False)
         self.backlog = self.kwargs.get("backlog", None)
         self.interface = self.kwargs.get("interface", None)
@@ -131,6 +132,15 @@
         self.myPort = port
         return port
 
+    def stopService(self):
+        """
+        Wait for outstanding requests to finish
+        @return: a Deferred which fires when all outstanding requests are complete
+        """
+        internet.TCPServer.stopService(self)
+        return self.httpFactory.waitForCompletion()
+
+
 class MaxAcceptSSLServer(internet.SSLServer):
     """
     SSL server which will uses MaxAcceptSSLPorts (and optionally,
@@ -139,7 +149,8 @@
 
     def __init__(self, *args, **kwargs):
         internet.SSLServer.__init__(self, *args, **kwargs)
-        self.args[1].myServer = self
+        self.httpFactory = self.args[1]
+        self.httpFactory.myServer = self
         self.inherit = self.kwargs.get("inherit", False)
         self.backlog = self.kwargs.get("backlog", None)
         self.interface = self.kwargs.get("interface", None)
@@ -156,3 +167,12 @@
         self.myPort = port
         return port
 
+    def stopService(self):
+        """
+        Wait for outstanding requests to finish
+        @return: a Deferred which fires when all outstanding requests are complete
+        """
+        internet.SSLServer.stopService(self)
+        return self.httpFactory.waitForCompletion()
+
+

Modified: CalendarServer/trunk/twext/web2/channel/http.py
===================================================================
--- CalendarServer/trunk/twext/web2/channel/http.py	2012-09-25 18:36:32 UTC (rev 9853)
+++ CalendarServer/trunk/twext/web2/channel/http.py	2012-09-25 21:46:36 UTC (rev 9854)
@@ -32,6 +32,7 @@
 from zope.interface import implements
 
 from twisted.internet import interfaces, protocol, reactor
+from twisted.internet.defer import succeed, Deferred
 from twisted.protocols import policies, basic
 
 from twext.python.log import Logger
@@ -1019,6 +1020,7 @@
         self.protocolArgs = kwargs
         self.protocolArgs['requestFactory'] = requestFactory
         self.connectedChannels = set()
+        self.deferred = None
 
 
     def buildProtocol(self, addr):
@@ -1044,16 +1046,33 @@
         """
         Remove a connected channel from the set of currently connected channels
         and decrease the outstanding request count.
+        If someone is waiting for all the requests to be completed, self.deferred
+        will be non-None; fire that callback when the number of outstanding requests
+        hits zero.
         """
         self.connectedChannels.remove(channel)
 
+        if self.deferred is not None:
+            if self.outstandingRequests == 0:
+                self.deferred.callback(None)
 
     @property
     def outstandingRequests(self):
         return len(self.connectedChannels)
 
 
+    def waitForCompletion(self):
+        """
+        Return a Deferred that will fire when all outstanding requests have completed.
+        @return: A Deferred with a result of None
+        """
+        if self.outstandingRequests == 0:
+            return succeed(None)
+        self.deferred = Deferred()
+        return self.deferred
 
+
+
 class HTTP503LoggingFactory (HTTPFactory):
     """
     Factory for HTTP server which emits a 503 response when overloaded.
@@ -1201,7 +1220,7 @@
 
     def removeConnectedChannel(self, channel):
         """
-        Override L{HTTPFactory.addConnectedChannel} to resume listening on the
+        Override L{HTTPFactory.removeConnectedChannel} to resume listening on the
         socket when there are too many outstanding channels.
         """
         HTTPFactory.removeConnectedChannel(self, channel)

Modified: CalendarServer/trunk/twext/web2/metafd.py
===================================================================
--- CalendarServer/trunk/twext/web2/metafd.py	2012-09-25 18:36:32 UTC (rev 9853)
+++ CalendarServer/trunk/twext/web2/metafd.py	2012-09-25 21:46:36 UTC (rev 9854)
@@ -81,14 +81,17 @@
     def stopService(self):
         """
         Stop reading on the inherited port.
+        @return: a Deferred which fires after the last outstanding request is complete.
         """
         Service.stopService(self)
         # XXX stopping should really be destructive, because otherwise we will
         # always leak a file descriptor; i.e. this shouldn't be restartable.
-        # XXX this needs to return a Deferred.
         self.reportingFactory.inheritedPort.stopReading()
 
+        # Let any outstanding requests finish
+        return self.reportingFactory.waitForCompletion()
 
+
     def createTransport(self, skt, peer, data, protocol):
         """
         Create a TCP transport, from a socket object passed by the parent.

Modified: CalendarServer/trunk/twext/web2/test/test_http.py
===================================================================
--- CalendarServer/trunk/twext/web2/test/test_http.py	2012-09-25 18:36:32 UTC (rev 9853)
+++ CalendarServer/trunk/twext/web2/test/test_http.py	2012-09-25 21:46:36 UTC (rev 9854)
@@ -14,7 +14,7 @@
 from twisted.internet.defer import waitForDeferred, deferredGenerator
 from twisted.protocols import loopback
 from twisted.python import util, runtime
-from twext.web2.channel.http import SSLRedirectRequest
+from twext.web2.channel.http import SSLRedirectRequest, HTTPFactory
 from twisted.internet.task import deferLater
 
 class PreconditionTestCase(unittest.TestCase):
@@ -449,6 +449,45 @@
         self.assertEquals(cxn.client.done, done)
 
 
+class GracefulShutdownTestCase(HTTPTests):
+
+    def _callback(self, result):
+        self.callbackFired = True
+
+    def testWaitForCompletionWithoutConnectedChannels(self):
+        """
+        waitForCompletion( ) should fire right away if no connected channels
+        """
+        self.callbackFired = False
+
+        factory = HTTPFactory(None)
+        factory.waitForCompletion().addCallback(self._callback)
+        self.assertTrue(self.callbackFired)  # now!
+
+    def testWaitForCompletionWithConnectedChannels(self):
+        """
+        waitForCompletion( ) should only fire after all connected channels
+        have been removed
+        """
+        self.callbackFired = False
+
+        factory = HTTPFactory(None)
+        factory.addConnectedChannel("A")
+        factory.addConnectedChannel("B")
+        factory.addConnectedChannel("C")
+
+        factory.waitForCompletion().addCallback(self._callback)
+
+        factory.removeConnectedChannel("A")
+        self.assertFalse(self.callbackFired) # wait for it...
+
+        factory.removeConnectedChannel("B")
+        self.assertFalse(self.callbackFired) # wait for it...
+
+        factory.removeConnectedChannel("C")
+        self.assertTrue(self.callbackFired)  # now!
+
+
 class CoreHTTPTestCase(HTTPTests):
     # Note: these tests compare the client output using string
     #       matching. It is acceptable for this to change and break
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120925/d0395115/attachment-0001.html>


More information about the calendarserver-changes mailing list