<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>README-Spawn</filename>
    </added>
    <added>
      <filename>lib/job.rb</filename>
    </added>
    <added>
      <filename>lib/job_queue.rb</filename>
    </added>
    <added>
      <filename>lib/job_step.rb</filename>
    </added>
    <added>
      <filename>lib/json_rpc_utils.rb</filename>
    </added>
    <added>
      <filename>lib/spawn/patches.rb</filename>
    </added>
    <added>
      <filename>lib/spawn/spawn.rb</filename>
    </added>
    <added>
      <filename>spec/enqueue_spec.rb</filename>
    </added>
    <added>
      <filename>spec/job_queue_spec.rb</filename>
    </added>
    <added>
      <filename>spec/job_spec.rb</filename>
    </added>
    <added>
      <filename>spec/job_step_spec.rb</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -65,6 +65,7 @@ route string must end with '/*method'. In future versions there might be a helpe
 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
@@ -94,14 +95,119 @@ slower.
 
 Also note that your JSON-RPC client class, &quot;Yonder&quot; in this example, never is instantiated.
 
-You can also pass a one-argument block to a remote procedure call:
 
-	Yonder.foo(bar, baz) { |x| x.kind_of?(Exception) ? scream(x) : process(x) }
+== 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 =&gt; 5.0, :sleep_factor =&gt; 2.0, :sleep_max =&gt; 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 =&gt; @user, :do =&gt; :do_something, :do_args =&gt; [x, [1, 2, 3], User.find(xxx)]
 	
