Skip to content

Commit 2427c97

Browse files
committed
Enable registering of multiple Resque hooks.
Calling `Resque.before_fork {}` would register a hook, but if any other gem, plugin or file also tried to set a before fork hook, the original would be replaced by the new hook. An example of this is the rpm_contrib gem which allows New Relic instrumentation of Resque jobs. The gem defines a `before_first_fork` hook and an `after_fork` hook. This results in conflicts if a user wants to define their own hooks of the same type. This fix allows multiple hooks to be registered and executes all of them instead of simply executing the last registered hook. I've consolidated the related tests into a single file and added some tests to ensure that the hooks are called, called at the right time, and that multiple hooks can be registered.
1 parent 2c874b3 commit 2427c97

File tree

4 files changed

+173
-23
lines changed

4 files changed

+173
-23
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Gemfile.lock
22
doc/
33
test/dump.rdb
4+
test/dump-cluster.rdb

lib/resque.rb

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,47 +85,51 @@ def redis_id
8585
# changes you make will be permanent for the lifespan of the
8686
# worker.
8787
#
88-
# Call with a block to set the hook.
89-
# Call with no arguments to return the hook.
88+
# Call with a block to register a hook.
89+
# Call with no arguments to return all registered hooks.
9090
def before_first_fork(&block)
91-
block ? (@before_first_fork = block) : @before_first_fork
91+
block ? register_hook(:before_first_fork, block) : hooks(:before_first_fork)
9292
end
9393

94-
# Set a proc that will be called in the parent process before the
95-
# worker forks for the first time.
96-
attr_writer :before_first_fork
94+
# Register a before_first_fork proc.
95+
def before_first_fork=(block)
96+
register_hook(:before_first_fork, block)
97+
end
9798

9899
# The `before_fork` hook will be run in the **parent** process
99100
# before every job, so be careful- any changes you make will be
100101
# permanent for the lifespan of the worker.
101102
#
102-
# Call with a block to set the hook.
103-
# Call with no arguments to return the hook.
103+
# Call with a block to register a hook.
104+
# Call with no arguments to return all registered hooks.
104105
def before_fork(&block)
105-
block ? (@before_fork = block) : @before_fork
106+
block ? register_hook(:before_fork, block) : hooks(:before_fork)
106107
end
107108

108-
# Set the before_fork proc.
109-
attr_writer :before_fork
109+
# Register a before_fork proc.
110+
def before_fork=(block)
111+
register_hook(:before_fork, block)
112+
end
110113

111114
# The `after_fork` hook will be run in the child process and is passed
112115
# the current job. Any changes you make, therefore, will only live as
113116
# long as the job currently being processed.
114117
#
115-
# Call with a block to set the hook.
116-
# Call with no arguments to return the hook.
118+
# Call with a block to register a hook.
119+
# Call with no arguments to return all registered hooks.
117120
def after_fork(&block)
118-
block ? (@after_fork = block) : @after_fork
121+
block ? register_hook(:after_fork, block) : hooks(:after_fork)
119122
end
120123

121-
# Set the after_fork proc.
122-
attr_writer :after_fork
124+
# Register an after_fork proc.
125+
def after_fork=(block)
126+
register_hook(:after_fork, block)
127+
end
123128

124129
# The `before_pause` hook will be run in the parent process before the
125130
# worker has paused processing (via #pause_processing or SIGUSR2).
126131
def before_pause(&block)
127-
@before_pause = block if block_given?
128-
@before_pause
132+
block ? register_hook(:before_pause, block) : hooks(:before_pause)
129133
end
130134

131135
# Set the after_pause proc.
@@ -134,8 +138,7 @@ def before_pause(&block)
134138
# The `after_pause` hook will be run in the parent process after the
135139
# worker has paused (via SIGCONT).
136140
def after_pause(&block)
137-
@after_pause = block if block_given?
138-
@after_pause
141+
block ? register_hook(:after_pause, block) : hooks(:after_pause)
139142
end
140143

141144
# Set the after_continue proc.
@@ -403,5 +406,29 @@ def keys
403406
key.sub("#{redis.namespace}:", '')
404407
end
405408
end
409+
410+
private
411+
412+
# Register a new proc as a hook. If the block is nil this is the
413+
# equivalent of removing all hooks of the given name.
414+
#
415+
# `name` is the hook that the block should be registered with.
416+
def register_hook(name, block)
417+
return clear_hooks(name) if block.nil?
418+
419+
@hooks ||= {}
420+
@hooks[name] ||= []
421+
@hooks[name] << block
422+
end
423+
424+
# Clear all hooks given a hook name.
425+
def clear_hooks(name)
426+
@hooks && @hooks[name] = []
427+
end
428+
429+
# Retrieve all hooks of a given name.
430+
def hooks(name)
431+
(@hooks && @hooks[name]) || []
432+
end
406433
end
407434

