public
Description: A Rails plugin implementing the JSON-RPC 1.1 protocol for remote procedure calls
Homepage: http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html
Clone URL: git://github.com/PeterBengtson/json-rpc-1-1.git
Click here to lend your support to: json-rpc-1-1 and make a donation at www.pledgie.com !
name age message
file README Sun Apr 19 01:18:25 -0700 2009 Many new features: queues, retries, etc. [PeterBengtson]
file README-Spawn Sun Apr 19 01:18:25 -0700 2009 Many new features: queues, retries, etc. [PeterBengtson]
file Rakefile Sun Apr 19 01:18:25 -0700 2009 Many new features: queues, retries, etc. [PeterBengtson]
file init.rb Sat Oct 03 06:02:09 -0700 2009 Fixed "ostruct" vs "openstruct" require. [PeterBengtson]
file install.rb Thu Apr 03 05:33:19 -0700 2008 Initial commit. [diino]
directory lib/ Sat Oct 03 06:13:05 -0700 2009 Argument error fixed (appears in 2.3.2 only, it... [PeterBengtson]
directory rdoc/ Tue May 20 08:25:28 -0700 2008 Fixed POST arg [] vs {} issue. [diino]
directory spec/ Sun Apr 19 01:18:25 -0700 2009 Many new features: queues, retries, etc. [PeterBengtson]
directory tasks/ Sun Apr 19 01:18:25 -0700 2009 Many new features: queues, retries, etc. [PeterBengtson]
file uninstall.rb Thu Apr 03 05:33:19 -0700 2008 Initial commit. [diino]
README
= JsonRpc

This is a complete implementation of the JSON-RPC 1.1 protocol, as described in
the JSON-RPC 1.1 Specification of 7 August 2007, which may be found at
http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html.

The implementation consists of two parts: a service side and a client side. The
server part is written specifically for Ruby on Rails, but the client does not really
require Rails. A Rails application may choose to act as a service provider,
as a client, or both.


== Server Side

A Rails app may host any number of services. A service is declared in a controller,
e.g.:

  class ExampleServiceController < ApplicationController
  
    json_rpc_service :name => 'DemoService',                                 # required
                     :id => 'urn:uuid:fdba4820-276b-11dc-ab85-0002a5d5c51b', # required
                     :version => '0.1',                                      # optional
                     :summary => 'A simple demonstration service.',          # optional
                     :help => 'http://127.0.0.1:3000/services/index.html',   # optional
                     :address => 'http://127.0.0.1:3000/services',           # optional
                     :disabled => false,                                     # optional
                     :logger => RAILS_DEFAULT_LOGGER                         # optional

    json_rpc_procedure :name => 'sum', :proc => :+,                          # required
                       :summary => 'Sums two numbers.',                      # optional
                       :help => 'http://127.0.0.1:3000/services/sum.html',   # optional
                       :idempotent => true,                                  # optional
                       :params => [{:name => 'a', :type => 'num'},           # optional
                                   {:name => 'b', :type => 'num'}],          # optional
                       :return => {:type => 'num'}                           # optional

    json_rpc_procedure :name => 'time', :proc => lambda { Time.now.to_s }
  
  end
  
For the exact meaning of the args, read the JSON-RPC 1.1 specification - it's very
straightforward. The only additions are the +:disabled+ and the :logger options.
+:disabled+, if true, will make the service not accept any incoming calls at all.
You can enable and disable a service dynamically too, using +ExampleServiceController.enable+ 
and +ExampleServiceController.disable+, respectively. +:logger+, if true, will make
the JSON-RPC client side log all JSON-RPC calls, their parameters and the returned
values using the logger, which should respond to the +debug+ message.


Create a controller for each service you need. Then declare routes for them in Rails'
config/routes.rb file:

  ActionController::Routing::Routes.draw do |map|
  
    map.connect 'services/*method', :controller => 'example_service', 
                :action => 'receive_json_rpc_request'

  end

+services+ is the external name of the service (it can be anything, of course). 
The complete URI to your service will be something like http://www.yoursite.com/services.

Please note that the +:action+ arg must have the exact value indicated above, and the
route string must end with '/*method'. In future versions there might be a helper to 
create JSON-RPC routes, but as yet that piece of syntactic sugar is not available.



== Client Side

The client side of things is the module JsonRpcClient. To connect to a service as defined
above (locally on your computer or anywhere in the world), define a class to encapsulate
your remote calls:


  class Yonder < JsonRpcClient
    json_rpc_service 'http://www.yoursite.com/services'
  end
      
You can now call procedures remotely on your new class as the receiving object:

  Yonder.time
  Yonder.sum 24, 6
  Yonder.sum :a => 24, :b => 6
  Yonder.sum :b => 6, :a => 24
  Yonder.sum :b => 6, '0' => 24
  Yonder.sum '0' => 24, '1' => 6
  
Note that all the calls to +sum+ are equivalent. For further information, see the JSON-RPC 1.1 
specifications. The use of named arguments is encouraged.

NB: If at all possible, use an IP number in the URI instead of a host name, as the Net::HTTP
library can be slow in resolving addresses, which may render your requests two magnitudes 
slower.

Also note that your JSON-RPC client class, "Yonder" in this example, never is instantiated.


== Client side retries

When a retriable failure occurs (e.g. the service is down server-side), by default a total of
three tries are attempted. You can modify this by passing a parameter to +json_rpc_service+,
+:retries+. This may be an integer specifying the number of retries to perform after the initial
one (thus this figure is always one less than the total number of tries made.)

However, +:retries+ may also be a hash with the following possible components:

  :max_retries      The total number of retries or nil (= infinite). Default: 2.
  :max_time           The total number of seconds allowed for retries starting from
                the beginning of the whole operation. Default: nil (no limit).
  :sleep                  The number of seconds to wait between retries.
                Default: nil (don't wait).
    :sleep_factor           A factor with which to multiply the sleep time after each
                sleep period. Default: 1.5.
  :sleep_max           Ceiling value for the modified sleep time.
                Default: nil (no upper limit).
              
Thus, if you specify nil or leave out +:retries+ altogether, the JSON-RPC client will do three tries
without waiting in between, after which the operation will fail with the original exception.

You can also change the retry parameters locally - per service - by using the class method 
+retry_strategy+:

  Yonder.retry_strategy(:sleep => 5.0, :sleep_factor => 2.0, :sleep_max => 5.minutes) do
    Yonder.sum 24, 6
  end


== Asynchronous queueing of client-side requests

It is possible to execute code asynchronously in other processes. This is not limited to JSON-RPC
calls. Any code involving ActiveRecord classes and instances can be enqueued for execution using
the simple method +enqueue+. It is possible to specify jobs consisting of any number of discrete
steps. For each job step, it is possible to specify rollback operations to execute in case the
job step fails, with configurable retry counts and much more. It is also possible to specify 
failure handlers for jobs which fail entirely.

The basic syntax is as follows:

  enqueue({job-step-1}, {job-step-2}, ... {job-step-n},
          :job-config-param-1,
          :job-config-param-2,
          ...
          :job-config-param-n)
  
The method +enqueue+ always returns true. This means that you cannot retrieve the result of the
job by normal means. If it's needed (which rarely is the case), you must use a job step to do so.

Here's a simple example of an +enqueue+ call:

  enqueue :target => @user, :do => :do_something, :do_args => [x, [1, 2, 3], User.find(xxx)]
  
This will set up a call to +user.do_something(x, [1,2,3], <some_user>)+ in another process.

Each job step may have a single ActiveRecord object as its target, but it is also possible to
invoke class methods:

  enqueue :target => MyClass, :do => :some_class_method
  
And of course, if there is more than one job step:

  enqueue({ :target => myobj, :do => :a_myobj_method, :do_args => ['foo'] },
          { :target => other, :do => :foo },
          { :target => myobj, :do => :something_final })
  
Please note that all args and targets are retrieved fresh from the database for each job step.
Thus the two references to +myobj+ in the above code are not the same object in memory, but
two distinct instances (which of course still may be identical).
  
Each job step can take the following key/value pairs:

  :target      An ActiveRecord class or instance
  :do          A symbol naming the method to invoke on the target
  :do_args    An array of arguments to pass to the invoked method. ActiveRecord instances
            and classes may appear in this array, but only on the top level.
  :rollback    A symbol naming a method to invoke on the target in case the +:do+ operation
              fails (i.e. an exception is raised).
  :rollback_args  An array of arguments to pass to the invoked rollback method. The same
            rules as for +:do_args+ apply to +:rollback_args+.
  :tries          An integer which specifies the total number of tries to be performed for 
                      this job step. Defaults to 3 (but if there is a :tries value defined
            for the whole job, then that value is used instead - more on this 
            further down).

Job configuration parameters can be specified as the last hash in the +enqueue+ call (which
thus may either be a job step or a hash of job configuration parameters - and remember that
any "loose" symbol/value pairs in any Ruby method call will be grouped together as a hash).

The job configuration parameters can be any or all of the following:

  :target      An ActiveRecord class or instance (only used with :on_failure)
  :on_failure    A symbol naming the method to invoke on the target if the operation fails.
            Any exceptions generated by the :on_failure call are suppressed silently.
  :failure_args   An array of arguments to pass to the +:on_failure+ handler. The same
            restrictions apply to this argument list as to the :do_args and
            :rollback_args argument lists.
  :tries      An integer which specifies the default total number of tries to be performed 
            for each job step (and which can be overridden individually in each
            job step). Defaults to nil.
  :unordered      If true, the job is permitted to execute in parallel with any other job.
            If false (the default), the job is *ordered*, meaning it will execute 
            in strict order compared to all other ordered jobs. Only one ordered
            job will be permitted to execute at one time. This means you can depend
            on the results of one ordered job for any following job.
  :synchronous  Normally false. If true, it overrides the value of :unordered and will
            cause all calls to +enqueue+ to be performed synchronously. This is
            the default value in the test and build environments.
            
            



=== Service Description

When the client is created, it will connect to its service and download its Service Description.
This makes the JSON-RPC 1.1 client interface self-configuring. You can obtain the Service Description
object by calling

   Yonder.system_describe

which will return a hash. The JSON-RPC specification requires that this method be called
'system.describe', but as this is not possible in Ruby the period has been replaced by an
underline character. This is only on the Ruby client side; the server, which is completely
standard compliant, will of course respond as required to 'system.describe'.


=== Parameter coercion

Limited conversion between parameter types will be performed: the JSON-RPC interface will convert
between numeric and string types as necessary, provided there is no ambiguity and no loss of precision.
Parameter types are checked by the services and errors returned if there is a type mismatch - except
for strings and numbers as described above.


=== GET and POST

The plugin will make GET requests for procedures which are known to be idempotent (roughly: which
return the same result for the same parameters always, and which do not mutate any data). Procedures
are declared idempotent on the service side. For all other procedures the plugin will choose do to
a POST. They are equivalent in speed, but GET posts have the added advantage of being cacheable by
proxies, etc.


=== Speed

The JSON-RPC 1.1 protocol is very lightweight. The JSON-RPC server side, under Rails, is able to
process about 4000 requests per second on my 2.16 GHz Intel Core Duo iMac. The client side is
comparable in speed, but the determining factor in this case is the Net:HTTP library, which can be
unnecessarily slow. It is important to use an IP number instead of a host name, for instance. On
my system, the client side can generate between 500 and 1000 requests per second. Note that these
figures apply to the production environment, not the development environment, which of course is
slower.

In future, a different HTTP library than Net::HTTP may be used on the client side.


  Peter Bengtson (peter@peterbengtson.com)