Ejemplo n.º 1
0
    def test_watch_stops_on_closed_connection(self):
        """Verify watches stops when the connection is closed."""

        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = self.get_zookeeper_client()
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        wait_callback = Deferred()
        finish_callback = Deferred()
        calls = []

        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback.callback(True)
            return finish_callback

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        
        # Hold off until callback is started.
        yield wait_callback

        # Change the topology.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Ensure that the watch has been called just once so far
        # (although still pending due to the finish_callback).
        self.assertEquals(len(calls), 1)

        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Change the topology again.
        topology.add_machine("m-2")
        yield self.set_topology(topology)

        # Allow the first call to be completed, starting a process of
        # watching for the next change. At this point, the watch will
        # encounter that the client is disconnected.
        finish_callback.callback(True)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watch was still not called.
        self.assertEquals(len(calls), 1)
Ejemplo n.º 2
0
    def test_watch_stops_on_closed_connection(self):
        """Verify watches stops when the connection is closed."""

        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = ZookeeperClient(get_test_zookeeper_address())
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        wait_callback = Deferred()
        finish_callback = Deferred()
        calls = []

        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback.callback(True)
            return finish_callback

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        
        # Hold off until callback is started.
        yield wait_callback

        # Change the topology.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Ensure that the watch has been called just once so far
        # (although still pending due to the finish_callback).
        self.assertEquals(len(calls), 1)

        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Change the topology again.
        topology.add_machine("m-2")
        yield self.set_topology(topology)

        # Allow the first call to be completed, starting a process of
        # watching for the next change. At this point, the watch will
        # encounter that the client is disconnected.
        finish_callback.callback(True)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watch was still not called.
        self.assertEquals(len(calls), 1)
Ejemplo n.º 3
0
    def test_watch_stops_on_early_closed_connection(self):
        """Verify watches stops when the connection is closed early.

        _watch_topology chains from an exists_and_watch to a
        get_and_watch. This test ensures that this chaining will fail
        gracefully if the connection is closed before this chaining
        can occur.
        """
        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = self.get_zookeeper_client()
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        calls = []

        @inlineCallbacks
        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Change the topology, this will trigger the watch.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watcher was never called, because its client was
        # disconnected.
        self.assertEquals(len(calls), 0)
Ejemplo n.º 4
0
    def test_watch_stops_on_early_closed_connection(self):
        """Verify watches stops when the connection is closed early.

        _watch_topology chains from an exists_and_watch to a
        get_and_watch. This test ensures that this chaining will fail
        gracefully if the connection is closed before this chaining
        can occur.
        """
        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = ZookeeperClient(get_test_zookeeper_address())
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        calls = []

        @inlineCallbacks
        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        
        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Change the topology, this will trigger the watch.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watcher was never called, because its client was
        # disconnected.
        self.assertEquals(len(calls), 0)
