faktory_worker_ruby 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +25 -0
- data/LICENSE +165 -0
- data/README.md +91 -0
- data/Rakefile +10 -0
- data/bin/faktory-worker +18 -0
- data/faktory_worker_ruby.gemspec +24 -0
- data/lib/faktory.rb +166 -0
- data/lib/faktory/cli.rb +296 -0
- data/lib/faktory/client.rb +227 -0
- data/lib/faktory/connection.rb +15 -0
- data/lib/faktory/exception_handler.rb +31 -0
- data/lib/faktory/fetch.rb +44 -0
- data/lib/faktory/job.rb +180 -0
- data/lib/faktory/job_logger.rb +24 -0
- data/lib/faktory/launcher.rb +66 -0
- data/lib/faktory/logging.rb +72 -0
- data/lib/faktory/manager.rb +129 -0
- data/lib/faktory/middleware/chain.rb +150 -0
- data/lib/faktory/middleware/i18n.rb +43 -0
- data/lib/faktory/processor.rb +176 -0
- data/lib/faktory/rails.rb +33 -0
- data/lib/faktory/util.rb +62 -0
- data/lib/faktory/version.rb +4 -0
- metadata +132 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Faktory
|
3
|
+
# Middleware is code configured to run before/after
|
4
|
+
# a job is processed. It is patterned after Rack
|
5
|
+
# middleware. Middleware exists for the client side
|
6
|
+
# (pushing jobs to a queue) as well as the worker
|
7
|
+
# side (when jobs are actually executed).
|
8
|
+
#
|
9
|
+
# To add middleware to run when a job is pushed to Faktory:
|
10
|
+
#
|
11
|
+
# Faktory.configure_client do |config|
|
12
|
+
# config.push_middleware do |chain|
|
13
|
+
# chain.add MyClientHook
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# To run middleware when a job is executed within the worker process,
|
18
|
+
# add it to the worker_middleware:
|
19
|
+
#
|
20
|
+
# Faktory.configure_worker do |config|
|
21
|
+
# config.worker_middleware do |chain|
|
22
|
+
# chain.add MyServerHook
|
23
|
+
# chain.remove ActiveRecord
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# To insert immediately preceding another entry:
|
28
|
+
#
|
29
|
+
# Faktory.configure_client do |config|
|
30
|
+
# config.middleware do |chain|
|
31
|
+
# chain.insert_before ActiveRecord, MyClientHook
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# To insert immediately after another entry:
|
36
|
+
#
|
37
|
+
# Faktory.configure_client do |config|
|
38
|
+
# config.middleware do |chain|
|
39
|
+
# chain.insert_after ActiveRecord, MyClientHook
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# This is an example of a minimal worker middleware:
|
44
|
+
#
|
45
|
+
# class MyServerHook
|
46
|
+
# def call(worker_instance, job)
|
47
|
+
# puts "Before work"
|
48
|
+
# yield
|
49
|
+
# puts "After work"
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# This is an example of a minimal client middleware, note
|
54
|
+
# the method must return the result or the job will not push
|
55
|
+
# to Redis:
|
56
|
+
#
|
57
|
+
# class MyClientHook
|
58
|
+
# def call(job, conn_pool)
|
59
|
+
# puts "Before push"
|
60
|
+
# result = yield
|
61
|
+
# puts "After push"
|
62
|
+
# result
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
module Middleware
|
67
|
+
class Chain
|
68
|
+
include Enumerable
|
69
|
+
attr_reader :entries
|
70
|
+
|
71
|
+
def initialize_copy(copy)
|
72
|
+
copy.instance_variable_set(:@entries, entries.dup)
|
73
|
+
end
|
74
|
+
|
75
|
+
def each(&block)
|
76
|
+
entries.each(&block)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize
|
80
|
+
@entries = []
|
81
|
+
yield self if block_given?
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove(klass)
|
85
|
+
entries.delete_if { |entry| entry.klass == klass }
|
86
|
+
end
|
87
|
+
|
88
|
+
def add(klass, *args)
|
89
|
+
remove(klass) if exists?(klass)
|
90
|
+
entries << Entry.new(klass, *args)
|
91
|
+
end
|
92
|
+
|
93
|
+
def prepend(klass, *args)
|
94
|
+
remove(klass) if exists?(klass)
|
95
|
+
entries.insert(0, Entry.new(klass, *args))
|
96
|
+
end
|
97
|
+
|
98
|
+
def insert_before(oldklass, newklass, *args)
|
99
|
+
i = entries.index { |entry| entry.klass == newklass }
|
100
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
101
|
+
i = entries.index { |entry| entry.klass == oldklass } || 0
|
102
|
+
entries.insert(i, new_entry)
|
103
|
+
end
|
104
|
+
|
105
|
+
def insert_after(oldklass, newklass, *args)
|
106
|
+
i = entries.index { |entry| entry.klass == newklass }
|
107
|
+
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
108
|
+
i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
|
109
|
+
entries.insert(i+1, new_entry)
|
110
|
+
end
|
111
|
+
|
112
|
+
def exists?(klass)
|
113
|
+
any? { |entry| entry.klass == klass }
|
114
|
+
end
|
115
|
+
|
116
|
+
def retrieve
|
117
|
+
map(&:make_new)
|
118
|
+
end
|
119
|
+
|
120
|
+
def clear
|
121
|
+
entries.clear
|
122
|
+
end
|
123
|
+
|
124
|
+
def invoke(*args)
|
125
|
+
chain = retrieve.dup
|
126
|
+
traverse_chain = lambda do
|
127
|
+
if chain.empty?
|
128
|
+
yield
|
129
|
+
else
|
130
|
+
chain.shift.call(*args, &traverse_chain)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
traverse_chain.call
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class Entry
|
138
|
+
attr_reader :klass
|
139
|
+
|
140
|
+
def initialize(klass, *args)
|
141
|
+
@klass = klass
|
142
|
+
@args = args
|
143
|
+
end
|
144
|
+
|
145
|
+
def make_new
|
146
|
+
@klass.new(*@args)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# Simple middleware to save the current locale and restore it when the job executes.
|
4
|
+
# Use it by requiring it in your initializer:
|
5
|
+
#
|
6
|
+
# require 'faktory/middleware/i18n'
|
7
|
+
#
|
8
|
+
module Faktory::Middleware::I18n
|
9
|
+
# Get the current locale and store it in the message
|
10
|
+
# to be sent to Faktory.
|
11
|
+
class Client
|
12
|
+
def call(payload, pool)
|
13
|
+
c = payload["custom"] ||= {}
|
14
|
+
c['locale'] ||= ::I18n.locale
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Pull the msg locale out and set the current thread to use it.
|
20
|
+
class Worker
|
21
|
+
def call(jobinst, payload)
|
22
|
+
I18n.locale = payload.dig("custom", "locale") || I18n.default_locale
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
I18n.locale = I18n.default_locale
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Faktory.configure_client do |config|
|
31
|
+
config.client_middleware do |chain|
|
32
|
+
chain.add Faktory::Middleware::I18n::Client
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Faktory.configure_worker do |config|
|
37
|
+
config.client_middleware do |chain|
|
38
|
+
chain.add Faktory::Middleware::I18n::Client
|
39
|
+
end
|
40
|
+
config.worker_middleware do |chain|
|
41
|
+
chain.add Faktory::Middleware::I18n::Worker
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'faktory/util'
|
3
|
+
require 'faktory/fetch'
|
4
|
+
require 'faktory/job_logger'
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
module Faktory
|
8
|
+
##
|
9
|
+
# The Processor is a standalone thread which:
|
10
|
+
#
|
11
|
+
# 1. fetches a job
|
12
|
+
# 2. executes the job
|
13
|
+
# a. instantiate the Worker
|
14
|
+
# b. run the middleware chain
|
15
|
+
# c. call #perform
|
16
|
+
#
|
17
|
+
# A Processor can exit due to shutdown (processor_stopped)
|
18
|
+
# or due to an error during job execution (processor_died)
|
19
|
+
#
|
20
|
+
# If an error occurs in the job execution, the
|
21
|
+
# Processor calls the Manager to create a new one
|
22
|
+
# to replace itself and exits.
|
23
|
+
#
|
24
|
+
class Processor
|
25
|
+
|
26
|
+
include Util
|
27
|
+
|
28
|
+
attr_reader :thread
|
29
|
+
attr_reader :job
|
30
|
+
|
31
|
+
@@busy_lock = Mutex.new
|
32
|
+
@@busy_count = 0
|
33
|
+
def self.busy_count
|
34
|
+
@@busy_count
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(mgr)
|
38
|
+
@mgr = mgr
|
39
|
+
@down = false
|
40
|
+
@done = false
|
41
|
+
@thread = nil
|
42
|
+
@reloader = Faktory.options[:reloader]
|
43
|
+
@logging = (mgr.options[:job_logger] || Faktory::JobLogger).new
|
44
|
+
@fetcher = Faktory::Fetcher.new(Faktory.options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def terminate(wait=false)
|
48
|
+
@done = true
|
49
|
+
return if !@thread
|
50
|
+
@thread.value if wait
|
51
|
+
end
|
52
|
+
|
53
|
+
def kill(wait=false)
|
54
|
+
@done = true
|
55
|
+
return if !@thread
|
56
|
+
# unlike the other actors, terminate does not wait
|
57
|
+
# for the thread to finish because we don't know how
|
58
|
+
# long the job will take to finish. Instead we
|
59
|
+
# provide a `kill` method to call after the shutdown
|
60
|
+
# timeout passes.
|
61
|
+
@thread.raise ::Faktory::Shutdown
|
62
|
+
@thread.value if wait
|
63
|
+
end
|
64
|
+
|
65
|
+
def start
|
66
|
+
@thread ||= safe_thread("processor", &method(:run))
|
67
|
+
end
|
68
|
+
|
69
|
+
private unless $TESTING
|
70
|
+
|
71
|
+
def run
|
72
|
+
begin
|
73
|
+
while !@done
|
74
|
+
process_one
|
75
|
+
end
|
76
|
+
@mgr.processor_stopped(self)
|
77
|
+
rescue Faktory::Shutdown
|
78
|
+
@mgr.processor_stopped(self)
|
79
|
+
rescue Exception => ex
|
80
|
+
@mgr.processor_died(self, ex)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_one
|
85
|
+
work = fetch
|
86
|
+
if work
|
87
|
+
@@busy_lock.synchronize do
|
88
|
+
@@busy_count = @@busy_count + 1
|
89
|
+
end
|
90
|
+
begin
|
91
|
+
process(work)
|
92
|
+
ensure
|
93
|
+
@@busy_lock.synchronize do
|
94
|
+
@@busy_count = @@busy_count - 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
else
|
98
|
+
sleep 1
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def fetch
|
103
|
+
begin
|
104
|
+
work = @fetcher.retrieve_work
|
105
|
+
(logger.info { "Faktory is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
|
106
|
+
work
|
107
|
+
rescue Faktory::Shutdown
|
108
|
+
rescue => ex
|
109
|
+
handle_fetch_exception(ex)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def handle_fetch_exception(ex)
|
114
|
+
if !@down
|
115
|
+
@down = Time.now
|
116
|
+
logger.error("Error fetching job: #{ex}")
|
117
|
+
ex.backtrace.each do |bt|
|
118
|
+
logger.error(bt)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
sleep(1)
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def dispatch(payload)
|
126
|
+
Faktory::Logging.with_job_hash_context(payload) do
|
127
|
+
@logging.call(payload) do
|
128
|
+
# Rails 5 requires a Reloader to wrap code execution. In order to
|
129
|
+
# constantize the worker and instantiate an instance, we have to call
|
130
|
+
# the Reloader. It handles code loading, db connection management, etc.
|
131
|
+
# Effectively this block denotes a "unit of work" to Rails.
|
132
|
+
@reloader.call do
|
133
|
+
klass = constantize(payload['jobtype'.freeze])
|
134
|
+
jobinst = klass.new
|
135
|
+
jobinst.jid = payload['jid'.freeze]
|
136
|
+
yield jobinst
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def process(work)
|
143
|
+
payload = work.job
|
144
|
+
begin
|
145
|
+
dispatch(payload) do |jobinst|
|
146
|
+
Faktory.worker_middleware.invoke(jobinst, payload) do
|
147
|
+
jobinst.perform(*payload['args'.freeze])
|
148
|
+
end
|
149
|
+
end
|
150
|
+
work.acknowledge
|
151
|
+
rescue Faktory::Shutdown
|
152
|
+
# Had to force kill this job because it didn't finish
|
153
|
+
# within the timeout. Don't acknowledge the work since
|
154
|
+
# we didn't properly finish it.
|
155
|
+
rescue Exception => ex
|
156
|
+
handle_exception(ex, { :context => "Job raised exception", :job => job })
|
157
|
+
work.fail(ex)
|
158
|
+
raise ex
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def thread_identity
|
163
|
+
@str ||= Thread.current.object_id.to_s(36)
|
164
|
+
end
|
165
|
+
|
166
|
+
def constantize(str)
|
167
|
+
names = str.split('::')
|
168
|
+
names.shift if names.empty? || names.first.empty?
|
169
|
+
|
170
|
+
names.inject(Object) do |constant, name|
|
171
|
+
constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Faktory
|
3
|
+
class Rails < ::Rails::Engine
|
4
|
+
config.after_initialize do
|
5
|
+
# This hook happens after all initializers are run, just before returning
|
6
|
+
# from config/environment.rb back to faktory/cli.rb.
|
7
|
+
# We have to add the reloader after initialize to see if cache_classes has
|
8
|
+
# been turned on.
|
9
|
+
#
|
10
|
+
# None of this matters on the client-side, only within the Faktory executor itself.
|
11
|
+
#
|
12
|
+
Faktory.configure_exec do |_|
|
13
|
+
Faktory.options[:reloader] = Faktory::Rails::Reloader.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Reloader
|
18
|
+
def initialize(app = ::Rails.application)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
@app.reloader.wrap do
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
"#<Faktory::Rails::Reloader @app=#{@app.class.name}>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end if defined?(::Rails)
|
33
|
+
end
|
data/lib/faktory/util.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'socket'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'faktory/exception_handler'
|
5
|
+
|
6
|
+
module Faktory
|
7
|
+
##
|
8
|
+
# This module is part of Faktory core and not intended for extensions.
|
9
|
+
#
|
10
|
+
module Util
|
11
|
+
include ExceptionHandler
|
12
|
+
|
13
|
+
EXPIRY = 60 * 60 * 24
|
14
|
+
|
15
|
+
def watchdog(last_words)
|
16
|
+
yield
|
17
|
+
rescue Exception => ex
|
18
|
+
handle_exception(ex, { context: last_words })
|
19
|
+
raise ex
|
20
|
+
end
|
21
|
+
|
22
|
+
def safe_thread(name, &block)
|
23
|
+
Thread.new do
|
24
|
+
Thread.current['faktory_label'.freeze] = name
|
25
|
+
watchdog(name, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def logger
|
30
|
+
Faktory.logger
|
31
|
+
end
|
32
|
+
|
33
|
+
def server(&block)
|
34
|
+
Faktory.server(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def hostname
|
38
|
+
ENV['DYNO'] || Socket.gethostname
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_nonce
|
42
|
+
@@process_nonce ||= SecureRandom.hex(6)
|
43
|
+
end
|
44
|
+
|
45
|
+
def identity
|
46
|
+
@@identity ||= "#{hostname}:#{$$}:#{process_nonce}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def fire_event(event, reverse=false)
|
50
|
+
arr = Faktory.options[:lifecycle_events][event]
|
51
|
+
arr.reverse! if reverse
|
52
|
+
arr.each do |block|
|
53
|
+
begin
|
54
|
+
block.call
|
55
|
+
rescue => ex
|
56
|
+
handle_exception(ex, { context: "Exception during Faktory lifecycle event.", event: event })
|
57
|
+
end
|
58
|
+
end
|
59
|
+
arr.clear
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|