Example #1
0
    def test_get_with_capabilities_bad_params(self):
        test_address_book = AddressBook()

        with self.assertRaises(ValueError):
            test_address_book.get_with_capabilities(required_capabilities=[
                SimpleCapability("NotImportantForThisTest")
            ],
                                                    match_threshold=-0.1)
        with self.assertRaises(ValueError):
            test_address_book.get_with_capabilities(required_capabilities=[
                SimpleCapability("NotImportantForThisTest")
            ],
                                                    match_threshold=1.1)
        with self.assertRaises(ValueError):
            test_address_book.get_with_capabilities(required_capabilities=[
                SimpleCapability("NotImportantForThisTest")
            ],
                                                    match_threshold=.5,
                                                    n=0)
        with self.assertRaises(ValueError):
            test_address_book.get_with_capabilities(required_capabilities=[
                SimpleCapability("NotImportantForThisTest")
            ],
                                                    match_threshold=.5,
                                                    n=-1)
        return
Example #2
0
 def __init__(self, pool_name: str):
     """
     Check this or child class has correctly implemented callable __call__ as needed to handle both the
     PubSub listener events and the work timer events.
     """
     self._address_book = AddressBook()
     super().__init__()
     self._call_lock = threading.Lock()
     self._handler = NotificationHandler(object_to_be_handler_for=self,
                                         throw_unhandled=False)
     self._handler.register_handler(self._get_task, WorkRequest)
     self._handler.register_handler(self._put_task, WorkNotificationDo)
     self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
     self._handler.register_handler(self._do_srcsink_ping_notification,
                                    SrcSinkPingNotification)
     self._handler.register_handler(self._do_work_finalise,
                                    WorkNotificationFinalise)
     self._handler.register_activity(
         handler_for_activity=self._do_pub,
         activity_interval=TaskPool.PUB_TIMER,
         activity_name="{}-do_pub_activity".format(pool_name))
     self._handler.register_activity(
         handler_for_activity=self._do_manage_presence,
         activity_interval=TaskPool.PRS_TIMER,
         activity_name="{}-do_manage_presence".format(pool_name))
     self._capabilities = self._get_capabilities()
     self._unique_topic = self._create_topic_and_subscription()
     return
Example #3
0
 def __init__(self, ether_name: str):
     """
     Register the notification handlers and install publication activity.
         Note: Make sure properties name & topic are defined in the sub class before super().__inti__ is called
         otherwise the activity timer will reference non existent properties in the sub class as the timer
         will fire (here) before they have been defined in the sub-class init()
     """
     self._stopped = False
     self._address_book = AddressBook()
     super().__init__()
     self._call_lock = threading.Lock()
     self._handler = NotificationHandler(object_to_be_handler_for=self,
                                         throw_unhandled=False)
     self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
     self._handler.register_handler(self._do_srcsink_ping_notification,
                                    SrcSinkPingNotification)
     return
Example #4
0
    def test_basics(self):
        test_srcsink = DummySrcSink("DummySrcSink-1")
        test_address_book = AddressBook()

        self.assertEqual(0, len(test_address_book.get()))

        test_address_book.update(test_srcsink)
        self.assertEqual(1, len(test_address_book.get()))
        self.assertTrue(test_srcsink in test_address_book.get())
        return
Example #5
0
    def __init__(self, agent_name: str):
        """
        Register all notification handlers & activities.
        """
        self._address_book = AddressBook()
        self._lock = threading.RLock()
        self._subscribed_topics = list()

        super().__init__()

        self._work_timer = Agent.WORK_TIMER
        self._timer = None
        self._task_consumption_policy = None
        self._handler = NotificationHandler(object_to_be_handler_for=self,
                                            throw_unhandled=False)
        self._handler.register_handler(self._do_notification, TaskNotification)
        self._handler.register_handler(self._do_work, WorkNotificationDo)
        self._handler.register_handler(self._do_work_finalise,
                                       WorkNotificationFinalise)
        self._handler.register_handler(self._do_work_initiate,
                                       WorkNotificationInitiate)
        self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
        self._handler.register_handler(self._do_srcsink_ping_notification,
                                       SrcSinkPingNotification)
        self._handler.register_activity(
            handler_for_activity=self._activity_check_work_to_do,
            activity_interval=self._work_timer,
            activity_name="{}-activity-work-to-do".format(agent_name))
        self._handler.register_activity(
            handler_for_activity=self._activity_manage_presence,
            activity_interval=Agent.PRS_TIMER,
            activity_name="{}-activity-manage-presence".format(agent_name))
        self._handler.register_activity(
            handler_for_activity=self._activity_initiate_work,
            activity_interval=Agent.WORK_INIT_TIMER,
            activity_name="{}-activity-work-init".format(agent_name))
        self._unique_topic = self._create_topic_and_subscriptions()
        self._capabilities = self._get_capabilities()
        return
