示例#1
0
    def test_3(self):
        t1 = Timestamp({"id": 0})
        t2 = Timestamp({"id": 1})

        t2.merge(t1)

        self.assertNotEqual(t2.replicas["id"], 0)
示例#2
0
    def test_2(self):
        t1 = Timestamp()
        t2 = Timestamp({"id": 1})

        t1.merge(t2)

        self.assertEqual(t1.replicas["id"], 1)
class Frontend(object):
    def __init__(self):

        self.id = "frontend-" + str(uuid.uuid4())  # The ID of this FE

        # Reflects the version of the replicated data accessed by the FE; contains an entry for every RM. The FE sends
        # this with every query or update operation. When a RM returns a value as the result of a query operation, it
        # supplies a new vector timestamp, since the RMs may have been updated since the last operation. Each returned
        # timestamp is merged with the FE's previous timestamp to record the version the data observed by the client.
        self.prev = Timestamp()

        self.ns = Pyro4.locateNS()  # The Pyro Name Server

    def get_replica_uri(self) -> List[Pyro4.URI]:
        """
        Gets the Pyro URI of a suitable RM to send a request too. Replicas with an ACTIVE status take priority, followed
        by OVERLOADED. An error is thrown if all replicas are OFFLINE.
        :return: The Pyro URI of an RM
        """

        replicas = self.ns.list(metadata_all={"resource:replica"})  # Get all registered replicas

        uris = []

        for (name, uri) in replicas.items():

            try:

                with Pyro4.Proxy(uri) as replica:

                    status: Status = Status(replica.get_status())

                    print("{} reporting {}".format(name, status))

                    if status is not Status.OFFLINE:
                        uris.append((name, uri))  # Add the replica to those we'll use

                    if len(uris) >= FAULT_TOLERANCE:
                        return uris  # We've found enough replicas: return those we already have

            except CommunicationError:

                print("{} reporting Status.OFFLINE".format(name))  # The replica is offline!

        if len(uris) > 0:
            return uris  # We've found some!

        raise ConnectionRefusedError("All replicas reported Status.OFFLINE")  # All replicas are offline: raise an error

    @Pyro4.expose
    def request(self, request: ClientRequest) -> Any:
        """
        Sends a client request to f RMs, and returns the most up-to-date response to the client
        :param request: A ClientRequest sent by a client
        :return: None if the request is an update, and a value if it is a read.
        """

        print("\nReceived request from client {0}\n".format(request))

        frontend_request: FrontendRequest = FrontendRequest(self.prev, request)  # Build a frontend request
        uris = self.get_replica_uri()  # Get the URIs of available replicas
        responses: List[ReplicaResponse] = []

        for (name, uri) in uris:  # Iterate through those URIs

            print("\nUsing {}\n".format(name))

            with Pyro4.Proxy(uri) as replica:

                print("Sent timestamp {0}".format(self.prev))

                if request.method in [Operation.READ, Operation.AVERAGE, Operation.ALL]:

                    response: ReplicaResponse = replica.query(frontend_request)  # Query the replica

                    responses.append(response)  # Add the response to those received

                    break

                else:

                    response: ReplicaResponse = replica.update(frontend_request)  # Update the replica

                    responses.append(response)  # Add the response to those received

        value = None

        for response in responses:

            self.prev.merge(response.label)  # Merge this FE's timestamp with the timestamp received

            if response.value is not None:

                value = response.value   # Return the response of the first RM to execute the request

        print("\nNew timestamp  {0}".format(self.prev))
        print("Returning '{0}'".format(value))

        return value
示例#4
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")