Service Agents
The service agent framework in OSE provides request/reply and publish/subscribe features similar to that found in message oriented middleware packages. Unlike most of the available packages, the service agent framework does not have a flat namespace with respect to naming, but uses an object oriented model, with each service having its own namespace with respect to subject names for subscriptions and request method names.
Building on this object oriented approach, it is possible to subscribe to the existence of specific services, or to groups of services as well as aspects of the services themselves. By using subscription to groups, an application can be setup to dynamically handle the introduction and withdrawal of new services rather than being hardwired. Services are also able to monitor when subscriptions occur and identify who is making the subscription if necessary.
All the features of the service agent framework can be applied within the scope of a single process, or across a group of distributed processes. A specific service need not even be aware that a service it makes use of is in a remote process as the interface and means of interacting with that service are the same. Services may therefore be moved around between processes or onto different machines and the key parts of the application will not need to be changed.
As the Python interface is simply a wrapper on top of functionality provided by the OSE C++ class library, you are not restricted to writing service agents in just Python. In a distributed application for example, one process may be entirely written in C++, another may use only the Python wrappers, and a third a mix of both if dynamic loading into a Python program were used. This flexibility means you can use Python where simplicity is important, but C++ where better performance may be desirable.
The major classes in the OSE C++ class library involved in providing this functionality are the "OTC_SVBroker", "OTC_SVRegistry" and "OTC_EVAgent" classes along with various event classes. In a distributed application the "OTC_Exchange" class comes into play along with the various classes used to implement the interprocess communications mechanism.
Service Naming
When using the C++ class library, implementation of a service agent entails the use of a number of different classes together. In the Python interface this has all been brought together in the "Service" class. If you wish to create your own instance of a service agent, you need only derive a class from the "Service" class and then instantiate it.
The most important aspect of creating a service agent is the need to assign it a name. This name is what is used by other services to access your particular instance of a service agent. Having selected a name, it should be supplied to the "Service" base class at the point of initialisation. If you wished to call your service "alarm-monitor", the constructor of your class might look as follows.
1 class AlarmMonitor(netsvc.Service):
2 def __init__(self, name="alarm-monitor"):
3 netsvc.Service.__init__(self, name)
In general there is no restriction on what you can put in a service name. It is suggested though that you avoid any form of whitespace or non printable characters so as to make debugging easier.
In assigning a name to a service agent, there is nothing to stop you from having more than one service with the same name. Often the ability to have more than one service with the same name is useful, but in other situations it may be regarded as an error. As a policy on how to handle more than one service with the same name will be dependent on the actual application, implementation of any scheme to deal with it is left up to the user.
If you want to query what the service name is for an instance of a service agent, it can be queried using the "serviceName()" member function. If you need to know the unique identity of a service agent, it can be queried using the "agentIdentity()" member function. Even when two services share the same name, they will still have distinct agent identities. These as well as other details relating to a service agent can also be obtained from the object returned by the "serviceBinding()" member function.
Note that the "Service" class ultimately derives from the "Agent" class and as such all features of the event system are also accessible from a service agent. The "Service" class also builds on the same model used by the "Agent" class with respect to destruction of an object instance and the cleaning up of circular references. As such the "Service" class contains a derived implementation of the "destroyAgent()" member function found in the "Agent" class. Any derived service agent should use this function in the same way as defined for the "Agent" class.
Service Audience
When you create a service, the existance of that service will be broadcast to all connected processes. If you wish to restrict visibility of a service to just the process the service is contained in, or a subset of the connected processes, a service audience can be defined.
To define the service audience, an extra argument needs to be supplied to the "Service" base class when it is initialised. By default the service audience is "\*" to indicate that knowledge of the service should be broadcast as widely as possible. Setting the service audience to an empty string, will restrict visibility of the service to the local process.
1 class AlarmMonitor(netsvc.Service):
2 def __init__(self, name="alarm-monitor", audience="*"):
3 netsvc.Service.__init__(self, name, audience)
Other values can be supplied for the service audience and their meaning will depend upon how the interprocess communications links of the service agent framework are configured. This aspect of the service audience field will be discussed when support for distributed applications is covered.
Note that in setting the service audience, you are also restricting your service agent as far as what services it can subscribe to. If you set the service audience to that indicating the local process only, you will only be able to subscribe to services which exist in the local process. This is because services in remote processes will not know anything about you. If you need to be able to subscribe to services no matter where they are, you would generally be best leaving the service audience set to the default value.
Anonymous Service
Although referred to as a service, a service agent can act in the role of either a client or server. That is, as a client it is a user of other services and would not expect to have subscriptions made against it or receive requests. In this situation the name assigned to the service is immaterial and it is valid to supply an empty service name. In fact, if you do not explicitly supply a service name when initialising the "Service" base class, it will default to an empty string.
1 class AnonymousService(netsvc.Service):
2 def __init__(self):
3 netsvc.Service.__init__(self)
In general it is still preferable to supply a non empty value for the service name. Doing so will mean that the service agent will appear as a separate entity within any debugging tools and although the application itself may not need to use that service agent in the role of a server, you might still include functionality which can be used from debugging tools so you know what the service agent is doing.
Service Groups
When a service agent is created, the name of the service is notionally listed in a global group. In respect of this global group, unless you track the coming into existance of every single service agent, there is no way to make conclusions about a subset of services. Even if you do track the creation of every single service agent, the only way you might be able to distinguish a service agent as belonging to some group, is to introduce into the name of the service agent some form of artificial naming hierarchy.
Rather than rely on an artificial means of grouping service agents based on the service agent names, a separate concept of service groups is implemented. To add a service agent to a specific group, the "joinGroup()" member function can be called at any point after the "Service" base class has been initialised. That is, adding a service agent to a service group does not specifically have to been done in the constructor but can be done at a later time. To remove a service agent from a service group it has joined, the "leaveGroup()" member function can be called.
1 class EquipmentAgent(netsvc.Service):
2 def __init__(self, name, audience="*"):
3 netsvc.Service.__init__(self, name, audience)
4 self.joinGroup("equipment-agents")
As with service names, it is recommended that you avoid using any form of whitespace or unprintable characters in service group names. The empty service group should also not be used to avoid confusion with the global group.
Service Registry
The service registry is where information about available services is recorded. Each process in a distributed applicaton has its own service registry. The service registry in a process will list any services which are local to that process as well as any of which knowledge has been imported into the process from a remote process.
That each process has its own service registry means that the service agent framework can work quite happily within the context of a single process, as well as within the context of a distributed application. That is, when you only have a single process it isn't necessary for that process to be connected to a central server for the system to work. In this respect, each service registry acts as a peer to other service registries and not in a client/server mode.
A further consequence of this is that even when a process is part of a distributed application and the central message exchange process is terminated, any processes which were connected to it are not forced to restart themselves. In this scenario, any interested parties would be notified of the fact that remote services are no longer accessible and would take any action as appropiate. When the central message process is restarted, processes would automatically reconnect, with interested parties being notified that the remote services are once more accessible.
Any service agent may make queries against its local service registry and get back an immediate result which reflects the current state of the service registry. A service agent may also subscribe to the service registry or aspects of it and be notified in real time of changes made to the service registry. When subscribing to the service registry itself, a service agent would be notified of all available services, when those services join or leave groups and when those services are withdrawn.
Subscribing to the service registry as a whole is a useful debugging tool as it can produce an audit trail relating to the creation and deletion of services as well as group memberships. When used as a debugging tool as well as in other cases, it may not be appropriate that a service agent be created merely that the service registry can be queried. To this end, the member functions of the "Service" class relating to the service registry are also available through the "Monitor" class. In fact, the "Service" class derives from the "Monitor" class.
To setup a subscription against the service registry as a whole, the member function "subscribeRegistry()" is used. A subscription to the service registry can later be removed using the member function "unsubscribeRegistry()".
1 class RegistryMonitor(netsvc.Monitor):
2 def __init__(self):
3 netsvc.Monitor.__init__(self)
4 self.subscribeRegistry(self.announce)
5 def announce(self, binding, group, status):
6 if group == None:
7 # global group
8 action = "WITHDRAWN"
9 if status == netsvc.SERVICE_AVAILABLE:
10 action = "AVAILABLE"
11 name = binding.serviceName()
12 identity = binding.agentIdentity()
13 print "SERVICE-%s: %s (%s)" % (action, `name`, identity)
14 else:
15 # specific group
16 action = "LEAVE"
17 if status == netsvc.SERVICE_AVAILABLE:
18 action = "JOIN"
19 name = binding.serviceName()
20 identity = binding.agentIdentity()
21 print "%s-GROUP[%s]: %s (%s)" % \
(action, `group`, `name`, identity)
When making queries or subscriptions against the service registry, details of a specific service are returned in the form of a service binding object. This is the same type of object returned by the "serviceBinding()" member function of a specific service agent. Where an operation needs to refer to a particular service it will be usually done in terms of this service binding object rather than the information it carries.
Member functions of a service binding object which may prove useful include "serviceName()", "agentIdentity()", "serviceAudience()", "processAddress()" and "serviceLocation()". Of these, "serviceLocation()" returns either "SERVICE_LOCAL" or "SERVICE_REMOTE", giving an indication if the service is located in the same process or a remote process. The "processAddress()" member function will return an internal address relating to the actual process the service is located in.
Although the shorthand "agentIdentity()" member function provides a more readable value, the "serviceAddress()" member function is also provided and returns the internal address used to identify the service. Note though that if in a distributed application an intermediary process along the route to the actual service is restarted, when all processes reconnect, the service address will be different where as the process identity and agent identity would be the same. This reflects the fact that it is still the same service, but the route used to contact the service has now changed as the intemediary process was restarted.
When subscribing to the service registry as a whole, each notification will also include a group and status value. When the group is "None", the notification refers to either the availability or withdrawal of a service. For any other value of group, it indicates that a specific service is joining or leaving that group. Whether a service has become available or has been withdrawn, or similarly whether a service has joined or left a group is given by the status value. When the status is "SERVICE_AVAILABLE", a service has become available or has joined a group as appropriate. When the status value is "SERVICE_WITHDRAWN" the service has either been withdrawn or has left a group as appropriate.
Note that when the status indicates that a service has become available it doesn't mean that the service only just got created. In the case that a service is in a remote process, it may be the case that a service has existed for some time, but because the local process has only just connected into a distributed application it has only just become aware of that fact.
Similarly, when a service is withdrawn, if the service was in a remote process it means the service can no longer be contacted. This may have occurred because the service itself has been destroyed, the process in which the service existed has been destroyed or that an intemediary process involved in the communication path for contacting that process has been destroyed and the remote process is currently no longer contactable.
By subscribing to the service registry it is possible to receive in real time notitifications regarding the availability of services as such events happen. If you only wish to find out which services are available at a particular instant in time, you can use the "serviceAgents()" member function. Note that depending on the number of service agents available, calling this member function repetitively can incur significant overhead. If possible this member function should be used sparingly and a subscription against the service registry used instead.
Service Announcements
If a service agent subscribes to the registry using a specific service name, the service agent will be notified when any service with that name becomes available or is subsequently withdrawn. When subscribing to the registry using a specific service name, no notification is given regarding groups that those same services may join.
1 class ServiceMonitor(netsvc.Monitor):
2 def __init__(self, name):
3 netsvc.Monitor.__init__(self)
4 self.subscribeServiceName(self.announce, name)
5 def announce(self, binding, status):
6 action = "WITHDRAWN"
7 if status == netsvc.SERVICE_AVAILABLE:
8 action = "AVAILABLE"
9 name = binding.serviceName()
10 identity = binding.agentIdentity()
11 print "SERVICE-%s: %s (%s)" % (action, `name`, identity)
The name of the member function for subscribing to the existance of a service agent by name is "subscribeServiceName()". A subscription can be cancelled by calling the member function "unsubscribeServiceName()".
Having identified a particular service agent, it is often useful to know when that specific service agent is no longer available. The notifications provided when you call the member function "subscribeServiceName()" will tell you that, but if the service binding had been received through some other means and you weren't receiving the notifications, it is preferable that you be able to receive a notification just in relation to the specific service agent you are using. In this case, the service address can be obtained from the service binding by calling "serviceAddress()" and the member function "subscribeServiceAddress()" used instead. This subscription can be cancelled by calling the "unsubscribeServiceAddress()" member function.
1 class ClientService(netsvc.Service):
2 def __init__(self, binding):
3 netsvc.Service.__init__(self)
4 address = binding.serviceAddress()
5 self.subscribeServiceAddress(self.announce, address)
6 self._binding = binding
7 # start using service
8 def self.announce(self, binding, group, status):
9 if group == None:
10 if binding.agentIdentity() == self._binding.agentIdentity():
11 if status == netsvc.SERVICE_WITHDRAWN:
12 self.unsubscribeServiceAddress(binding.serviceAddress())
13 self._binding = None
14 # stop using service
If a service has already been withdrawn at the time the subscription by service address is performed, no subsequent notification will ever be received. To detect that a service may already have been withdrawn, the result of the "subscribeServiceAddress()" member function should be checked. If the service has already been withdrawn then "SERVICE_WITHDRAWN" will be returned, otherwise the result will be "SERVICE_AVAILABLE".
Group Announcements
If a service agent subscribes to the service registry using a specific service group, it will be notified when any service joins or leaves that group. Notice that a service has left a particular group will also be notified when the service is withdrawn and the service hadn't explicitly left the group before hand. The member functions relating to service group subscriptions are "subscribeServiceGroup()" and "unsubscribeServiceGroup()".
Subscription to a service group is most often used as a way of finding out what services exist which perform a certain function. As an example, service agents which provide an interface to equipment in a telecommunications network could join a particular group. A service which has the task of monitoring alarms generated by the same equipment could then subscribe to that service group and be notified about each equipment agent. Knowing about each equipment agent, the alarm monitor could then subscribe to any alarm reports generated by the equipment agents.
1 class EquipmentMonitor(netsvc.Service):
2 def __init__(self):
3 netsvc.Service.__init__(self, "equipment-monitor")
4 self.subscribeServiceGroup(self.announce, "equipment-agents")
5 def announce(self, binding, group, status):
6 if status == netsvc.SERVICE_AVAILABLE:
7 self.monitorReports(self.alarm, binding, "alarm.*")
8 else:
9 self.ignoreReports(binding)
10 def alarm(self, service, subject, content):
11 print subject, content
By using a service group it is therefore possible to make an application respond dynamically to the introduction of new service agents. In the case of the equipment alarm monitor for a telecommunications network, it would not be necessary to hardwire in details of the equipment. Instead, when adding a new piece of a equipment, the service agent providing an interface to that equipment need only add itself to the appropriate service group.
Such a mechanism could also be used to monitor alarms raised as a result of problems in the application itself and need not be alarms generated by some piece of equipment. This mechanism could therefore also be used as the basis of an application health monitoring system.
Service Lookup
The ability to subscribe to the service registry provides a means of tracking the existence of service agents over time. The alternative to subscribing to the service registry to find out about available services, is to do a lookup against the service registry. Performing a lookup will tell you immediately what service agents exist at that particular point in time. No subscription will be registered when doing a lookup though, so if you need to know when a service agent is subsequently withdrawn it still may be appropriate to subscribe to the registry using the address of the specific service agent you use.
1 class PollingService(netsvc.Service):
2 def __init__(self, name, period=60):
3 netsvc.Service.__init__(self)
4 self._name = name
5 self._period = period
6 self.initiateRequests("poll")
7 def initiateRequests(self, tag):
8 bindings = self.lookupServiceName(self._name)
9 for binding in bindings:
10 service = self.serviceEndPoint(binding)
11 # presume remote service provides uptime method
12 id = service.uptime()
13 self.processResponse(self.handleResult, id)
14 self.startTimer(self.initiateRequests, self._period, "poll")
15 def handleResult(self,result):
16 address = self.currentResponse().sender()
17 binding = self.lookupServiceAddress(address)
18 if binding != None:
19 print binding.agentIdentity(), result
A number of different types of lookup can be made against the service registry. The first two allow you to lookup all service agents which have a particular service name, or all service agents which are currently a member of a specific service group. The two member functions corresponding to these lookups are "lookupServiceName()" and "lookupServiceGroup()". Both these lookup functions return a list of service binding objects corresponding to the service agents found. If there are no service agents matching the search criteria, an empty list is returned.
The third type of lookup is that of looking up a specific service agent using its service address. In this case you will need to have been able to obtain the service address by some other means first. The member function here is "lookupServiceAddress()". The result will be the service binding object corresponding to that service agent, or "None" if the service agent is no longer available.
In order to obtain a list of all services known of by the service registry, the member function "serviceAgents()" can be used. This should however be used sparingly because of the overhead which might be incurred when there are large numbers of services. If possible, subscription against the service registry should still be used if it is necessary to track all available services. Overhead can be reduced by using subscription and caching the results as Python data structures, with Python objects accessing the cache directly. This avoids the translation from C++ data structures to Python data structures.
Similarly to service agents, a list of all service groups can be obtained by calling the member function "serviceGroups()". If it is necessary to determine which service groups a particular service agent is a member of, an optional argument can be supplied to "serviceGroups()", that argument being the service address of the service agent of interest.