Example #6
0
    def __init__(self,
                 name: str,
                 capability: SimpleCapability = SimpleCapability(
                     capability_name='DummySrcSink'),
                 ping_topic: str = Ether.ETHER_BACK_PLANE_TOPIC):
        # SrcSink - Standard boot-strap & protected members
        #
        self._name = name
        self._topic = UniqueTopic().topic()

        self._address_book = AddressBook()

        super().__init__()
        self._capabilities = [capability]

        self._lock = threading.Lock()
        self._handler = NotificationHandler(object_to_be_handler_for=self,
                                            throw_unhandled=True)
        self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
        self._handler.register_handler(self._do_srcsink_ping_notification,
                                       SrcSinkPingNotification)
        self._handler.register_handler(self._do_notification, TaskNotification)
        self._handler.register_handler(self._do_work, WorkNotificationDo)
        self._handler.register_handler(self._do_work_finalise,
                                       WorkNotificationFinalise)

        # Public Members just for Unit Test Asserts
        #
        self.pings = list()
        self.ping_notifications = list()
        self.task_notification = list()
        self.work_notification = list()
        self.work_finalise = list()

        self._ping_topic = ping_topic  # The topic to issue ping's on

        # Get connected !
        self.setup_subscriptions()
        return
Example #7
0
    def test_get_with_capabilities_single_ss(self):
        test_srcsink = DummySrcSink("DummySrcSink-1")
        test_address_book = AddressBook()

        self.assertEqual(
            None,
            test_address_book.get_with_capabilities(test_srcsink.capabilities))
        test_address_book.update(test_srcsink)
        self.assertEqual(
            test_srcsink,
            test_address_book.get_with_capabilities(
                test_srcsink.capabilities)[0])
        return
