.. _odds-and-ends: Odds And Ends ************* This section presents different applications of **ansar-connect**. They are intended to demonstrate aspects of the library that might otherwise escape attention. Download materials supporting this guide and setup the environment with the following commands; .. code:: $ cd $ git clone https://github.com/mr-ansar/odds-and-ends.git $ cd odds-and-ends $ python3 -m venv .env $ source .env/bin/activate $ pip3 install ansar-connect pyinstaller .. _connecting-to-anywhere: Connecting To Anywhere ====================== The publish-and-subscribe networking built into **ansar-connect** extends to the exchange of messages across the Internet, i.e. WAN messaging. A client can initiate connection to a server that may be anywhere in the world, potentially reaching outside the LAN, across different ISPs (e.g. cellular providers) and into the server's LAN. It does this using the **ansar-wan** service - a short, one-time configuration is required. A typical use of WAN messaging will involve a broader configuration that includes services such as **ansar-host** and **ansar-lan**. However, it is possible to omit these intermediate levels and just run communications at the WAN level. The benefit of doing so is universal connect-ability. It doesn't matter where the two ends are located or whether the two ends are moving between different locations, the **ansar-wan** service will make the necessary associations. A service on an office desktop will always be available to a client on a laptop, as that laptop roams around the world. The downside to WAN-level communications is reduced throughput and this reduction will be experienced in all scenarios, i.e. even where the client and server are connected to the same LAN. The ``odds-and-ends`` repo inclues a server and a client suitable for demonstration of WAN-level messaging. To prepare for a messaging session, use the following commands substituting your own details for fields such as company name and the ``directory-id``. .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ ansar signup .. $ ansar account --show-identities + Account Acme Ltd (bdd583b6-c593-4274-a0c9-0ab9102e9427) + + Number of logins: 4 + + Number of directores: 4 + + Number of relays: 32 + + Logins (1) + + + Err (52518971-2f52-4230-80f9-06880150eaff) + + + + Login email: e.flynn@gmail.com + + + + Family name: Flynn + + + + Given name: Errol + + + + Nick name: Err + + + + Honorific: Mr + + Directories (1) + + + Ansar Networking/TESTING (f900ab49-5e36-4c00-a1b9-f8f373ec997e) + + + + Number of tokens: 4 + + + + Connected routes: 64 + + + + Messages per second: 8192 + + + + Bytes per second: 65536 + + + + Exported token (0) $ ansar directory --directory-id=f900ab49-5e36-4c00-a1b9-f8f373ec997e --export --access-name=anywhere --export-file=anywhere.access $ make anywhere pyinstaller --onefile --hidden-import=_cffi_backend --log-level ERROR -p . anywhere-server.py pyinstaller --onefile --hidden-import=_cffi_backend --log-level ERROR -p . anywhere-client.py ansar create ansar deploy dist ansar add anywhere-server server ansar add anywhere-client client ansar network server --connect-scope=GROUP --connect-file=anywhere.access ansar network client --connect-scope=GROUP --connect-file=anywhere.access $ ansar network server + WAN Ansar Networking/TESTING + GROUP 127.0.0.1:38197 $ Details within the output will vary according to the information supplied to :ref:`ansar signup `. Passwords should contain at least 12 characters and include alphas, digits and at least one symbol. * :ref:`ansar signup ` - create an online account, * :ref:`ansar account ` - display all available information about the new account, * :ref:`ansar directory ` - request an access file for the specified directory, * :ref:`ansar network ` - connect the local **ansar-group** to the cloud. The :ref:`ansar directory ` command requests the creation of an access file. The ``directory-id`` is copy/pasted from the output of the :ref:`ansar account ` command and the result is a file called ``anywhere.access``. This file contains the information needed to make a runtime connection to **ansar-wan** and should be treated as security sensitive. Passing ``anywhere`` as the make target causes the creation of the ``.ansar-home`` folder and the configuraton of all the details needed for sessions of the server and the client. To run the server use the make command; .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ make run-server This will produce a few screens of DEBUG logs. Starting the client from the same folder (perhaps in a different terminal) looks like this; .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ make run-client ansar --debug-level=CONSOLE run client --group-name=client --main-role=client 03:45:16.868 ^ <00000010>ansar - Detect status of associated roles (group.client) 03:45:16.873 ^ <00000010>ansar - Detect status of associated roles (client) 03:45:16.878 ^ <00000010>ansar - Running "client" (group client) at "/home/buster/odds-and-ends/.ansar-home", searched for "client" 03:45:19.797 ^ <00000013>anywhere_client - Acked after 0.399007s 03:45:20.198 ^ <00000013>anywhere_client - Acked after 0.400376s 03:45:20.596 ^ <00000013>anywhere_client - Acked after 0.398219s 03:45:20.994 ^ <00000013>anywhere_client - Acked after 0.397961s 03:45:21.396 ^ <00000013>anywhere_client - Acked after 0.401867s 03:45:21.795 ^ <00000013>anywhere_client - Acked after 0.398983s 03:45:22.194 ^ <00000013>anywhere_client - Acked after 0.398536s 03:45:22.593 ^ <00000013>anywhere_client - Acked after 0.398638s 03:45:22.991 ^ <00000013>anywhere_client - Acked after 0.398312s 03:45:23.390 ^ <00000013>anywhere_client - Acked after 0.3988s ^ansar: aborted (user or software interrupt) The client successfully connects to the server and runs an endless sequence of timed, request-response messages. A control-c is used to terminate both the client and the server. The process to create a running instance of the client can be repeated on different hosts. The :ref:`ansar signup ` command should be replaced with the :ref:`ansar login ` command. Each new instance of the client will create another session with the server. Either the same ``anywhere.access`` file is copied to each new host or a new access file is created each time, using the :ref:`ansar directory ` command. By default, only a small number of access files can be created for any one directory. .. _dining-philosophers: Dining Philosophers =================== The dining philosphers problem is about resource contention. The solution appearing in this section is based on an article in `Microsoft Learn `_ that adopted the actor, or agent, model of operation to avoid the use of multi-threading primitives such as semaphores. It has been rewritten here using the messaging primitives within **ansar-connect** that largely come from SDL, a model of operation that has similarities to the actor model. Most importantly, both are about message-passing and both represent a means of eliminating multi-threading primitives from the respective solutions. The problem is summarized below. For a better definition look `here `_; * there are a group of philosophers coming to dinner, * they will be seated at a round table, * there will be forks placed between the philosophers, * a philosopher needs 2 forks to eat, * philosophers are either waiting, eating or thinking. Each philosopher is forced to share their forks with the philosophers to their left and right. .. literalinclude:: include/dining-philosophers.py :language: python .. note:: This version includes an optimization - the odd philosophers are seated before the evens. This gives the first group the opportunity to start eating immediately. Taking that optimization out reduces the number of dinners eating at any one time. Both arrangements avoid the problem of deadlocks. Both the philosophers and the holders (where the forks are at rest), are implemented as finite-state machines. The philosophers send :class:`~.lifecycle.Enquiry` messages to the holders to express their desire to eat. The holders send forks to the philospher who has been waiting the longest, as their fork becomes available. As a demonstration of capability, the philosophers are each assigned their own platform thread (note the use of :class:`~.point.Threaded`). The inheritance list can equally be switched to match that of the holders without any observable effect. To start the party, use the following commands; .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ python3 dining-philosophers.py --debug-level=CONSOLE 22:16:56.451 ^ <0000001f>Philosopher[INITIAL] - "Socrates" is WAITING 22:16:56.451 ^ <00000020>Philosopher[INITIAL] - "Aristotle" is WAITING 22:16:56.451 ^ <0000001f>Philosopher[WAITING] - "Socrates" is EATING (waited 0.000262s) 22:16:56.451 ^ <00000021>Philosopher[INITIAL] - "Sartre" is WAITING 22:16:56.451 ^ <00000021>Philosopher[WAITING] - "Sartre" is EATING (waited 0.000205s) 22:16:56.451 ^ <00000022>Philosopher[INITIAL] - "Seneca" is WAITING 22:16:56.451 ^ <00000020>Philosopher[WAITING] - "Aristotle" is EATING (waited 0.000487s) 22:16:56.452 ^ <00000023>Philosopher[INITIAL] - "Aurelius" is WAITING 22:16:56.452 ^ <00000022>Philosopher[WAITING] - "Seneca" is EATING (waited 0.000271s) 22:16:56.452 ^ <00000023>Philosopher[WAITING] - "Aurelius" is EATING (waited 0.000232s) 22:16:56.452 ^ <00000024>Philosopher[INITIAL] - "Dante" is WAITING 22:16:56.452 ^ <00000024>Philosopher[WAITING] - "Dante" is EATING (waited 0.000177s) 22:16:56.452 ^ <00000025>Philosopher[INITIAL] - "Voltaire" is WAITING 22:16:56.452 ^ <00000025>Philosopher[WAITING] - "Voltaire" is EATING (waited 0.000187s) 22:16:56.452 ^ <00000026>Philosopher[INITIAL] - "Descartes" is WAITING 22:16:56.453 ^ <00000027>Philosopher[INITIAL] - "Nietzsche" is WAITING 22:16:56.453 ^ <00000028>Philosopher[INITIAL] - "Amdahl" is WAITING 22:16:56.453 ^ <00000029>Philosopher[INITIAL] - "Epicurus" is WAITING .. The solution is presented here to illustrate the potential of message-based programming. Writing a solution from scratch, using multi-threading primitives is probably a good way to brush up on your multi-threading skills. It's also likely to be a longer and more painful development than the **ansar-connect** version. A case can be made that the message-passing solutions are better articulations of the problem. At the source level it is easier to see the flow of execution. Code that is more readable is easier to maintain. It can also be said that the **ansar-connect** version is more concise than the C/C++ version referenced above. Perhaps the actor model of execution sits more comfortably within Python syntax than C/C++ syntax. .. _fun-with-addresses: Fun With Addresses ================== Addresses are a crucial part of **ansar-connect**. They are inherited from **ansar-create** which established the :meth:`~.Point.create` and :meth:`~.Point.send` methods for the creation of objects and the transfer of messages between those objects. Addresses in **ansar-connect** refer to objects within a host process - there is no awareness of network addresses such as "127.0.0.1". Spoofing The Source Of Messages +++++++++++++++++++++++++++++++ Every ansar object has three addresses available to it; * ``self.address`` - *address of this object* * ``self.parent_address`` - *address of the object that created this object* * ``self.return_address`` - *address of the object that sent the current message* During every :meth:`~.Point.send` the ``self.address`` value is discreetly included in the transfer and at the receiving end, it is used to discreetly update the ``self.return_address`` value. This is the mechanism by which ansar objects are always able to respond to the correct sender. The :meth:`~.Point.forward` method allows for the spoofing of the source address. The address passed as the ``return_address`` is used instead of the ``self.address`` value, causing any response messages to be sent to the specified address. This is used quite commonly to delegate work (e.g. the processing of a job message) to another object. By using forwarding the item of work is transferred along with the address of the requesting party, as a single operation. Consider the following code; .. code:: python import ansar.connect as ar def job_processor(self, **kw): while True: m = self.select(ar.Enquiry, ar.Stop) if isinstance(m, ar.Enquiry): # Request self.reply(ar.Ack()) else: return ar.Aborted() ar.bind(job_processor) def job_done(self): while True: m = self.select(ar.Ack, ar.Stop) if isinstance(m, ar.Stop): return ar.Aborted() self.console(f'Done') ar.bind(job_done) # # def main(self): j = self.create(job_processor) d = self.create(job_done) self.send(ar.Enquiry(), j) m = self.select(ar.Ack, ar.Stop) if isinstance(m, ar.Stop): return ar.Aborted() self.forward(ar.Enquiry(), j, d) self.send(ar.Enquiry(), j) m = self.select(ar.Ack, ar.Stop) if isinstance(m, ar.Stop): return ar.Aborted() return None ar.bind(main) if __name__ == '__main__': ar.create_object(main) Two objects are created - a ``job_processor`` and a ``job_done``. The first request is sent to the processor and there is a call to :meth:`~.Buffering.select` to receive the expected response. The second request is *forwarded* to the processor on behalf of the second object. The reply in the processor returns the response to that second object, which simply notes the completion of work. Having forwarded the work item the ``main`` routine plays no further role in the completion of work. To avoid a termination of the application before the second request has been processed, the ``main`` routine places a third request with the processor and waits for the response. To see the delegation in action, use the following commands; .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ python3 forwarding.py --debug-level=CONSOLE Delegating Work To Other Processes ++++++++++++++++++++++++++++++++++ Delegation of requests works particularly well in a multi-processing scenario. Consider a front-end processor that accepts connections from clients and maintains connections to various back-end processes including a database server. Requests can be received, authorized and forwarded to the proper back-end process. The front-end processor is immediately free to focus on the next request. Responses are returned to the proper client. For this to work there needs to be some fancy handling of messages. Consider the moment the database server completes a request and calls :meth:`~.point.reply`. The original client - upstream of the front-end processor - is *two processes away*. The response needs to travel back over the connection to the front-end processor, and then over the second connection to the original client. Fortunately **ansar-connect** understands these scenarios and performs a *relay* function that bounces messages on to the next process, without requiring any deliberate attention from the host process. A demo-only database server looks like this; .. code:: python import ansar.connect as ar # # def db(self, settings): ar.listen(self, settings.listening_ipp) m = self.select(ar.Listening, ar.NotListening, ar.Stop) if isinstance(m, ar.NotListening): return m elif isinstance(m, ar.Stop): return ar.Aborted() while True: m = self.select(ar.Enquiry, # A request. ar.Accepted, # New connection. ar.Abandoned, ar.Closed, # Lost existing connection. ar.Stop) # Control-c. if isinstance(m, ar.Accepted): continue elif isinstance(m, (ar.Abandoned, ar.Closed)): continue elif isinstance(m, ar.Stop): return ar.Aborted() self.reply(ar.Ack()) ar.bind(db) # Configuration for this executable. class Settings(object): def __init__(self, listening_ipp=None): self.listening_ipp = listening_ipp or ar.HostPort() SETTINGS_SCHEMA = { 'listening_ipp': ar.UserDefined(ar.HostPort), } ar.bind(Settings, object_schema=SETTINGS_SCHEMA) # Initial values. factory_settings = Settings(listening_ipp=ar.HostPort('127.0.0.1', 30121)) if __name__ == '__main__': ar.create_object(db, factory_settings=factory_settings) Run this server with the following commands; .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ python3 db-server.py --debug-level=DEBUG This establishes the ``db`` service at "127.0.0.1:30121". An :class:`~.lifecycle.Enquiry` is expected as a request and an :class:`~.lifecycle.Ack` is sent in response. A partial listing of the front-end looks like this; .. code:: python def front_end(self, settings): ar.listen(self, settings.listening_ipp) m = self.select(ar.Listening, ar.NotListening, ar.Stop) if isinstance(m, ar.NotListening): return m elif isinstance(m, ar.Stop): return ar.Aborted() ar.connect(self, settings.connecting_ipp) m = self.select(ar.Connected, ar.NotConnected, ar.Stop) if isinstance(m, ar.NotConnected): return m elif isinstance(m, ar.Stop): return ar.Aborted() db = self.return_address while True: m = self.select(ar.Enquiry, # A request. ar.Accepted, # New connection. ar.Abandoned, ar.Closed, # Lost existing connection. ar.Stop) # Control-c. if isinstance(m, ar.Accepted): continue elif isinstance(m, (ar.Abandoned, ar.Closed)): if self.return_address == db: return m continue elif isinstance(m, ar.Stop): return ar.Aborted() self.forward(m, db, self.return_address) ar.bind(front_end) The front-end must :func:`~.listen` for clients and :func:`~.connect` to the ``db``. To run the front-end use the following commands in a second terminal; .. code:: $ cd <../odds-and-ends> $ source .env/bin/activate $ python3 front-end.py --debug-level=DEBUG The ``front_end`` service is established at "127.0.0.1:30122". Finally there is the client that simply makes a request and expects a response; .. code:: python def client(self, settings): ar.connect(self, settings.connecting_ipp) m = self.select(ar.Connected, ar.NotConnected, ar.Stop) if isinstance(m, ar.NotConnected): return m elif isinstance(m, ar.Stop): return ar.Aborted() front_end = self.return_address self.send(ar.Enquiry(), front_end) m = self.select(ar.Ack, # A response. ar.Abandoned, ar.Closed, # Lost connection. ar.Stop) # Control-c. if isinstance(m, (ar.Abandoned, ar.Closed)): return m elif isinstance(m, ar.Stop): return ar.Aborted() return m ar.bind(client) Running the client from a third terminal produces the following logs (edited); .. code-block:: :emphasize-lines: 11,14 $ cd <../odds-and-ends> $ source .env/bin/activate $ python3 client.py -dl=DEBUG .. 21:51:32.002 + <00000010>client - Created by <0000000f> 21:51:32.003 + <00000011>SocketProxy[INITIAL] - Created by <0000000d> 21:51:32.003 ~ <0000000d>SocketSelect - Connected to "127.0.0.1:30122", at local address "127.0.0.1:59320" 21:51:32.003 > <0000000d>SocketSelect - Forward Connected to <00000010> (from <00000011>) 21:51:32.003 < <00000011>SocketProxy[INITIAL] - Received Start from <0000000d> 21:51:32.003 < <00000010>client - Received Connected from <00000011> 21:51:32.003 > <00000010>client - Sent Enquiry to <00000011> 21:51:32.003 < <00000011>SocketProxy[NORMAL] - Received Enquiry from <00000010> 21:51:32.004 > <0000000d>SocketSelect - Forward Ack to <00000010> (from <00000011>) 21:51:32.004 < <00000010>client - Received Ack from <00000011> 21:51:32.004 X <00000010>client - Destroyed .. $ Logs show the ``client`` sending the request and receiving the response. From this point of view it is a trivial request-response sequence. The interesting activity is occurring elsewhere. Here are the pertinent logs from the front-end; .. code-block:: :emphasize-lines: 6,7,9,10 21:51:32.003 ~ <0000000d>SocketSelect - Accepted "127.0.0.1:59320", requested "127.0.0.1:30122" 21:51:32.003 > <0000000d>SocketSelect - Forward Accepted to <00000010> (from <00000017>) 21:51:32.003 < <00000010>front_end - Received Accepted from <00000017> 21:51:32.003 < <00000017>SocketProxy[INITIAL] - Received Start from <0000000d> 21:51:32.003 > <0000000d>SocketSelect - Forward Enquiry to <00000010> (from <00000017>) 21:51:32.003 < <00000010>front_end - Received Enquiry from <00000017> 21:51:32.003 > <00000010>front_end - Forward Enquiry to <00000011> (from <00000017>) 21:51:32.003 < <00000011>SocketProxy[NORMAL] - Received Enquiry from <00000017> 21:51:32.004 > <0000000d>SocketSelect - Forward Relay to <00000017> (from <00000011>) 21:51:32.004 < <00000017>SocketProxy[NORMAL] - Received Relay from <00000011> 21:51:32.105 > <0000000d>SocketSelect - Sent Stop to <00000017> 21:51:32.105 > <0000000d>SocketSelect - Forward Abandoned to <00000010> (from <00000017>) And the db-server; .. code-block:: :emphasize-lines: 2,3 21:51:32.004 > <0000000d>SocketSelect - Forward Enquiry to <00000010> (from <00000013>) 21:51:32.004 < <00000010>db - Received Enquiry from <00000013> 21:51:32.004 > <00000010>db - Sent Ack to <00000013> Now it is possible to see the :class:`~.lifecycle.Enquiry` being received and forwarded in the ``front_end``, received and responded to in the ``db``, relayed in the ``front_end`` and finally, received in the ``client``. .. note:: Auto-connection should be implemented in the front-end and client modules. A technique is provided in the library and documented :ref:`here `. As-is, the collection of demo modules are start-order dependent. Note also that most production-quality services will be implemented as :ref:`finite-state machines `. Addresses Are Portable ++++++++++++++++++++++ The delegation idiom is a clear use case for a more general capability in **ansar-connect**. Stated as concisely as possible - ansar addresses are *portable*. An address that travels across network transports retains its proper operational significance. Messages sent to such addresses find their way back to the appropriate object. Within the delegation idiom this property of addresses is leveraged in an *implicit* way. Portability of addresses is used extensively in the publish-subscribe section of **ansar-connect**, in an *explicit* way. Consider the following message declarations; .. code:: python class ServiceListing(object): def __init__(self, requested_name=None, address=None, requested_scope=ScopeOfService.WAN, listing_id=None): self.requested_name = requested_name self.address = address self.requested_scope = requested_scope self.listing_id = listing_id class FindService(object): def __init__(self, requested_search=None, address=None, requested_scope=ScopeOfService.WAN): self.requested_search = requested_search self.address = address self.requested_scope = requested_scope class PushedDirectory(object): def __init__(self, listing=None, find=None): self.listing = listing or ar.default_vector() self.find = find or ar.default_vector() def empty(self): if len(self.listing) == 0 and len(self.find) == 0: return True return False SERVICE_FIND_SCHEMA = { 'requested_name': str, 'requested_search': str, 'requested_scope': ScopeOfService, 'listing_id': ar.UUID(), 'address': ar.Address(), } ar.bind(ServiceListing, object_schema=SERVICE_FIND_SCHEMA) ar.bind(FindService, object_schema=SERVICE_FIND_SCHEMA) PUSHED_DIRECTORY_SCHEMA = { 'listing': ar.VectorOf(ar.UserDefined(ServiceListing)), 'find': ar.VectorOf(ar.UserDefined(FindService)), } ar.bind(PushedDirectory, object_schema=PUSHED_DIRECTORY_SCHEMA) These are simplified versions of the messages that are passed around by services such as **ansar-host** and **ansar-lan**. The ``address`` members of the ``ServiceListing`` and ``FindService`` classes are declared as type :class:`~.encode.portable.Address`. A ``PushedDirectory`` message is effectively a phonebook of publishers and subscribers. Wherever it goes within the *connection graph* of the publish-subscribe services, the ``address`` member can be used to start a conversation with the referenced object. In the normal operation of the publish-subscribe services that can mean a journey through half a dozen processes. .. note:: A *connection graph* is a collection of processes connected by network transports. This may be a central service process with a set of connecting client processes, or it may be a long chain of workflow processes. Ansar addresses that have travelled over network connections rely on the presence of those same connections to make the return journey. The loss of any relevant connection renders associated addresses null and void. A recovered connection does not restore the status of associated addresses.