faktory_worker_ruby 0.8.0 → 1.0.3
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 +5 -5
- data/Changes.md +26 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +23 -15
- data/README.md +8 -5
- data/faktory_worker_ruby.gemspec +4 -4
- data/lib/active_job/queue_adapters/faktory_adapter.rb +6 -2
- data/lib/faktory.rb +3 -1
- data/lib/faktory/batch.rb +178 -0
- data/lib/faktory/cli.rb +5 -0
- data/lib/faktory/client.rb +156 -34
- data/lib/faktory/connection.rb +1 -1
- data/lib/faktory/io.rb +51 -0
- data/lib/faktory/job.rb +47 -30
- data/lib/faktory/launcher.rb +10 -5
- data/lib/faktory/logging.rb +1 -1
- data/lib/faktory/manager.rb +3 -0
- data/lib/faktory/middleware/batch.rb +38 -0
- data/lib/faktory/mutate.rb +91 -0
- data/lib/faktory/processor.rb +6 -4
- data/lib/faktory/rails.rb +11 -1
- data/lib/faktory/testing.rb +2 -2
- data/lib/faktory/tracking.rb +41 -0
- data/lib/faktory/version.rb +1 -1
- metadata +20 -16
data/lib/faktory/connection.rb
CHANGED
data/lib/faktory/io.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "io/wait"
|
2
|
+
|
3
|
+
# this is the necessary magic to get a line-oriented protocol to
|
4
|
+
# respect a read timeout. unfortunately Ruby sockets do not provide any
|
5
|
+
# timeout support directly, delegating that to the IO reactor.
|
6
|
+
module Faktory
|
7
|
+
class TimeoutError < Timeout::Error; end
|
8
|
+
|
9
|
+
module ReadTimeout
|
10
|
+
CRLF = "\r\n"
|
11
|
+
BUFSIZE = 16_384
|
12
|
+
|
13
|
+
# Ruby's TCP sockets do not implement timeouts.
|
14
|
+
# We have to implement them ourselves by using
|
15
|
+
# nonblocking IO and IO.select.
|
16
|
+
def initialize(**opts)
|
17
|
+
@buf = "".dup
|
18
|
+
@timeout = opts[:timeout] || 5
|
19
|
+
end
|
20
|
+
|
21
|
+
def gets
|
22
|
+
while (crlf = @buf.index(CRLF)).nil?
|
23
|
+
@buf << read_timeout(BUFSIZE)
|
24
|
+
end
|
25
|
+
|
26
|
+
@buf.slice!(0, crlf + 2)
|
27
|
+
end
|
28
|
+
|
29
|
+
def read(nbytes)
|
30
|
+
result = @buf.slice!(0, nbytes)
|
31
|
+
result << read_timeout(nbytes - result.bytesize) while result.bytesize < nbytes
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def read_timeout(nbytes)
|
37
|
+
loop do
|
38
|
+
result = @sock.read_nonblock(nbytes, exception: false)
|
39
|
+
if result == :wait_readable
|
40
|
+
raise Faktory::TimeoutError unless @sock.wait_readable(@timeout)
|
41
|
+
elsif result == :wait_writable
|
42
|
+
raise Faktory::TimeoutError unless @sock.wait_writeable(@timeout)
|
43
|
+
elsif result == nil
|
44
|
+
raise Errno::ECONNRESET
|
45
|
+
else
|
46
|
+
return result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/faktory/job.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'faktory/tracking'
|
3
|
+
|
2
4
|
module Faktory
|
3
5
|
|
4
6
|
##
|
@@ -20,6 +22,9 @@ module Faktory
|
|
20
22
|
# Note that perform_async is a class method, perform is an instance method.
|
21
23
|
module Job
|
22
24
|
attr_accessor :jid
|
25
|
+
attr_accessor :bid
|
26
|
+
|
27
|
+
include Faktory::Trackable
|
23
28
|
|
24
29
|
def self.included(base)
|
25
30
|
raise ArgumentError, "You cannot include Faktory::Job in an ActiveJob: #{base.name}" if base.ancestors.any? {|c| c.name == 'ActiveJob::Base' }
|
@@ -28,6 +33,16 @@ module Faktory
|
|
28
33
|
base.faktory_class_attribute :faktory_options_hash
|
29
34
|
end
|
30
35
|
|
36
|
+
def self.set(options)
|
37
|
+
Setter.new(options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def batch
|
41
|
+
if bid
|
42
|
+
@batch ||= Faktory::Batch.new(bid)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
31
46
|
def logger
|
32
47
|
Faktory.logger
|
33
48
|
end
|
@@ -42,7 +57,7 @@ module Faktory
|
|
42
57
|
end
|
43
58
|
|
44
59
|
def perform_async(*args)
|
45
|
-
|
60
|
+
client_push(@opts.merge('args'.freeze => args))
|
46
61
|
end
|
47
62
|
|
48
63
|
# +interval+ must be a timestamp, numeric or something that acts
|
@@ -53,34 +68,53 @@ module Faktory
|
|
53
68
|
ts = (int < 1_000_000_000 ? now + int : int)
|
54
69
|
at = Time.at(ts).utc.to_datetime.rfc3339(9)
|
55
70
|
|
56
|
-
@opts.merge
|
71
|
+
item = @opts.merge('args'.freeze => args, 'at'.freeze => at)
|
72
|
+
|
57
73
|
# Optimization to enqueue something now that is scheduled to go out now or in the past
|
58
|
-
|
59
|
-
|
74
|
+
item.delete('at'.freeze) if ts <= now
|
75
|
+
|
76
|
+
client_push(item)
|
60
77
|
end
|
61
78
|
alias_method :perform_at, :perform_in
|
79
|
+
|
80
|
+
def client_push(item) # :nodoc:
|
81
|
+
# stringify
|
82
|
+
item.keys.each do |key|
|
83
|
+
item[key.to_s] = item.delete(key)
|
84
|
+
end
|
85
|
+
item["jid"] ||= SecureRandom.hex(12)
|
86
|
+
item["queue"] ||= "default"
|
87
|
+
|
88
|
+
pool = Thread.current[:faktory_via_pool] || item["pool"] || Faktory.server_pool
|
89
|
+
item.delete("pool")
|
90
|
+
|
91
|
+
# the payload hash is shallow copied by `merge` calls BUT we don't deep clone
|
92
|
+
# the 'custom' child hash which can be problematic if we mutate it within middleware.
|
93
|
+
# Proactively dup it first.
|
94
|
+
item["custom"] = item["custom"].dup if item["custom"]
|
95
|
+
|
96
|
+
Faktory.client_middleware.invoke(item, pool) do
|
97
|
+
pool.with do |c|
|
98
|
+
c.push(item)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
62
102
|
end
|
63
103
|
|
64
104
|
module ClassMethods
|
65
105
|
|
66
106
|
def set(options)
|
67
|
-
Setter.new(options.merge
|
107
|
+
Setter.new(options.merge('jobtype'.freeze => self))
|
68
108
|
end
|
69
109
|
|
70
110
|
def perform_async(*args)
|
71
|
-
|
111
|
+
set(get_faktory_options).perform_async(*args)
|
72
112
|
end
|
73
113
|
|
74
114
|
# +interval+ must be a timestamp, numeric or something that acts
|
75
115
|
# numeric (like an activesupport time interval).
|
76
116
|
def perform_in(interval, *args)
|
77
|
-
|
78
|
-
now = Time.now.to_f
|
79
|
-
ts = (int < 1_000_000_000 ? now + int : int)
|
80
|
-
item = { 'jobtype'.freeze => self, 'args'.freeze => args }
|
81
|
-
|
82
|
-
item['at'] = Time.at(ts).utc.to_datetime.rfc3339(9) if ts > now
|
83
|
-
client_push(item)
|
117
|
+
set(get_faktory_options).perform_in(interval, *args)
|
84
118
|
end
|
85
119
|
alias_method :perform_at, :perform_in
|
86
120
|
|
@@ -102,23 +136,6 @@ module Faktory
|
|
102
136
|
self.faktory_options_hash ||= Faktory.default_job_options
|
103
137
|
end
|
104
138
|
|
105
|
-
def client_push(item) # :nodoc:
|
106
|
-
pool = Thread.current[:faktory_via_pool] || get_faktory_options['pool'.freeze] || Faktory.server_pool
|
107
|
-
item = get_faktory_options.merge(item)
|
108
|
-
# stringify
|
109
|
-
item.keys.each do |key|
|
110
|
-
item[key.to_s] = item.delete(key)
|
111
|
-
end
|
112
|
-
item["jid"] ||= SecureRandom.hex(12)
|
113
|
-
item["queue"] ||= "default"
|
114
|
-
|
115
|
-
Faktory.client_middleware.invoke(item, pool) do
|
116
|
-
pool.with do |c|
|
117
|
-
c.push(item)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
139
|
def faktory_class_attribute(*attrs)
|
123
140
|
instance_reader = true
|
124
141
|
instance_writer = true
|
data/lib/faktory/launcher.rb
CHANGED
@@ -11,7 +11,7 @@ module Faktory
|
|
11
11
|
def initialize(options)
|
12
12
|
merged_options = Faktory.options.merge(options)
|
13
13
|
@manager = Faktory::Manager.new(merged_options)
|
14
|
-
@
|
14
|
+
@current_state = nil
|
15
15
|
@options = merged_options
|
16
16
|
end
|
17
17
|
|
@@ -22,7 +22,7 @@ module Faktory
|
|
22
22
|
|
23
23
|
# Stops this instance from processing any more jobs,
|
24
24
|
def quiet
|
25
|
-
@
|
25
|
+
@current_state = 'quiet'
|
26
26
|
@manager.quiet
|
27
27
|
end
|
28
28
|
|
@@ -32,13 +32,17 @@ module Faktory
|
|
32
32
|
def stop
|
33
33
|
deadline = Time.now + @options[:timeout]
|
34
34
|
|
35
|
-
@
|
35
|
+
@current_state = 'terminate'
|
36
36
|
@manager.quiet
|
37
37
|
@manager.stop(deadline)
|
38
38
|
end
|
39
39
|
|
40
40
|
def stopping?
|
41
|
-
@
|
41
|
+
@current_state == 'terminate'
|
42
|
+
end
|
43
|
+
|
44
|
+
def quiet?
|
45
|
+
@current_state == 'quiet'
|
42
46
|
end
|
43
47
|
|
44
48
|
PROCTITLES = []
|
@@ -50,12 +54,13 @@ module Faktory
|
|
50
54
|
PROCTITLES << proc { title }
|
51
55
|
PROCTITLES << proc { "[#{Processor.busy_count} of #{@options[:concurrency]} busy]" }
|
52
56
|
PROCTITLES << proc { "stopping" if stopping? }
|
57
|
+
PROCTITLES << proc { "quiet" if quiet? }
|
53
58
|
|
54
59
|
loop do
|
55
60
|
$0 = PROCTITLES.map {|p| p.call }.join(" ")
|
56
61
|
|
57
62
|
begin
|
58
|
-
result = Faktory.server {|c| c.beat }
|
63
|
+
result = Faktory.server {|c| c.beat(@current_state) }
|
59
64
|
case result
|
60
65
|
when "OK"
|
61
66
|
# all good
|
data/lib/faktory/logging.rb
CHANGED
@@ -29,7 +29,7 @@ module Faktory
|
|
29
29
|
def self.job_hash_context(job_hash)
|
30
30
|
# If we're using a wrapper class, like ActiveJob, use the "wrapped"
|
31
31
|
# attribute to expose the underlying thing.
|
32
|
-
klass = job_hash
|
32
|
+
klass = job_hash.dig('custom', 'wrapped') || job_hash["jobtype"]
|
33
33
|
"#{klass} JID-#{job_hash['jid']}"
|
34
34
|
end
|
35
35
|
|
data/lib/faktory/manager.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# Simple middleware to save the current batch and restore it when the job executes.
|
4
|
+
#
|
5
|
+
module Faktory::Middleware::Batch
|
6
|
+
class Client
|
7
|
+
def call(payload, pool)
|
8
|
+
b = Thread.current[:faktory_batch]
|
9
|
+
if b
|
10
|
+
payload["custom"] ||= {}
|
11
|
+
payload["custom"]["bid"] = b.bid
|
12
|
+
end
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Worker
|
18
|
+
def call(jobinst, payload)
|
19
|
+
jobinst.bid = payload.dig("custom", "bid")
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Faktory.configure_client do |config|
|
26
|
+
config.client_middleware do |chain|
|
27
|
+
chain.add Faktory::Middleware::Batch::Client
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Faktory.configure_worker do |config|
|
32
|
+
config.client_middleware do |chain|
|
33
|
+
chain.add Faktory::Middleware::Batch::Client
|
34
|
+
end
|
35
|
+
config.worker_middleware do |chain|
|
36
|
+
chain.add Faktory::Middleware::Batch::Worker
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'faktory/client'
|
2
|
+
|
3
|
+
##
|
4
|
+
#
|
5
|
+
# Faktory's MUTATE API allows you to scan the sorted sets
|
6
|
+
# within Redis (retries, scheduled, dead) and take action
|
7
|
+
# (delete, enqueue, kill) on entries.
|
8
|
+
#
|
9
|
+
# require 'faktory/mutate'
|
10
|
+
# cl = Faktory::Client.new
|
11
|
+
# cl.discard(Faktory::RETRIES) do |filter|
|
12
|
+
# filter.with_type("QuickBooksSyncJob")
|
13
|
+
# filter.matching("*uid:12345*"))
|
14
|
+
# end
|
15
|
+
module Faktory
|
16
|
+
|
17
|
+
# Valid targets
|
18
|
+
RETRIES = "retries"
|
19
|
+
SCHEDULED = "scheduled"
|
20
|
+
DEAD = "dead"
|
21
|
+
|
22
|
+
module Mutator
|
23
|
+
class Filter
|
24
|
+
attr_accessor :hash
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@hash = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# This must be the exact type of the job, no pattern matching
|
31
|
+
def with_type(jobtype)
|
32
|
+
@hash[:jobtype] = jobtype
|
33
|
+
end
|
34
|
+
|
35
|
+
# This is a regexp that will be passed as is to Redis's SCAN.
|
36
|
+
# Notably you should surround it with * to ensure it matches
|
37
|
+
# substrings within the job payload.
|
38
|
+
# See https://redis.io/commands/scan for details.
|
39
|
+
def matching(regexp)
|
40
|
+
@hash[:regexp] = regexp
|
41
|
+
end
|
42
|
+
|
43
|
+
# One or more JIDs to target:
|
44
|
+
# filter.jids << 'abcdefgh1234'
|
45
|
+
# filter.jids = ['abcdefgh1234', '1234567890']
|
46
|
+
def jids
|
47
|
+
@hash[:jids] ||= []
|
48
|
+
end
|
49
|
+
def jids=(ary)
|
50
|
+
@hash[:jids] = Array(ary)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def discard(target, &block)
|
55
|
+
filter = Filter.new
|
56
|
+
block.call(filter) if block
|
57
|
+
mutate('discard', target, filter)
|
58
|
+
end
|
59
|
+
|
60
|
+
def kill(target, &block)
|
61
|
+
filter = Filter.new
|
62
|
+
block.call(filter) if block
|
63
|
+
mutate('kill', target, filter)
|
64
|
+
end
|
65
|
+
|
66
|
+
def requeue(target, &block)
|
67
|
+
filter = Filter.new
|
68
|
+
block.call(filter) if block
|
69
|
+
mutate('requeue', target, filter)
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear(target)
|
73
|
+
mutate('discard', target, nil)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def mutate(cmd, target, filter)
|
79
|
+
payload = {:cmd => cmd,:target => target}
|
80
|
+
payload[:filter] = filter.hash if filter && !filter.hash.empty?
|
81
|
+
|
82
|
+
transaction do
|
83
|
+
command("MUTATE", JSON.dump(payload))
|
84
|
+
ok
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
Faktory::Client.send(:include, Faktory::Mutator)
|
data/lib/faktory/processor.rb
CHANGED
@@ -88,6 +88,7 @@ module Faktory
|
|
88
88
|
@@busy_count = @@busy_count + 1
|
89
89
|
end
|
90
90
|
begin
|
91
|
+
@job = work.job
|
91
92
|
process(work)
|
92
93
|
ensure
|
93
94
|
@@busy_lock.synchronize do
|
@@ -148,10 +149,11 @@ module Faktory
|
|
148
149
|
end
|
149
150
|
end
|
150
151
|
work.acknowledge
|
151
|
-
rescue Faktory::Shutdown
|
152
|
-
# Had to force kill this job because it didn't finish
|
153
|
-
#
|
154
|
-
#
|
152
|
+
rescue Faktory::Shutdown => shut
|
153
|
+
# Had to force kill this job because it didn't finish within
|
154
|
+
# the timeout. Fail it so we can release any locks server-side
|
155
|
+
# and immediately restart it.
|
156
|
+
work.fail(shut)
|
155
157
|
rescue Exception => ex
|
156
158
|
handle_exception(ex, { :context => "Job raised exception", :job => work.job })
|
157
159
|
work.fail(ex)
|
data/lib/faktory/rails.rb
CHANGED
@@ -23,9 +23,19 @@ module Faktory
|
|
23
23
|
if ::Rails::VERSION::MAJOR < 5
|
24
24
|
raise "Your current version of Rails, #{::Rails::VERSION::STRING}, is not supported"
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
Faktory.options[:reloader] = Faktory::Rails::Reloader.new
|
28
28
|
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
# https://github.com/rails/rails/pull/41248
|
32
|
+
if defined?(::Mail::SMTP)
|
33
|
+
::Mail::SMTP::DEFAULTS[:read_timeout] ||= 5
|
34
|
+
::Mail::SMTP::DEFAULTS[:open_timeout] ||= 5
|
35
|
+
end
|
36
|
+
rescue => ex
|
37
|
+
# ignore
|
38
|
+
end
|
29
39
|
end
|
30
40
|
|
31
41
|
class Reloader
|