-If a block is present, the result of executing +foo+ on +bar+ and +baz+ will be yielded to the
-block. However, if for any reason an exception should occur at any point, including HTTP setup,
-remote evaluation or decoding of the JSON reply, the block will instead receive the exception.
-This allows you to program continuation style, or to implement execution queues for retries, etc.
+This will set up a call to +user.do_something(x, [1,2,3], &lt;some_user&gt;)+ 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 =&gt; MyClass, :do =&gt; :some_class_method
+	
+And of course, if there is more than one job step:
+
+	enqueue({ :target =&gt; myobj, :do =&gt; :a_myobj_method, :do_args =&gt; ['foo'] },
+	        { :target =&gt; other, :do =&gt; :foo },
+	        { :target =&gt; myobj, :do =&gt; :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 &quot;loose&quot; 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</diff>
      <filename>README</filename>
    </modified>
    <modified>
      <diff>@@ -1,17 +1,16 @@
 require 'rake'
-require 'rake/testtask'
-require 'rake/rdoctask'
+require 'spec/rake/spectask'
 
-desc 'Default: run unit tests.'
-task :default =&gt; :test
+desc 'Default: run specs.'
+task :default =&gt; :spec
 
-desc 'Test the json_rpc plugin.'
-Rake::TestTask.new(:test) do |t|
-  t.libs &lt;&lt; 'lib'
-  t.pattern = 'test/**/*_test.rb'
-  t.verbose = true
+desc 'Run the specs'
+Spec::Rake::SpecTask.new(:spec) do |t|
+  t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
+  t.spec_files = FileList['spec/**/*_spec.rb']
 end
 
+
 desc 'Generate documentation for the JSON-RPC plugin.'
 Rake::RDocTask.new(:rdoc) do |rdoc|
   rdoc.rdoc_dir = 'rdoc'</diff>
      <filename>Rakefile</filename>
    </modified>
    <modified>
      <diff>@@ -1,2 +1,41 @@
 # Include hook code here
+
+require 'json/ext'
+require 'json/add/rails'
+require 'openstruct'
+
+require 'job'
+require 'job_step'
+require 'job_queue'
+require 'json_rpc_client'
+require 'json_rpc_service'
+require 'json_rpc_utils'
+
+require 'spawn/spawn'
+require 'spawn/patches'
+ActionController::Base.send :include, Spawn
+if defined?(ActiveRecord)
+  ActiveRecord::Base.send :include, Spawn
+  ActiveRecord::Observer.send :include, Spawn
+end
+
+#
+# This might be right or it might not: the default to_json
+# method for strings encodes *all* Unicode characters using
+# the &quot;\uFFFF&quot; syntax, which is unnecessary since the JSON
+# spec explicitly states that Unicode characters are fully
+# supported. Rails only uses UTF-8 and never decodes the 
+# \uFFFF it creates (insert expletive here). Therefore, we 
+# monkey patch String#to_json to not use the \uFFFF syntax 
+# at all. This might have unforeseen side effects. We shall
+# see.
+#
+class ::String
+  def to_json(options = nil) #:nodoc:
+    '&quot;' + gsub(/[\010\f\n\r\t&quot;\\&gt;&lt;&amp;]/) { |s| ActiveSupport::JSON::Encoding::ESCAPED_CHARS[s] } + '&quot;'
+  end
+end
+
+
+
 ActionController::Base.send(:include, JsonRpcService)</diff>
      <filename>init.rb</filename>
    </modified>
    <modified>
      <diff>@@ -4,20 +4,24 @@
 #
 class JsonRpcClient
   
-  require 'json'
   require 'net/http'
   require 'uri'
-  
+  require 'timeout'  
     
   #
-  # Our runtime error class.
+  # Our runtime error classes. Indentation reflects inheritance.
   #
   class Error &lt; RuntimeError; end
-  class ServiceError &lt; Error; end
-  class ServiceDown &lt; Error; end
-  class NotAService &lt; Error; end
-  class ServiceReturnsJunk &lt; Error; end
+    class ServiceError &lt; Error; end
+    class ServiceReturnsJunk &lt; Error; end
   
+    class RetriableError &lt; Error; end
+     class NotAService &lt; Error; end
+     class ServiceDown &lt; RetriableError; end
+     class ServiceUnavailable &lt; RetriableError; end
+     class Timeout &lt; RetriableError; end
+       class ServerTimeout &lt; Timeout; end
+       class GatewayTimeout &lt; Timeout; end
   
   #
   # Execute this &quot;declaration&quot; with a string or URI object describing the base URI
@@ -27,7 +31,9 @@ class JsonRpcClient
   # If you pass :no_auto_config =&gt; true, no attempt will be made to contact the service
   # to obtain a description of its available services, which means POST will be used
   # for all requests. This is sometimes useful when a server is non-compliant and does not
-  # provide a system.describe call.
+  # provide a system.describe call. If you pass :uri_encode_post_bodies =&gt; true, all
+  # bodies used in POSTs will be URI encoded (fixing a tricky case with UTF-8 characters and
+  # non-compliant JSON-RPC servers written in Perl).
   #
   def self.json_rpc_service(base_uri, opts={})
     @uri = URI.parse base_uri
@@ -40,10 +46,24 @@ class JsonRpcClient
     @get_procs = []
     @post_procs = []
     @no_auto_config = opts[:no_auto_config]
+    @uri_encode_post_bodies = opts[:uri_encode_post_bodies]
     @logger = opts[:debug]
+    @retry_strategy = analyze_retry_strategy(opts[:retries])
     @uri
   end
   
+  
+  def self.analyze_retry_strategy(strat)
+    strat ||= 2
+    strat = { :max_retries =&gt; strat } if strat.kind_of?(Integer)
+    { :max_time =&gt; nil, 
+      :sleep =&gt; nil, 
+      :sleep_factor =&gt; 1.5, 
+      :sleep_max =&gt; nil 
+    }.merge(strat)
+  end
+  
+  
   #
   # Changes the URI for this service. Used in setups where several identical 
   # services can be reached on different hosts.
@@ -57,7 +77,20 @@ class JsonRpcClient
       @proxy_port = @proxy.port
     end
     @uri
-  end     
+  end
+  
+  
+  #
+  # This block handler establishes a local scope for a service retry strategy
+  #
+  def self.retry_strategy(new_strategy)
+    old_strategy = @retry_strategy
+    @retry_strategy = analyze_retry_strategy(new_strategy)
+    yield
+  ensure
+    @retry_strategy = old_strategy
+  end
+
 
   #
   # This allows us to call methods remotely with the same syntax as if they were local.
@@ -71,27 +104,49 @@ class JsonRpcClient
   def self.method_missing(name, *args)
     system_describe unless (@no_auto_config || @service_description)
     name = name.to_s
-    @logger.debug &quot;JSON-RPC call: #{self}.#{name}(#{args.join(',')})&quot; if @logger
-    req_wrapper = @get_procs.include?(name) ? Get.new(self, name, args) : Post.new(self, name, args)
+    @logger.debug &quot;JSON-RPC call (#{host_and_port}): #{self}.#{name}(#{args.inspect})&quot; if @logger
+    req_wrapper = @get_procs.include?(name) ? Get.new(self, name, args) : 
+                                              Post.new(self, name, args, @uri_encode_post_bodies)
     req = req_wrapper.req
+    remaining_retries = @retry_strategy[:max_retries]
+    started_at = Time.now
+    sleep_time = @retry_strategy[:sleep]
     begin
       begin
         Net::HTTP.start(@host, @port, @proxy_host, @proxy_port) do |http|
           res = http.request req
           if res.content_type != 'application/json'
-            @logger.debug &quot;JSON-RPC server returned non-JSON data: #{res.body}&quot; if @logger
-            raise NotAService, &quot;Returned #{res.content_type} (status code #{res.code}: #{res.message}) rather than application/json&quot;
+            @logger.debug &quot;JSON-RPC server (#{host_and_port}) returned non-JSON data: #{res.body}&quot; if @logger
+            raise GatewayTimeout,     &quot;#{res.message} (#{host_and_port})&quot; if res.code == 504 || res.code == &quot;504&quot;
+            raise ServiceUnavailable, &quot;#{res.message} (#{host_and_port})&quot; if res.code == 503 || res.code == &quot;503&quot;
+            raise NotAService,        &quot;Returned #{res.content_type} (status code #{res.code}: #{res.message}) (#{host_and_port}) rather than application/json&quot;
           end
           json = JSON.parse(res.body) rescue raise(ServiceReturnsJunk)
-          raise ServiceError, &quot;JSON-RPC error #{json['error']['code']}: #{json['error']['message']}&quot; if json['error']
-          @logger.debug &quot;JSON-RPC result: #{self.class}.#{name} =&gt; #{res.body}&quot; if @logger
-          return (block_given? ? yield(json['result']) : json['result'])
+          raise ServiceError, &quot;JSON-RPC error #{json['error']['code']} (#{host_and_port}): #{json['error']['message']}&quot; if json['error']
+          @logger.debug &quot;JSON-RPC result (#{host_and_port}): #{self.class}.#{name} =&gt; #{res.body}&quot; if @logger
+          return json['result']
         end
       rescue Errno::ECONNREFUSED
-        raise ServiceDown, &quot;Connection refused&quot;
+        raise ServiceDown, &quot;Connection refused (#{host_and_port})&quot;
+      rescue ::Timeout::Error
+        raise ServerTimeout, &quot;Server timeout (#{host_and_port})&quot;
+      end
+    rescue RetriableError =&gt; e
+      if @retry_strategy[:max_retries]
+        remaining_retries -= 1
+        raise e if remaining_retries &lt; 0
+      end
+      if @retry_strategy[:max_time] &amp;&amp; (Time.now &gt;= (started_at + @retry_strategy[:max_time]))
+        raise e
       end
+      if sleep_time
+        sleep(sleep_time)
+        sleep_time = sleep_time * @retry_strategy[:sleep_factor] if @retry_strategy[:sleep_factor]
+        sleep_time = [sleep_time, @retry_strategy[:sleep_max]].min if @retry_strategy[:sleep_max]   
+      end
+      retry  
     rescue Exception =&gt; e
-      block_given? ? yield(e) : raise(e)
+      raise(e)
     end
   end
       
@@ -135,8 +190,8 @@ class JsonRpcClient
   def self.system_describe
     @service_description = :in_progress
     @service_description = method_missing('system.describe')
-    raise &quot;JSON-RPC server failed to return a service description&quot; unless @service_description
-    raise &quot;JSON-RPC server failed to return a standard-compliant service description&quot; unless @service_description['procs'].kind_of?(Array)
+    raise &quot;JSON-RPC server (#{host_and_port}) failed to return a service description&quot; unless @service_description
+    raise &quot;JSON-RPC server (#{host_and_port}) failed to return a standard-compliant service description&quot; unless @service_description['procs'].kind_of?(Array)
     @service_description['procs'].each do |p|
       @post_procs &lt;&lt; p['name']
       @get_procs &lt;&lt; p['name'] if p['idempotent']
@@ -177,6 +232,7 @@ class JsonRpcClient
   # in the arglist.  
   #
   class Get &lt; Request
+
     def initialize(klass, name, args)
       if args.length == 0
         query = ''
@@ -201,7 +257,7 @@ class JsonRpcClient
       @req = Net::HTTP::Get.new(uri)
       super()
     end
-    
+
   end
   
   
@@ -217,14 +273,21 @@ class JsonRpcClient
   #
   class Post &lt; Request
     
-    def initialize(klass, name, args)
+    def initialize(klass, name, args, uri_encode)
       @req = Net::HTTP::Post.new(klass.service_path)
       super()
       @req.add_field 'Content-Type', 'application/json'
       args = args[0] if args.length == 1 &amp;&amp; args[0].is_a?(Hash)
       body = { :version =&gt; '1.1', :method =&gt; name, :params =&gt; args }.to_json
-      @req.body = body
-      klass.logger.debug &quot;JSON-RPC POST request to URI #{klass.host_and_port}#{klass.service_path} with body #{body}&quot; if klass.logger
+      @req.body = uri_encode ? Post.uri_escape_sanely(body) : body
+      klass.logger.debug &quot;JSON-RPC POST request #{uri_encode ? '(with URI encoded body) ' : ''}to URI #{klass.host_and_port}#{klass.service_path} with body #{body}&quot; if klass.logger
+    end
+    
+    
+    OUR_UNSAFE = /[^ _\.!~*'()a-zA-Z\d;\/?:@&amp;=+$,{}\&quot;-]/nm unless defined?(OUR_UNSAFE)
+ 
+    def self.uri_escape_sanely(str)
+      URI.escape(str, OUR_UNSAFE).gsub('%5B', '[').gsub('%5D', ']')  # Ruby's regexes are BRAIN-DEAD.
     end
     
   end</diff>
      <filename>lib/json_rpc_client.rb</filename>
    </modified>
    <modified>
      <diff>@@ -2,9 +2,7 @@
 # This is the JSON-RPC Service.
 #
 module JsonRpcService
-      
-  require 'json'
-      
+            
   def self.included(base)
     base.extend ClassMethods
   end
@@ -66,7 +64,7 @@ module JsonRpcService
       render :text =&gt; req.response, :status =&gt; req.status_code
     end
   end
-  
+
 
   #
   # This class defines the server side of the JSON-RPC protocol handler.
@@ -390,7 +388,7 @@ module JsonRpcService
           super service, req, par
           set_error(999, &quot;Content-Type header must be application/json&quot;) and return unless req.env['CONTENT_TYPE'] == 'application/json'
           begin
-            body = JSON.parse req.raw_post
+            body = JSON.parse(req.raw_post) rescue JSON.parse(URI.decode(req.raw_post))
           rescue Exception =&gt; e
             set_error 999, &quot;JSON did not parse&quot; 
             return</diff>
      <filename>lib/json_rpc_service.rb</filename>
    </modified>
    <modified>
      <diff>@@ -2,16 +2,19 @@ require File.dirname(__FILE__) + '/spec_helper'
 
 describe JsonRpcClient do
   
-  before do
-    class FooController &lt; ApplicationController
+  before :all do
+    class FuBarController &lt; ApplicationController
       json_rpc_service :name =&gt; 'TestService', :id =&gt; 'skdjfhsdhfkjshdjkhskdhfkjshdf'
       json_rpc_procedure :name =&gt; 'add', :params =&gt; [{:name =&gt; 'x', :type =&gt; 'any'}, 
                                                      {:name =&gt; 'y', :type =&gt; 'any'}], 
                          :proc =&gt; :+, :idempotent =&gt; true
+      json_rpc_procedure :name =&gt; 'sub', :params =&gt; [{:name =&gt; 'x', :type =&gt; 'any'}, 
+                                                     {:name =&gt; 'y', :type =&gt; 'any'}], 
+                         :proc =&gt; :-, :idempotent =&gt; true
     end
         
-    class Foo &lt; JsonRpcClient
-      json_rpc_service 'http://localhost:8888/da_service'
+    class FuBar &lt; JsonRpcClient
+      json_rpc_service 'http://localhost:8888/da_service', :no_auto_config =&gt; true, :retries =&gt; 2
     end
         
   end
@@ -19,13 +22,14 @@ describe JsonRpcClient do
   
   it &quot;should request the system description when first called&quot; do
     Net::HTTP.should_receive(:start).with(&quot;localhost&quot;, 8888, nil, nil).
-              and_return(stringify_symbols_in_hash(FooController.service.system_describe))
-    Foo.system_describe.should == {&quot;procs&quot;=&gt;[{&quot;name&quot;=&gt;&quot;add&quot;, 
-                                              &quot;idempotent&quot;=&gt;true, 
-                                              &quot;params&quot;=&gt;[{&quot;name&quot;=&gt;&quot;x&quot;, &quot;type&quot;=&gt;&quot;any&quot;}, 
-                                                         {&quot;name&quot;=&gt;&quot;y&quot;, &quot;type&quot;=&gt;&quot;any&quot;}], 
-                                              &quot;return&quot;=&gt;{&quot;type&quot;=&gt;&quot;any&quot;}}], 
-                                   &quot;name&quot;=&gt;&quot;TestService&quot;, &quot;id&quot;=&gt;&quot;skdjfhsdhfkjshdjkhskdhfkjshdf&quot;, &quot;sdversion&quot;=&gt;&quot;1.0&quot;}
+              and_return(stringify_symbols_in_hash(FuBarController.service.system_describe))
+    FuBar.system_describe.should == {&quot;procs&quot;=&gt;[{&quot;name&quot;=&gt;&quot;sub&quot;, &quot;idempotent&quot;=&gt;true, &quot;params&quot;=&gt;[{&quot;name&quot;=&gt;&quot;x&quot;, &quot;type&quot;=&gt;&quot;any&quot;}, {&quot;name&quot;=&gt;&quot;y&quot;, &quot;type&quot;=&gt;&quot;any&quot;}], &quot;return&quot;=&gt;{&quot;type&quot;=&gt;&quot;any&quot;}}, 
+                                               {&quot;name&quot;=&gt;&quot;add&quot;, &quot;idempotent&quot;=&gt;true, &quot;params&quot;=&gt;[{&quot;name&quot;=&gt;&quot;x&quot;, &quot;type&quot;=&gt;&quot;any&quot;}, {&quot;name&quot;=&gt;&quot;y&quot;, &quot;type&quot;=&gt;&quot;any&quot;}], &quot;return&quot;=&gt;{&quot;type&quot;=&gt;&quot;any&quot;}}
+                                              ], 
+                                     &quot;name&quot;=&gt;&quot;TestService&quot;, 
+                                     &quot;id&quot;=&gt;&quot;skdjfhsdhfkjshdjkhskdhfkjshdf&quot;, 
+                                     &quot;sdversion&quot;=&gt;&quot;1.0&quot;
+                                    }
   end
   
   
@@ -34,40 +38,68 @@ describe JsonRpcClient do
               and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest, 
                                                                      :body =&gt; '{&quot;error&quot;: {&quot;code&quot;: 123, &quot;message&quot;: &quot;Disaster!&quot;}}',
                                                                      :content_type =&gt; 'application/json')))
-    lambda { Foo.add 1, 2 }.should raise_error(JsonRpcClient::ServiceError, 'JSON-RPC error 123: Disaster!')
+    lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::ServiceError, 'JSON-RPC error 123 (localhost:8888): Disaster!')
   end
   
+  
   it &quot;should raise a JsonRpcClient::ServiceDown error when the client cannot reach the remote service&quot; do
-    lambda { Foo.add 1, 2 }.should raise_error(JsonRpcClient::ServiceDown)
+    lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::ServiceDown)
   end
   
-  it &quot;should raise a JsonRpcClient::NotAService when the service does not return JSON&quot; do
+  
+  it &quot;should raise a JsonRpcClient::NotAService when the service returns non-JSON data&quot; do
     Net::HTTP.should_receive(:start).with(&quot;localhost&quot;, 8888, nil, nil).
-              and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest, 
+              and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest,
+                                                                     :code =&gt; 12345, :message =&gt; 'blablabla',
                                                                      :body =&gt; '{&quot;error&quot;: {&quot;code&quot;: 123, &quot;message&quot;: &quot;Disaster!&quot;}}',
                                                                      :content_type =&gt; 'text/html')))
-    lambda { Foo.add 1, 2 }.should raise_error(JsonRpcClient::NotAService)
+    lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::NotAService)
+  end
+  
+  
+  it &quot;should raise a JsonRpcClient::GatewayTimeout when the service returns non-JSON data with a status code of 504&quot; do
+    Net::HTTP.should_receive(:start).exactly(3).times.with(&quot;localhost&quot;, 8888, nil, nil).
+              and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest,
+                                                                     :code =&gt; 504, :message =&gt; 'blablabla',
+                                                                     :body =&gt; '{&quot;error&quot;: {&quot;code&quot;: 504, &quot;message&quot;: &quot;Disaster!&quot;}}',
+                                                                     :content_type =&gt; 'text/html')))
+    lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::GatewayTimeout)
   end
   
