Skip to content

Initial implementation of ActiveJob AsyncAdapter. - #21257

Merged
kaspth merged 1 commit into
rails:masterfrom
jdantonio:async-job
Aug 25, 2015
Merged

Initial implementation of ActiveJob AsyncAdapter.#21257
kaspth merged 1 commit into
rails:masterfrom
jdantonio:async-job

Conversation

@jdantonio

Copy link
Copy Markdown
Contributor

Now that activesupport has a runtime dependency on concurrent-ruby, we can begin taking advantage of those tools in more ways. This PR creates a simple asynchronous ActiveJob adapter that posts jobs to a concurrent-ruby thread pool. Within the context of ActiveJob it provides functionality comparable to sucker_punch. Rails 5 users will now be able to create simple asynchronous jobs without installing additional gems simply by setting the new adapter:

# config/application.rb
module YourApp
  class Application < Rails::Application
    config.active_job.queue_adapter = :async
  end
end

A simple benchmark script which compares enqueue performance vs. sucker_punch can be found here. Performance is comparable on both Ruby 2.2.2 and JRuby 9000.

If this PR is accepted I can add more features such as job prioritization and per-queue thread pools.

@jdantonio

Copy link
Copy Markdown
Contributor Author

NOTE: I think my benchmarks are uninformative. Since I used the test helpers to setup ActiveJob, it looks like both adapters are performing synchronously, not asynchronously. I will gather better benchmarks as soon as I figure out how to setup the test properly.

@jdantonio

Copy link
Copy Markdown
Contributor Author

The benchmark script and associated data have been updated. They now represent asynchronous behavior. Performance is roughly the same for AsyncAdapter and SuckerPunchAdapter.

@rafaelfranca

Copy link
Copy Markdown
Member

Thank you for the pull request but I don't think we should maintain a queue
system inside Rails. There are a lot of good implementations out there that
I think we just end up with a poor implementation inside the framework, to
not say the maintenance overhead that it will give us.

On Sun, Aug 16, 2015, 15:46 jdantonio notifications@github.com wrote:

The benchmark script and associated data have been updated. They now
represent asynchronous behavior. Performance is roughly the same for
AsyncAdapter and SuckerPunchAdapter.


Reply to this email directly or view it on GitHub
#21257 (comment).

@rafaelfranca

Copy link
Copy Markdown
Member

@dhh @tenderlove @matthewd @senny @chancancode what are your thoughts about a default async adapter in Rails itself?

@dhh

dhh commented Aug 16, 2015

Copy link
Copy Markdown
Member

I actually would like to see a default async queue for ActiveJob, mostly for testing. No, you shouldn't use that in production unless you don't care about losing jobs if a process crashes, but it would be nice for testing, so you wouldn't have to install Sucker Punch.

@rafaelfranca

Copy link
Copy Markdown
Member

If that is the goal 👍 too. It should be explicit that it should not be used on production.

@dhh

dhh commented Aug 16, 2015

Copy link
Copy Markdown
Member

Agree. Or only used in production for things with no criticality.

On Sun, Aug 16, 2015 at 5:21 PM, Rafael Mendonça França <
notifications@github.com> wrote:

If that is the goal [image: 👍] too. It should be explicit that it
should not be used on production.


Reply to this email directly or view it on GitHub
#21257 (comment).

@jdantonio

Copy link
Copy Markdown
Contributor Author

I'll begin working on the full feature set tomorrow (Monday). Scheduled tasks (#set(wait: 1.week)) won't survive a restart, but that should be fine for testing. I'll update the guide to be abundantly clear that the :async adapter is for testing only. I'll update this PR when it is feature-complete.

@dhh

dhh commented Aug 17, 2015

Copy link
Copy Markdown
Member

Thanks! Yeah, would be really great to have scheduled tasks work for
testing. Sucker Punch doesn't support those. I had that issue with some
tasks that had a 15s delay that I couldn't test.

On Sun, Aug 16, 2015 at 11:23 PM, jdantonio notifications@github.com
wrote:

