Navigation Menu

Skip to content

metonymous/json-rpc-1-1

 
 

Repository files navigation

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 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 => ‘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 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)

About

A Rails plugin implementing the JSON-RPC 1.1 protocol for remote procedure calls

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Ruby 100.0%