Example #8
0
class Agent(SrcSink):
    WORK_TIMER = float(.25)
    WORK_TIMER_MAX = float(30)
    PRS_TIMER = float(.25)
    PRD_TIMER_MAX = float(60)
    WORK_INIT_TIMER = float(.25)
    WORK_INIT_TIMER_MAX = float(180)
    AGENT_TOPIC_PREFIX = "agent"

    def __init__(self, agent_name: str):
        """
        Register all notification handlers & activities.
        """
        self._address_book = AddressBook()
        self._lock = threading.RLock()
        self._subscribed_topics = list()

        super().__init__()

        self._work_timer = Agent.WORK_TIMER
        self._timer = None
        self._task_consumption_policy = None
        self._handler = NotificationHandler(object_to_be_handler_for=self,
                                            throw_unhandled=False)
        self._handler.register_handler(self._do_notification, TaskNotification)
        self._handler.register_handler(self._do_work, WorkNotificationDo)
        self._handler.register_handler(self._do_work_finalise,
                                       WorkNotificationFinalise)
        self._handler.register_handler(self._do_work_initiate,
                                       WorkNotificationInitiate)
        self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
        self._handler.register_handler(self._do_srcsink_ping_notification,
                                       SrcSinkPingNotification)
        self._handler.register_activity(
            handler_for_activity=self._activity_check_work_to_do,
            activity_interval=self._work_timer,
            activity_name="{}-activity-work-to-do".format(agent_name))
        self._handler.register_activity(
            handler_for_activity=self._activity_manage_presence,
            activity_interval=Agent.PRS_TIMER,
            activity_name="{}-activity-manage-presence".format(agent_name))
        self._handler.register_activity(
            handler_for_activity=self._activity_initiate_work,
            activity_interval=Agent.WORK_INIT_TIMER,
            activity_name="{}-activity-work-init".format(agent_name))
        self._unique_topic = self._create_topic_and_subscriptions()
        self._capabilities = self._get_capabilities()
        return

    def __del__(self):
        """
        Shut down
        """
        self._handler.activity_state(paused=True)
        with self._lock:
            sub_list = deepcopy(self._subscribed_topics)
        for topic in sub_list:
            pub.unsubscribe(self, topic)
        return

    def __call__(self, notification: Notification):
        """ Handle notification requests
        :param notification: The notification to be passed to the handler
        """
        if isinstance(notification, Notification):
            self._handler.call_handler(notification)
        else:
            raise ValueError(
                "{} is an un supported notification type for Agent}".format(
                    type(notification).__name__))
        return

    def _create_topic_and_subscriptions(self) -> str:
        """
        Create the unique topic for the agent that it will listen on for work (task) deliveries that it has
        requested from the task-pool
        """
        topics = list()
        unique_topic = UniqueTopic().topic(Agent.AGENT_TOPIC_PREFIX)
        topics.append(unique_topic)
        for topic in self._work_topics():
            topics.append(topic)
        self.add_subscription_topics(topics=topics)
        for topic in topics:  # do potentially high latency subscriptions outside of the lock.
            pub.subscribe(self, topic)
        return unique_topic

    def add_subscription_topics(self, topics: List[str]) -> None:
        """
        Add the given list of topics to the list of topics agent is subscribed to
        """
        with self._lock:
            for topic in topics:
                if topic not in self._subscribed_topics:
                    self._subscribed_topics.append(topic)
        return

    @staticmethod
    def _get_capabilities() -> List[Capability]:
        """
        The capabilities of this Agent
        :return: List of Capabilities
        """
        return [
            SimpleCapability(capability_name=str(CapabilityRegister.AGENT))
        ]

    @abstractmethod
    @purevirtual
    def _do_notification(self, task_notification: TaskNotification):
        """
        callback to Notify agent of a task that needs attention. The agent can optionally grab the task from the
        task pool and work on it or ignore it.
        :param task_notification: The notification event for task requiring attention
        """
        pass

    @abstractmethod
    @purevirtual
    def _do_work(self, work_notification: WorkNotificationDo) -> None:
        """
        Process any out standing tasks associated with the agent.
        """
        pass

    @abstractmethod
    @purevirtual
    def _do_work_initiate(self,
                          work_notification: WorkNotificationInitiate) -> None:
        """
        Handle the initiation the given work item from this agent
        """
        pass

    @abstractmethod
    @purevirtual
    def _do_work_finalise(
            self,
            work_notification_finalise: WorkNotificationFinalise) -> None:
        """
        Take receipt of the given completed work item that was initiated from this agent and do any
        final processing.
        """
        pass

    @abstractmethod
    @purevirtual
    def work_initiate(self, work_notification: WorkNotificationDo) -> None:
        """
        Initiate the given work item with the agent as the owner of the work.
        """
        pass

    @abstractmethod
    @purevirtual
    def _activity_check_work_to_do(self,
                                   current_activity_interval: float) -> float:
        """
        Are there any tasks associated with the Agent that need working on ? of so schedule them by calling work
        execute handler.
        :param current_activity_interval: The current delay in seconds before activity is re-triggered.
        :return: The new delay in seconds before the activity is re-triggered.
        """

        pass

    @purevirtual
    @abstractmethod
    def _activity_manage_presence(self,
                                  current_activity_interval: float) -> float:
        """
        Ensure that we are known on the ether & our address book has the name of at least one local pool in it.
        :param current_activity_interval: The current delay in seconds before activity is re-triggered.
        :return: The new delay in seconds before the activity is re-triggered.
        """
        pass

    @purevirtual
    @abstractmethod
    def _activity_initiate_work(self,
                                current_activity_interval: float) -> float:
        """
        If the agent is a source (origin) of work then this activity will create and inject the new tasks. Zero
        or more tasks may be created depending on the specific task creation policy.
        :param current_activity_interval: The current delay in seconds before activity is re-triggered.
        :return: The new delay in seconds before the activity is re-triggered.
        """
        pass

    @purevirtual
    @abstractmethod
    def _work_topics(self) -> List[str]:
        """
        The list of topics to subscribe to based on the Work Topics (status transitions) supported by the
        agent.
        """
        pass

    @property
    def task_consumption_policy(self) -> TaskConsumptionPolicy:
        """
        Get the policy that the agent uses to decide to process (or not) the task based on tasks meta data
        :return: The consumption policy
        """
        return self._task_consumption_policy

    @task_consumption_policy.setter
    def task_consumption_policy(self, p: TaskConsumptionPolicy) -> None:
        """
        Set the policy that the agent uses to decide to process (or not) the task based on tasks meta data
        """
        self._task_consumption_policy = p

    @abstractmethod
    @purevirtual
    def reset(self) -> None:
        """
        Return the Actor to the same state at which it was constructed
        """
        pass

    # ----- P R O P E R T I E S -----

    @property
    @abstractmethod
    @purevirtual
    def name(self) -> str:
        """
        The unique name of the Agent
        :return: The Agent name
        """
        pass

    @property
    @abstractmethod
    @purevirtual
    def capacity(self) -> int:
        """
        The current work capacity of the actor.
        :return: Current work capacity as int
        """
        pass

    @property
    @abstractmethod
    @purevirtual
    def from_state(self) -> State:
        """
        The state the actor expects to receive tasks in
        :return: from state
        """
        pass

    @abstractmethod
    @purevirtual
    @property
    def to_state(self) -> State:
        """
        The state the actor will process tasks into
        :return: to state
        """
        pass

    @abstractmethod
    @purevirtual
    @property
    def failure_rate(self) -> float:
        """
        The rate at which completed tasks fail.
        :return: Failure state of the actor
        """
        pass

    @property
    def work_interval(self) -> float:
        """
        The wait period between doing work.
        :return: Wait interval between work timer events in seconds
        """
        return self._work_timer

    @property
    def capabilities(self) -> List[Capability]:
        """
        The collection of capabilities of the SrcSink
        :return: The collection of capabilities
        """
        return self._capabilities

    def get_addressbook(self) -> List[SrcSink]:
        """
        The list of srcsinks known to the Ether
        :return: srcsinks
        """
        return self._address_book.get()

    def _update_addressbook(self, srcsink: SrcSink) -> None:
        """
        Update the given src_sink in the collection of registered srcsinks. If src_sink is not in the collection
        add it with a current time stamp.
        :param srcsink: The src_sink to update / add.
        """
        self._address_book.update(srcsink)
        return