+  
+  it &quot;should raise a JsonRpcClient::ServiceUnavailable when the service returns non-JSON data with a status code of 503&quot; do
+    Net::HTTP.should_receive(:start).exactly(3).times.with(&quot;localhost&quot;, 8888, nil, nil).
+              and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest,
+                                                                     :code =&gt; 503, :message =&gt; 'blablabla',
+                                                                     :body =&gt; '{&quot;error&quot;: {&quot;code&quot;: 503, &quot;message&quot;: &quot;Disaster!&quot;}}',
+                                                                     :content_type =&gt; 'text/html')))
+    lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::ServiceUnavailable)
+  end
+  
+  
   it &quot;should raise a JsonRpcClient::ServiceReturnsJunk when the service returns unparseable JSON&quot; do
     Net::HTTP.should_receive(:start).with(&quot;localhost&quot;, 8888, nil, nil).
               and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest, 
                                                                      :body =&gt; '&lt;this&gt;is not&lt;json /&gt;but some sort of&lt;/markup&gt;',
                                                                      :content_type =&gt; 'application/json')))
-    lambda { Foo.add 1, 2 }.should raise_error(JsonRpcClient::ServiceReturnsJunk)
+    lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::ServiceReturnsJunk)
   end
   
-  it &quot;should yield the exception to a block, if there is one provided&quot; do
-    Foo.add(1, 2) { |x| &quot;x is #{x.class}&quot; }.should == &quot;x is JsonRpcClient::ServiceDown&quot;
-  end
   
-  it &quot;should yield the result to a block, if there is one provided&quot; do
+  # it &quot;should translate ::Timeout::Error exceptions to JsonRpcClient::ServerTimeout exceptions&quot; do
+  #   Net::HTTP.should_receive(:start).with(&quot;localhost&quot;, 8888, nil, nil).and_raise(::Timeout::Error)
+  #   lambda { FuBar.add 1, 2 }.should raise_error(JsonRpcClient::ServerTimeout)
+  # end
+  
+  
+  it &quot;should be possible to wrap a call in a retry_strategy block&quot; do
     Net::HTTP.should_receive(:start).with(&quot;localhost&quot;, 8888, nil, nil).
               and_yield(mock_model(Net::HTTP, :request =&gt; mock_model(Net::HTTPRequest, 
-                                                                     :body =&gt; '{&quot;result&quot;: 3}',
+                                                                     :body =&gt; '{&quot;result&quot;: 30}',
                                                                      :content_type =&gt; 'application/json')))
-    Foo.add(1, 2) { |x| &quot;x is #{x}&quot; }.should == &quot;x is 3&quot;
+    FuBar.retry_strategy(nil) do
+      FuBar.add(10, 20)
+    end
   end
-  
-  
+    
 end</diff>
      <filename>spec/json_rpc_client_spec.rb</filename>
    </modified>
    <modified>
      <diff>@@ -23,34 +23,34 @@ describe JsonRpcService do
       @request.env['HTTP_USER_AGENT'] = nil
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;User-Agent header not specified\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;User-Agent header not specified&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
   
     it &quot;should require a HTTP Accept header specifying JSON&quot; do
       @request.env['HTTP_ACCEPT'] = 'application/text'
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;Accept header must be application\\/json\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;Accept header must be application/json&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
     
     it &quot;should be able to add two strings&quot; do
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: \&quot;1223\&quot;}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;&quot;1223&quot;, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should aggregate multiple occurrences of the same arg name&quot; do
       @request.env['QUERY_STRING'] = 'x=AAA&amp;y=BBB&amp;y=CCCCCC&amp;x=DDDDDD'
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: [\&quot;AAA\&quot;,\&quot;DDDDDD\&quot;,\&quot;BBB\&quot;,\&quot;CCCCCC\&quot;]}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;[&quot;AAA&quot;, &quot;DDDDDD&quot;, &quot;BBB&quot;, &quot;CCCCCC&quot;], &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should handle named args&quot; do
       @request.env['QUERY_STRING'] = 'y=223&amp;x=1'
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: \&quot;1223\&quot;}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;&quot;1223&quot;, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
     
     it &quot;should also be callable using POST&quot; do
