[MERGE] hpss startup/shutdown hooks

John Arbash Meinel john at arbash-meinel.com
Thu Apr 5 15:15:27 BST 2007


Robert Collins wrote:
> On Mon, 2007-03-26 at 01:47 -0400, Andrew Bennetts wrote:
> 
> The branch failed on PQM because of race conditions in the existing
> startup-shutdown code. I've fixed these I believe, but it is enough of a
> change that i'd like a re-review.
> 
> Cheers,
> Rob

...

>      def _restoreHooks(self):
> -        bzrlib.branch.Branch.hooks = self._preserved_hooks
> +        for klass, hooks in self._preserved_hooks.items():
> +            setattr(klass, 'hooks', hooks)

Why are you using setattr() here? Rather than just
klass.hooks = hooks

I think setattr is when the member is unknown.

...

> +    def test_server_closes_listening_sock_on_shutdown_after_request(self):
> +        """The server should close its listening socket when it's stopped."""
> +        self.setUpServer()
> +        server = self.server
> +        self.transport.has('.')
> +        self.tearDownServer()
> +        # if the listening socket has closed, we should get a BADFD error
> +        # when connecting, rather than a hang.
> +        transport = smart.SmartTCPTransport(server.get_url())
> +        self.assertRaises(errors.ConnectionError, transport.has, '.')
> +

Tests that "fail" by hanging aren't particularly nice. Especially when
they don't actually fail because of an eventual timeout.

I know we don't want to be strictly timing related (because then you
have tests which randomly fail because the machine needed to swap). Is
there a better way to test this?

>  
>  class WritableEndToEndTests(SmartTCPTests):
>      """Client to server tests that require a writable transport."""
> @@ -901,7 +926,47 @@
>          self.setUpServer(readonly=True)
>          self.assertRaises(errors.TransportNotPossible, self.transport.mkdir,
>              'foo')

v- I would generally prefer to track the 'reason' for why the callback
was called. So that you aren't accidentally getting 'server_started'
calls when you should be getting 'server_stopped'. I do realize that you
have only one hook registered, so I suppose that is good enough.
> -        
> +
> +
> +class TestServerHooks(SmartTCPTests):
> +
> +    def capture_server_call(self, backing_url, public_url):
> +        """Record a server_started|stopped hook firing."""
> +        self.hook_calls.append((backing_url, public_url))
> +
> +    def test_server_started_hook(self):
> +        """The server_started hook fires when the server is started."""
> +        self.hook_calls = []
> +        smart.SmartTCPServer.hooks.install_hook('server_started',
> +            self.capture_server_call)
> +        self.setUpServer()
> +        # at this point, the server will be starting a thread up.
> +        # there is no indicator at the moment, so bodge it by doing a request.
> +        self.transport.has('.')
> +        self.assertEqual([(self.backing_transport.base, self.transport.base)],
> +            self.hook_calls)
> +
> +    def test_server_stopped_hook_simple(self):
> +        """The server_stopped hook fires when the server is stopped."""
> +        self.hook_calls = []
> +        smart.SmartTCPServer.hooks.install_hook('server_stopped',
> +            self.capture_server_call)
> +        self.setUpServer()
> +        result = [(self.backing_transport.base, self.transport.base)]
> +        # check the stopping message isn't emitted up front.
> +        self.assertEqual([], self.hook_calls)
> +        # nor after a single message
> +        self.transport.has('.')
> +        self.assertEqual([], self.hook_calls)
> +        # clean up the server
> +        server = self.server
> +        self.tearDownServer()
> +        # now it should have fired.
> +        self.assertEqual(result, self.hook_calls)
> +
> +# TODO: test that when the server suffers an exception that it calls the
> +# server-stopped hook.
> +

...

v- Invalid markup. Did you want:

:cvar hooks: An instance ...


>  class SmartTCPServer(object):
> -    """Listens on a TCP socket and accepts connections from smart clients"""
> +    """Listens on a TCP socket and accepts connections from smart clients
> +
> +    hooks: An instance of SmartServerHooks.
> +    """
>  

v- Do I remember correctly that hooks with timeouts cause problems
because of non-blocking I/O? (You get exceptions for "operation would
block"). I *think* we can handle that as an EAGAIN, but I don't remember
exactly.

> +        from socket import error as socket_error
> +        self._socket_error = socket_error
>          self._server_socket = socket.socket()
>          self._server_socket.bind((host, port))
> -        self.port = self._server_socket.getsockname()[1]
> +        self._sockname = self._server_socket.getsockname()
> +        self.port = self._sockname[1]
>          self._server_socket.listen(1)
>          self._server_socket.settimeout(1)
>          self.backing_transport = backing_transport
> +        self._started = threading.Event()
> +        self._stopped = threading.Event()
>  
>      def serve(self):
>          # let connections timeout so that we get a chance to terminate
>          # Keep a reference to the exceptions we want to catch because the socket
>          # module's globals get set to None during interpreter shutdown.
>          from socket import timeout as socket_timeout
> -        from socket import error as socket_error
>          self._should_terminate = False
> -        while not self._should_terminate:
> -            try:
> -                self.accept_and_serve()
> -            except socket_timeout:
> -                # just check if we're asked to stop
> -                pass
> -            except socket_error, e:
> -                trace.warning("client disconnected: %s", e)
> -                pass
> +        for hook in SmartTCPServer.hooks['server_started']:
> +            hook(self.backing_transport.base, self.get_url())
> +        self._started.set()
> +        try:
> +            try:
> +                while not self._should_terminate:
> +                    try:
> +                        conn, client_addr = self._server_socket.accept()
> +                    except socket_timeout:
> +                        # just check if we're asked to stop
> +                        pass
> +                    except self._socket_error, e:
> +                        # if the socket is closed by stop_background_thread
> +                        # we might get a EBADF here, any other socket errors
> +                        # should get logged.
> +                        if e.args[0] != errno.EBADF:
> +                            trace.warning("listening socket error: %s", e)
> +                    else:
> +                        self.serve_conn(conn)

...

v- Is there a reason you are doing this as:

class C1():
  ...

class C2():
  ...

C1.hooks = C2()

Rather than doing it (IMO) the right way:

class C2():
  ...

class C1():
  hooks = C2()
  ...


Modifying classes after their definitions seems harder to follow and
understand. I understand that you are adding the doc string at least.

> +class SmartServerHooks(Hooks):
> +    """Hooks for the smart server."""
> +
> +    def __init__(self):
> +        """Create the default hooks.
> +
> +        These are all empty initially, because by default nothing should get
> +        notified.
> +        """
> +        Hooks.__init__(self)
> +        # Introduced in 0.16:
> +        # invoked whenever the server starts serving a directory.
> +        # The api signature is (backing url, public url).
> +        self['server_started'] = []
> +        # Introduced in 0.16:
> +        # invoked whenever the server stops serving a directory.
> +        # The api signature is (backing url, public url).
> +        self['server_stopped'] = []
> +
> +SmartTCPServer.hooks = SmartServerHooks()
>  

Otherwise +1 from me.

John
=:->




More information about the bazaar mailing list