Example #9
0
    def test_get_with_capabilities_partial_matches(self):
        test_address_book = AddressBook()

        cap1 = SimpleCapability("Cap1")
        cap2 = SimpleCapability("Cap2")
        cap3 = SimpleCapability("Cap3")
        cap4 = SimpleCapability("Cap4")
        capx = SimpleCapability("Cap?")

        ss1 = DummySrcSink("DummySrcSink-1")
        ss1.capabilities = [cap1]

        ss2 = DummySrcSink("DummySrcSink-2")
        ss2.capabilities = [cap1, cap2]

        ss3 = DummySrcSink("DummySrcSink-3")
        ss3.capabilities = [cap1, cap2, cap3]

        ss4 = DummySrcSink("DummySrcSink-4")
        ss4.capabilities = [cap2, cap4]

        test_address_book.update(ss1)
        time.sleep(.01)
        test_address_book.update(ss2)
        time.sleep(.01)
        test_address_book.update(ss3)
        time.sleep(.01)
        test_address_book.update(ss4)

        scenarios = [
            [1, [cap1, capx], 1.0, [None], 1],
            [2, [cap1, cap2, cap3], 1.0, [ss3], 1],
            [3, [cap1, cap2], 1.0, [ss3],
             1],  # As ss-3 also has Cap1, Cap2 and was created last
            [4, [capx], 0.0, [ss4],
             1],  # Zero => match any so will return last one created
            [5, [capx], 0.3142, [None], 1],
            [6, [capx], 1.0, [None], 1],
            [7, [cap1], 1.0, [ss3], 1],
            [8, [cap1, cap2], 1.0, [ss3], 1],
            [9, [cap1, cap2], 0.5, [ss4], 1],
            [10, [cap1], 1.0, [ss3, ss2], 2],
            [11, [cap1], 1.0, [ss3, ss2, ss1], 3],
            [12, [cap1], 1.0, [ss3, ss2, ss1], 4],
            [13, [cap1], 0.0, [ss4, ss3, ss2, ss1], 4],
            [14, [cap1], 1.0, [ss3, ss2, ss1], 5],
            [15, [cap1], 1.0, [ss3, ss2, ss1], 500],
            [16, [cap4, capx], 1.0, [None], 10],
            [17, [cap4, capx], 0.5, [ss4], 10],
            [18, [cap3, cap4, capx], 1.0, [None], 10],
            [19, [cap3, cap4, capx], 0.33, [ss4, ss3], 10]
        ]

        for scenario in scenarios:
            case_num, caps, threshold, expected, n = scenario
            logging.info(
                "test_get_with_capabilities_partial_matches case {}".format(
                    case_num))
            res = test_address_book.get_with_capabilities(
                required_capabilities=caps, match_threshold=threshold, n=n)
            if res is None:
                self.assertEqual(expected[0], res)
                self.assertEqual(len(expected), 1)
            else:
                self.assertEqual(len(expected),
                                 len(res))  # Should have same num results.
                for i in range(len(expected)):  # Order dependent equality.
                    self.assertEqual(expected[i], res[i])
            logging.info("case {} passed OK".format(case_num))

        return
