-
Notifications
You must be signed in to change notification settings - Fork 480
Description
Description
When resque-scheduler processes delayed jobs via handle_delayed_items (moving them from the delayed queue to the regular Resque queue), it calls both before_delayed_enqueue and after_schedule hooks. This is unexpected because after_schedule should only be called when a job is initially scheduled, not when it's being moved to the regular queue. That's also what the Readme states.
Expected Behavior
This is what I would say is expected:
after_scheduleshould be called once when a job is scheduled viaResque.enqueue_atbefore_delayed_enqueueshould be called once when the job is moved from the delayed queue to the regular queue- These hooks should be mutually exclusive - a job is either being scheduled or being enqueued
It could of course also be that I am misunderstanding or missing something.
Actual Behavior
When handle_delayed_items processes a delayed job:
before_delayed_enqueueis calledafter_scheduleis also called (incorrect - the job is not being scheduled, it's being enqueued)
Root Cause
The issue as I see it is in enqueue_from_config (scheduler.rb:334) which wraps the enqueue operation in process_schedule_hooks:
Resque::Scheduler::Plugin.process_schedule_hooks(klass, *params) do
Resque::Scheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
# ... enqueue job
end
and process_schedule_hooks (plugin.rb:6-13) unconditionally calls run_after_schedule_hooks:
def self.process_schedule_hooks(klass, *args)
return unless run_before_schedule_hooks(klass, *args)
yield
run_after_schedule_hooks(klass, *args)
end
Reproduction Script
#!/usr/bin/env ruby
#
# Standalone script to demonstrate a bug in resque-scheduler where the
# `after_schedule` hook is called during `handle_delayed_items`.
#
# Prerequisites:
# Redis running on localhost:6379
#
# Run with: ruby <filename.rb>
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "resque", "~> 2.6"
gem "resque-scheduler", "~> 4.11"
end
require "resque"
require "resque-scheduler"
redis = Redis.new(db: 15)
Resque.redis = redis
redis.flushdb
# Test job that tracks hook calls
class TestHookJob
@queue = :default
@hook_counts = {after_schedule: 0, before_delayed_enqueue: 0}
class << self
attr_reader :hook_counts
end
def self.perform(*args)
puts "Job performed with args: #{args.inspect}"
end
def self.after_schedule(*args)
@hook_counts[:after_schedule] += 1
puts "HOOK: after_schedule called (count: #{@hook_counts[:after_schedule]})"
end
def self.before_delayed_enqueue(*args)
@hook_counts[:before_delayed_enqueue] += 1
puts "HOOK: before_delayed_enqueue called (count: #{@hook_counts[:before_delayed_enqueue]})"
end
end
puts "=== Step 1: Schedule a job 1 hour from now ==="
Resque.enqueue_at(Time.now + 3600, TestHookJob, "test_arg")
puts ""
puts "=== Step 2: Simulate time passing and process delayed items ==="
# ########
# Expected: only before_delayed_enqueue should run here.
# Actual: after_schedule is being called again (bug).
# ########
Resque::Scheduler.handle_delayed_items(Time.now + 4000)
puts ""
puts "=== Summary ==="
puts "Hook call counts:"
puts " after_schedule: #{TestHookJob.hook_counts[:after_schedule]} (expected: 1)"
puts " before_delayed_enqueue: #{TestHookJob.hook_counts[:before_delayed_enqueue]} (expected: 1)"
# Cleanup
redis.flushdb
Output:
=== Step 1: Schedule a job 1 hour from now ===
HOOK: after_schedule called (count: 1)
=== Step 2: Simulate time passing and process delayed items ===
resque-scheduler: [INFO] 2025-12-13T10:37:53+01:00: Processing Delayed Items
HOOK: before_delayed_enqueue called (count: 1)
HOOK: after_schedule called (count: 2)
=== Summary ===
Hook call counts:
after_schedule: 2 (expected: 1)
before_delayed_enqueue: 1 (expected: 1)
I ran this with:
- resque-scheduler version: 4.11.0
- resque version: 2.7.0
- ruby version: 3.4.4
Suggested Fix
I would say enqueue_from_config should not call process_schedule_hooks since the job is being enqueued, not scheduled. It should only call run_before_delayed_enqueue_hooks without the wrapping process_schedule_hooks.
As probably many projects already rely on the hooks being this way, there could as well be a configuration flag
Resque::Scheduler.configure do |config|
config.skip_after_schedule_on_enqueue = true # defaults to false
end
Also maybe a separate after_delayed_enqueue hook could be the solution, additionally to the current after_schedule and before_delayed_enqueue.
Let me know what you think. Maybe this is intended and/or I might misunderstand or miss something.