Example #1
0
    def test_1(self):
        t1 = Timestamp({"id": 0})
        t2 = Timestamp({"id": 1})

        l = list(t1.compare(t2))

        self.assertEqual(l[0], "id")
Example #2
0
class Replica(object):
    # Accessor methods for properties of this RM; used for gossip
    @Pyro4.expose
    @property
    def id(self):

        return self._id

    @Pyro4.expose
    @property
    def replica_timestamp(self):

        return self._replica_timestamp

    @Pyro4.expose
    @property
    def update_log(self):

        return self._update_log

    def __init__(self):

        self._id = "replica-" + str(uuid.uuid4())  # This RM's ID

        # Represents updates currently reflected in the value. Contains one entry for every replica manager, and is
        # updated whenever an update operation is applied to the value.
        self.value_timestamp = Timestamp({self.id: 0})

        # Represents those updates that have been accepted by the RM (placed in the RM's update log). Differs from the
        # value timestamp because not all updates in the log are stable.
        self._replica_timestamp = Timestamp({self.id: 0})

        # The Pyro name server. Storing it locally removes the overhead of re-locating it every time this RM gets
        # replicas_with_updates.
        self.ns = Pyro4.locateNS()

        # This RM's update log, containing Records.
        self._update_log = Log()

    # The value of the application state as maintained by the RM. Each RM is a state machine, which begins with a
    # specified initial value and is thereafter solely the result of applying update operations to that state.
    database = DB()

    # The same update may arrive at a given replica manager from a FE and in gossip messages from other RMs. To prevent
    # an update being applied twice, this table contains the unique FE IDs of updates that have been applied to the
    # value. The RM checks this table before adding an update to the log.
    executed_operation_table: List[str] = []

    # Contains a vector timestamp for each other RM, filled with timestamps that arrive from them in gossip messages.
    # Used to establish whether an update has been applied to all RMs.
    timestamp_table: Dict[str, Timestamp] = {}

    def query(self, query: FrontendRequest) -> ReplicaResponse:
        """
        Execute a query from an FE. If this RM holds outdated information (i.e. FE's prev > RM's value), gossip, then
        execute the query.
        :param query: A FrontendRequest, comprising the FE's timestamp and the request from the client.
        :return: A ReplicaResponse, containing the requested value and this RM's value timestamp.
        """

        print("Received query from FE", query.prev, end="\n\n")

        prev: Timestamp = query.prev  # The FE timestamp, representing the state of the information it last accessed.
        request: ClientRequest = query.request  # The request passed to the FE

        # q can be applied to the replica's value if q.prev <= valueTS. If it can't, gossip so that it can.
        if (prev <= self.value_timestamp) is False:
            self.gossip(prev)

        result = self.database.execute_request(request)

        return ReplicaResponse(result, self.value_timestamp)

    def update(self, update: FrontendRequest) -> ReplicaResponse:
        """
        Execute an update from an FE. If this RM holds outdated information (i.e. FE's prev > RM's value), gossip, then
        execute the update.
        :param update: A FrontendRequest, comprising the FE's timestamp and the request from the client.
        :return: A ReplicaResponse, containing a database message and this RM's value timestamp.
        """

        print("Received update from FE", update.prev, end="\n\n")

        id: str = update.id
        prev: Timestamp = update.prev  # The previous timestamp
        request: ClientRequest = update.request  # The request passed to the FE

        if id not in self.executed_operation_table:  # Update has not already been applied

            if id not in self._update_log:  # Update has not been seen before

                self._replica_timestamp.replicas[
                    self.id] += 1  # This RM has accepted an update

                if (prev <= self.value_timestamp
                    ) is False:  # If we're missing information, gossip

                    self.gossip(prev)

                ts = prev.copy()
                ts.replicas[self.id] = self._replica_timestamp.replicas[
                    self.id]  # Update the timestamp to reflect this

                record = Record(self.id, ts, request, prev,
                                id)  # Create a record of this update
                self._update_log += record  # Add it to the log

                result = self.apply_update(record)

                return ReplicaResponse(result, ts)

        return ReplicaResponse("Update has already been performed",
                               self.value_timestamp)

    def apply_update(self, record: Record) -> str:
        """
        Apply an update, held within a Record in this RM's update log
        :param record: a stable Record in this RM's update log
        :return: a message from the Database
        """

        self.value_timestamp.merge(
            record.ts
        )  # Merge this RM's value timestamp with the timestamp of the record
        self.executed_operation_table.append(
            record.id)  # Add the record's ID to the executed operation table

        return self.database.execute_request(
            record.request)  # Execute the request

    def get_status(self) -> Status:
        """
        :return: An arbitrary status
        """

        return Status.random

    def apply_gossip(self, replica: 'Replica'):

        print("Gossiping with {0}".format(replica.id))

        log: Log = replica.update_log  # The RM's update_log
        ts: Timestamp = replica.replica_timestamp  # The RM's replica timestamp
        old_log_length = len(self._update_log)

        self._update_log.merge(log,
                               self._replica_timestamp)  # Merge update logs

        print("Merging update logs ({} new record(s))".format(
            len(self._update_log) - old_log_length))

        self._replica_timestamp.merge(ts)  # Merge replica timestamps

        print("New replica timestamp", self._replica_timestamp)

        stable: List[Record] = self._update_log.stable(
            self._replica_timestamp)  # Get stable records
        applied_updates = 0

        print()

        for record in stable:  # Iterate through stable records

            if record.id not in self.executed_operation_table:  # If the record has not already been applied

                self.apply_update(record)  # Apply the update

                self.timestamp_table[id] = ts  # Update the timestamp table

                applied_updates += 1

        print("Applied {} stable update(s)\n".format(applied_updates))

    def gossip(self, prev: Timestamp) -> None:
        """
        Gossip with other replicas.
        :param prev:
        :return: None
        """

        for replica_id in self._replica_timestamp.compare(
                prev):  # Iterate through a  list of RMs that this RM needs
            # updates from

            uri = self.ns.lookup(replica_id)  # Get the URI of the RM

            try:

                with Pyro4.Proxy(uri) as replica:

                    self.apply_gossip(replica)  # Apply gossip

            except CommunicationError:

                print("{} with required updates reporting Status.OFFLINE\n".
                      format(replica_id))  # A replica with a
                # required update is offline. Shouldn't matter, as requests are sent to multiple RMs when they are
                # received at a FM.

        print("Finished gossiping\n")