Example #10
0
    def test_get_with_capabilities_multi_ss(self):
        ss_list = list()
        for i in range(10):
            ss_list.append(DummySrcSink("DummySrcSink-{}".format(i)))

        # Confirm empty at create time.
        test_address_book = AddressBook()
        self.assertEqual(0, len(test_address_book.get()))

        # Confirm nothing matches empty address book in terms of capabilities
        for ss in ss_list:
            self.assertEqual(
                None, test_address_book.get_with_capabilities(ss.capabilities))

        # Add to the AddressBook with a time delay so the address timestamps are clearly different.
        # The last one added should always bethe one returned as it the last in time.
        for ss in ss_list:
            time.sleep(0.1)
            test_address_book.update(ss)
            self.assertEqual(
                ss,
                test_address_book.get_with_capabilities(ss.capabilities)[0])

        # Check everything is in the addressBook
        addr_bk = test_address_book.get()
        for ss in ss_list:
            self.assertTrue(ss in addr_bk)

        # Wait 5 seconds and then ask for items newer than 10 seconds - should be none
        delay = 2
        time.sleep(delay)
        ss_last = ss_list[-1]
        self.assertEqual(
            None,
            test_address_book.get_with_capabilities(ss_last.capabilities,
                                                    max_age_in_seconds=delay -
                                                    0.1))
        self.assertEqual(
            ss_last,
            test_address_book.get_with_capabilities(ss_last.capabilities,
                                                    max_age_in_seconds=100)[0])

        # Add one more and make sure it comes back
        ss_new = DummySrcSink("DummySrcSink-new")
        test_address_book.update(ss_new)
        self.assertEqual(
            ss_new,
            test_address_book.get_with_capabilities(
                ss_new.capabilities, max_age_in_seconds=None)[0])
        self.assertEqual(
            ss_new,
            test_address_book.get_with_capabilities(ss_new.capabilities,
                                                    max_age_in_seconds=0.5)[0])
        return