@@ -60,7 +60,7 @@ describe JsonRpcService do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;1.1&quot;, &quot;method&quot;: &quot;add&quot;, &quot;params&quot;: [&quot;100&quot;, &quot;33&quot;]}'
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: \&quot;10033\&quot;}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;&quot;10033&quot;, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
   end
@@ -89,69 +89,69 @@ describe JsonRpcService do
       @request.env['QUERY_STRING'] = 'x=12&amp;y=23'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;This method is not idempotent and can only be called using POST.\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;This method is not idempotent and can only be called using POST.&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
   
     it &quot;should require POST data specifying a version&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;method&quot;: &quot;add&quot;, &quot;params&quot;: [10, 25]}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;JSON-RPC client protocol version must be specified in POSTs\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;JSON-RPC client protocol version must be specified in POSTs&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should handle positional arguments&quot; do
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: 67}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;67, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
     
     it &quot;should handle named arguments&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;method&quot;: &quot;sub&quot;, &quot;params&quot;: {&quot;y&quot;: 33, &quot;x&quot;: 100}}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: 67}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;67, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should handle arguments with numerical order&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;method&quot;: &quot;sub&quot;, &quot;params&quot;: {&quot;1&quot;: 33, &quot;0&quot;: 100}}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: 67}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;67, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should handle mixed arguments&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;method&quot;: &quot;sub&quot;, &quot;params&quot;: {&quot;y&quot;: 33, &quot;0&quot;: 100}}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should be_nil
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;result\&quot;: 67}\n&quot;
+      JSON.parse(req.response).should == {&quot;result&quot;=&gt;67, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should require a Content-Type header specifying JSON&quot; do
       @request.env['CONTENT_TYPE'] = 'application/html'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;Content-Type header must be application\\/json\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;Content-Type header must be application/json&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should require POST data specifying the method to call&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;params&quot;: [10, 25]}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;Method not specified\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;Method not specified&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should require POST data specifying an existing method to call&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;method&quot;: &quot;zzz&quot;, &quot;params&quot;: [10, 25]}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;This JSON-RPC service does not provide a 'zzz' method.\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;This JSON-RPC service does not provide a 'zzz' method.&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
     
     it &quot;should barf on missing numerical arguments&quot; do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;method&quot;: &quot;sub&quot;, &quot;params&quot;: [10]}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;The arg y must be numeric (was null)\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;The arg y must be numeric (was null)&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
     it &quot;should barf on excess arguments&quot; do