lib/resque/worker.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,14 @@ def register_worker
425425

426426
# Runs a named hook, passing along any arguments.
427427
def run_hook(name, *args)
428-
return unless hook = Resque.send(name)
429-
msg = "Running #{name} hook"
428+
return unless hooks = Resque.send(name)
429+
msg = "Running #{name} hooks"
430430
msg << " with #{args.inspect}" if args.any?
431431
log msg
432432

433-
args.any? ? hook.call(*args) : hook.call
433+
hooks.each do |hook|
434+
args.any? ? hook.call(*args) : hook.call
435+
end
434436
end
435437

436438
# Unregisters ourself as a worker. Useful when shutting down.

test/resque_hook_test.rb

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
require 'test_helper'
2+
3+
describe "Resque Hooks" do
4+
before do
5+
Resque.redis.flushall
6+
7+
Resque.before_first_fork = nil
8+
Resque.before_fork = nil
9+
Resque.after_fork = nil
10+
11+
@worker = Resque::Worker.new(:jobs)
12+
13+
$called = false
14+
15+
class CallNotifyJob
16+
def self.perform
17+
$called = true
18+
end
19+
end
20+
end
21+
22+
it 'retrieving hooks if none have been set' do
23+
assert_equal [], Resque.before_first_fork
24+
assert_equal [], Resque.before_fork
25+
assert_equal [], Resque.after_fork
26+
end
27+
28+
it 'it calls before_first_fork once' do
29+
counter = 0
30+
31+
Resque.before_first_fork { counter += 1 }
32+
2.times { Resque::Job.create(:jobs, CallNotifyJob) }
33+
34+
assert_equal(0, counter)
35+
@worker.work(0)
36+
assert_equal(1, counter)
37+
end
38+
39+
it 'it calls before_fork before each job' do
40+
counter = 0
41+
42+
Resque.before_fork { counter += 1 }
43+
2.times { Resque::Job.create(:jobs, CallNotifyJob) }
44+
45+
assert_equal(0, counter)
46+
@worker.work(0)
47+
assert_equal(2, counter)
48+
end
49+
50+
it 'it calls after_fork after each job' do
51+
counter = 0
52+
53+
Resque.after_fork { counter += 1 }
54+
2.times { Resque::Job.create(:jobs, CallNotifyJob) }
55+
56+
assert_equal(0, counter)
57+
@worker.work(0)
58+
assert_equal(2, counter)
59+
end
60+
61+
it 'it calls before_first_fork before forking' do
62+
Resque.before_first_fork { assert(!$called) }
63+
64+
Resque::Job.create(:jobs, CallNotifyJob)
65+
@worker.work(0)
66+
end
67+
68+
it 'it calls before_fork before forking' do
69+
Resque.before_fork { assert(!$called) }
70+
71+
Resque::Job.create(:jobs, CallNotifyJob)
72+
@worker.work(0)
73+
end
74+
75+
it 'it calls after_fork after forking' do
76+
Resque.after_fork { assert($called) }
77+
78+
Resque::Job.create(:jobs, CallNotifyJob)
79+
@worker.work(0)
80+
end
81+
82+
it 'it registeres multiple before_first_forks' do
83+
first = false
84+
second = false
85+
86+
Resque.before_first_fork { first = true }
87+
Resque.before_first_fork { second = true }
88+
Resque::Job.create(:jobs, CallNotifyJob)
89+
90+
assert(!first && !second)
91+
@worker.work(0)
92+
assert(first && second)
93+
end
94+
95+
it 'it registers multiple before_forks' do
96+
first = false
97+
second = false
98+
99+
Resque.before_fork { first = true }
100+
Resque.before_fork { second = true }
101+
Resque::Job.create(:jobs, CallNotifyJob)
102+
103+
assert(!first && !second)
104+
@worker.work(0)
105+
assert(first && second)
106+
end
107+
108+
it 'it registers multiple after_forks' do
109+
first = false
110+
second = false
111+
112+
Resque.after_fork { first = true }
113+
Resque.after_fork { second = true }
114+
Resque::Job.create(:jobs, CallNotifyJob)
115+
116+
assert(!first && !second)
117+
@worker.work(0)
118+
assert(first && second)
119+
end
120+
end

0 commit comments

Comments
 (0)