Example #11
0
class DummySrcSink(SrcSink):
    def __init__(self,
                 name: str,
                 capability: SimpleCapability = SimpleCapability(
                     capability_name='DummySrcSink'),
                 ping_topic: str = Ether.ETHER_BACK_PLANE_TOPIC):
        # SrcSink - Standard boot-strap & protected members
        #
        self._name = name
        self._topic = UniqueTopic().topic()

        self._address_book = AddressBook()

        super().__init__()
        self._capabilities = [capability]

        self._lock = threading.Lock()
        self._handler = NotificationHandler(object_to_be_handler_for=self,
                                            throw_unhandled=True)
        self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
        self._handler.register_handler(self._do_srcsink_ping_notification,
                                       SrcSinkPingNotification)
        self._handler.register_handler(self._do_notification, TaskNotification)
        self._handler.register_handler(self._do_work, WorkNotificationDo)
        self._handler.register_handler(self._do_work_finalise,
                                       WorkNotificationFinalise)

        # Public Members just for Unit Test Asserts
        #
        self.pings = list()
        self.ping_notifications = list()
        self.task_notification = list()
        self.work_notification = list()
        self.work_finalise = list()

        self._ping_topic = ping_topic  # The topic to issue ping's on

        # Get connected !
        self.setup_subscriptions()
        return

    def __call__(self, notification: Notification):
        """ Handle notification requests
        :param notification: The notification to be passed to the handler
        """
        if isinstance(notification, Notification):
            self._handler.call_handler(notification)
        else:
            raise ValueError(
                "{} un supported notification type {} RX'ed".format(
                    self.name,
                    type(notification).__name__))
        return

    def setup_subscriptions(self) -> None:
        pub.subscribe(self, self._topic)
        return

    def __del__(self):
        pub.unsubscribe(self, self._topic)
        return

    @property
    def capabilities(self) -> List[Capability]:
        return self._capabilities

    @capabilities.setter
    def capabilities(self, capabilities: List[Capability]):
        """
        Override capabilities, only really meaningful in a test setting.
        :param capabilities:
        :return:
        """
        self._capabilities = capabilities
        return

    def _do_srcsink_ping_notification(self,
                                      ping_notification: Notification) -> None:
        logging.info("{} :: {} RX handled by".format(
            self.__class__.__name__, self.name, "_srcsink_ping_notification"))
        with self._lock:
            self.ping_notifications.append(ping_notification)
        return

    def _do_srcsink_ping(self, ping_request: Notification) -> None:
        logging.info("{} :: {} RX handled by".format(self.__class__.__name__,
                                                     self.name,
                                                     "_srcsink_ping"))
        with self._lock:
            self.pings.append(ping_request)
        return

    def _do_notification(self, task_notification: Notification):
        logging.info("{} :: {} RX handled by".format(self.__class__.__name__,
                                                     self.name,
                                                     "_do_notification"))
        with self._lock:
            self.task_notification.append(task_notification)
        return

    def _do_work(self, work_notification: Notification):
        logging.info("{} :: {} RX handled by".format(self.__class__.__name__,
                                                     self.name, "_do_work"))
        with self._lock:
            self.work_notification.append(work_notification)
        return

    def _do_work_finalise(self, do_work_finalise: WorkNotificationFinalise):
        logging.info("{} :: {} RX handled by".format(self.__class__.__name__,
                                                     self.name,
                                                     "_do_work_finalise"))
        with self._lock:
            self.work_finalise.append(do_work_finalise)

        if self.topic != do_work_finalise.originator.topic:
            pub.sendMessage(topicName=do_work_finalise.originator.topic,
                            notification=do_work_finalise)
        return

    def send_ping(self,
                  required_capabilities: List[Capability]) -> UniqueWorkRef:
        logging.info("{} :: {} Sent Ping".format(self.__class__.__name__,
                                                 self.name, "send_ping"))
        ping = SimpleSrcSinkPing(sender_srcsink=self,
                                 required_capabilities=required_capabilities)
        pub.sendMessage(self._ping_topic, notification=ping)
        return ping.work_ref

    @property
    def name(self) -> str:
        return self._name

    @property
    def topic(self) -> str:
        return self._topic

    @property
    def get_addressbook(self) -> List['SrcSink']:
        logging.info("{} :: {} get_addressbook".format(self.__class__.__name__,
                                                       self.name,
                                                       "get_addressbook"))
        return self._address_book.get()

    def _update_addressbook(self, srcsink: 'SrcSink') -> None:
        logging.info("{} :: {} update_addressbook".format(
            self.__class__.__name__, self.name, "update_addressbook"))
        self._address_book.update(srcsink)
        return

    def __str__(self):
        return "DummySrcSink name := {}".format(self.name)

    def __repr__(self):
        return self.__str__()
