resque / resque/resque-scheduler

after_schedule hook is called during handle_delayed_items re-enqueue

Open
#810 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
1.7k
Forks
477
PR merge metrics
No merged PRs in 30d

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_schedule should be called once when a job is scheduled via Resque.enqueue_at
  • before_delayed_enqueue should 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:

  1. before_delayed_enqueue is called
  2. after_schedule is 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with enqueue_from_config in scheduler.rb and process_schedule_hooks in plugin.rb, then run the supplied Ruby reproduction against Redis to confirm the hook counts. Trace how handle_delayed_items re-enqueues the job and verify that scheduling invokes after_schedule once while delayed enqueue invokes before_delayed_enqueue without invoking after_schedule again.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.