faktory_worker_ruby 0.8.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7877d9a875449524da22d1e8efd6f29a23381fe0
4
- data.tar.gz: 665d0631430befd3f019588366502558f7e6f9c8
2
+ SHA256:
3
+ metadata.gz: ca8f1a3c8f94200639fe96c6ed6664ab02e3240a4176d73463cfd185bf0dd577
4
+ data.tar.gz: c0a039afe5d4a3ca54d5c8ef779c282cd2266bce6665998c8da60d1829fa4cb2
5
5
  SHA512:
6
- metadata.gz: 3b6c62d76a317adecdf96e161a42ad817a5cc4b3e23c0bab49b6217e7240a7aef2348b0ff2117b156d10eaed6f832196e38e53e75fabae154263069a2e7ee29f
7
- data.tar.gz: b0d68a791832e2e916578757b93675c408461e93022483da0cd24f1dcce3d4fe5b825505f1d6c1985a0b6880958db2ede88ccf689a9e031de98aae2813327820
6
+ metadata.gz: 3ccf18cb61e1428fa6443c7f9862861d959bbf40d0914f94c8457642c97e72be367c5e3d019ae3a9cfc30f65dae22ad865b591892f6985a3a460d44dc442c378
7
+ data.tar.gz: c9822086ef38bf740abd2a6b6afd099305e0d1998e0adcdefb6dbf5ccb84d7b4a6497ef22359320d13fef70899dfb7ebf29c361bd405ea7bb93eb41463719070
data/Changes.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changes
2
2
 