@@ -164,7 +164,7 @@ describe JsonRpcService do
       @request.env['RAW_POST_DATA'] = '{&quot;version&quot;: &quot;2.99&quot;, &quot;method&quot;: &quot;sub&quot;, &quot;params&quot;: {&quot;y&quot;: 10, &quot;x&quot;: &quot;blah&quot;}}'
       req = FooController.service.process @request, {:method =&gt; ['sub']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;The arg x must be numeric (was \\\&quot;blah\\\&quot;)\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;The arg x must be numeric (was \&quot;blah\&quot;)&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
 
   end
@@ -190,7 +190,7 @@ describe JsonRpcService do
       @request.env['REQUEST_METHOD'] = 'PUT'
       req = FooController.service.process @request, {:method =&gt; ['add']}
       req.status_code.should == 500
-      req.response.should == &quot;{\&quot;version\&quot;: \&quot;1.1\&quot;, \&quot;error\&quot;: {\&quot;code\&quot;:999,\&quot;name\&quot;:\&quot;JSONRPCError\&quot;,\&quot;message\&quot;:\&quot;Only POST and GET supported\&quot;}}\n&quot;
+      JSON.parse(req.response).should == {&quot;error&quot;=&gt;{&quot;message&quot;=&gt;&quot;Only POST and GET supported&quot;, &quot;name&quot;=&gt;&quot;JSONRPCError&quot;, &quot;code&quot;=&gt;999}, &quot;version&quot;=&gt;&quot;1.1&quot;}
     end
     
   end  </diff>
      <filename>spec/json_rpc_service_call_spec.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,11 +1,20 @@
-require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
+begin
+  require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
+rescue LoadError
+  puts &quot;You need to install rspec in your base app&quot;
+  exit
+end
 
 plugin_spec_dir = File.dirname(__FILE__)
 ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + &quot;/debug.log&quot;)
 