Ejemplo n.º 5
0
class StateBaseTest(StateTestBase):
    """
    Test the StateBase class, which provides helpers to other
    state management classes.

    Note that some underscored methods in this class are like this with
    the intention of implementing "protected" semantics, not "private".
    """

    @inlineCallbacks
    def setUp(self):
        yield super(StateBaseTest, self).setUp()
        self.base = StateBase(self.client)

    def parse_topology(self, content):
        topology = InternalTopology()
        topology.parse(content)
        return topology

    @inlineCallbacks
    def test_read_empty_topology(self):
        """
        When the state is empty (no /topology) file, reading the
        topology should return an empty one.
        """
        topology = yield self.base._read_topology()
        empty_topology = InternalTopology()
        self.assertEquals(topology.dump(), empty_topology.dump())

    @inlineCallbacks
    def test_non_empty_topology(self):
        """
        When there's something in /topology already, it should of
        course be read and parsed when we read the topology.
        """
        test_topology = InternalTopology()
        test_topology.add_machine("m-0")
        topology_dump = test_topology.dump()
        yield self.client.create("/topology", topology_dump)
        topology = yield self.base._read_topology()
        self.assertEquals(topology.dump(), topology_dump)

    @inlineCallbacks
    def test_change_empty_topology(self):
        """
        Attempting to change a non-existing topology should create
        it with the initial content in place.
        """

        def change_topology(topology):
            topology.add_machine("m-0")

        yield self.base._retry_topology_change(change_topology)

        content, stat = yield self.client.get("/topology")
        topology = self.parse_topology(content)
        self.assertTrue(topology.has_machine("m-0"))

    @inlineCallbacks
    def test_change_non_empty_topology(self):
        """
        Attempting to change a pre-existing topology should modify
        it accordingly.
        """
        test_topology = InternalTopology()
        test_topology.add_machine("m-0")
        topology_dump = test_topology.dump()
        yield self.client.create("/topology", topology_dump)

        def change_topology(topology):
            topology.add_machine("m-1")
        yield self.base._retry_topology_change(change_topology)

        content, stat = yield self.client.get("/topology")
        topology = self.parse_topology(content)
        self.assertTrue(topology.has_machine("m-0"))
        self.assertTrue(topology.has_machine("m-1"))

    @inlineCallbacks
    def test_watch_topology_when_being_created(self):
        """
        It should be possible to start watching the topology even
        before it is created.  In this case, the callback will be
        made when it's actually introduced.
        """
        wait_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)

        # Start watching.
        self.base._watch_topology(watch_topology)

        # Callback is still untouched.
        self.assertEquals(calls, [])

        # Create the topology, and wait for callback.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        yield wait_callback[0]

        # The first callback must have been fired, and it must have None
        # as the first argument because that's the first topology seen.
        self.assertEquals(len(calls), 1)
        old_topology, new_topology = calls[0]
        self.assertEquals(old_topology, None)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), False)

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)
        yield wait_callback[1]

        # Now the watch callback must have been fired with two
        # different topologies.  The old one, and the new one.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_watch_topology_when_it_already_exists(self):
        """
        It should also be possible to start watching the topology when
        the topology already exists, of course. In this case, the callback
        should fire immediately, and should have an old_topology of None
        so that the callback has a chance to understand that it's the
        first time a topology is being processed, even if it already
        existed before.
        """
        # Create the topology ahead of time.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        wait_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)

        # Start watching, and wait on callback immediately.
        self.base._watch_topology(watch_topology)
        yield wait_callback[0]

        # The first callback must have been fired, and it must have None
        # as the first argument because that's the first topology seen.
        self.assertEquals(len(calls), 1)
        old_topology, new_topology = calls[0]
        self.assertEquals(old_topology, None)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), False)

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)
        yield wait_callback[1]

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Now the watch callback must have been fired with two
        # different topologies.  The old one, and the new one.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_watch_topology_when_it_goes_missing(self):
        """
        We consider a deleted /topology to be an error, and will not
        warn the callback about it. Instead, we'll wait until a new
        topology is brought up, and then will fire the callback with
        the full delta.
        """
        # Create the topology ahead of time.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        wait_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)

        # Start watching, and wait on callback immediately.
        self.base._watch_topology(watch_topology)

        # Ignore the first callback with the initial state, as
        # this is already tested above.
        yield wait_callback[0]

        log = self.capture_logging()

        # Kill the /topology node entirely.
        yield self.client.delete("/topology")

        # Create a new topology from the ground up.
        topology.add_machine("m-1")
        yield self.set_topology(topology)
        yield wait_callback[1]

        # The issue should have been logged.
        self.assertIn("The /topology node went missing!",
                      log.getvalue())

        # Check that we've only perceived the delta.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_watch_topology_may_defer(self):
        """
        The watch topology may return a deferred so that it performs
        some of its logic asynchronously.  In this case, it must not
        be called a second time before its postponed logic is finished
        completely.
        """
        wait_callback = [Deferred() for i in range(10)]
        finish_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)
            return finish_callback[len(calls)-1]

        # Start watching.
        self.base._watch_topology(watch_topology)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        # Hold off until callback is started.
        yield wait_callback[0]

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure we still have a single call.
        self.assertEquals(len(calls), 1)

        # Allow the first call to be completed, and wait on the
        # next one.
        finish_callback[0].callback(None)
        yield wait_callback[1]
        finish_callback[1].callback(None)

        # We should have the second change now.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_stop_watch(self):
        """
        A watch that fires a `StopWatcher` exception will end the
        watch."""
        wait_callback = [Deferred() for i in range(5)]
        calls = []

        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)
            if len(calls) == 2:
                raise StopWatcher()

        # Start watching.
        self.base._watch_topology(watcher)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        # Hold off until callback is started.
        yield wait_callback[0]

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        yield wait_callback[1]
        self.assertEqual(len(calls), 2)

        # Change the topology again, we shouldn't see this.
        topology.add_machine("m-2")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure we still have a single call.
        self.assertEquals(len(calls), 2)

    @inlineCallbacks
    def test_watch_stops_on_closed_connection(self):
        """Verify watches stops when the connection is closed."""

        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = self.get_zookeeper_client()
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        wait_callback = Deferred()
        finish_callback = Deferred()
        calls = []

        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback.callback(True)
            return finish_callback

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        
        # Hold off until callback is started.
        yield wait_callback

        # Change the topology.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Ensure that the watch has been called just once so far
        # (although still pending due to the finish_callback).
        self.assertEquals(len(calls), 1)

        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Change the topology again.
        topology.add_machine("m-2")
        yield self.set_topology(topology)

        # Allow the first call to be completed, starting a process of
        # watching for the next change. At this point, the watch will
        # encounter that the client is disconnected.
        finish_callback.callback(True)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watch was still not called.
        self.assertEquals(len(calls), 1)

    @inlineCallbacks
    def test_watch_stops_on_early_closed_connection(self):
        """Verify watches stops when the connection is closed early.

        _watch_topology chains from an exists_and_watch to a
        get_and_watch. This test ensures that this chaining will fail
        gracefully if the connection is closed before this chaining
        can occur.
        """
        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = self.get_zookeeper_client()
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        calls = []

        @inlineCallbacks
        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Change the topology, this will trigger the watch.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watcher was never called, because its client was
        # disconnected.
        self.assertEquals(len(calls), 0)