Example #12
0
class TaskPool(SrcSink):
    PUB_TIMER = float(.25)
    PUB_TIMER_MAX = float(30)
    PRS_TIMER = float(.25)
    PRS_TIMER_MAX = float(30)
    POOL_TOPIC_PREFIX = "TaskPool"

    def __init__(self, pool_name: str):
        """
        Check this or child class has correctly implemented callable __call__ as needed to handle both the
        PubSub listener events and the work timer events.
        """
        self._address_book = AddressBook()
        super().__init__()
        self._call_lock = threading.Lock()
        self._handler = NotificationHandler(object_to_be_handler_for=self,
                                            throw_unhandled=False)
        self._handler.register_handler(self._get_task, WorkRequest)
        self._handler.register_handler(self._put_task, WorkNotificationDo)
        self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
        self._handler.register_handler(self._do_srcsink_ping_notification,
                                       SrcSinkPingNotification)
        self._handler.register_handler(self._do_work_finalise,
                                       WorkNotificationFinalise)
        self._handler.register_activity(
            handler_for_activity=self._do_pub,
            activity_interval=TaskPool.PUB_TIMER,
            activity_name="{}-do_pub_activity".format(pool_name))
        self._handler.register_activity(
            handler_for_activity=self._do_manage_presence,
            activity_interval=TaskPool.PRS_TIMER,
            activity_name="{}-do_manage_presence".format(pool_name))
        self._capabilities = self._get_capabilities()
        self._unique_topic = self._create_topic_and_subscription()
        return

    def __del__(self):
        """
        Clean up
        """
        self._handler.activity_state(paused=True)
        pub.unsubscribe(self, self._unique_topic)
        return

    def __call__(self, notification: Notification):
        """ Handle notification requests
        :param notification: The notification to be passed to the handler
        """
        if isinstance(notification, Notification):
            self._handler.call_handler(notification)
        else:
            raise ValueError(
                "{} un supported notification type for Task Pool".format(
                    type(notification).__name__))
        return

    def _create_topic_and_subscription(self) -> str:
        """
        Create the unique topic for the agent that it will listen on for work (task) deliveries that it has
        requested from the task-pool
        """
        unique_topic = UniqueTopic().topic(TaskPool.POOL_TOPIC_PREFIX)
        pub.subscribe(self, unique_topic)
        return unique_topic

    @staticmethod
    def _get_capabilities() -> List[Capability]:
        """
        The capabilities of this Agent
        :return: List of Capabilities
        """
        return [SimpleCapability(capability_name=str(CapabilityRegister.POOL))]

    @purevirtual
    @abstractmethod
    def _put_task(self, work_initiate: WorkNotificationDo) -> None:
        """
        Add a task to the task pool which will cause it to be advertised via the relevant topic unless the task
        is in it's terminal state.
        :param work_initiate: Details of the task being injected.
        """
        pass

    @purevirtual
    @abstractmethod
    def _get_task(self, work_request: WorkRequest) -> None:
        """
        Send the requested task to the consumer if the task has not already been sent to a consumer
        :param work_request: The details of the task and the consumer
        """
        pass

    @purevirtual
    @abstractmethod
    def _do_pub(self, current_interval: float) -> float:
        """
        Check for any pending tasks and advertise or re-advertise them on the relevant topic
        :param current_interval: The current activity interval
        :return: The optionally revised interval before the action is invoked again
        """
        pass

    @purevirtual
    @abstractmethod
    def _do_manage_presence(self, current_interval: float) -> float:
        """
        Ensure that we are known on the ether.
        :param current_interval: The current activity interval
        :return: The optionally revised interval before the action is invoked again
        """
        pass

    @purevirtual
    @abstractmethod
    def _do_work_finalise(
            self, work_notification_final: WorkNotificationFinalise) -> None:
        """
        Handle the event where a task is in terminal state with no work to do. Default is to notify the
        originator of the task that their work is done by forwarding the finalise notification.
        """
        pass

    @classmethod
    def topic_for_capability(cls, state: State) -> str:
        """
        The topic string on which tasks needing work in that state are published on
        :param state: The state for which the topic is required
        :return: The topic string for the given state
        """
        # TODO https://github.com/parrisma/AI-Intuition/issues/1
        return "topic-{}".format(str(state.id()))

    @property
    @abstractmethod
    @purevirtual
    def name(self) -> str:
        """
        The name of the task pool
        :return: The name of the task pool as string
        """
        pass

    @property
    def capabilities(self) -> List[Capability]:
        """
        The collection of capabilities of the SrcSink
        :return: The collection of capabilities
        """
        return self._capabilities

    def get_addressbook(self) -> List[SrcSink]:
        """
        The list of srcsinks known to the Ether
        :return: srcsinks
        """
        return self._address_book.get()

    def _update_addressbook(self, srcsink: SrcSink) -> None:
        """
        Update the given src_sink in the collection of registered srcsinks. If src_sink is not in the collection
        add it with a current time stamp.
        :param srcsink: The src_sink to update / add.
        """
        self._address_book.update(srcsink)
        return

    def _get_recent_ether_address(self) -> SrcSink:
        """
        Get a recent Ether address from the AddressBook. If there is no recent Ether then return None
        :return: Ether SrcSink or None
        """
        ss = self._address_book.get_with_capabilities(required_capabilities=[
            SimpleCapability(str(CapabilityRegister.ETHER))
        ],
                                                      max_age_in_seconds=60,
                                                      n=1)
        if ss is not None:
            ss = ss[0]
        return ss
