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.
@@ -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