faktory_worker_ruby 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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