faktory_worker_ruby 0.8.1 → 1.0.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 +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
|