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;
$ cd <folder-of-repos>
$ 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
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.
$ 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 ansar signup. Passwords should contain at least 12 characters and include alphas, digits and at least one symbol.
ansar signup - create an online account,
ansar account - display all available information about the new account,
ansar directory - request an access file for the specified directory,
ansar network - connect the local ansar-group to the cloud.
The ansar directory command requests the creation of an access file. The directory-id
is copy/pasted from the output of the 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;
$ 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;
$ 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 ansar signup
command should be replaced with the 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 ansar directory command. By default, only a small number
of access files can be created for any one directory.
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.
import ansar.connect as ar
class INITIAL: pass
class READY: pass
class WAITING: pass
class EATING: pass
class THINKING: pass
# The shared resource.
class Fork(object):
def __init__(self):
pass
ar.bind(Fork)
# Resource at rest.
class Holder(ar.Point, ar.StateMachine):
def __init__(self):
ar.Point.__init__(self)
ar.StateMachine.__init__(self, INITIAL)
self.fork = None
self.enquired = ar.deque()
def Holder_INITIAL_Start(self, message):
# Start with the fork down.
self.fork = Fork()
return READY
def Holder_READY_Enquiry(self, message):
# Someone is hungry. Add them to the tail of the queue.
self.enquired.append(self.return_address)
if self.fork:
a = self.enquired.popleft() # Waiting the longest.
self.send(self.fork, a)
self.fork = None
return READY
def Holder_READY_Fork(self, message):
# Fork is being returned. Is someone waiting?
if self.enquired:
a = self.enquired.popleft()
self.send(message, a)
return READY
self.fork = message
return READY
HOLDER_DISPATCH = {
INITIAL: (
(ar.Start,), ()
),
READY: (
(Fork, ar.Enquiry), ()
),
}
ar.bind(Holder, HOLDER_DISPATCH)
#
#
class Philosopher(ar.Threaded, ar.StateMachine):
def __init__(self, name, left_holder, right_holder):
ar.Threaded.__init__(self)
ar.StateMachine.__init__(self, INITIAL)
self.name = name
self.left_holder = left_holder
self.right_holder = right_holder
# Start with no forks.
self.holding = ar.deque()
self.retry = None
self.mark = None
def next(self):
# Introduce variation in eating/thinking times.
if self.retry is None:
intervals = ar.RetryIntervals(regular_steps=2.0, randomized=0.25, truncated=0.5)
self.retry = ar.smart_intervals(intervals)
p = next(self.retry)
return p
def Philosopher_INITIAL_Start(self, message):
# Hungry. Wait for both forks.
self.send(ar.Enquiry(), self.left_holder)
self.send(ar.Enquiry(), self.right_holder)
self.mark = ar.clock_now()
self.console(f'"{self.name}" is WAITING')
return WAITING
def Philosopher_WAITING_Fork(self, message):
# A fork has been relinquished. Got a pair?
self.holding.append(message)
if len(self.holding) == 2:
span = ar.clock_now() - self.mark
d = ar.span_to_text(span)
self.console(f'"{self.name}" is EATING (waited {d})')
self.start(ar.T1, self.next())
return EATING
return WAITING
def Philosopher_EATING_T1(self, message):
# Enough for a while. Put forks down.
self.send(self.holding.pop(), self.left_holder)
self.send(self.holding.pop(), self.right_holder)
self.start(ar.T2, self.next())
self.console(f'"{self.name}" is THINKING')
return THINKING
def Philosopher_THINKING_T2(self, message):
# Hungry again.
self.send(ar.Enquiry(), self.left_holder)
self.send(ar.Enquiry(), self.right_holder)
self.mark = ar.clock_now()
self.console(f'"{self.name}" is WAITING')
return WAITING
PHILOSOPHER_DISPATCH = {
INITIAL: (
(ar.Start,), ()
),
WAITING: (
(Fork,), ()
),
EATING: (
(ar.T1,), ()
),
THINKING: (
(ar.T2,), ()
),
}
ar.bind(Philosopher, PHILOSOPHER_DISPATCH)
#
#
philosopher = [
"Socrates",
"Descartes",
"Aristotle",
"Nietzsche",
"Sartre",
"Amdahl",
"Seneca",
"Epicurus",
"Aurelius",
"Confucius",
"Dante",
"Pascale",
"Voltaire",
"Schopenhauer",
]
def main(self):
# Set the table. One holder/fork between each
# seated philosopher.
holder = [self.create(Holder) for p in philosopher]
# Seat the 1st, 3rd, ... etc.
i = 0
left_holder = holder[-1] # Wrap back to the last holder/fork.
for h, p in zip(holder, philosopher):
if i % 2 == 0:
a = self.create(Philosopher, p, left_holder, h)
left_holder = h
i += 1
# Seat the 2nd, 4th, ... etc.
i = 0
left_holder = holder[-1]
for h, p in zip(holder, philosopher):
if i % 2 == 1:
a = self.create(Philosopher, p, left_holder, h)
left_holder = h
i += 1
# Wait for control-c.
self.select(ar.Stop)
ar.bind(main)
#
#
if __name__ == '__main__':
ar.create_object(main)
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 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 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;
$ 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
Addresses are a crucial part of ansar-connect. They are inherited from ansar-create which established
the create() and 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 objectself.parent_address- address of the object that created this objectself.return_address- address of the object that sent the current message
During every 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 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;
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
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;
$ 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 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;
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;
$ 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 Enquiry is expected as a request and
an Ack is sent in response. A partial listing of the front-end looks like this;
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 listen() for clients and connect() to the db. To run the front-end use the following
commands in a second terminal;
$ 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;
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);
$ 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;
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;
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 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 here. As-is, the collection of demo modules are start-order dependent. Note also that most production-quality services will be implemented as 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;
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 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.