jjb / ruby-clock

A ruby job scheduler which runs jobs each in their own thread in a persistent process.
MIT License
78 stars 1 forks source link

Spawning Clockfile jobs inside tests #38

Open jensb opened 1 year ago

jensb commented 1 year ago

Hi, I started using ruby-clock in a Rails project and so far I love it, great job! I have one question though: Is it possible to directly execute specific Clockfile tasks synchronously, as in 'now'? I think this would make a lot of sense for integration testing. For example,

# app/models/user.rb
# ...
def self.create_remote_users
   where(state: 'pending').each do |user|
      if MyRemoteApi.create_user(...) ; user.activate! ; end
   end
end

# Clockfile
every '1 minute' do
    # ... more minutely jobs
    User.create_remote_users
end

# tests/integration/user_integration_test.rb
# this should create a User account
# A cronjob then checks for new Users and creates a user profile on a remote system too
test 'does create remote user too' do
   post '/signup', params: {...}
   assert_response :success
   RubyClock.execute_tasks_for.every '1 minute'       # or RubyClock.execute_tasks_for.cron '0 0 0 * *' , or ...
   assert_equal 'success', MyRemoteApi.login_as(...)
end

This way, I can not just check if my async remote APi job works well (this would be a unit test) but also if I call it at the right point and if it is included in the correct cronjob.

Is something like this possible with ruby-clock? If not, do you think it makes sense to implement it?

jjb commented 1 year ago

Thanks for the feedback and ideas, i've wanted something similar myself!

here's something you can try. it's hacky but i think will work, and then you can give me feedback on the API/ergonomics. when you have something you like, i'll work a nicer api into the next release.

just put this code directly into your test. no need for invoking the clock executable

require 'ruby-clock'
require "ruby-clock/dsl"
RubyClock.detect_and_load_rails_app
require 'rufus_monkeypatch'
RubyClock.instance.listen_to_signals
RubyClock.instance.prepare_rake
RubyClock.instance.schedule.pause
RubyClock.instance.add_rails_executor_to_around_actions
# above is the beginning of exe/clock

# load your Clockfile
load 'Clockfile'

# Now, everything is loaded and paused. You can access all jobs with this.
RubyClock.instance.schedule

# You can iterate through them and check the identifier.
# read about names/identifiers/slugs in the readme
RubyClock.instance.schedule.jobs.each do |j|
  puts j.name
  puts j.identifier
  puts j.slug

  j.call
end
jensb commented 1 year ago

Thank you, this actually worked on the first attempt! Here's what I did.

  1. I put your above code until the load Clockfile into a test/test_helper_rubyclock.rb.
  2. I added a function to select a single clockjob to this file: def run_clockjob(name) = RubyClock.instance.schedule.jobs.find {|j| j.name == name }.call
  3. I require this file in all my integration test files
  4. I gave all my clockjobs explicit names (I found no other safe way to identify them).
  5. When I want to run a clockjob inside one of my tests, I call run_clockjob('every_hour')

This also works on the console:

# Clockfile
using RubyClock::DSL
every '1 minute'  , name: 'every_1min' do Rails.logger.info "Clockfile is active at #{Time.now}" end
$ rails c
Loading development environment (Rails 7.0.7.2)

rb(main):001:0> load 'test/test_helper_clock.rb'
Detected rails app has been loaded.
RUBY_CLOCK_SHUTDOWN_WAIT_SECONDS is set to 29
=> true

irb(main):002:0> RubyClock.instance.schedule.jobs.find {|j| j.name == 'every_1min' }.call
Clockfile is active at 2023-08-27 21:43:04 +0200
=> 49

Maybe we can package this into a require 'ruby-clock/tests' for tests, and then get something like

require 'ruby-clock/tests'
test "see if we can run a clockjob inside here" do
   User.create(...)
   RubyClock.run('create_remote_accounts')     # short, easy, to the point DSL
   assert RemoteApi.login_as(...)
end

What do you think? Also, will this work when calling jobs twice or running tests in parallel? (Rake has issues here)

jjb commented 1 year ago

Nice!! Thanks so much for sharing your code, I will use this to guide me for the next release.

I gave all my clockjobs explicit names (I found no other safe way to identify them)

in case you missed it, you can check in CI that your jobs all have unique slugs, and then you can find by slug instead of name. https://github.com/jjb/ruby-clock/#testing --check-slug-uniqueness

will this work when calling jobs twice or running tests in parallel? (Rake has issues here)

regarding rake, you can use the safest invocation method, rake, to ensure the jobs don't overlap. see the code and article linked in this section https://github.com/jjb/ruby-clock/#rake-tasks

do you also mean at the rufus-scheduler level... i think if your job is threadsafe (instance variables directly in the block are almost certainly safe... but maybe it accesses other classes that aren't threadsafe), then you should be good. let me know if you meant something else.