神刀安全网

Solving Backwards Compatibility in Ruby with a Proxy Object

byBrandon Hilkert | No Comments

This article was originally published on Brandon Hilkert’s personal site, and with their permission, we’re sharing it here for Codeship readers.

In a previous article, I documented the upcoming public API changes slated for Sucker Punch v2 . Because of a poor initial design, these API changes are backwards incompatible .

When I published the previous article, Mike Perham rightly pointed out the opportunity to support the previous versions’s API through an opt-in module . I was hesitant to include support for the old syntax by default, but allowing a developer to require a file to get the old syntax made complete sense to me. My intent was never to abandon existing Sucker Punch users, but it felt necessary for the success of the project going forward.

The Problem

The following is an example of enqueueing a background job with Sucker Punch using the old syntax:

LogJob.new.async.perform("new_user")

And with the new syntax:

LogJob.perform_async("new_user")

How do we support the old syntax in the new version?

Let’s step back and reminder ourselves of what a typical job class looks like:

class LogJob   include SuckerPunch::Job    def perform(event)     Log.new(event).track   end end

Important points to notice:

  1. Each job includes the SuckerPunch::Job module to gain access to asynchronous behavior
  2. Each job executes its logic using the perform instance method
  3. Each job passes arguments needed for its logic as arguments to the perform instance method

The Solution

We’ll start with the test:

# test/sucker_punch/async_syntax_test.rb  require 'test_helper'  module SuckerPunch   class AsyncSyntaxTest < Minitest::Test     def setup       require 'sucker_punch/async_syntax'     end      def test_perform_async_runs_job_asynchronously       arr = []       latch = Concurrent::CountDownLatch.new       FakeLatchJob.new.async.perform(arr, latch)       latch.wait(0.2)       assert_equal 1, arr.size     end      private      class FakeLatchJob       include SuckerPunch::Job        def perform(arr, latch)         arr.push true         latch.count_down       end     end   end end

Note: Some details of this are complex because the job’s code is running in another thread. I’ll walk through those details in a future article.

The basic sequence is: 1. require sucker_punch/async_syntax 2. Execute a background job using the async syntax 3. Assert changes made in that job were successful

Running the tests above, we get the following error:

1) Error: SuckerPunch::AsyncSyntaxTest#test_perform_async_runs_job_asynchronously: LoadError: cannot load such file -- sucker_punch/async_syntax /Users/bhilkert/Dropbox/code/sucker_punch/test/sucker_punch/async_syntax_test.rb:6:in `require'   /Users/bhilkert/Dropbox/code/sucker_punch/test/sucker_punch/async_syntax_test.rb:6:in`setup'  1 runs, 0 assertions, 0 failures, 1 errors, 0 skips 

Ok, so the file doesn’t exist. Let’s create it and re-run the tests:

1) Error: SuckerPunch::AsyncSyntaxTest#test_perform_async_runs_job_asynchronously: NoMethodError: undefined method `async' for #<SuckerPunch::AsyncSyntaxTest::FakeLatchJob:0x007fbc73cbf548>   /Users/bhilkert/Dropbox/code/sucker_punch/test/sucker_punch/async_syntax_test.rb:12:in `test_perform_async_runs_job_asynchronously' 

Progress! The job doesn’t have an async method. Let’s add it:

module SuckerPunch   module Job     def async # <--- New method     end   end end

Notice: We’re monkey-patching the SuckerPunch::Job module. This will allow us to add methods to the background job since it’s included in the job.

The tests now:

1) Error: SuckerPunch::AsyncSyntaxTest#test_perform_async_runs_job_asynchronously: NoMethodError: undefined method `perform' for nil:NilClass   /Users/bhilkert/Dropbox/code/sucker_punch/test/sucker_punch/async_syntax_test.rb:12:in `test_perform_async_runs_job_asynchronously'

More progress…the async method we added returns nil, and because of the syntax async.perform , there’s no perform method on the output of async . In short, we need to return something from async that responds to perform and can run the job.

In its most basic form, suppose we create a proxy object that responds to perform :

class AsyncProxy   def perform   end end

We’ll need to do some work in perform to execute the job, but this’ll do for now. Now, let’s integrate this new proxy to our async_syntax.rb file and return a new instance of the proxy from the async method:

module SuckerPunch   module Job     def async       AsyncProxy.new # <--- new instance of the proxy     end   end    class AsyncProxy     def perform     end   end end

Running our tests gives us the following:

1) Error: SuckerPunch::AsyncSyntaxTest#test_perform_async_runs_job_asynchronously: ArgumentError: wrong number of arguments (2 for 0)   /Users/bhilkert/Dropbox/code/sucker_punch/lib/sucker_punch/async_syntax.rb:9:in `perform'   /Users/bhilkert/Dropbox/code/sucker_punch/test/sucker_punch/async_syntax_test.rb:12:in `test_perform_async_runs_job_asynchronously'

Now we’re on to something. We see an error related to the number of arguments on the perform method. Because each job’s argument list will be different, we need to find a way to be flexible for whatever’s passed in, something like…the splat operator! Let’s try it:

module SuckerPunch   module Job     def async       AsyncProxy.new     end   end    class AsyncProxy     def perform(*args) # <--- Adding the splat operator, will handle any # of args     end   end end

The tests now:

1) Failure: SuckerPunch::AsyncSyntaxTest#test_perform_async_runs_job_asynchronously [/Users/bhilkert/Dropbox/code/sucker_punch/test/sucker_punch/async_syntax_test.rb:14]: Expected: 1 Actual: 0

At this point, we’ve reached the end of test output suggesting the path forward. This error is saying, “Your assertions failed.”. This is good because it means our syntax implementation will work and it’s just about executing the actual job code in the proxy’s perform method.

We want to leverage our new syntax ( perform_async ) to run the actual job asynchronously so it passes through the standard code path. To do so, we’ll need a reference to the original job in the proxy object. Let’s pass that to the proxy during instantiation:

module SuckerPunch   module Job     def async       AsyncProxy.new(self) # <--- Pass the job instance     end   end    class AsyncProxy     def initialize(job) # <--- Handle job passed in       @job = job     end      def perform(*args)     end   end end

Now that the proxy has a reference to the job instance, we can call the perform_async class method to execute the job:

module SuckerPunch   module Job     def async       AsyncProxy.new(self)     end   end    class AsyncProxy     def initialize(job)       @job = job     end      def perform(*args)       @job.class.perform_async(*args) # <---- Execute the job     end   end end

Lastly, the tests:

press ENTER or type command to continue bundle exec rake test TEST="test/sucker_punch/async_syntax_test.rb" Run options: --seed 43886  # Running:  .  1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Success!

Just like that, new users of Sucker Punch will be able to add require 'sucker_punch/async_syntax' to their projects to use the old syntax. This will allow existing projects using Sucker Punch to take advantage of the reworked internals without the need to make sweeping changes to the enqueueing syntax.

Support for the old syntax will be available for foreseeable future via this include. All new code/applications should use the new syntax going forward.

Conclusion

Before realizing a proxy object would work, I tinkered with alias_method and a handful of other approaches to latching on to the job’s perform method and saving it off to execute later. While some combinations of these might have worked, the proxy object solution is simple and elegant. There’s no magic, which means less maintenance going forward. The last thing I want is to make a breaking change, add support for the old syntax and find the support to be bug-ridden.

Ruby is incredibly flexible. Sometimes a 9-line class is enough to get the job done without reaching for an overly complex metaprogramming approach.

Having said all that, Sucker Punch v2 has been released !

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Solving Backwards Compatibility in Ruby with a Proxy Object

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址