I'll begin working on the full feature set tomorrow (Monday). Scheduled
tasks (#set(wait: 1.week)) won't survive a restart, but that should be
fine for testing. I'll update the guide to be abundantly clear that the
:async adapter is for testing only. I'll update this PR when it is
feature-complete.


Reply to this email directly or view it on GitHub
#21257 (comment).

@jdantonio

Copy link
Copy Markdown
Contributor Author

@dhh @rafaelfranca I'm sorry it took me a few days to get back to this, but it's ready now. I've added job scheduling and support for custom queues. I've also updated the documentation to briefly explain why this adapter is for dev/test and not prod.

@cristianbica

Copy link
Copy Markdown
Member

Really nice. I do have 2 suggestions:

  • Follow the model of all the adapters: have a different class (ex: ActiveJob::AsyncJob) that handles the queueing, execution, queue creating and the configurations (ex: executor) and the adapter should just call enqueue / enqueue_at on the AsyncJob class.
  • it seems a bit complicated to have to create the queues upfront. I would rather see the async class create the queue if not exists

@jdantonio

Copy link
Copy Markdown
Contributor Author

@carllerche I'll start work on both of those changes this evening.

@jdantonio

Copy link
Copy Markdown
Contributor Author

@cristianbica This update implements both of your suggestions. Now that ActiveJob::AsyncJob is a separate class I would like to add a set of tests that test it directly, but I'd like to get your feedback on this update before I write those. (The adapter tests still pass.)

@kaspth

kaspth commented Aug 20, 2015

Copy link
Copy Markdown
Contributor

executor might be close to the concurrent-ruby terminology, but it feels very removed from Active Job. It would be great to find a term closer to home.

In fact we might not need to expose that executor information at all. As the adapter seems to have a lot of configuration for something that's mostly for testing. What need is there to change the pool size in testing as long as we're using a minimum of 2?

Comment thread activejob/lib/active_job/async_job.rb Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets do:

queue_name = queue_name.to_sym

hash[queue_name] = \
  if queue_name == :default
    ActiveJob::AsyncJob.default_executor
  else
    ActiveJob::AsyncJob.create_executor
  end

@jdantonio

Copy link
Copy Markdown
Contributor Author

@kaspth I was definitely overthinking things. Running the tests requires that the job runner perform synchronously. The original version was a proof-of-concept that didn't support queues or scheduled tasks, so I simply injected an ImmediateExecutor when running the tests. When I started adding new features I lost sight of the original intent and made things more complicated than necessary.

This update has the same features yet is simpler:

  • ActiveJob::AsyncJob has a set_test_mode! class method that the test helper calls. It is undocumented and irreversible. I expect that it will only be called from the test helper in this repo.
  • All queues, including the default queue, are created with the same configuration.
  • Queues are automatically created the first time a job is post to the queue.
  • Although it's a narrow use case, queues can still be manually created and configured. The only option in this case is to pass a manually created thread pool as an argument. I think we can assume that anyone choosing to manually configure a queue is also capable of manually configuring the thread pool.

This update also includes a set of unit tests for the AsyncJob class.

Comment thread activejob/lib/active_job/async_job.rb Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's Active Job 😁

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also why do we need to run async adapter jobs synchronously in our own tests? Won't that hurt ourselves down the line?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My apologies regarding the gem name. I've fixed that.

I must have misunderstood what was happening in the test helpers. Some of the test helpers set a "test" backend or mode. When I run the tests without a test mode one of the serialization tests fails. I'll try to figure out what's happening there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction--all the adapter tests fail when there is no test mode.

test/adapters/delayed_job.rb:

Delayed::Worker.delay_jobs = false
Delayed::Worker.backend    = :test

test/adapters/qu.rb:

require 'qu-immediate'

test/adapters/resque.rb:

Resque.inline = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries about the name 😁

I'm not sure I follow you. Perhaps we're talking about different things?

My confusion is how the async adapter implements a test mode by not doing what it says on the tin. I can understand if we need the adapter to run synchronously for its own tests, but shouldn't we also have integration tests that mimic how an actual user tests with the adapter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. I think I see what you are saying. If I follow, you are suggesting another set of tests that actually post background jobs then verify that the jobs actually run in the background. Is that correct? That's not a problem. I'll add a set of tests for that.

The latest update is a rebase against the latest Rails master and also incorporates the other suggestions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly that 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaspth I apologize if I'm missing something, but it appears that the file test/integration/queuing_test.rb currently tests the asynchronous behavior of all adapters. It posts jobs with perform_later then uses timers (such as wait_for_jobs_to_finish_for(2.seconds)) to verify that the jobs post. It looks like the adapter tests put all the adapters in synchronous mode, the existing integration tests put the adapters in asynchronous mode, and the Rakefile controls the environment appropriately for each set of tests. However, I didn't notice this earlier so I have not setup the necessary test helper for making the integration tests work with the Async Job. The integration tests (which do not run with rake test) are failing. I'll fix that.

@kaspth

kaspth commented Aug 23, 2015

Copy link
Copy Markdown
Contributor

This just keeps getting better 👏

@jdantonio

Copy link
Copy Markdown
Contributor Author

@kaspth The test cases in test/integration/queuing_test.rb are precisely the type of test you suggested. When I created an appropriate test helper and ran those tests I discovered a localization bug. The latest version has the following changes:

  • Fix the aforementioned locale (serialization) bug
  • Follows the pattern of the other adapters and serializes the job within the adapter
  • Follows the pattern of the other adapters and explicitly passes the queue name from the adapter
  • Adds a test job and AsyncJob unit test case for queue_as
  • Adds a set_normal_mode! method for consistent unit test setup/teardown.

All of the tests unit tests (rake test:async) and integration tests (rake test:integration:async) pass.

Comment thread activejob/test/cases/async_job_test.rb Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✂️ this line

Comment thread activejob/lib/active_job/async_job.rb Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the use case is so very narrow should we really add this method? Generally we'd like to keep as closed off a public interface as possible.

@kaspth

kaspth commented Aug 24, 2015

Copy link
Copy Markdown
Contributor

@robin850 can you check the documentation?

@cristianbica what do you think about this? I'm not too familiar with Active Job's tests, so how are these looking?

Comment thread activejob/lib/active_job/async_job.rb Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a missing "i" in fallback_policy.

@robin850

Copy link
Copy Markdown
Member

Great job @jdantonio! This is looking good! 👏

Comment thread activejob/lib/active_job/async_job.rb Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not synchronize this method?

@jdantonio

Copy link
Copy Markdown
Contributor Author

@kaspth @robin850 Updated based on latest feedback and rebased against latest master.

kaspth added a commit that referenced this pull request Aug 25, 2015
Initial implementation of ActiveJob AsyncAdapter.
@kaspth
kaspth merged commit c5a88e5 into rails:master Aug 25, 2015
@kaspth

kaspth commented Aug 25, 2015

Copy link
Copy Markdown
Contributor

Thanks @jdantonio, this is great 👍

@jdantonio

Copy link
Copy Markdown
Contributor Author

@kaspth Thank you very much for your feedback! This PR is much better because of your help.

@kaspth

kaspth commented Aug 25, 2015

Copy link
Copy Markdown
Contributor

❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants