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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5ffc1c6d5392fd64f4463ae4a47972f7d2c13ebc
4
- data.tar.gz: cc1e0294bc5c333f7ce684f112ee1182e20d2095
2
+ SHA256:
3
+ metadata.gz: 2a183f21103562c224add2460da73a145a0c12c32a167525fd18dd02fe334241
4
+ data.tar.gz: b7c76d7001ba133f685427ffcc00bcdd75d026747106fac705b741e57f3d7b94
5
5
  SHA512:
6
- metadata.gz: 9f2a3b5f59d0a50c77f0f22ec86f291289762ba5e27dc012fa9cce34807883aee616b7ca9b7a937139b67137725f450c0e7fc504909f0a84136ebd4b6e67586e
7
- data.tar.gz: 7ef1fd93ff1d803bb7d895d63749a3deb9cd4fe4000936df5b8e1facfca884e22e439574979b821972936b16b1ea15ef31f97c3db306b90b4d7514ea214d1437
6
+ metadata.gz: 6bae416fa9d2675c586a8a40ccce0005f665eb2ab2bc19e6a5e12405cafee67aef22984d9e84f6db96012143eee87307d81e089fad5fd81d6dd278489fe62437
7
+ data.tar.gz: 42806775422a7542a1a7c7e912bc373dfe8c0d2b1d47d8cccb3ce8d91a0c1a95f8a835cb99cc1b9da2262d17ac3f50e7447ecefcf4eff6ba3ef0d9672e8b1f06
data/Changes.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changes
2
2
 
3
+ ## 1.0.3
4
+
5
+ - Fix corruption in `custom` hash elements [#55]
6
+
7
+ ## 1.0.2
8
+
9
+ - Fix "batch not open" errors
10
+
11
+ ## 1.0.1
12
+
13
+ - Run client middleware before pushing a job to Faktory [#48]
14
+ - Implement read timeouts for Faktory::Client for faktory#297
15
+
16
+ ## 1.0.0
17
+
18
+ - Ruby 2.5+ is now required
19
+ - Support for Faktory Enterprise, job batches and job tracking
20
+ - Support for the MUTATE command.
21
+ - Notify Faktory when a worker process is going quiet so that the UI shows this
22
+ - Refactor Faktory::Client error handling for faktory#208
23
+
24
+ ## 0.8.1
25
+
26
+ - Fix breakage with non-ActiveJobs [#29]
27
+ - Ruby 2.3+ is now required
28
+
3
29
  ## 0.8.0
4
30
 
5
31
  - Add `-l LABEL` argument for adding labels to a process [#27, jpwinans]
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.0)
5
- connection_pool (~> 2.2, >= 2.2.1)
4
+ faktory_worker_ruby (1.0.2)
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.16.3
51
+ 2.1.4
data/README.md CHANGED
@@ -17,7 +17,7 @@ Faktory background job server. It is similar to [Sidekiq](http://sidekiq.org).
17
17
  +-----------------+ +-------------------+
18
18
  | | | |
19
19
  | Client | | Worker |
20
- | pushes | | pulls |
20
+ | pushes | | fetches |
21
21
  | jobs | | jobs |
22
22
  | | | |
23
23
  | | | |
@@ -32,11 +32,14 @@ Faktory background job server. It is similar to [Sidekiq](http://sidekiq.org).
32
32
  This gem contains only the client and worker parts. The
33
33
  server part is [here](https://github.com/contribsys/faktory/)
34
34
 
35
- ## Installation
35
+ ## Requirements
36
+
37
+ * Ruby 2.5 or higher
38
+ * Faktory 1.2 or higher [Installation](https://github.com/contribsys/faktory/wiki/Installation)
36
39
 
37
- First, make sure you have the [Faktory server](https://github.com/contribsys/faktory/#installation) installed.
40
+ Optionally, Rails 5.2+ for ActiveJob.
38
41
 
39
- Next, install this gem:
42
+ ## Installation
40
43
 
41
44
  gem install faktory_worker_ruby
42
45
 
@@ -94,4 +97,4 @@ PRs to improve this are very welcome).
94
97
 
95
98
  ## Author
96
99
 
97
- Mike Perham, @mperham, mike @ contribsys.com
100
+ Mike Perham, @getajobmike, mike @ contribsys.com
@@ -14,11 +14,11 @@ 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.2.2"
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.add_dependency 'connection_pool', '~> 2.2', ">= 2.2.2"
20
+ gem.add_development_dependency 'activejob', '>= 5.2.0'
21
21
  gem.add_development_dependency 'minitest', '~> 5'
22
22
  gem.add_development_dependency 'minitest-hooks'
23
- gem.add_development_dependency 'rake', '~> 12'
23
+ gem.add_development_dependency 'rake'
24
24
  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
+ @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
@@ -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
 
@@ -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,95 @@ 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
57
96
  end
97
+ bid
58
98
  end
59
99
 
100
+ def batch_status(bid)
101
+ transaction do
102
+ command "BATCH STATUS", bid
103
+ Faktory.load_json result!
104
+ end
105
+ end
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
+ # Push a hash corresponding to a job payload to Faktory.
145
+ # Hash must contain "jid", "jobtype" and "args" elements at minimum.
146
+ # Returned value will either be the JID String if successful OR
147
+ # a symbol corresponding to an error.
60
148
  def push(job)
61
149
  transaction do
62
- command "PUSH", JSON.generate(job)
63
- ok!
64
- job["jid"]
150
+ command "PUSH", Faktory.dump_json(job)
151
+ ok(job["jid"])
65
152
  end
66
153
  end
67
154
 
155
+ # Returns either a job hash or falsy.
68
156
  def fetch(*queues)
69
157
  job = nil
70
158
  transaction do
71
159
  command("FETCH", *queues)
72
- job = result
160
+ job = result!
73
161
  end
74
162
  JSON.parse(job) if job
75
163
  end
@@ -77,34 +165,42 @@ module Faktory
77
165
  def ack(jid)
78
166
  transaction do
79
167
  command("ACK", %Q[{"jid":"#{jid}"}])
80
- ok!
168
+ ok
81
169
  end
82
170
  end
83
171
 
84
172
  def fail(jid, ex)
85
173
  transaction do
86
- command("FAIL", JSON.dump({ message: ex.message[0...1000],
174
+ command("FAIL", Faktory.dump_json({ message: ex.message[0...1000],
87
175
  errtype: ex.class.name,
88
176
  jid: jid,
89
177
  backtrace: ex.backtrace}))
90
- ok!
178
+ ok
91
179
  end
92
180
  end
93
181
 
94
182
  # Sends a heartbeat to the server, in order to prove this
95
183
  # worker process is still alive.
96
184
  #
185
+ # You can pass in the current_state of the process, for example during shutdown
186
+ # quiet and/or terminate can be supplied.
187
+ #
97
188
  # Return a string signal to process, legal values are "quiet" or "terminate".
98
189
  # The quiet signal is informative: the server won't allow this process to FETCH
99
190
  # any more jobs anyways.
100
- def beat
191
+ def beat(current_state = nil)
101
192
  transaction do
102
- command("BEAT", %Q[{"wid":"#{@@random_process_wid}"}])
103
- str = result
193
+ if current_state.nil?
194
+ command("BEAT", %Q[{"wid":"#{@@random_process_wid}"}])
195
+ else
196
+ command("BEAT", %Q[{"wid":"#{@@random_process_wid}", "current_state":"#{current_state}"}])
197
+ end
198
+
199
+ str = result!
104
200
  if str == "OK"
105
201
  str
106
202
  else
107
- hash = JSON.parse(str)
203
+ hash = Faktory.load_json(str)
108
204
  hash["state"]
109
205
  end
110
206
  end
@@ -113,8 +209,8 @@ module Faktory
113
209
  def info
114
210
  transaction do
115
211
  command("INFO")
116
- str = result
117
- JSON.parse(str) if str
212
+ str = result!
213
+ Faktory.load_json(str) if str
118
214
  end
119
215
  end
120
216
 
@@ -129,12 +225,14 @@ module Faktory
129
225
  @location.scheme =~ /tls/
130
226
  end
131
227
 
132
- def open
228
+ def open(timeout = DEFAULT_TIMEOUT)
133
229
  if tls?
134
230
  sock = TCPSocket.new(@location.hostname, @location.port)
231
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
232
+
135
233
  ctx = OpenSSL::SSL::SSLContext.new
136
234
  ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
137
- ctx.ssl_version = :TLSv1_2
235
+ ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
138
236
 
139
237
  @sock = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket|
140
238
  socket.sync_close = true
@@ -176,11 +274,10 @@ module Faktory
176
274
  end
177
275
  end
178
276
 
179
- command("HELLO", JSON.dump(payload))
180
- ok!
277
+ command("HELLO", Faktory.dump_json(payload))
278
+ ok
181
279
  end
182
280
 
183
-
184
281
  def command(*args)
185
282
  cmd = args.join(" ")
186
283
  @sock.puts(cmd)
@@ -189,12 +286,22 @@ module Faktory
189
286
 
190
287
  def transaction
191
288
  retryable = true
289
+
290
+ # When using Faktory::Testing, you can get a client which does not actually
291
+ # have an underlying socket. Now if you disable testing and try to use that
292
+ # client, it will crash without a socket. This open() handles that case to
293
+ # transparently open a socket.
294
+ open(@timeout) if !@sock
295
+
192
296
  begin
193
297
  yield
194
- rescue Errno::EPIPE, Errno::ECONNRESET
298
+ rescue SystemCallError, SocketError, TimeoutError
195
299
  if retryable
196
300
  retryable = false
197
- open
301
+
302
+ @sock.close rescue nil
303
+ @sock = nil
304
+ open(@timeout)
198
305
  retry
199
306
  else
200
307
  raise
@@ -205,7 +312,7 @@ module Faktory
205
312
  # I love pragmatic, simple protocols. Thanks antirez!
206
313
  # https://redis.io/topics/protocol
207
314
  def result
208
- line = @sock.gets
315
+ line = gets
209
316
  debug "< #{line}" if @debug
210
317
  raise Errno::ECONNRESET, "No response" unless line
211
318
  chr = line[0]
@@ -214,11 +321,20 @@ module Faktory
214
321
  elsif chr == '$'
215
322
  count = line[1..-1].strip.to_i
216
323
  return nil if count == -1
217
- data = @sock.read(count) if count > 0
218
- line = @sock.gets # read extra linefeeds
324
+ data = read(count) if count > 0
325
+ line = gets # read extra linefeeds
219
326
  data
220
327
  elsif chr == '-'
221
- raise CommandError, line[1..-1]
328
+ # Server can respond with:
329
+ #
330
+ # -ERR Something unexpected
331
+ # We raise a CommandError
332
+ #
333
+ # -NOTUNIQUE Job not unique
334
+ # We return ["NOTUNIQUE", "Job not unique"]
335
+ err = line[1..-1].split(" ", 2)
336
+ raise CommandError, err[1] if err[0] == "ERR"
337
+ err
222
338
  else
223
339
  # this is bad, indicates we need to reset the socket
224
340
  # and start fresh
@@ -226,10 +342,17 @@ module Faktory
226
342
  end
227
343
  end
228
344
 
229
- def ok!
345
+ def ok(retval=true)
230
346
  resp = result
231
- raise CommandError, resp if resp != "OK"
232
- true
347
+ return retval if resp == "OK"
348
+ return resp[0].to_sym
349
+ end
350
+
351
+ def result!
352
+ resp = result
353
+ return nil if resp == nil
354
+ raise CommandError, resp[0] if !resp.is_a?(String)
355
+ resp
233
356
  end
234
357
 
235
358
  # FAKTORY_PROVIDER=MY_FAKTORY_URL
@@ -253,4 +376,3 @@ module Faktory
253
376
 
254
377
  end
255
378
  end
256
-