3
+ ## 1.1.0
4
+
5
+ - Send process RSS to Faktory for /Busy page
6
+ - Implement QUEUE PAUSE/RESUME client commands
7
+ - Fix broken `complete` callback with batches
8
+
9
+ ## 1.0.3
10
+
11
+ - Fix corruption in `custom` hash elements [#55]
12
+
13
+ ## 1.0.2
14
+
15
+ - Fix "batch not open" errors
16
+
17
+ ## 1.0.1
18
+
19
+ - Run client middleware before pushing a job to Faktory [#48]
20
+ - Implement read timeouts for Faktory::Client for faktory#297
21
+
22
+ ## 1.0.0
23
+
24
+ - Ruby 2.5+ is now required
25
+ - Support for Faktory Enterprise, job batches and job tracking
26
+ - Support for the MUTATE command.
27
+ - Notify Faktory when a worker process is going quiet so that the UI shows this
28
+ - Refactor Faktory::Client error handling for faktory#208
29
+
3
30
  ## 0.8.1
4
31
 
5
32
  - Fix breakage with non-ActiveJobs [#29]
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'simplecov', require: false, group: :test
4
+
3
5
  gemspec
data/Gemfile.lock CHANGED
@@ -1,43 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- faktory_worker_ruby (0.8.1)
5
- connection_pool (~> 2.2, >= 2.2.1)
4
+ faktory_worker_ruby (1.1.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.1)
11
- activesupport (= 5.2.1)
10
+ activejob (6.0.3.2)
11
+ activesupport (= 6.0.3.2)
12
12
  globalid (>= 0.3.6)
13
- activesupport (5.2.1)
13
+ activesupport (6.0.3.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.0.5)
19
- connection_pool (2.2.2)
20
- globalid (0.4.1)
18
+ zeitwerk (~> 2.2, >= 2.2.2)
19
+ concurrent-ruby (1.1.6)
20
+ connection_pool (2.2.3)
21
+ docile (1.3.2)
22
+ globalid (0.4.2)
21
23
  activesupport (>= 4.2.0)
22
- i18n (1.1.0)
24
+ i18n (1.8.5)
23
25
  concurrent-ruby (~> 1.0)
24
- minitest (5.11.3)
26
+ minitest (5.14.1)
25
27
  minitest-hooks (1.5.0)
26
28
  minitest (> 5.3)
27
- rake (12.3.1)
29
+ rake (13.0.1)
30
+ simplecov (0.18.5)
31
+ docile (~> 1.1)
32
+ simplecov-html (~> 0.11)
33
+ simplecov-html (0.12.2)
28
34
  thread_safe (0.3.6)
29
- tzinfo (1.2.5)
35
+ tzinfo (1.2.7)
30
36
  thread_safe (~> 0.1)
37
+ zeitwerk (2.4.0)
31
38
 
32
39
  PLATFORMS
33
40
  ruby
34
41
 
35
42
  DEPENDENCIES
36
- activejob (>= 5.1.5)
43
+ activejob (>= 5.2.0)
37
44
  faktory_worker_ruby!
38
45
  minitest (~> 5)
39
46
  minitest-hooks
40
- rake (~> 12)
47
+ rake
48
+ simplecov
41
49
 
42
50
  BUNDLED WITH
43
- 1.17.1
51
+ 2.1.4
data/README.md CHANGED
@@ -34,8 +34,10 @@ server part is [here](https://github.com/contribsys/faktory/)
34
34
 
35
35
  ## Requirements
36
36
 
37
- * Ruby 2.3 or higher
38
- * Faktory 0.9 or higher [Installation](https://github.com/contribsys/faktory/wiki/Installation)
37
+ * Ruby 2.5 or higher
38
+ * Faktory 1.2 or higher [Installation](https://github.com/contribsys/faktory/wiki/Installation)
39
+
40
+ Optionally, Rails 5.2+ for ActiveJob.
39
41
 
40
42
  ## Installation
41
43
 
@@ -95,4 +97,4 @@ PRs to improve this are very welcome).
95
97
 
96
98
  ## Author
97
99
 
98
- Mike Perham, @mperham, mike @ contribsys.com
100
+ Mike Perham, @getajobmike, mike @ contribsys.com
@@ -14,11 +14,19 @@ 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.3.0"
17
+ gem.required_ruby_version = ">= 2.5.0"
18
18
 
19
- gem.add_dependency 'connection_pool', '~> 2.2', ">= 2.2.1"
20
- gem.add_development_dependency 'activejob', '>= 5.1.5'
19
+ gem.metadata = {
20
+ "homepage_uri" => "https://contribsys.com/faktory",
21
+ "bug_tracker_uri" => "https://github.com/contribsys/faktory_worker_ruby/issues",
22
+ "documentation_uri" => "https://github.com/contribsys/faktory_worker_ruby/wiki",
23
+ "changelog_uri" => "https://github.com/contribsys/faktory_worker_ruby/blob/master/Changes.md",
24
+ "source_code_uri" => "https://github.com/contribsys/faktory_worker_ruby",
25
+ }
26
+
27
+ gem.add_dependency 'connection_pool', '~> 2.2', ">= 2.2.2"
28
+ gem.add_development_dependency 'activejob', '>= 5.2.0'
21
29
  gem.add_development_dependency 'minitest', '~> 5'
22
30
  gem.add_development_dependency 'minitest-hooks'
23
- gem.add_development_dependency 'rake', '~> 12'
31
+ gem.add_development_dependency 'rake'
24
32
  end
@@ -32,8 +32,12 @@ module ActiveJob
32
32
  hash["retry"] = opts.delete("retry") if opts.has_key?("retry")
33
33
  hash["custom"] = opts.merge(hash["custom"])
34
34
  end
35
- # Faktory::Client does not support symbols as keys
36
- Faktory::Client.new.push(hash)
35
+ pool = Thread.current[:faktory_via_pool] || Faktory.server_pool
36
+ Faktory.client_middleware.invoke(hash, pool) do
37
+ pool.with do |c|
38
+ c.push(hash)
39
+ end
40
+ end
37
41
  end
38
42
 
39
43
  class JobWrapper #:nodoc:
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.faktory = { :size => 1, :url => 'myhost:7419' }
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
+ @complete = 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
@@ -8,6 +8,11 @@ require 'optparse'
8
8
  require 'erb'
9
9
  require 'fileutils'
10
10
 
11
+ module Faktory
12
+ class CLI
13
+ end
14
+ end
15
+
11
16
  require 'faktory'
12
17
  require 'faktory/util'
13
18
 
@@ -207,7 +212,7 @@ module Faktory
207
212
  if !File.exist?(options[:require]) ||
208
213
  (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
209
214
  logger.info "=================================================================="
210
- logger.info " Please point Faktory to a Rails 5 application or a Ruby file "
215
+ logger.info " Please point Faktory to a Rails application or a Ruby file "
211
216
  logger.info " to load your worker classes with -r [DIR|FILE]."
212
217
  logger.info "=================================================================="
213
218
  logger.info @parser
@@ -3,14 +3,27 @@ require 'json'
3
3
  require 'uri'
4
4
  require 'digest'
5
5
  require 'securerandom'
6
+ require 'timeout'
7
+ require 'faktory/io'
6
8
 
7
9
  module Faktory
8
- class CommandError < StandardError;end
9
- class ParseError < StandardError;end
10
-
10
+ class BaseError < StandardError; end
11
+ class CommandError < BaseError; end
12
+ class ParseError < BaseError; end
13
+
14
+ # Faktory::Client provides a low-level connection to a Faktory server
15
+ # and APIs which map to Faktory commands.
16
+ #
17
+ # Most APIs will return `true` if the operation succeeded or raise a
18
+ # Faktory::BaseError if there was an unexpected error.
11
19
  class Client
20
+ # provides gets() and read() that respect a read timeout
21
+ include Faktory::ReadTimeout
22
+
12
23
  @@random_process_wid = ""
13
24
 
25
+ DEFAULT_TIMEOUT = 5.0
26
+
14
27
  HASHER = proc do |iter, pwd, salt|
15
28
  sha = Digest::SHA256.new
16
29
  hashing = pwd + salt
@@ -36,10 +49,13 @@ module Faktory
36
49
  # MY_FAKTORY_URL=tcp://:somepass@my-server.example.com:7419
37
50
  #
38
51
  # Note above, the URL can contain the password for secure installations.
39
- def initialize(url: uri_from_env || 'tcp://localhost:7419', debug: false)
52
+ def initialize(url: uri_from_env || 'tcp://localhost:7419', debug: false, timeout: DEFAULT_TIMEOUT)
53
+ super
40
54
  @debug = debug
41
55
  @location = URI(url)
42
- open
56
+ @timeout = timeout
57
+
58
+ open(@timeout)
43
59
  end
44
60
 
45
61
  def close
@@ -53,23 +69,111 @@ module Faktory
53
69
  def flush
54
70
  transaction do
55
71
  command "FLUSH"
56
- ok!
72
+ ok
73
+ end
74
+ end
75
+
76
+ def create_batch(batch, &block)
77
+ bid = transaction do
78
+ command "BATCH NEW", Faktory.dump_json(batch.to_h)
79
+ result!
80
+ end
81
+ batch.instance_variable_set(:@bid, bid)
82
+
83
+ old = Thread.current[:faktory_batch]
84
+ begin
85
+ Thread.current[:faktory_batch] = batch
86
+ # any jobs pushed in this block will implicitly have
87
+ # their `bid` attribute set so they are associated
88
+ # with the current batch.
89
+ yield batch
90
+ ensure
91
+ Thread.current[:faktory_batch] = old
92
+ end
93
+ transaction do
94
+ command "BATCH COMMIT", bid
95
+ ok
96
+ end
97
+ bid
98
+ end
99
+
100
+ def batch_status(bid)
101
+ transaction do
102
+ command "BATCH STATUS", bid
103
+ Faktory.load_json result!
57
104
  end
58
105
  end
59
106
 
107
+ def reopen_batch(b)
108
+ transaction do
109
+ command "BATCH OPEN", b.bid
110
+ ok
111
+ end
112
+ old = Thread.current[:faktory_batch]
113
+ begin
114
+ Thread.current[:faktory_batch] = b
115
+ # any jobs pushed in this block will implicitly have
116
+ # their `bid` attribute set so they are associated
117
+ # with the current batch.
118
+ yield b
119
+ ensure
120
+ Thread.current[:faktory_batch] = old
121
+ end
122
+ transaction do
123
+ command "BATCH COMMIT", b.bid
124
+ ok
125
+ end
126
+ end
127
+
128
+ def get_track(jid)
129
+ transaction do
130
+ command "TRACK GET", jid
131
+ hashstr = result!
132
+ JSON.parse(hashstr)
133
+ end
134
+ end
135
+
136
+ # hash must include a 'jid' element
137
+ def set_track(hash)
138
+ transaction do
139
+ command("TRACK SET", Faktory.dump_json(hash))
140
+ ok
141
+ end
142
+ end
143
+
144
+ def pause_queues(queues)
145
+ qs = Array(queues)
146
+ transaction do
147
+ command "QUEUE PAUSE", qs.join(" ")
148
+ ok
149
+ end
150
+ end
151
+
152
+ def resume_queues(queues)
153
+ qs = Array(queues)
154
+ transaction do
155
+ command "QUEUE RESUME", qs.join(" ")
156
+ ok
157
+ end
158
+ end
159
+
160
+ # Push a hash corresponding to a job payload to Faktory.
161
+ # Hash must contain "jid", "jobtype" and "args" elements at minimum.
162
+ # Returned value will either be the JID String if successful OR
163
+ # a symbol corresponding to an error.
60
164
  def push(job)
61
165
  transaction do
62
- command "PUSH", JSON.generate(job)
63
- ok!
64
- job["jid"]
166
+ command "PUSH", Faktory.dump_json(job)
167
+ ok(job["jid"])
65
168
  end
66
169
  end
67
170
 
171
+ # Returns either a job hash or falsy.
68
172
  def fetch(*queues)
69
173
  job = nil
70
174
  transaction do
71
175
  command("FETCH", *queues)
72
- job = result
176
+ job = result!
73
177
  end
74
178
  JSON.parse(job) if job
75
179
  end
@@ -77,34 +181,40 @@ module Faktory
77
181
  def ack(jid)
78
182
  transaction do
79
183
  command("ACK", %Q[{"jid":"#{jid}"}])
80
- ok!
184
+ ok
81
185
  end
82
186
  end
83
187
 
84
188
  def fail(jid, ex)
85
189
  transaction do
86
- command("FAIL", JSON.dump({ message: ex.message[0...1000],
190
+ command("FAIL", Faktory.dump_json({ message: ex.message[0...1000],
87
191
  errtype: ex.class.name,
88
192
  jid: jid,
89
193
  backtrace: ex.backtrace}))
90
- ok!
194
+ ok
91
195
  end
92
196
  end
93
197
 
94
198
  # Sends a heartbeat to the server, in order to prove this
95
199
  # worker process is still alive.
96
200
  #
201
+ # You can pass in the current_state of the process, for example during shutdown
202
+ # quiet and/or terminate can be supplied.
203
+ #
97
204
  # Return a string signal to process, legal values are "quiet" or "terminate".
98
205
  # The quiet signal is informative: the server won't allow this process to FETCH
99
206
  # any more jobs anyways.
100
- def beat
207
+ def beat(current_state = nil, hash)
101
208
  transaction do
102
- command("BEAT", %Q[{"wid":"#{@@random_process_wid}"}])
103
- str = result
209
+ hash["wid"] = @@random_process_wid
210
+ hash["current_state"] = current_state if current_state
211
+ command("BEAT", Faktory.dump_json(hash))
212
+
213
+ str = result!
104
214
  if str == "OK"
105
215
  str
106
216
  else
107
- hash = JSON.parse(str)
217
+ hash = Faktory.load_json(str)
108
218
  hash["state"]
109
219
  end
110
220
  end
@@ -113,8 +223,8 @@ module Faktory
113
223
  def info
114
224
  transaction do
115
225
  command("INFO")
116
- str = result
117
- JSON.parse(str) if str
226
+ str = result!
227
+ Faktory.load_json(str) if str
118
228
  end
119
229
  end
120
230
 
@@ -129,12 +239,14 @@ module Faktory
129
239
  @location.scheme =~ /tls/
130
240
  end
131
241
 
132
- def open
242
+ def open(timeout = DEFAULT_TIMEOUT)
133
243
  if tls?
134
244
  sock = TCPSocket.new(@location.hostname, @location.port)
245
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
246
+
135
247
  ctx = OpenSSL::SSL::SSLContext.new
136
248
  ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
137
- ctx.ssl_version = :TLSv1_2
249
+ ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
138
250
 
139
251
  @sock = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
140
252
  socket.sync_close = true
@@ -176,11 +288,10 @@ module Faktory
176
288
  end
177
289
  end
178
290
 
179
- command("HELLO", JSON.dump(payload))
180
- ok!
291
+ command("HELLO", Faktory.dump_json(payload))
292
+ ok
181
293
  end
182
294
 
183
-
184
295
  def command(*args)
185
296
  cmd = args.join(" ")
186
297
  @sock.puts(cmd)
@@ -189,12 +300,22 @@ module Faktory
189
300
 
190
301
  def transaction
191
302
  retryable = true
303
+
304
+ # When using Faktory::Testing, you can get a client which does not actually
305
+ # have an underlying socket. Now if you disable testing and try to use that
306
+ # client, it will crash without a socket. This open() handles that case to
307
+ # transparently open a socket.
308
+ open(@timeout) if !@sock
309
+
192
310
  begin
193
311
  yield
194
- rescue Errno::EPIPE, Errno::ECONNRESET
312
+ rescue SystemCallError, SocketError, TimeoutError
195
313
  if retryable
196
314
  retryable = false
197
- open
315
+
316
+ @sock.close rescue nil
317
+ @sock = nil
318
+ open(@timeout)
198
319
  retry
199
320
  else
200
321
  raise
@@ -205,7 +326,7 @@ module Faktory
205
326
  # I love pragmatic, simple protocols. Thanks antirez!
206
327
  # https://redis.io/topics/protocol
207
328
  def result
208
- line = @sock.gets
329
+ line = gets
209
330
  debug "< #{line}" if @debug
210
331
  raise Errno::ECONNRESET, "No response" unless line
211
332
  chr = line[0]
@@ -214,11 +335,20 @@ module Faktory
214
335
  elsif chr == '$'
215
336
  count = line[1..-1].strip.to_i
216
337
  return nil if count == -1
217
- data = @sock.read(count) if count > 0
218
- line = @sock.gets # read extra linefeeds
338
+ data = read(count) if count > 0
339
+ line = gets # read extra linefeeds
219
340
  data
220
341
  elsif chr == '-'
221
- raise CommandError, line[1..-1]
342
+ # Server can respond with:
343
+ #
344
+ # -ERR Something unexpected
345
+ # We raise a CommandError
346
+ #
347
+ # -NOTUNIQUE Job not unique
348
+ # We return ["NOTUNIQUE", "Job not unique"]
349
+ err = line[1..-1].split(" ", 2)
350
+ raise CommandError, err[1] if err[0] == "ERR"
351
+ err
222
352
  else
223
353
  # this is bad, indicates we need to reset the socket
224
354
  # and start fresh
@@ -226,10 +356,17 @@ module Faktory
226
356
  end
227
357
  end
228
358
 
229
- def ok!
359
+ def ok(retval=true)
360
+ resp = result
361
+ return retval if resp == "OK"
362
+ return resp[0].to_sym
363
+ end
364
+
365
+ def result!
230
366
  resp = result
231
- raise CommandError, resp if resp != "OK"
232
- true
367
+ return nil if resp == nil
368
+ raise CommandError, resp[0] if !resp.is_a?(String)
369
+ resp
233
370
  end
234
371
 
235
372
  # FAKTORY_PROVIDER=MY_FAKTORY_URL
@@ -253,4 +390,3 @@ module Faktory
253
390
 
254
391
  end
255
392
  end
256
-