Imagine an application architecture in which 10 lines of Perl or Python, using
the data types native to each language, are enough to implement a method that
can then be deployed and invoked seamlessly across hundreds of servers. You
have just imagined developing with OpenSRF – it is truly that simple. Under the
covers, of course, the OpenSRF language bindings do an incredible amount of
work on behalf of the developer. An OpenSRF application consists of one or more
OpenSRF services that expose methods: for example, the opensrf.simple-text
demonstration
service exposes the opensrf.simple-text.split()
and
opensrf.simple-text.reverse()
methods. Each method accepts zero or more
arguments and returns zero or one results. The data types supported by OpenSRF
arguments and results are typical core language data types: strings, numbers,
booleans, arrays, and hashes.
To implement a new OpenSRF service, perform the following steps:
For example, the following code implements an OpenSRF service. The service
includes one method named opensrf.simple-text.reverse()
that accepts one
string as input and returns the reversed version of that string:
#!/usr/bin/perl package OpenSRF::Application::Demo::SimpleText; use strict; use OpenSRF::Application; use parent qw/OpenSRF::Application/; sub text_reverse { my ($self , $conn, $text) = @_; my $reversed_text = scalar reverse($text); return $reversed_text; } __PACKAGE__->register_method( method => 'text_reverse', api_name => 'opensrf.simple-text.reverse' );
Ten lines of code, and we have a complete OpenSRF service that exposes a single
method and could be deployed quickly on a cluster of servers to meet your
application’s ravenous demand for reversed strings! If you’re unfamiliar with
Perl, the use OpenSRF::Application; use parent qw/OpenSRF::Application/;
lines tell this package to inherit methods and properties from the
OpenSRF::Application
module. For example, the call to
__PACKAGE__->register_method()
is defined in OpenSRF::Application
but due to
inheritance is available in this package (named by the special Perl symbol
__PACKAGE__
that contains the current package name). The register_method()
procedure is how we introduce a method to the rest of the OpenSRF world.
Two files control most of the configuration for OpenSRF:
opensrf.xml
contains the configuration for the service itself as well as
a list of which application servers in your OpenSRF cluster should start
the service
opensrf_core.xml
(often referred to as the "bootstrap configuration"
file) contains the OpenSRF networking information, including the XMPP server
connection credentials for the public and private routers; you only need to touch
this for a new service if the new service needs to be accessible via the
public router
Begin by defining the service itself in opensrf.xml
. To register the
opensrf.simple-text
service, add the following section to the <apps>
element (corresponding to the XPath /opensrf/default/apps/
):
<apps> <opensrf.simple-text> <!-- --> <keepalive>3</keepalive> <!-- --> <stateless>1</stateless> <!-- --> <language>perl</language> <!-- --> <implementation>OpenSRF::Application::Demo::SimpleText</implementation> <!-- --> <max_requests>100</max_requests> <!-- --> <unix_config> <max_requests>1000</max_requests> <!-- --> <unix_log>opensrf.simple-text_unix.log</unix_log> <!-- --> <unix_sock>opensrf.simple-text_unix.sock</unix_sock> <!-- --> <unix_pid>opensrf.simple-text_unix.pid</unix_pid> <!-- --> <min_children>5</min_children> <!-- --> <max_children>15</max_children> <!-- --> <min_spare_children>2</min_spare_children> <!-- --> <max_spare_children>5</max_spare_children> <!-- --> </unix_config> </opensrf.simple-text> <!-- other OpenSRF services registered here... --> </apps>
The element name is the name that the OpenSRF control scripts use to refer to the service. | |
Specifies the interval (in seconds) between checks to determine if the service is still running. | |
Specifies whether OpenSRF clients can call methods from this service
without first having to create a connection to a specific service backend
process for that service. If the value is | |
Specifies the programming language in which the service is implemented | |
Specifies the name of the library or module in which the service is implemented | |
(C implementations): Specifies the maximum number of requests a process serves before it is killed and replaced by a new process. | |
(Perl implementations): Specifies the maximum number of requests a process serves before it is killed and replaced by a new process. | |
The name of the log file for language-specific log messages such as syntax warnings. | |
The name of the UNIX socket used for inter-process communications. | |
The name of the PID file for the master process for the service. | |
The minimum number of child processes that should be running at any given time. | |
The maximum number of child processes that should be running at any given time. | |
The minimum number of child processes that should be available to handle incoming requests. If there are fewer than this number of spare child processes, new processes will be spawned. | |
The maximum number of child processes that should be available to handle incoming requests. If there are more than this number of spare child processes, the extra processes will be killed. |
To make the service accessible via the public router, you must also
edit the opensrf_core.xml
configuration file to add the service to the list
of publicly accessible services:
Making a service publicly accessible in opensrf_core.xml
.
<router> <!-- --> <!-- This is the public router. On this router, we only register applications which should be accessible to everyone on the opensrf network --> <name>router</name> <domain>public.localhost</domain> <!-- --> <services> <service>opensrf.math</service> <service>opensrf.simple-text</service> <!-- --> </services> </router>
This section of the | |
| |
Each |
Once you have defined the new service, you must restart the OpenSRF Router to retrieve the new configuration and start or restart the service itself.
OpenSRF clients in any supported language can invoke OpenSRF services in any
supported language. So let’s see a few examples of how we can call our fancy
new opensrf.simple-text.reverse()
method:
srfsh
is a command-line tool installed with OpenSRF that you can use to call
OpenSRF methods. To call an OpenSRF method, issue the request
command and pass
the OpenSRF service and method name as the first two arguments; then pass a list
of JSON objects as the arguments to the method being invoked.
The following example calls the opensrf.simple-text.reverse
method of the
opensrf.simple-text
OpenSRF service, passing the string "foobar"
as the
only method argument:
$ srfsh srfsh # request opensrf.simple-text opensrf.simple-text.reverse "foobar" Received Data: "raboof" =------------------------------------ Request Completed Successfully Request Time in seconds: 0.016718 =------------------------------------
The srfsh
client also gives you command-line access to retrieving metadata
about OpenSRF services and methods. For a given OpenSRF method, for example,
you can retrieve information such as the minimum number of required arguments,
the data type and a description of each argument, the package or library in
which the method is implemented, and a description of the method. To retrieve
the documentation for an opensrf method from srfsh
, issue the introspect
command, followed by the name of the OpenSRF service and (optionally) the
name of the OpenSRF method. If you do not pass a method name to the introspect
command, srfsh
lists all of the methods offered by the service. If you pass
a partial method name, srfsh
lists all of the methods that match that portion
of the method name.
The quality and availability of the descriptive information for each method depends on the developer to register the method with complete and accurate information. The quality varies across the set of OpenSRF and Evergreen APIs, although some effort is being put towards improving the state of the internal documentation.
srfsh# introspect opensrf.simple-text "opensrf.simple-text.reverse" --> opensrf.simple-text Received Data: { "__c":"opensrf.simple-text", "__p":{ "api_level":1, "stream":0, \ # "object_hint":"OpenSRF_Application_Demo_SimpleText", "remote":0, "package":"OpenSRF::Application::Demo::SimpleText", \ # "api_name":"opensrf.simple-text.reverse", \ # "server_class":"opensrf.simple-text", "signature":{ \ # "params":[ \ # { "desc":"The string to reverse", "name":"text", "type":"string" } ], "desc":"Returns the input string in reverse order\n", \ # "return":{ \ # "desc":"Returns the input string in reverse order", "type":"string" } }, "method":"text_reverse", \ # "argc":1 \ # } }
| |
| |
| |
| |
| |
| |
| |
| |
|
To call an OpenSRF method from Perl, you must connect to the OpenSRF service, issue the request to the method, and then retrieve the results.
#/usr/bin/perl use strict; use OpenSRF::AppSession; use OpenSRF::System; OpenSRF::System->bootstrap_client(config_file => '/openils/conf/opensrf_core.xml'); # my $session = OpenSRF::AppSession->create("opensrf.simple-text"); # print "substring: Accepts a string and a number as input, returns a string\n"; my $result = $session->request("opensrf.simple-text.substring", "foobar", 3); # my $request = $result->gather(); # print "Substring: $request\n\n"; print "split: Accepts two strings as input, returns an array of strings\n"; $request = $session->request("opensrf.simple-text.split", "This is a test", " "); # my $output = "Split: ["; my $element; while ($element = $request->recv()) { # $output .= $element->content . ", "; # } $output =~ s/, $/]/; print $output . "\n\n"; print "statistics: Accepts an array of strings as input, returns a hash\n"; my @many_strings = [ "First I think I'll have breakfast", "Then I think that lunch would be nice", "And then seventy desserts to finish off the day" ]; $result = $session->request("opensrf.simple-text.statistics", @many_strings); # $request = $result->gather(); # print "Length: " . $result->{'length'} . "\n"; print "Word count: " . $result->{'word_count'} . "\n"; $session->disconnect(); #
The | |
The | |
The | |
The | |
This | |
The | |
While the | |
This | |
The result object returns a hash reference via | |
The |
Of course, the example of accepting a single string and returning a single string is not very interesting. In real life, our applications tend to pass around multiple arguments, including arrays and hashes. Fortunately, OpenSRF makes that easy to deal with; in Perl, for example, returning a reference to the data type does the right thing. In the following example of a method that returns a list, we accept two arguments of type string: the string to be split, and the delimiter that should be used to split the string.
Text splitting method - streaming mode.
sub text_split { my $self = shift; my $conn = shift; my $text = shift; my $delimiter = shift || ' '; my @split_text = split $delimiter, $text; return \@split_text; } __PACKAGE__->register_method( method => 'text_split', api_name => 'opensrf.simple-text.split' );
We simply return a reference to the list, and OpenSRF does the rest of the work for us to convert the data into the language-independent format that is then returned to the caller. As a caller of a given method, you must rely on the documentation used to register to determine the data structures - if the developer has added the appropriate documentation.
OpenSRF is agnostic about objects; its role is to pass JSON back and forth between OpenSRF clients and services, and it allows the specific clients and services to define their own semantics for the JSON structures. On top of that infrastructure, Evergreen offers the fieldmapper: an object-relational mapper that provides a complete definition of all objects, their properties, their relationships to other objects, the permissions required to create, read, update, or delete objects of that type, and the database table or view on which they are based.
The Evergreen fieldmapper offers a great deal of convenience for working with complex system objects beyond the basic mapping of classes to database schemas. Although the result is passed over the wire as a JSON object containing the indicated fields, fieldmapper-aware clients then turn those JSON objects into native objects with setter / getter methods for each field.
All of this metadata about Evergreen objects is defined in the
fieldmapper configuration file (/openils/conf/fm_IDL.xml
), and access to
these classes is provided by the open-ils.cstore
, open-ils.pcrud
, and
open-ils.reporter-store
OpenSRF services which parse the fieldmapper
configuration file and dynamically register OpenSRF methods for creating,
reading, updating, and deleting all of the defined classes.
Example fieldmapper class definition for "Open User Summary".
<class id="mous" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="money::open_user_summary" oils_persist:tablename="money.open_usr_summary" reporter:label="Open User Summary"> <!-- --> <fields oils_persist:primary="usr" oils_persist:sequence=""> <!-- --> <field name="balance_owed" reporter:datatype="money" /> <!-- --> <field name="total_owed" reporter:datatype="money" /> <field name="total_paid" reporter:datatype="money" /> <field name="usr" reporter:datatype="link"/> </fields> <links> <link field="usr" reltype="has_a" key="id" map="" class="au"/> <!-- --> </links> <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> <!-- --> <actions> <retrieve permission="VIEW_USER"> <!-- --> <context link="usr" field="home_ou"/> <!-- --> </retrieve> </actions> </permacrud> </class>
The
| |
The
| |
Each
| |
The
| |
The | |
The
| |
The rarely-used |
When you retrieve an instance of a class, you can ask for the result to flesh some or all of the linked fields of that class, so that the linked instances are returned embedded directly in your requested instance. In that same request you can ask for the fleshed instances to in turn have their linked fields fleshed. By bundling all of this into a single request and result sequence, you can avoid the network overhead of requiring the client to request the base object, then request each linked object in turn.
You can also iterate over a collection of instances and set the automatically
generated isdeleted
, isupdated
, or isnew
properties to indicate that
the given instance has been deleted, updated, or created respectively.
Evergreen can then act in batch mode over the collection to perform the
requested actions on any of the instances that have been flagged for action.
In the previous implementation of the opensrf.simple-text.split
method, we
returned a reference to the complete array of results. For small values being
delivered over the network, this is perfectly acceptable, but for large sets of
values this can pose a number of problems for the requesting client. Consider a
service that returns a set of bibliographic records in response to a query like
"all records edited in the past month"; if the underlying database is
relatively active, that could result in thousands of records being returned as
a single network request. The client would be forced to block until all of the
results are returned, likely resulting in a significant delay, and depending on
the implementation, correspondingly large amounts of memory might be consumed
as all of the results are read from the network in a single block.
OpenSRF offers a solution to this problem. If the method returns results that
can be divided into separate meaningful units, you can register the OpenSRF
method as a streaming method and enable the client to loop over the results one
unit at a time until the method returns no further results. In addition to
registering the method with the provided name, OpenSRF also registers an additional
method with .atomic
appended to the method name. The .atomic
variant gathers
all of the results into a single block to return to the client, giving the caller
the ability to choose either streaming or atomic results from a single method
definition.
In the following example, the text splitting method has been reimplemented to support streaming; very few changes are required:
Text splitting method - streaming mode.
sub text_split { my $self = shift; my $conn = shift; my $text = shift; my $delimiter = shift || ' '; my @split_text = split $delimiter, $text; foreach my $string (@split_text) { # $conn->respond($string); } return undef; } __PACKAGE__->register_method( method => 'text_split', api_name => 'opensrf.simple-text.split', stream => 1 # );
Rather than returning a reference to the array, a streaming method loops
over the contents of the array and invokes the | |
Registering the method as a streaming method instructs OpenSRF to also
register an atomic variant ( |
As hard as it may be to believe, it is true: applications sometimes do not
behave in the expected manner, particularly when they are still under
development. The server language bindings for OpenSRF include integrated
support for logging messages at the levels of ERROR, WARNING, INFO, DEBUG, and
the extremely verbose INTERNAL to either a local file or to a syslogger
service. The destination of the log files, and the level of verbosity to be
logged, is set in the opensrf_core.xml
configuration file. To add logging to
our Perl example, we just have to add the OpenSRF::Utils::Logger
package to our
list of used Perl modules, then invoke the logger at the desired logging level.
You can include many calls to the OpenSRF logger; only those that are higher than your configured logging level will actually hit the log. The following example exercises all of the available logging levels in OpenSRF:
use OpenSRF::Utils::Logger; my $logger = OpenSRF::Utils::Logger; # some code in some function { $logger->error("Hmm, something bad DEFINITELY happened!"); $logger->warn("Hmm, something bad might have happened."); $logger->info("Something happened."); $logger->debug("Something happened; here are some more details."); $logger->internal("Something happened; here are all the gory details.") }
If you call the mythical OpenSRF method containing the preceding OpenSRF logger statements on a system running at the default logging level of INFO, you will only see the INFO, WARN, and ERR messages, as follows:
Results of logging calls at the default level of INFO.
[2010-03-17 22:27:30] opensrf.simple-text [ERR :5681:SimpleText.pm:277:] Hmm, something bad DEFINITELY happened! [2010-03-17 22:27:30] opensrf.simple-text [WARN:5681:SimpleText.pm:278:] Hmm, something bad might have happened. [2010-03-17 22:27:30] opensrf.simple-text [INFO:5681:SimpleText.pm:279:] Something happened.
If you then increase the the logging level to INTERNAL (5), the logs will contain much more information, as follows:
Results of logging calls at the default level of INTERNAL.
[2010-03-17 22:48:11] opensrf.simple-text [ERR :5934:SimpleText.pm:277:] Hmm, something bad DEFINITELY happened! [2010-03-17 22:48:11] opensrf.simple-text [WARN:5934:SimpleText.pm:278:] Hmm, something bad might have happened. [2010-03-17 22:48:11] opensrf.simple-text [INFO:5934:SimpleText.pm:279:] Something happened. [2010-03-17 22:48:11] opensrf.simple-text [DEBG:5934:SimpleText.pm:280:] Something happened; here are some more details. [2010-03-17 22:48:11] opensrf.simple-text [INTL:5934:SimpleText.pm:281:] Something happened; here are all the gory details. [2010-03-17 22:48:11] opensrf.simple-text [ERR :5934:SimpleText.pm:283:] Resolver did not find a cache hit [2010-03-17 22:48:21] opensrf.simple-text [INTL:5934:Cache.pm:125:] Stored opensrf.simple-text.test_cache.masaa => "here" in memcached server [2010-03-17 22:48:21] opensrf.simple-text [DEBG:5934:Application.pm:579:] Coderef for [OpenSRF::Application::Demo::SimpleText::test_cache] has been run [2010-03-17 22:48:21] opensrf.simple-text [DEBG:5934:Application.pm:586:] A top level Request object is responding de nada [2010-03-17 22:48:21] opensrf.simple-text [DEBG:5934:Application.pm:190:] Method duration for [opensrf.simple-text.test_cache]: 10.005 [2010-03-17 22:48:21] opensrf.simple-text [INTL:5934:AppSession.pm:780:] Calling queue_wait(0) [2010-03-17 22:48:21] opensrf.simple-text [INTL:5934:AppSession.pm:769:] Resending...0 [2010-03-17 22:48:21] opensrf.simple-text [INTL:5934:AppSession.pm:450:] In send [2010-03-17 22:48:21] opensrf.simple-text [DEBG:5934:AppSession.pm:506:] AppSession sending RESULT to opensrf@private.localhost/_dan-karmic-liblap_1268880489.752154_5943 with threadTrace [1] [2010-03-17 22:48:21] opensrf.simple-text [DEBG:5934:AppSession.pm:506:] AppSession sending STATUS to opensrf@private.localhost/_dan-karmic-liblap_1268880489.752154_5943 with threadTrace [1] ...
To see everything that is happening in OpenSRF, try leaving your logging level set to INTERNAL for a few minutes - just ensure that you have a lot of free disk space available if you have a moderately busy system!
If you have ever used an application that depends on a remote Web service
outside of your control-say, if you need to retrieve results from a
microblogging service-you know the pain of latency and dependability (or the
lack thereof). To improve response time in OpenSRF applications, you can take
advantage of the support offered by the OpenSRF::Utils::Cache
module for
communicating with a local instance or cluster of memcache daemons to store
and retrieve persistent values.
use OpenSRF::Utils::Cache; # sub test_cache { my $self = shift; my $conn = shift; my $test_key = shift; my $cache = OpenSRF::Utils::Cache->new('global'); # my $cache_key = "opensrf.simple-text.test_cache.$test_key"; # my $result = $cache->get_cache($cache_key) || undef; # if ($result) { $logger->info("Resolver found a cache hit"); return $result; } sleep 10; # my $cache_timeout = 300; # $cache->put_cache($cache_key, "here", $cache_timeout); # return "There was no cache hit."; }
This example:
Imports the OpenSRF::Utils::Cache module | |
Creates a cache object | |
Creates a unique cache key based on the OpenSRF method name and request input value | |
Checks to see if the cache key already exists; if so, it immediately returns that value | |
If the cache key does not exist, the code sleeps for 10 seconds to simulate a call to a slow remote Web service, or an intensive process | |
Sets a value for the lifetime of the cache key in seconds | |
When the code has retrieved its value, then it can create the cache entry, with the cache key, value to be stored ("here"), and the timeout value in seconds to ensure that we do not return stale data on subsequent calls |
When an OpenSRF service is started, it looks for a procedure called
initialize()
to set up any global variables shared by all of the children of
the service. The initialize()
procedure is typically used to retrieve
configuration settings from the opensrf.xml
file.
An OpenSRF service spawns one or more children to actually do the work
requested by callers of the service. For every child process an OpenSRF service
spawns, the child process clones the parent environment and then each child
process runs the child_init()
process (if any) defined in the OpenSRF service
to initialize any child-specific settings.
When the OpenSRF service kills a child process, it invokes the child_exit()
procedure (if any) to clean up any resources associated with the child process.
Similarly, when the OpenSRF service is stopped, it calls the DESTROY()
procedure to clean up any remaining resources.
The settings for OpenSRF services are maintained in the opensrf.xml
XML
configuration file. The structure of the XML document consists of a root
element <opensrf>
containing two child elements:
<default>
contains an <apps>
element describing all
OpenSRF services running on this system — see the section called “Registering a service with the OpenSRF configuration files” --, as
well as any other arbitrary XML descriptions required for global configuration
purposes. For example, Evergreen uses this section for email notification and
inter-library patron privacy settings.
<hosts>
contains one element per host that participates in
this OpenSRF system. Each host element must include an <activeapps>
element
that lists all of the services to start on this host when the system starts
up. Each host element can optionally override any of the default settings.
OpenSRF includes a service named opensrf.settings
to provide distributed
cached access to the configuration settings with a simple API:
opensrf.settings.default_config.get
: accepts zero arguments and returns
the complete set of default settings as a JSON document
opensrf.settings.host_config.get
: accepts one argument (hostname) and
returns the complete set of settings, as customized for that hostname, as a
JSON document
opensrf.settings.xpath.get
: accepts one argument (an
XPath expression) and returns the portion of
the configuration file that matches the expression as a JSON document
For example, to determine whether an Evergreen system uses the opt-in
support for sharing patron information between libraries, you could either
invoke the opensrf.settings.default_config.get
method and parse the
JSON document to determine the value, or invoke the opensrf.settings.xpath.get
method with the XPath /opensrf/default/share/user/opt_in
argument to
retrieve the value directly.
In practice, OpenSRF includes convenience libraries in all of its client
language bindings to simplify access to configuration values. C offers
osrfConfig.c, Perl offers OpenSRF::Utils::SettingsClient
, Java offers
org.opensrf.util.SettingsClient
, and Python offers osrf.set
. These
libraries locally cache the configuration file to avoid network roundtrips for
every request and enable the developer to request specific values without
having to manually construct XPath expressions.