Example #13
0
class Ether(SrcSink):
    ETHER_BACK_PLANE_TOPIC = "ether-back-plane"
    PUB_TIMER = float(.25)

    def __init__(self, ether_name: str):
        """
        Register the notification handlers and install publication activity.
            Note: Make sure properties name & topic are defined in the sub class before super().__inti__ is called
            otherwise the activity timer will reference non existent properties in the sub class as the timer
            will fire (here) before they have been defined in the sub-class init()
        """
        self._stopped = False
        self._address_book = AddressBook()
        super().__init__()
        self._call_lock = threading.Lock()
        self._handler = NotificationHandler(object_to_be_handler_for=self,
                                            throw_unhandled=False)
        self._handler.register_handler(self._do_srcsink_ping, SrcSinkPing)
        self._handler.register_handler(self._do_srcsink_ping_notification,
                                       SrcSinkPingNotification)
        return

    def __del__(self):
        self._handler.activity_state(paused=True)
        return

    def __call__(self, notification: Notification):
        """ Handle notification requests
        :param notification: The notification to be passed to the handler
        """
        if isinstance(notification, Notification):
            self._handler.call_handler(notification)
        else:
            raise ValueError(
                "{} un supported notification type for Task Pool".format(
                    type(notification).__name__))
        return

    def stop(self) -> None:
        self._handler.activity_state(paused=True)
        return

    def start(self) -> None:
        self._handler.activity_state(paused=False)
        return

    @classmethod
    def back_plane_topic(cls) -> str:
        """
        The back-plane topic to which all Ether objects subscribe
        :return:
        """
        return cls.ETHER_BACK_PLANE_TOPIC

    @purevirtual
    @abstractmethod
    def _do_pub(self) -> None:
        """
        Publish any changes to known src sinks in the ether
        """
        pass

    @property
    @purevirtual
    @abstractmethod
    def name(self) -> str:
        """
        The unique name of the SrcSink
        :return: The SrcSink name
        """
        pass

    @property
    @purevirtual
    @abstractmethod
    def topic(self) -> str:
        """
        The unique topic name that SrcSink listens on for activity specific to it.
        :return: The unique SrcSink listen topic name
        """
        pass

    def get_addressbook(self) -> List[SrcSink]:
        """
        The list of srcsinks known to the Ether
        :return: srcsinks
        """
        return self._address_book.get()

    def _update_addressbook(self, srcsink: SrcSink) -> None:
        """
        Update the given src_sink in the collection of registered srcsinks. If src_sink is not in the collection
        add it with a current time stamp.
        :param srcsink: The src_sink to update / add.
        """
        self._address_book.update(srcsink)
        return

    def _get_addresses_with_capabilities(
            self, required_capabilities: List[Capability]) -> List[SrcSink]:
        """
        Get the top five addresses in the address book that match the required capabilities. Always include
        this Ether (self) in the list.
        :param required_capabilities: The capabilities required
        :return: The list of matching addresses
        """
        addr = self._address_book.get_with_capabilities(
            required_capabilities=required_capabilities,
            match_threshold=float(1),
            max_age_in_seconds=600,
            n=5)
        if addr is None:
            addr = list()
        addr.append(self)
        return addr