class KeyStoreTestCase(unittest.TestCase): """Test cases for the key-value store mixin.""" def setUp(self): self.clock = task.Clock() self.storage = {} self.keystore = KeyStoreMixin(self.clock, self.storage) self.gossiper = mock() self.gossiper.name = 'self' self.keystore.make_connection(self.gossiper) def test_replicate_when_remote_peer_changed_value(self): self.keystore.value_changed('a', 'k', (0, 'value')) verify(self.gossiper).set('k', (0, 'value')) self.assertNotIn('k', self.storage) def test_ignore_replication_when_remote_peer_has_old_value(self): self.storage['k'] = (1, 'value') self.keystore.value_changed('a', 'k', (0, 'value')) verify(self.gossiper, times=0).set('k', (0, 'value')) def test_persist_value_when_set_on_local_peer(self): self.keystore.value_changed('self', 'k', (0, 'value')) self.assertIn('k', self.storage) self.assertEquals(self.storage['k'], (0, 'value')) def test_keys_returns_all_keys_in_gossiper(self): when(self.gossiper).keys().thenReturn(['a', 'b']) self.assertEquals(self.keystore.keys(), ['a', 'b']) def test_keys_can_be_filtered(self): when(self.gossiper).keys().thenReturn(['ab', 'ba']) self.assertEquals(self.keystore.keys('b*'), ['ba']) def test_contains_use_keys_from_gossiper(self): when(self.gossiper).keys().thenReturn(['a', 'b']) self.assertIn('a', self.keystore) def test_get_results_default_value_if_value_not_present(self): when(self.gossiper).keys().thenReturn([]) self.assertEquals(self.keystore.get('a', '!'), '!') def test_get_returns_value_from_gossip_state(self): when(self.gossiper).keys().thenReturn(['a']) when(self.gossiper).get('a').thenReturn((0, '!')) self.assertEquals(self.keystore.get('a'), '!')
class ClusterNode(service.Service, KeyStoreMixin, LeaderElectionMixin): """Gossip participant that both implements our replicated key-value store and a leader-election mechanism. """ def __init__(self, clock, storage, client=client): self.election = _LeaderElectionProtocol(clock, self) self.keystore = KeyStoreMixin(clock, storage, [self.election.LEADER_KEY, self.election.VOTE_KEY, self.election.PRIO_KEY]) self.client = client self.storage = storage def startService(self): self.keystore.load_from(self.storage) service.Service.startService(self) def value_changed(self, peer, key, value): """A peer changed one of its values.""" if key == '__heartbeat__': return if self.election.value_changed(peer, key, value): # This value change was handled by the leader election # protocol. return self.keystore.value_changed(peer, key, value) if self.election.is_leader and peer.name == self.gossiper.name: # This peer is the leader of the cluster, which means that # we're responsible for firing notifications. if not key.startswith('watcher:'): self._check_notify(key) def make_connection(self, gossiper): """Make connection to gossip instance.""" self.gossiper = gossiper self.election.make_connection(gossiper) self.keystore.make_connection(gossiper) self.gossiper.set(self.election.PRIO_KEY, 0) def peer_alive(self, peer): """The gossiper reports that C{peer} is alive.""" self.election.peer_alive(peer) def peer_dead(self, peer): """The gossiper reports that C{peer} is dead.""" self.election.peer_alive(peer) def leader_elected(self, is_leader): """Leader elected.""" print "is leader?", is_leader if is_leader: # Go through and possible trigger all notifications. for key in self.keystore.keys('app:*'): self._check_notify(key) for key in self.keystore.keys('srv:*'): self._check_notify(key) def _notify(self, wkey, watcher): """Notification watcher about change.""" def done(result): watcher['last-hit'] = self.clock.seconds() # Verify that the watcher has not been deleted. if wkey in self and self.keystore[wkey] is not None: self.keystore.set(wkey, watcher) d = self.client.getPage(str(watcher['endpoint']), method='POST', postdata=json.dumps({'name': watcher['name'], 'uri': watcher['uri']}), timeout=3) return d.addCallback(done).addErrback(log.err) def _check_notify(self, key): """Possible notify listener that something has changed.""" for wkey in self.keystore.keys('watcher:*'): watcher = self.keystore.get(wkey) if watcher is None: continue timestamp = self.keystore.timestamp_for_key(key) if (key.startswith(watcher['pattern']) and watcher['last-hit'] < timestamp): self._notify(wkey, watcher)