faktory_worker_ruby 0.8.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Changes.md +8 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +16 -11
- data/faktory_worker_ruby.gemspec +2 -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 +150 -29
- data/lib/faktory/connection.rb +1 -1
- data/lib/faktory/job.rb +41 -29
- data/lib/faktory/launcher.rb +10 -5
- data/lib/faktory/manager.rb +3 -0
- data/lib/faktory/middleware/batch.rb +38 -0
- data/lib/faktory/mutate.rb +85 -0
- data/lib/faktory/processor.rb +6 -4
- data/lib/faktory/testing.rb +2 -2
- data/lib/faktory/tracking.rb +41 -0
- data/lib/faktory/version.rb +1 -1
- metadata +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 52c0a1a508d1d2451f4c350b71f0d4091e7c134b8c4b57b802023e0f1527bd1d
|
4
|
+
data.tar.gz: 98fedca04b49b7ab68d8ce398099cfeb92b74d4bd4d3b8951c4f2a15d8d27d10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3a1766171e19dd6750a558dd93eb4734529412674aac18f18b792c716643d5d09e668f0e5ddaf5b291a0c861729a9a29a7918db6b56ac314bbd6d761ab31b6f
|
7
|
+
data.tar.gz: 5c1f3c6d16d42ecb2e90e82ff926d147526ce483cc5b4ac178459935e4b22c6bf4e9816038f82fa317871c53df54ee631a817ebbfc47c42f85a519e12cccee55
|
data/Changes.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Changes
|
2
2
|
|
3
|
+
## 1.0.0
|
4
|
+
|
5
|
+
- Ruby 2.5+ is now required
|
6
|
+
- Support for Faktory Enterprise, job batches and job tracking
|
7
|
+
- Support for the MUTATE command.
|
8
|
+
- Notify Faktory when a worker process is going quiet so that the UI shows this
|
9
|
+
- Refactor Faktory::Client error handling for faktory#208
|
10
|
+
|
3
11
|
## 0.8.1
|
4
12
|
|
5
13
|
- Fix breakage with non-ActiveJobs [#29]
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,30 +1,37 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
faktory_worker_ruby (0.
|
5
|
-
connection_pool (~> 2.2, >= 2.2.
|
4
|
+
faktory_worker_ruby (1.0.0)
|
5
|
+
connection_pool (~> 2.2, >= 2.2.2)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activejob (5.2.
|
11
|
-
activesupport (= 5.2.
|
10
|
+
activejob (5.2.2)
|
11
|
+
activesupport (= 5.2.2)
|
12
12
|
globalid (>= 0.3.6)
|
13
|
-
activesupport (5.2.
|
13
|
+
activesupport (5.2.2)
|
14
14
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
15
15
|
i18n (>= 0.7, < 2)
|
16
16
|
minitest (~> 5.1)
|
17
17
|
tzinfo (~> 1.1)
|
18
|
-
concurrent-ruby (1.
|
18
|
+
concurrent-ruby (1.1.4)
|
19
19
|
connection_pool (2.2.2)
|
20
|
-
|
20
|
+
docile (1.3.1)
|
21
|
+
globalid (0.4.2)
|
21
22
|
activesupport (>= 4.2.0)
|
22
|
-
i18n (1.
|
23
|
+
i18n (1.5.3)
|
23
24
|
concurrent-ruby (~> 1.0)
|
25
|
+
json (2.1.0)
|
24
26
|
minitest (5.11.3)
|
25
27
|
minitest-hooks (1.5.0)
|
26
28
|
minitest (> 5.3)
|
27
29
|
rake (12.3.1)
|
30
|
+
simplecov (0.16.1)
|
31
|
+
docile (~> 1.1)
|
32
|
+
json (>= 1.8, < 3)
|
33
|
+
simplecov-html (~> 0.10.0)
|
34
|
+
simplecov-html (0.10.2)
|
28
35
|
thread_safe (0.3.6)
|
29
36
|
tzinfo (1.2.5)
|
30
37
|
thread_safe (~> 0.1)
|
@@ -38,6 +45,4 @@ DEPENDENCIES
|
|
38
45
|
minitest (~> 5)
|
39
46
|
minitest-hooks
|
40
47
|
rake (~> 12)
|
41
|
-
|
42
|
-
BUNDLED WITH
|
43
|
-
1.17.1
|
48
|
+
simplecov
|
data/faktory_worker_ruby.gemspec
CHANGED
@@ -14,9 +14,9 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.files = `git ls-files | grep -Ev '^(test|myapp|examples)'`.split("\n")
|
15
15
|
gem.test_files = []
|
16
16
|
gem.version = Faktory::VERSION
|
17
|
-
gem.required_ruby_version = ">= 2.
|
17
|
+
gem.required_ruby_version = ">= 2.5.0"
|
18
18
|
|
19
|
-
gem.add_dependency 'connection_pool', '~> 2.2', ">= 2.2.
|
19
|
+
gem.add_dependency 'connection_pool', '~> 2.2', ">= 2.2.2"
|
20
20
|
gem.add_development_dependency 'activejob', '>= 5.1.5'
|
21
21
|
gem.add_development_dependency 'minitest', '~> 5'
|
22
22
|
gem.add_development_dependency 'minitest-hooks'
|
data/lib/faktory.rb
CHANGED
@@ -52,6 +52,7 @@ module Faktory
|
|
52
52
|
# config.worker_middleware do |chain|
|
53
53
|
# chain.add MyServerHook
|
54
54
|
# end
|
55
|
+
# config.default_job_options = { retry: 3 }
|
55
56
|
# end
|
56
57
|
def self.configure_worker
|
57
58
|
yield self if worker?
|
@@ -61,7 +62,7 @@ module Faktory
|
|
61
62
|
# Configuration for Faktory client, use like:
|
62
63
|
#
|
63
64
|
# Faktory.configure_client do |config|
|
64
|
-
# config.
|
65
|
+
# config.default_job_options = { retry: 3 }
|
65
66
|
# end
|
66
67
|
def self.configure_client
|
67
68
|
yield self unless worker?
|
@@ -164,3 +165,4 @@ module Faktory
|
|
164
165
|
end
|
165
166
|
|
166
167
|
require 'faktory/rails' if defined?(::Rails::Engine)
|
168
|
+
require 'faktory/batch'
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require "faktory/middleware/batch"
|
2
|
+
|
3
|
+
module Faktory
|
4
|
+
##
|
5
|
+
# A Batch is a set of jobs which can be tracked as a group, with
|
6
|
+
# callbacks that can fire after all the jobs are attempted or successful.
|
7
|
+
# Every batch must define at least one callback.
|
8
|
+
#
|
9
|
+
# * The "complete" callback is fired when all jobs in the batch have been attempted.
|
10
|
+
# Some might have failed.
|
11
|
+
# * The "success" callback is fired when all jobs in the batch have succeeded. This
|
12
|
+
# might never be fired if a job continues to error until it runs out of retries.
|
13
|
+
#
|
14
|
+
# **Please note that batches are only available in Faktory Enterprise.** This is
|
15
|
+
# the client-side code required to implement batches, it won't work without
|
16
|
+
# the server-side component.
|
17
|
+
#
|
18
|
+
# Simple example:
|
19
|
+
#
|
20
|
+
# b = Faktory::Batch.new
|
21
|
+
# b.description = "Process all documents for user 12345"
|
22
|
+
# # a callback can be defined as just a Ruby job class
|
23
|
+
# b.success = "MySuccessCallbackJob"
|
24
|
+
# # or the full job hash...
|
25
|
+
# b.complete = { jobtype: "MyCompleteCallbackJob", args: [12345], queue: "critical" }
|
26
|
+
# b.jobs do
|
27
|
+
# SomeJob.perform_async(xyz)
|
28
|
+
# AnotherJob.perform_async(user_id)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# At the end of the `jobs` call, the batch is persisted to the Faktory server. It must
|
32
|
+
# not be modified further with one exception: jobs within the batch can "reopen" the batch
|
33
|
+
# in order to dynamically add more jobs or child batches.
|
34
|
+
#
|
35
|
+
# Any job within a batch may "reopen" its own batch to dynamically add more jobs.
|
36
|
+
# A job can get access to its batch by using the `bid` or `batch` accessor on
|
37
|
+
# `Faktory::Job`. You can use the `bid` accessor to test if the job is part of a batch.
|
38
|
+
#
|
39
|
+
# Reopen example:
|
40
|
+
#
|
41
|
+
# class MyJob
|
42
|
+
# include Faktory::Job
|
43
|
+
#
|
44
|
+
# def perform
|
45
|
+
# batch.jobs do
|
46
|
+
# SomeOtherJob.perform_async
|
47
|
+
# end if bid
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Batches may be nested without limit by setting `parent_bid` when creating a
|
51
|
+
# batch. Generally you create child batches if you wish that subset of jobs to have
|
52
|
+
# their own callback for your application logic purposes. Otherwise you can reopen the
|
53
|
+
# current batch and add more jobs.
|
54
|
+
#
|
55
|
+
# Batch parent/child relationship is never implicit: you must manually set
|
56
|
+
# `parent_bid` if you wish to define a child batch.
|
57
|
+
#
|
58
|
+
# Nested example:
|
59
|
+
#
|
60
|
+
# class MyJob
|
61
|
+
# include Faktory::Job
|
62
|
+
#
|
63
|
+
# def perform
|
64
|
+
# child = Faktory::Batch.new
|
65
|
+
#
|
66
|
+
# # MyJob is executing as part of a previously defined batch.
|
67
|
+
# # Add a new child batch to this batch.
|
68
|
+
# child.parent_bid = bid
|
69
|
+
# child.success = ...
|
70
|
+
# child.jobs do |cb|
|
71
|
+
# SomeJob.perform_async
|
72
|
+
#
|
73
|
+
# gchild = Faktory::Batch.new
|
74
|
+
# gchild.parent_bid = cb.bid
|
75
|
+
# gchild.success = ...
|
76
|
+
# gchild.jobs do |gcb|
|
77
|
+
# ChildJob.perform_async
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# Callbacks are guaranteed to be called hierarchically: child's success callback
|
84
|
+
# will not be called until gchild's success callback has executed successfully.
|
85
|
+
#
|
86
|
+
class Batch
|
87
|
+
attr_reader :bid
|
88
|
+
attr_accessor :description, :parent_bid
|
89
|
+
|
90
|
+
def initialize(bid=nil)
|
91
|
+
@bid = bid
|
92
|
+
end
|
93
|
+
|
94
|
+
def success=(val)
|
95
|
+
raise "Batch cannot be modified once created" if bid
|
96
|
+
@success = to_callback(val)
|
97
|
+
end
|
98
|
+
|
99
|
+
def complete=(val)
|
100
|
+
raise "Batch cannot be modified once created" if bid
|
101
|
+
@success = to_callback(val)
|
102
|
+
end
|
103
|
+
|
104
|
+
def jobs(&block)
|
105
|
+
Faktory.server do |client|
|
106
|
+
if @bid.nil?
|
107
|
+
@bid = client.create_batch(self, &block)
|
108
|
+
else
|
109
|
+
client.reopen_batch(self, &block)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_h
|
115
|
+
raise ArgumentError, "Callback required" unless defined?(@success) || defined?(@complete)
|
116
|
+
|
117
|
+
hash = {}
|
118
|
+
hash["parent_bid"] = parent_bid if parent_bid
|
119
|
+
hash["description"] = description if description
|
120
|
+
hash["success"] = @success if defined?(@success)
|
121
|
+
hash["complete"] = @complete if defined?(@complete)
|
122
|
+
hash
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def to_callback(val)
|
128
|
+
case val
|
129
|
+
when String
|
130
|
+
basic_job.merge({ "jobtype" => val })
|
131
|
+
when Class
|
132
|
+
basic_job.merge({ "jobtype" => val })
|
133
|
+
when Hash
|
134
|
+
basic_job.merge(val)
|
135
|
+
else
|
136
|
+
raise ArgumentError, "Unknown callback #{val}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def basic_job
|
141
|
+
{
|
142
|
+
"jid" => SecureRandom.hex(12),
|
143
|
+
"args" => [],
|
144
|
+
"queue" => "default",
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class BatchStatus
|
150
|
+
def initialize(bid)
|
151
|
+
@bid = bid
|
152
|
+
end
|
153
|
+
|
154
|
+
def hash
|
155
|
+
@hash ||= Faktory.server{|c| c.batch_status(@bid) }
|
156
|
+
end
|
157
|
+
|
158
|
+
def created_at
|
159
|
+
hash["created_at"]
|
160
|
+
end
|
161
|
+
|
162
|
+
def description
|
163
|
+
hash["description"]
|
164
|
+
end
|
165
|
+
|
166
|
+
def parent_bid
|
167
|
+
hash["parent_bid"]
|
168
|
+
end
|
169
|
+
|
170
|
+
def total
|
171
|
+
hash["total"]
|
172
|
+
end
|
173
|
+
|
174
|
+
def pending
|
175
|
+
hash["pending"]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/faktory/cli.rb
CHANGED
data/lib/faktory/client.rb
CHANGED
@@ -5,12 +5,20 @@ require 'digest'
|
|
5
5
|
require 'securerandom'
|
6
6
|
|
7
7
|
module Faktory
|
8
|
-
class
|
9
|
-
class
|
10
|
-
|
8
|
+
class BaseError < StandardError; end
|
9
|
+
class CommandError < BaseError; end
|
10
|
+
class ParseError < BaseError; end
|
11
|
+
|
12
|
+
# Faktory::Client provides a low-level connection to a Faktory server
|
13
|
+
# and APIs which map to Faktory commands.
|
14
|
+
#
|
15
|
+
# Most APIs will return `true` if the operation succeeded or raise a
|
16
|
+
# Faktory::BaseError if there was an unexpected error.
|
11
17
|
class Client
|
12
18
|
@@random_process_wid = ""
|
13
19
|
|
20
|
+
DEFAULT_TIMEOUT = 5.0
|
21
|
+
|
14
22
|
HASHER = proc do |iter, pwd, salt|
|
15
23
|
sha = Digest::SHA256.new
|
16
24
|
hashing = pwd + salt
|
@@ -36,10 +44,12 @@ module Faktory
|
|
36
44
|
# MY_FAKTORY_URL=tcp://:somepass@my-server.example.com:7419
|
37
45
|
#
|
38
46
|
# Note above, the URL can contain the password for secure installations.
|
39
|
-
def initialize(url: uri_from_env || 'tcp://localhost:7419', debug: false)
|
47
|
+
def initialize(url: uri_from_env || 'tcp://localhost:7419', debug: false, timeout: DEFAULT_TIMEOUT)
|
40
48
|
@debug = debug
|
41
49
|
@location = URI(url)
|
42
|
-
|
50
|
+
@timeout = timeout
|
51
|
+
|
52
|
+
open(@timeout)
|
43
53
|
end
|
44
54
|
|
45
55
|
def close
|
@@ -53,23 +63,95 @@ module Faktory
|
|
53
63
|
def flush
|
54
64
|
transaction do
|
55
65
|
command "FLUSH"
|
56
|
-
ok
|
66
|
+
ok
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_batch(batch, &block)
|
71
|
+
bid = transaction do
|
72
|
+
command "BATCH NEW", Faktory.dump_json(batch.to_h)
|
73
|
+
result!
|
74
|
+
end
|
75
|
+
batch.instance_variable_set(:@bid, bid)
|
76
|
+
|
77
|
+
old = Thread.current["faktory_batch"]
|
78
|
+
Thread.current["faktory_batch"] = batch
|
79
|
+
begin
|
80
|
+
# any jobs pushed in this block will implicitly have
|
81
|
+
# their `bid` attribute set so they are associated
|
82
|
+
# with the current batch.
|
83
|
+
yield batch
|
84
|
+
ensure
|
85
|
+
Thread.current[:faktory_batch] = old
|
86
|
+
end
|
87
|
+
transaction do
|
88
|
+
command "BATCH COMMIT", bid
|
89
|
+
ok
|
57
90
|
end
|
91
|
+
bid
|
58
92
|
end
|
59
93
|
|
94
|
+
def batch_status(bid)
|
95
|
+
transaction do
|
96
|
+
command "BATCH STATUS", bid
|
97
|
+
Faktory.load_json result!
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def reopen_batch(b)
|
102
|
+
transaction do
|
103
|
+
command "BATCH OPEN", b.bid
|
104
|
+
ok
|
105
|
+
end
|
106
|
+
old = Thread.current[:faktory_batch]
|
107
|
+
Thread.current[:faktory_batch] = b
|
108
|
+
begin
|
109
|
+
# any jobs pushed in this block will implicitly have
|
110
|
+
# their `bid` attribute set so they are associated
|
111
|
+
# with the current batch.
|
112
|
+
yield b
|
113
|
+
ensure
|
114
|
+
Thread.current[:faktory_batch] = old
|
115
|
+
end
|
116
|
+
transaction do
|
117
|
+
command "BATCH COMMIT", b.bid
|
118
|
+
ok
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_track(jid)
|
123
|
+
transaction do
|
124
|
+
command "TRACK GET", jid
|
125
|
+
hashstr = result!
|
126
|
+
JSON.parse(hashstr)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# hash must include a 'jid' element
|
131
|
+
def set_track(hash)
|
132
|
+
transaction do
|
133
|
+
command("TRACK SET", Faktory.dump_json(hash))
|
134
|
+
ok
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Push a hash corresponding to a job payload to Faktory.
|
139
|
+
# Hash must contain "jid", "jobtype" and "args" elements at minimum.
|
140
|
+
# Returned value will either be the JID String if successful OR
|
141
|
+
# a symbol corresponding to an error.
|
60
142
|
def push(job)
|
61
143
|
transaction do
|
62
|
-
command "PUSH",
|
63
|
-
ok
|
64
|
-
job["jid"]
|
144
|
+
command "PUSH", Faktory.dump_json(job)
|
145
|
+
ok(job["jid"])
|
65
146
|
end
|
66
147
|
end
|
67
148
|
|
149
|
+
# Returns either a job hash or falsy.
|
68
150
|
def fetch(*queues)
|
69
151
|
job = nil
|
70
152
|
transaction do
|
71
153
|
command("FETCH", *queues)
|
72
|
-
job = result
|
154
|
+
job = result!
|
73
155
|
end
|
74
156
|
JSON.parse(job) if job
|
75
157
|
end
|
@@ -77,34 +159,42 @@ module Faktory
|
|
77
159
|
def ack(jid)
|
78
160
|
transaction do
|
79
161
|
command("ACK", %Q[{"jid":"#{jid}"}])
|
80
|
-
ok
|
162
|
+
ok
|
81
163
|
end
|
82
164
|
end
|
83
165
|
|
84
166
|
def fail(jid, ex)
|
85
167
|
transaction do
|
86
|
-
command("FAIL",
|
168
|
+
command("FAIL", Faktory.dump_json({ message: ex.message[0...1000],
|
87
169
|
errtype: ex.class.name,
|
88
170
|
jid: jid,
|
89
171
|
backtrace: ex.backtrace}))
|
90
|
-
ok
|
172
|
+
ok
|
91
173
|
end
|
92
174
|
end
|
93
175
|
|
94
176
|
# Sends a heartbeat to the server, in order to prove this
|
95
177
|
# worker process is still alive.
|
96
178
|
#
|
179
|
+
# You can pass in the current_state of the process, for example during shutdown
|
180
|
+
# quiet and/or terminate can be supplied.
|
181
|
+
#
|
97
182
|
# Return a string signal to process, legal values are "quiet" or "terminate".
|
98
183
|
# The quiet signal is informative: the server won't allow this process to FETCH
|
99
184
|
# any more jobs anyways.
|
100
|
-
def beat
|
185
|
+
def beat(current_state = nil)
|
101
186
|
transaction do
|
102
|
-
|
103
|
-
|
187
|
+
if current_state.nil?
|
188
|
+
command("BEAT", %Q[{"wid":"#{@@random_process_wid}"}])
|
189
|
+
else
|
190
|
+
command("BEAT", %Q[{"wid":"#{@@random_process_wid}", "current_state":"#{current_state}"}])
|
191
|
+
end
|
192
|
+
|
193
|
+
str = result!
|
104
194
|
if str == "OK"
|
105
195
|
str
|
106
196
|
else
|
107
|
-
hash =
|
197
|
+
hash = Faktory.load_json(str)
|
108
198
|
hash["state"]
|
109
199
|
end
|
110
200
|
end
|
@@ -113,8 +203,8 @@ module Faktory
|
|
113
203
|
def info
|
114
204
|
transaction do
|
115
205
|
command("INFO")
|
116
|
-
str = result
|
117
|
-
|
206
|
+
str = result!
|
207
|
+
Faktory.load_json(str) if str
|
118
208
|
end
|
119
209
|
end
|
120
210
|
|
@@ -129,9 +219,17 @@ module Faktory
|
|
129
219
|
@location.scheme =~ /tls/
|
130
220
|
end
|
131
221
|
|
132
|
-
def open
|
222
|
+
def open(timeout = DEFAULT_TIMEOUT)
|
223
|
+
# this is the read/write timeout, not open.
|
224
|
+
secs = Integer(timeout)
|
225
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
226
|
+
optval = [secs, usecs].pack("l_2")
|
133
227
|
if tls?
|
134
228
|
sock = TCPSocket.new(@location.hostname, @location.port)
|
229
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
230
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval)
|
231
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval)
|
232
|
+
|
135
233
|
ctx = OpenSSL::SSL::SSLContext.new
|
136
234
|
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
137
235
|
ctx.ssl_version = :TLSv1_2
|
@@ -143,6 +241,8 @@ module Faktory
|
|
143
241
|
else
|
144
242
|
@sock = TCPSocket.new(@location.hostname, @location.port)
|
145
243
|
@sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
244
|
+
@sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval)
|
245
|
+
@sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval)
|
146
246
|
end
|
147
247
|
|
148
248
|
payload = {
|
@@ -176,11 +276,10 @@ module Faktory
|
|
176
276
|
end
|
177
277
|
end
|
178
278
|
|
179
|
-
command("HELLO",
|
180
|
-
ok
|
279
|
+
command("HELLO", Faktory.dump_json(payload))
|
280
|
+
ok
|
181
281
|
end
|
182
282
|
|
183
|
-
|
184
283
|
def command(*args)
|
185
284
|
cmd = args.join(" ")
|
186
285
|
@sock.puts(cmd)
|
@@ -189,12 +288,19 @@ module Faktory
|
|
189
288
|
|
190
289
|
def transaction
|
191
290
|
retryable = true
|
291
|
+
|
292
|
+
# When using Faktory::Testing, you can get a client which does not actually
|
293
|
+
# have an underlying socket. Now if you disable testing and try to use that
|
294
|
+
# client, it will crash without a socket. This open() handles that case to
|
295
|
+
# transparently open a socket.
|
296
|
+
open(@timeout) if !@sock
|
297
|
+
|
192
298
|
begin
|
193
299
|
yield
|
194
300
|
rescue Errno::EPIPE, Errno::ECONNRESET
|
195
301
|
if retryable
|
196
302
|
retryable = false
|
197
|
-
open
|
303
|
+
open(@timeout)
|
198
304
|
retry
|
199
305
|
else
|
200
306
|
raise
|
@@ -218,7 +324,16 @@ module Faktory
|
|
218
324
|
line = @sock.gets # read extra linefeeds
|
219
325
|
data
|
220
326
|
elsif chr == '-'
|
221
|
-
|
327
|
+
# Server can respond with:
|
328
|
+
#
|
329
|
+
# -ERR Something unexpected
|
330
|
+
# We raise a CommandError
|
331
|
+
#
|
332
|
+
# -NOTUNIQUE Job not unique
|
333
|
+
# We return ["NOTUNIQUE", "Job not unique"]
|
334
|
+
err = line[1..-1].split(" ", 2)
|
335
|
+
raise CommandError, err[1] if err[0] == "ERR"
|
336
|
+
err
|
222
337
|
else
|
223
338
|
# this is bad, indicates we need to reset the socket
|
224
339
|
# and start fresh
|
@@ -226,10 +341,17 @@ module Faktory
|
|
226
341
|
end
|
227
342
|
end
|
228
343
|
|
229
|
-
def ok
|
344
|
+
def ok(retval=true)
|
230
345
|
resp = result
|
231
|
-
|
232
|
-
|
346
|
+
return retval if resp == "OK"
|
347
|
+
return resp[0].to_sym
|
348
|
+
end
|
349
|
+
|
350
|
+
def result!
|
351
|
+
resp = result
|
352
|
+
return nil if resp == nil
|
353
|
+
raise CommandError, resp[0] if !resp.is_a?(String)
|
354
|
+
resp
|
233
355
|
end
|
234
356
|
|
235
357
|
# FAKTORY_PROVIDER=MY_FAKTORY_URL
|
@@ -253,4 +375,3 @@ module Faktory
|
|
253
375
|
|
254
376
|
end
|
255
377
|
end
|
256
|
-
|
data/lib/faktory/connection.rb
CHANGED
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,12 +68,32 @@ 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
|
+
Faktory.client_middleware.invoke(item, pool) do
|
92
|
+
pool.with do |c|
|
93
|
+
c.push(item)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
62
97
|
end
|
63
98
|
|
64
99
|
module ClassMethods
|
@@ -68,19 +103,13 @@ module Faktory
|
|
68
103
|
end
|
69
104
|
|
70
105
|
def perform_async(*args)
|
71
|
-
|
106
|
+
set(get_faktory_options).perform_async(*args)
|
72
107
|
end
|
73
108
|
|
74
109
|
# +interval+ must be a timestamp, numeric or something that acts
|
75
110
|
# numeric (like an activesupport time interval).
|
76
111
|
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)
|
112
|
+
set(get_faktory_options).perform_in(interval, *args)
|
84
113
|
end
|
85
114
|
alias_method :perform_at, :perform_in
|
86
115
|
|
@@ -102,23 +131,6 @@ module Faktory
|
|
102
131
|
self.faktory_options_hash ||= Faktory.default_job_options
|
103
132
|
end
|
104
133
|
|
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
134
|
def faktory_class_attribute(*attrs)
|
123
135
|
instance_reader = true
|
124
136
|
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/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,85 @@
|
|
1
|
+
require 'faktory/client'
|
2
|
+
|
3
|
+
# require 'faktory/mutate'
|
4
|
+
# cl = Faktory::Client.new
|
5
|
+
# cl.discard(Faktory::RETRIES) do |filter|
|
6
|
+
# filter.with_type("QuickBooksSyncJob")
|
7
|
+
# filter.matching("*uid:12345*"))
|
8
|
+
# end
|
9
|
+
module Faktory
|
10
|
+
|
11
|
+
# Valid targets
|
12
|
+
RETRIES = "retries"
|
13
|
+
SCHEDULED = "scheduled"
|
14
|
+
DEAD = "dead"
|
15
|
+
|
16
|
+
module Mutator
|
17
|
+
class Filter
|
18
|
+
attr_accessor :hash
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@hash = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
# This must be the exact type of the job, no pattern matching
|
25
|
+
def with_type(jobtype)
|
26
|
+
@hash[:jobtype] = jobtype
|
27
|
+
end
|
28
|
+
|
29
|
+
# This is a regexp that will be passed as is to Redis's SCAN.
|
30
|
+
# Notably you should surround it with * to ensure it matches
|
31
|
+
# substrings within the job payload.
|
32
|
+
# See https://redis.io/commands/scan for details.
|
33
|
+
def matching(regexp)
|
34
|
+
@hash[:regexp] = regexp
|
35
|
+
end
|
36
|
+
|
37
|
+
# One or more JIDs to target:
|
38
|
+
# filter.jids << 'abcdefgh1234'
|
39
|
+
# filter.jids = ['abcdefgh1234', '1234567890']
|
40
|
+
def jids
|
41
|
+
@hash[:jids] ||= []
|
42
|
+
end
|
43
|
+
def jids=(ary)
|
44
|
+
@hash[:jids] = Array(ary)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def discard(target, &block)
|
49
|
+
filter = Filter.new
|
50
|
+
block.call(filter) if block
|
51
|
+
mutate('discard', target, filter)
|
52
|
+
end
|
53
|
+
|
54
|
+
def kill(target, &block)
|
55
|
+
filter = Filter.new
|
56
|
+
block.call(filter) if block
|
57
|
+
mutate('kill', target, filter)
|
58
|
+
end
|
59
|
+
|
60
|
+
def requeue(target, &block)
|
61
|
+
filter = Filter.new
|
62
|
+
block.call(filter) if block
|
63
|
+
mutate('requeue', target, filter)
|
64
|
+
end
|
65
|
+
|
66
|
+
def clear(target)
|
67
|
+
mutate('discard', target, nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def mutate(cmd, target, filter)
|
73
|
+
payload = {:cmd => cmd,:target => target}
|
74
|
+
payload[:filter] = filter.hash if filter && !filter.hash.empty?
|
75
|
+
|
76
|
+
transaction do
|
77
|
+
command("MUTATE", JSON.dump(payload))
|
78
|
+
ok
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
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/testing.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Faktory
|
2
|
+
module Trackable
|
3
|
+
|
4
|
+
##
|
5
|
+
# Tracking allows a long-running Faktory job to report its progress:
|
6
|
+
#
|
7
|
+
# def perform(...)
|
8
|
+
# track_progress(10, "Calculating values")
|
9
|
+
# # do some work
|
10
|
+
#
|
11
|
+
# track_progress(20, "Sending emails")
|
12
|
+
# # do some more work
|
13
|
+
#
|
14
|
+
# track_progress(20, "Sending emails", reserve_until: 10.minutes.from_now)
|
15
|
+
# # do some more work
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Note:
|
19
|
+
# 1. jobs should be small and fine-grained (and so fast) if possible.
|
20
|
+
# 2. tracking is useful for long-running jobs, tracking a fast job will only add overhead
|
21
|
+
# 3. tracking only works with a single job, use Batches to monitor a group of jobs
|
22
|
+
# 4. reserve_until allows a job to dynamically extend its reservation so it is not garbage collected by Faktory while running
|
23
|
+
# 5. you can only reserve up to 24 hours.
|
24
|
+
#
|
25
|
+
def track_progress(percent, desc=nil, reserve_until:nil)
|
26
|
+
hash = { 'jid' => jid, 'percent' => percent.to_i, 'desc' => desc }
|
27
|
+
hash["reserve_until"] = convert(reserve_until) if reserve_until
|
28
|
+
Faktory.server {|c| c.set_track(hash) }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def convert(ts)
|
34
|
+
raise ArgumentError, "Timestamp in the past: #{ts}" if Time.now > ts
|
35
|
+
raise ArgumentError, "Timestamp too far in the future: #{ts}" if (Time.now + 86400) < ts
|
36
|
+
|
37
|
+
tsf = ts.to_f
|
38
|
+
Time.at(tsf).utc.iso8601
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/faktory/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faktory_worker_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Perham
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: '2.2'
|
20
20
|
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 2.2.
|
22
|
+
version: 2.2.2
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: '2.2'
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 2.2.
|
32
|
+
version: 2.2.2
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: activejob
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- faktory_worker_ruby.gemspec
|
107
107
|
- lib/active_job/queue_adapters/faktory_adapter.rb
|
108
108
|
- lib/faktory.rb
|
109
|
+
- lib/faktory/batch.rb
|
109
110
|
- lib/faktory/cli.rb
|
110
111
|
- lib/faktory/client.rb
|
111
112
|
- lib/faktory/connection.rb
|
@@ -116,11 +117,14 @@ files:
|
|
116
117
|
- lib/faktory/launcher.rb
|
117
118
|
- lib/faktory/logging.rb
|
118
119
|
- lib/faktory/manager.rb
|
120
|
+
- lib/faktory/middleware/batch.rb
|
119
121
|
- lib/faktory/middleware/chain.rb
|
120
122
|
- lib/faktory/middleware/i18n.rb
|
123
|
+
- lib/faktory/mutate.rb
|
121
124
|
- lib/faktory/processor.rb
|
122
125
|
- lib/faktory/rails.rb
|
123
126
|
- lib/faktory/testing.rb
|
127
|
+
- lib/faktory/tracking.rb
|
124
128
|
- lib/faktory/util.rb
|
125
129
|
- lib/faktory/version.rb
|
126
130
|
- lib/faktory_worker_ruby.rb
|
@@ -136,15 +140,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
140
|
requirements:
|
137
141
|
- - ">="
|
138
142
|
- !ruby/object:Gem::Version
|
139
|
-
version: 2.
|
143
|
+
version: 2.5.0
|
140
144
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
145
|
requirements:
|
142
146
|
- - ">="
|
143
147
|
- !ruby/object:Gem::Version
|
144
148
|
version: '0'
|
145
149
|
requirements: []
|
146
|
-
|
147
|
-
rubygems_version: 2.6.13
|
150
|
+
rubygems_version: 3.0.3
|
148
151
|
signing_key:
|
149
152
|
specification_version: 4
|
150
153
|
summary: Ruby worker for Faktory
|