Ejemplo n.º 6
0
 def setUp(self):
     yield super(StateBaseTest, self).setUp()
     self.base = StateBase(self.client)
Ejemplo n.º 7
0
 def setUp(self):
     yield super(StateBaseTest, self).setUp()
     self.base = StateBase(self.client)
Ejemplo n.º 8
0
class StateBaseTest(StateTestBase):
    """
    Test the StateBase class, which provides helpers to other
    state management classes.

    Note that some underscored methods in this class are like this with
    the intention of implementing "protected" semantics, not "private".
    """

    @inlineCallbacks
    def setUp(self):
        yield super(StateBaseTest, self).setUp()
        self.base = StateBase(self.client)

    def parse_topology(self, content):
        topology = InternalTopology()
        topology.parse(content)
        return topology

    @inlineCallbacks
    def test_read_empty_topology(self):
        """
        When the state is empty (no /topology) file, reading the
        topology should return an empty one.
        """
        topology = yield self.base._read_topology()
        empty_topology = InternalTopology()
        self.assertEquals(topology.dump(), empty_topology.dump())

    @inlineCallbacks
    def test_non_empty_topology(self):
        """
        When there's something in /topology already, it should of
        course be read and parsed when we read the topology.
        """
        test_topology = InternalTopology()
        test_topology.add_machine("m-0")
        topology_dump = test_topology.dump()
        yield self.client.create("/topology", topology_dump)
        topology = yield self.base._read_topology()
        self.assertEquals(topology.dump(), topology_dump)

    @inlineCallbacks
    def test_change_empty_topology(self):
        """
        Attempting to change a non-existing topology should create
        it with the initial content in place.
        """

        def change_topology(topology):
            topology.add_machine("m-0")

        yield self.base._retry_topology_change(change_topology)

        content, stat = yield self.client.get("/topology")
        topology = self.parse_topology(content)
        self.assertTrue(topology.has_machine("m-0"))

    @inlineCallbacks
    def test_change_non_empty_topology(self):
        """
        Attempting to change a pre-existing topology should modify
        it accordingly.
        """
        test_topology = InternalTopology()
        test_topology.add_machine("m-0")
        topology_dump = test_topology.dump()
        yield self.client.create("/topology", topology_dump)

        def change_topology(topology):
            topology.add_machine("m-1")
        yield self.base._retry_topology_change(change_topology)

        content, stat = yield self.client.get("/topology")
        topology = self.parse_topology(content)
        self.assertTrue(topology.has_machine("m-0"))
        self.assertTrue(topology.has_machine("m-1"))

    @inlineCallbacks
    def test_watch_topology_when_being_created(self):
        """
        It should be possible to start watching the topology even
        before it is created.  In this case, the callback will be
        made when it's actually introduced.
        """
        wait_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)

        # Start watching.
        self.base._watch_topology(watch_topology)

        # Callback is still untouched.
        self.assertEquals(calls, [])

        # Create the topology, and wait for callback.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        yield wait_callback[0]

        # The first callback must have been fired, and it must have None
        # as the first argument because that's the first topology seen.
        self.assertEquals(len(calls), 1)
        old_topology, new_topology = calls[0]
        self.assertEquals(old_topology, None)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), False)

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)
        yield wait_callback[1]

        # Now the watch callback must have been fired with two
        # different topologies.  The old one, and the new one.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_watch_topology_when_it_already_exists(self):
        """
        It should also be possible to start watching the topology when
        the topology already exists, of course. In this case, the callback
        should fire immediately, and should have an old_topology of None
        so that the callback has a chance to understand that it's the
        first time a topology is being processed, even if it already
        existed before.
        """
        # Create the topology ahead of time.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        wait_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)

        # Start watching, and wait on callback immediately.
        self.base._watch_topology(watch_topology)
        yield wait_callback[0]

        # The first callback must have been fired, and it must have None
        # as the first argument because that's the first topology seen.
        self.assertEquals(len(calls), 1)
        old_topology, new_topology = calls[0]
        self.assertEquals(old_topology, None)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), False)

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)
        yield wait_callback[1]

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Now the watch callback must have been fired with two
        # different topologies.  The old one, and the new one.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_watch_topology_when_it_goes_missing(self):
        """
        We consider a deleted /topology to be an error, and will not
        warn the callback about it. Instead, we'll wait until a new
        topology is brought up, and then will fire the callback with
        the full delta.
        """
        # Create the topology ahead of time.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        wait_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)

        # Start watching, and wait on callback immediately.
        self.base._watch_topology(watch_topology)

        # Ignore the first callback with the initial state, as
        # this is already tested above.
        yield wait_callback[0]

        log = self.capture_logging()

        # Kill the /topology node entirely.
        yield self.client.delete("/topology")

        # Create a new topology from the ground up.
        topology.add_machine("m-1")
        yield self.set_topology(topology)
        yield wait_callback[1]

        # The issue should have been logged.
        self.assertIn("The /topology node went missing!",
                      log.getvalue())

        # Check that we've only perceived the delta.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_watch_topology_may_defer(self):
        """
        The watch topology may return a deferred so that it performs
        some of its logic asynchronously.  In this case, it must not
        be called a second time before its postponed logic is finished
        completely.
        """
        wait_callback = [Deferred() for i in range(10)]
        finish_callback = [Deferred() for i in range(10)]

        calls = []

        def watch_topology(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)
            return finish_callback[len(calls)-1]

        # Start watching.
        self.base._watch_topology(watch_topology)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        # Hold off until callback is started.
        yield wait_callback[0]

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure we still have a single call.
        self.assertEquals(len(calls), 1)

        # Allow the first call to be completed, and wait on the
        # next one.
        finish_callback[0].callback(None)
        yield wait_callback[1]
        finish_callback[1].callback(None)

        # We should have the second change now.
        self.assertEquals(len(calls), 2)
        old_topology, new_topology = calls[1]
        self.assertEquals(old_topology.has_machine("m-0"), True)
        self.assertEquals(old_topology.has_machine("m-1"), False)
        self.assertEquals(new_topology.has_machine("m-0"), True)
        self.assertEquals(new_topology.has_machine("m-1"), True)

    @inlineCallbacks
    def test_stop_watch(self):
        """
        A watch that fires a `StopWatcher` exception will end the
        watch."""
        wait_callback = [Deferred() for i in range(5)]
        calls = []

        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback[len(calls)-1].callback(True)
            if len(calls) == 2:
                raise StopWatcher()

        # Start watching.
        self.base._watch_topology(watcher)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)

        # Hold off until callback is started.
        yield wait_callback[0]

        # Change the topology again.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        yield wait_callback[1]
        self.assertEqual(len(calls), 2)

        # Change the topology again, we shouldn't see this.
        topology.add_machine("m-2")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure we still have a single call.
        self.assertEquals(len(calls), 2)

    @inlineCallbacks
    def test_watch_stops_on_closed_connection(self):
        """Verify watches stops when the connection is closed."""

        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = ZookeeperClient(get_test_zookeeper_address())
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        wait_callback = Deferred()
        finish_callback = Deferred()
        calls = []

        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))
            wait_callback.callback(True)
            return finish_callback

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        
        # Hold off until callback is started.
        yield wait_callback

        # Change the topology.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Ensure that the watch has been called just once so far
        # (although still pending due to the finish_callback).
        self.assertEquals(len(calls), 1)

        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Change the topology again.
        topology.add_machine("m-2")
        yield self.set_topology(topology)

        # Allow the first call to be completed, starting a process of
        # watching for the next change. At this point, the watch will
        # encounter that the client is disconnected.
        finish_callback.callback(True)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watch was still not called.
        self.assertEquals(len(calls), 1)

    @inlineCallbacks
    def test_watch_stops_on_early_closed_connection(self):
        """Verify watches stops when the connection is closed early.

        _watch_topology chains from an exists_and_watch to a
        get_and_watch. This test ensures that this chaining will fail
        gracefully if the connection is closed before this chaining
        can occur.
        """
        # Use a separate client connection for watching so it can be
        # disconnected.
        watch_client = ZookeeperClient(get_test_zookeeper_address())
        yield watch_client.connect()
        watch_base = StateBase(watch_client)

        calls = []

        @inlineCallbacks
        def watcher(old_topology, new_topology):
            calls.append((old_topology, new_topology))

        # Create the topology.
        topology = InternalTopology()
        topology.add_machine("m-0")
        yield self.set_topology(topology)
        
        # Now disconnect the client.
        watch_client.close()
        self.assertFalse(watch_client.connected)
        self.assertTrue(self.client.connected)

        # Start watching.
        yield watch_base._watch_topology(watcher)

        # Change the topology, this will trigger the watch.
        topology.add_machine("m-1")
        yield self.set_topology(topology)

        # Give a chance for something bad to happen.
        yield self.poke_zk()

        # Ensure the watcher was never called, because its client was
        # disconnected.
        self.assertEquals(len(calls), 0)