+
 require 'spec'
 require 'spec/rails'
 
+load File.expand_path(File.dirname(__FILE__) + '/../lib/json_rpc_client.rb')  # To get rid of the redefinition in laplace
+
+
 class Hash
   
   def except(*keys)
@@ -34,6 +43,7 @@ def stringify_symbols_in_hash(h)
   end
   res
 end
+
 def stringify_symbols_in_array(h)
   res = []
   h.each do |v|</diff>
      <filename>spec/spec_helper.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,7 @@
-# desc &quot;Explaining what the task does&quot;
-# task :json_rpc do
-#   # Task goes here
-# end
+desc &quot;Copies the local version of the json_rpc to the shared plugin for later submission to git&quot;
+task :json_rpc do
+  from = RAILS_ROOT + '/vendor/plugins/json_rpc/*'
+  to = File.expand_path RAILS_ROOT + '/../json_rpc/'
+  `cp -Rf #{from} #{to}`
+  puts &quot;The json_rpc plugin has been updated. You must now submit the changes to git in the usual manner.&quot;
+end</diff>
      <filename>tasks/json_rpc_tasks.rake</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>91ebd7a3c7fbad123314615b95ce15c8a85b7300</id>
    </parent>
  </parents>
  <author>
    <name>Peter Bengtson</name>
    <email>peter@peterbengtson.com</email>
  </author>
  <url>http://github.com/PeterBengtson/json-rpc-1-1/commit/1c0b064b67d4f6f32485eb5098d2bb6fe7067f8f</url>
  <id>1c0b064b67d4f6f32485eb5098d2bb6fe7067f8f</id>
  <committed-date>2009-04-19T01:18:25-07:00</committed-date>
  <authored-date>2009-04-19T01:18:25-07:00</authored-date>
  <message>Many new features: queues, retries, etc.</message>
  <tree>12dba5bca3e49c90c12576ebdcfeef52eefd31dd</tree>
  <committer>
    <name>Peter Bengtson</name>
    <email>peter@peterbengtson.com</email>
  </committer>
</commit>
