Example #1
0
    def start(self):
        super(self.__class__, self).start()

        print 'Node %d started' % (self.addr,)

        self.client_journal = ClientJournal(self)
        self.txn_journal = TransactionJournal(self)
        for cookie in self.txn_journal.pendingOperations:
            self.completed_transactions.add(cookie)

        for command in self.client_journal.commands:
            node_addr, cmd_name, cmd_str = command.split(None, 2)
            proc = self.op(cmd_name, cmd_str)
            self.pending_cmds[command] = proc
            value = proc.next()
            self.act(int(node_addr), command, value)

        self.tid = self.client_journal.count
        # wrap up a commit that we failed during, if necessary:
        commit_snap = SnapshotCommitJournal(self, self.fs)
        commit_snap.completePendingOps()

        # Manually set up Paxos
        nodes = {0, 1, 2}
        nodes.remove(self.addr)
        other_nodes = ' '.join(str(addr) for addr in nodes)
        PaxosNode.onCommand(self, 'paxos_setup ' + other_nodes)
Example #2
0
class ChitterNode(ServerNode, ClientNode, PaxosNode):
    def __init__(self):
        PaxosNode.__init__(self)
        ClientNode.__init__(self)

        self.pending_cmds = {}

        self.sid = 0
        self.last_sid = 0
        self.tid = 0
        self.pid = 0
        self.completed_transactions = set()

        self.snapshots = {}
        self.cache = TTLDict()
        self.op = RemoteOp()

        self.pending_proposals = {}

    @server
    def next_sid(self):
        """Issues a new session ID"""

        sid = self.sid
        self.sid += 1
        return sid

    @client
    def next_tid(self):
        """Generates a new transaction ID"""

        tid = self.tid
        self.tid += 1
        return tid

    @server
    def next_pid(self):
        """Generates a new Paxos ID"""

        pid = self.pid
        self.pid += 1
        return pid

    @override
    def start(self):
        super(self.__class__, self).start()

        print 'Node %d started' % (self.addr,)

        self.client_journal = ClientJournal(self)
        self.txn_journal = TransactionJournal(self)
        for cookie in self.txn_journal.pendingOperations:
            self.completed_transactions.add(cookie)

        for command in self.client_journal.commands:
            node_addr, cmd_name, cmd_str = command.split(None, 2)
            proc = self.op(cmd_name, cmd_str)
            self.pending_cmds[command] = proc
            value = proc.next()
            self.act(int(node_addr), command, value)

        self.tid = self.client_journal.count
        # wrap up a commit that we failed during, if necessary:
        commit_snap = SnapshotCommitJournal(self, self.fs)
        commit_snap.completePendingOps()

        # Manually set up Paxos
        nodes = {0, 1, 2}
        nodes.remove(self.addr)
        other_nodes = ' '.join(str(addr) for addr in nodes)
        PaxosNode.onCommand(self, 'paxos_setup ' + other_nodes)

    @override
    def fail(self):
        print 'failed'
        super(self.__class__, self).fail()

    @override
    def onRIOReceive(self, from_addr, protocol, msg):
        ServerNode.onRIOReceive(self, from_addr, protocol, msg)
        ClientNode.onRIOReceive(self, from_addr, protocol, msg)
        PaxosNode.onRIOReceive(self, from_addr, protocol, msg)

    @override
    def onCommand(self, command):
        """Called when a command is issued through reply or console"""

        super(self.__class__, self).onCommand(command)

        node_addr, cmd_name, cmd_str = command.split(None, 2)
        proc = self.op(cmd_name, cmd_str)

        self.client_journal.push(command)
        self.pending_cmds[command] = proc

        value = proc.next()
        self.act(int(node_addr), command, value)

    @client
    def on_complete(self, req):
        """Called when a client request is complete"""

        command = req['command']
        if command not in self.pending_cmds:
            print 'unknown response: %r' % (req,)
            return

        if 'error' in req:
            print 'error: %s' % (req['error'],)
            return

        # Place read results in cache
        if req['action'] == 'do' and req['name'] == 'read':
            filename, content = req['args'][0], req['result']
            self.cache[filename] = content

        # Resume the corresponding generator
        proc = self.pending_cmds[command]
        value = proc.send(req.get('result', None))

        node_addr = req['dest']
        self.act(node_addr, command, value, req['sid'], req['tid'])
        self.pump_recv_queue()

    @client
    def act(self, node_addr, command, value, sid=None, tid=None):
        """Given an original command and a value yielded from a generator, act."""

        message = {'command': command, 'dest': node_addr}

        if isinstance(value, Transaction):
            # Start a transaction with a new tid
            message.update(action='begin', tid=self.next_tid())
            self.send_rpc(message)
        elif isinstance(value, RPC):
            # Perform an action
            name, args = value()
            message.update(action='do', name=name, args=args, sid=sid, tid=tid)

            # Try hitting the cache
            should_intercept_send = self.before_send_rpc(message)
            if not should_intercept_send:
                self.send_rpc(message)
        elif value is Transaction.Commit:
            # Commit the transaction
            message.update(action='commit', sid=sid, tid=tid)
            self.send_rpc(message)
        else:
            # The operation completed with a return value
            self.pending_cmds.pop(command)
            self.on_command_complete(command, value)

    @client
    def on_command_complete(self, command, result):
        print '[done] %s = %r' % (command, result)
        self.client_journal.complete(command)

    @client
    def before_send_rpc(self, message):
        """Called before self.send_rpc is called. Returning True will prevent
        send_rpc from being called."""

        if not message['action'] == 'do':
            return False

        # A write is about to happen. Evict from cache if possible.
        if message['name'] in ('overwrite', 'append'):
            filename = message['args'][0]

            try:
                del self.cache[filename]
            except KeyError:
                pass

            return False

        # A read is about to happen. Try to load from cache and avoid network
        if message['name'] == 'read':
            filename = message['args'][0]

            if filename not in self.cache:
                return False

            message['result'] = self.cache[filename]
            self.recv_queue.put(message)

            return True

    @server
    def on_request(self, req, src_addr=None):
        """Called when a server receives a request"""

        action = req['action']

        if action == 'begin':
            return self.respond_begin(req, src_addr)
        elif action == 'do':
            return self.respond_do(req, src_addr)
        elif action == 'commit':
            return self.respond_commit(req, src_addr)

    @server
    def respond_begin(self, req, src_addr):
        cookie = (src_addr, req['tid'])
        if cookie in self.completed_transactions:
            req['error'] = 'transaction already completed'
            return req

        sid = self.next_sid()
        self.snapshots[sid] = Snapshot(self.fs)
        req['sid'] = sid
        return req

    @server
    def respond_do(self, req, src_addr):
        sid = req['sid']
        if sid not in self.snapshots:
            req['error'] = 'invalid session ID'
            return req

        snapshot = self.snapshots[sid]
        result = getattr(snapshot, req['name'])(*req['args'])
        req['result'] = result
        return req

    @server
    def respond_commit(self, req, src_addr):
        sid = req['sid']
        if sid not in self.snapshots:
            req['error'] = 'invalid session ID'
            return req

        if self.last_sid > sid:
            if False:
                # TODO: optimize for transactions that don't touch other files
                pass
            else:
                req['error'] = 'transaction failed: sid: %d, last sid: %d' % (
                    sid, self.last_sid
                )
                return req
        else:
            snapshot = self.snapshots.pop(sid)

            pid = self.next_pid()
            self.pending_proposals[pid] = {
                'snapshot': snapshot, 'req': req, 'src_addr': src_addr,
                'sid': sid
            }

            proposal = {'value': snapshot.proposal, 'pid': pid}

            if not snapshot.empty:
                self.propose(proposal)
            else:
                self.on_paxos_consensus(True, proposal)
            return None

    @override
    def on_paxos_consensus(self, success, proposal):
        pid = proposal['pid']
        if pid not in self.pending_proposals:
            # Ignore for now
            return

        memo = self.pending_proposals.pop(pid)
        req, src_addr = memo['req'], memo['src_addr']
        snapshot, sid = memo['snapshot'], memo['sid']

        if success:
            snapshot.commit(self)

            cookie = (src_addr, req['tid'])
            self.completed_transactions.add(cookie)
            self.txn_journal.push(cookie)

            self.last_sid = sid
            req['result'] = True

        else:
            req['error'] = 'Paxos proposal rejected'

        out = Serialization.encode(req)
        print 'replying to %d: %r' % (src_addr, req)
        self.RIOSend(src_addr, Protocol.CHITTER_RPC_REPLY, out)

    @override
    def on_paxos_learned(self, value):
        proposal = value['value']
        for filename, content in proposal.iteritems():
            if content is None:
                self.fs.delete(filename)
                return

            if not self.fs.exists(filename):
                self.fs.create(filename)

            self.fs.overwriteIfNotChanged(filename, content, -1)