queekiq 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +170 -52
- data/lib/queekiq/backoff.rb +31 -0
- data/lib/queekiq/configuration.rb +7 -1
- data/lib/queekiq/instrumenter.rb +54 -0
- data/lib/queekiq/message.rb +48 -3
- data/lib/queekiq/queue.rb +251 -37
- data/lib/queekiq/serializers.rb +36 -0
- data/lib/queekiq/version.rb +1 -1
- data/lib/queekiq/worker.rb +236 -32
- data/lib/queekiq.rb +25 -2
- metadata +12 -7
data/lib/queekiq/queue.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Queekiq
|
|
2
|
-
# A durable work queue: a Redis Stream read through a consumer group,
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# A durable work queue: a Redis Stream read through a consumer group, a sorted
|
|
3
|
+
# set of "deferred" messages that come back once their delay has elapsed, and
|
|
4
|
+
# a "dead" stream for messages that gave up.
|
|
5
5
|
#
|
|
6
6
|
# queue = Queekiq::Queue.new("emails")
|
|
7
7
|
# queue.enqueue("user-42")
|
|
@@ -12,51 +12,99 @@ module Queekiq
|
|
|
12
12
|
# #reclaim hands pending messages that have been idle too long to another
|
|
13
13
|
# consumer. Handlers must therefore be idempotent.
|
|
14
14
|
#
|
|
15
|
+
# Besides the payload, every entry carries a small envelope (attempt, defers,
|
|
16
|
+
# enqueued_at) so retries and deferrals keep their history. Entries written by
|
|
17
|
+
# Queekiq 0.1, or by anything else that only sets the payload field, are read
|
|
18
|
+
# as attempt 1 with no history.
|
|
19
|
+
#
|
|
15
20
|
# Requires Redis >= 7.0.
|
|
16
21
|
class Queue
|
|
17
22
|
DEFAULT_FIELD = "payload".freeze
|
|
18
23
|
DEFAULT_RECLAIM_AFTER_MS = 30_000
|
|
19
|
-
|
|
24
|
+
DEFAULT_MAX_ATTEMPTS = 5
|
|
25
|
+
RESERVED_FIELDS = %w[attempt defers enqueued_at].freeze
|
|
20
26
|
|
|
21
27
|
# Moves due members of the deferred set onto the stream in one atomic step,
|
|
22
|
-
# so a crash can't lose a
|
|
28
|
+
# so a crash can't lose a message between "removed" and "re-enqueued", and
|
|
23
29
|
# two workers can't both promote the same one.
|
|
30
|
+
# Members look like "attempt|defers|enqueued_at|raw payload". A member that
|
|
31
|
+
# doesn't match (written by 0.1: just the payload) is promoted as a new entry.
|
|
24
32
|
# KEYS: deferred set, stream ARGV: now_ms, limit, field name
|
|
25
33
|
PROMOTE_SCRIPT = <<~LUA.freeze
|
|
26
34
|
local due = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, ARGV[2])
|
|
27
35
|
for _, member in ipairs(due) do
|
|
28
36
|
redis.call('ZREM', KEYS[1], member)
|
|
29
|
-
|
|
37
|
+
local attempt, defers, enqueued_at, raw = string.match(member, '^(%d+)|(%d+)|(%d+)|(.*)$')
|
|
38
|
+
if raw then
|
|
39
|
+
redis.call('XADD', KEYS[2], '*', ARGV[3], raw, 'attempt', attempt, 'defers', defers, 'enqueued_at', enqueued_at)
|
|
40
|
+
else
|
|
41
|
+
redis.call('XADD', KEYS[2], '*', ARGV[3], member, 'enqueued_at', ARGV[1])
|
|
42
|
+
end
|
|
30
43
|
end
|
|
31
44
|
return #due
|
|
32
45
|
LUA
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
# Resets a pending message's idle timer, but only while it still belongs to
|
|
48
|
+
# the given consumer, so a worker that stalled and was reclaimed can't steal
|
|
49
|
+
# the message back. Returns 1 if it did, 0 if not.
|
|
50
|
+
# KEYS: stream ARGV: group, consumer, entry id
|
|
51
|
+
TOUCH_SCRIPT = <<~LUA.freeze
|
|
52
|
+
local pending = redis.call('XPENDING', KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)
|
|
53
|
+
if pending[1] and pending[1][2] == ARGV[2] then
|
|
54
|
+
redis.call('XCLAIM', KEYS[1], ARGV[1], ARGV[2], 0, ARGV[3], 'JUSTID')
|
|
55
|
+
return 1
|
|
56
|
+
end
|
|
57
|
+
return 0
|
|
58
|
+
LUA
|
|
59
|
+
|
|
60
|
+
# A message in the dead stream. `raw` is the stored payload (not decoded).
|
|
61
|
+
DeadMessage = Struct.new(:id, :raw, :reason, :error_class, :error_message,
|
|
62
|
+
:attempt, :defers, :enqueued_at, :dead_at, :original_id, keyword_init: true)
|
|
63
|
+
|
|
64
|
+
attr_reader :name, :stream, :group, :deferred_key, :dead_key, :field, :serializer,
|
|
65
|
+
:reclaim_after_ms, :max_attempts, :max_defers, :retry_backoff, :defer_backoff
|
|
35
66
|
|
|
36
67
|
# name - used to derive the default Redis keys.
|
|
37
|
-
# stream/group/deferred_key
|
|
68
|
+
# stream/group/deferred_key/dead_key
|
|
38
69
|
# - override the derived names, e.g. to adopt keys an
|
|
39
|
-
# application already uses. (On Redis Cluster the stream
|
|
40
|
-
# and
|
|
70
|
+
# application already uses. (On Redis Cluster the stream,
|
|
71
|
+
# deferred and dead keys must share a hash tag.)
|
|
41
72
|
# field - the stream field holding the payload.
|
|
73
|
+
# serializer - see Queekiq::Serializers (default: Raw, plain strings).
|
|
42
74
|
# reclaim_after_ms - idle time after which a pending message may be reclaimed.
|
|
43
|
-
#
|
|
75
|
+
# Workers heartbeat while a handler runs, so this only has
|
|
76
|
+
# to cover a worker dying, not your slowest job.
|
|
77
|
+
# max_attempts - how many times a message may be handled before it is
|
|
78
|
+
# dead-lettered; nil retries forever.
|
|
79
|
+
# max_defers - how many times a message may be deferred before it is
|
|
80
|
+
# dead-lettered; nil (default) defers forever.
|
|
81
|
+
# retry_backoff - delay before retrying a failed message.
|
|
82
|
+
# defer_backoff - delay before a deferred message comes back.
|
|
44
83
|
# delete_on_ack - also XDEL entries once acked, so the stream doesn't
|
|
45
84
|
# grow forever. Only safe with a single consumer group
|
|
46
85
|
# (which is what Queekiq uses).
|
|
47
86
|
# redis - a client for this queue; defaults to Queekiq.redis.
|
|
48
|
-
def initialize(name, stream: nil, group: nil, deferred_key: nil, field: DEFAULT_FIELD,
|
|
49
|
-
|
|
87
|
+
def initialize(name, stream: nil, group: nil, deferred_key: nil, dead_key: nil, field: DEFAULT_FIELD,
|
|
88
|
+
serializer: Serializers::Raw, reclaim_after_ms: DEFAULT_RECLAIM_AFTER_MS,
|
|
89
|
+
max_attempts: DEFAULT_MAX_ATTEMPTS, max_defers: nil,
|
|
90
|
+
retry_backoff: Backoff.new(base_ms: 1_000, max_ms: 300_000),
|
|
91
|
+
defer_backoff: Backoff.new(base_ms: 200, max_ms: 5_000),
|
|
50
92
|
delete_on_ack: false, redis: nil)
|
|
51
93
|
raise ArgumentError, "queue name can't be blank" if name.to_s.strip.empty?
|
|
94
|
+
raise ArgumentError, "field can't be one of #{RESERVED_FIELDS.join(", ")}" if RESERVED_FIELDS.include?(field.to_s)
|
|
52
95
|
|
|
53
96
|
@name = name.to_s
|
|
54
97
|
@stream = stream || "queekiq:{#{@name}}:stream"
|
|
55
98
|
@group = group || "queekiq-#{@name}"
|
|
56
99
|
@deferred_key = deferred_key || "queekiq:{#{@name}}:deferred"
|
|
57
|
-
@
|
|
100
|
+
@dead_key = dead_key || "queekiq:{#{@name}}:dead"
|
|
101
|
+
@field = field.to_s
|
|
102
|
+
@serializer = serializer
|
|
58
103
|
@reclaim_after_ms = reclaim_after_ms
|
|
59
|
-
@
|
|
104
|
+
@max_attempts = max_attempts
|
|
105
|
+
@max_defers = max_defers
|
|
106
|
+
@retry_backoff = retry_backoff
|
|
107
|
+
@defer_backoff = defer_backoff
|
|
60
108
|
@delete_on_ack = delete_on_ack
|
|
61
109
|
@redis = redis
|
|
62
110
|
end
|
|
@@ -65,25 +113,79 @@ module Queekiq
|
|
|
65
113
|
@redis || Queekiq.redis
|
|
66
114
|
end
|
|
67
115
|
|
|
68
|
-
#
|
|
69
|
-
|
|
116
|
+
# --- producing -----------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
# Appends a message to the stream and returns its entry id. The payload goes
|
|
119
|
+
# through the serializer (the default one stores `payload.to_s`).
|
|
70
120
|
def enqueue(payload)
|
|
71
|
-
redis.xadd(stream, { field => payload
|
|
121
|
+
redis.xadd(stream, { field => serializer.dump(payload), "enqueued_at" => now_ms })
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# --- moving messages around ----------------------------------------------
|
|
125
|
+
|
|
126
|
+
# Schedules a message to come back later. Given a delivered Message, the
|
|
127
|
+
# delivery is acked in the same atomic step, so the message is never both
|
|
128
|
+
# pending and deferred. Given a bare payload it just schedules a new one.
|
|
129
|
+
# The delay comes from defer_backoff (by how often it was deferred before)
|
|
130
|
+
# unless `delay_ms` is given. Returns the delay used.
|
|
131
|
+
def defer(message_or_payload, delay_ms: nil)
|
|
132
|
+
message = as_message(message_or_payload)
|
|
133
|
+
defers = message.defers + 1
|
|
134
|
+
delay = delay_ms || defer_backoff.delay_ms(defers)
|
|
135
|
+
schedule(message, delay, attempt: message.attempt, defers: defers)
|
|
136
|
+
delay
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Schedules a failed message for another attempt (attempt + 1) and acks the
|
|
140
|
+
# current delivery, atomically. The delay comes from retry_backoff unless
|
|
141
|
+
# `delay_ms` is given. Returns the delay used.
|
|
142
|
+
def retry_later(message, delay_ms: nil)
|
|
143
|
+
delay = delay_ms || retry_backoff.delay_ms(message.attempt)
|
|
144
|
+
schedule(message, delay, attempt: message.attempt + 1, defers: message.defers)
|
|
145
|
+
delay
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Moves a message to the dead stream, with the reason and the error that
|
|
149
|
+
# caused it, and acks the delivery, atomically.
|
|
150
|
+
def dead_letter(message, reason:, error: nil)
|
|
151
|
+
entry = { field => message.raw, "reason" => reason.to_s, "attempt" => message.attempt,
|
|
152
|
+
"defers" => message.defers, "enqueued_at" => message.enqueued_at,
|
|
153
|
+
"dead_at" => now_ms, "original_id" => message.id }
|
|
154
|
+
if error
|
|
155
|
+
entry["error_class"] = error.class.name
|
|
156
|
+
entry["error_message"] = error.message.to_s[0, 1_000]
|
|
157
|
+
end
|
|
158
|
+
redis.multi do |tx|
|
|
159
|
+
tx.xadd(dead_key, entry.compact)
|
|
160
|
+
ack_commands(tx, message.id) if message.id
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# True once a message has used up its attempts (attempt > max_attempts), e.g.
|
|
165
|
+
# because it keeps crashing its workers. Workers dead-letter these without
|
|
166
|
+
# running the handler again.
|
|
167
|
+
def attempts_exhausted?(message)
|
|
168
|
+
!max_attempts.nil? && message.attempt > max_attempts
|
|
72
169
|
end
|
|
73
170
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def defer(payload, delay_ms: defer_delay_ms)
|
|
78
|
-
redis.zadd(deferred_key, now_ms + delay_ms, payload.to_s)
|
|
171
|
+
# True if the message is on its last allowed attempt, so a failure now is final.
|
|
172
|
+
def last_attempt?(message)
|
|
173
|
+
!max_attempts.nil? && message.attempt >= max_attempts
|
|
79
174
|
end
|
|
80
175
|
|
|
81
|
-
#
|
|
176
|
+
# True if the message has already been deferred max_defers times.
|
|
177
|
+
def defers_exhausted?(message)
|
|
178
|
+
!max_defers.nil? && message.defers >= max_defers
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Re-enqueues up to `limit` deferred messages whose delay has elapsed.
|
|
82
182
|
# Returns how many were promoted.
|
|
83
183
|
def promote_due!(limit: 50)
|
|
84
184
|
redis.eval(PROMOTE_SCRIPT, keys: [ deferred_key, stream ], argv: [ now_ms, limit, field ])
|
|
85
185
|
end
|
|
86
186
|
|
|
187
|
+
# --- consuming -----------------------------------------------------------
|
|
188
|
+
|
|
87
189
|
# Creates the consumer group (and the stream if needed). Safe to call
|
|
88
190
|
# repeatedly. The group starts at the beginning of the stream, so messages
|
|
89
191
|
# enqueued before the group existed are still delivered.
|
|
@@ -98,37 +200,97 @@ module Queekiq
|
|
|
98
200
|
# pending until #ack.
|
|
99
201
|
def read(consumer:, count: 1, block_ms: 5_000)
|
|
100
202
|
result = redis.xreadgroup(group, consumer, stream, ">", count: count, block: block_ms)
|
|
101
|
-
|
|
203
|
+
(result&.dig(stream) || []).map { |id, fields| build_message(id, fields) }
|
|
102
204
|
end
|
|
103
205
|
|
|
104
206
|
# Takes over pending messages that have been idle for reclaim_after_ms
|
|
105
|
-
# (their consumer probably died) and returns them as Messages.
|
|
207
|
+
# (their consumer probably died) and returns them as Messages. Their
|
|
208
|
+
# `attempt` includes the deliveries that never finished.
|
|
106
209
|
def reclaim(consumer:, count: 10)
|
|
107
210
|
result = redis.xautoclaim(stream, group, consumer, reclaim_after_ms, "0-0", count: count)
|
|
108
211
|
entries = result.is_a?(Hash) ? result["entries"] : result[1]
|
|
109
|
-
|
|
212
|
+
(entries || []).map { |id, fields| build_message(id, fields, deliveries: delivery_count(id)) }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Heartbeat: resets the idle timer of a message this consumer is still
|
|
216
|
+
# working on, so it isn't reclaimed. Returns false if the message no longer
|
|
217
|
+
# belongs to this consumer (it was acked or reclaimed meanwhile).
|
|
218
|
+
def touch(message_or_id, consumer:)
|
|
219
|
+
id = message_or_id.respond_to?(:id) ? message_or_id.id : message_or_id
|
|
220
|
+
redis.eval(TOUCH_SCRIPT, keys: [ stream ], argv: [ group, consumer, id ]) == 1
|
|
110
221
|
end
|
|
111
222
|
|
|
112
223
|
# Marks a message as done. Accepts a Message or a bare stream entry id.
|
|
113
224
|
def ack(message_or_id)
|
|
114
225
|
id = message_or_id.respond_to?(:id) ? message_or_id.id : message_or_id
|
|
115
|
-
redis.
|
|
116
|
-
redis.xdel(stream, id) if @delete_on_ack
|
|
226
|
+
redis.multi { |tx| ack_commands(tx, id) }
|
|
117
227
|
end
|
|
118
228
|
|
|
119
|
-
#
|
|
229
|
+
# --- inspecting ----------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
# Number of entries currently in the stream (including acked ones that
|
|
232
|
+
# haven't been deleted).
|
|
120
233
|
def size
|
|
121
234
|
redis.xlen(stream)
|
|
122
235
|
end
|
|
123
236
|
|
|
124
|
-
# Number of
|
|
237
|
+
# Number of messages waiting in the deferred set.
|
|
125
238
|
def deferred_size
|
|
126
239
|
redis.zcard(deferred_key)
|
|
127
240
|
end
|
|
128
241
|
|
|
129
|
-
#
|
|
242
|
+
# Number of messages in the dead stream.
|
|
243
|
+
def dead_size
|
|
244
|
+
redis.xlen(dead_key)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# A snapshot for dashboards and alerts:
|
|
248
|
+
# backlog - entries not yet delivered to the group (nil if Redis can't tell)
|
|
249
|
+
# pending - delivered but not acked
|
|
250
|
+
# oldest_pending_idle_ms - how long the oldest pending message has been
|
|
251
|
+
# idle; large values mean stuck or dead workers
|
|
252
|
+
# deferred / dead - sizes of those sets
|
|
253
|
+
# consumers - [{ name:, pending:, idle_ms: }] per consumer
|
|
254
|
+
def stats
|
|
255
|
+
info = group_info
|
|
256
|
+
{
|
|
257
|
+
backlog: info && info["lag"],
|
|
258
|
+
pending: info ? info["pending"] : 0,
|
|
259
|
+
oldest_pending_idle_ms: info && info["pending"].positive? ? oldest_pending_idle_ms : nil,
|
|
260
|
+
deferred: deferred_size,
|
|
261
|
+
dead: dead_size,
|
|
262
|
+
consumers: info ? consumers : []
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# The oldest `count` dead messages, as DeadMessage structs.
|
|
267
|
+
def dead_messages(count: 100)
|
|
268
|
+
redis.xrange(dead_key, "-", "+", count: count).map { |id, fields| build_dead_message(id, fields) }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Puts a dead message back on the stream with a fresh attempt counter and
|
|
272
|
+
# removes it from the dead stream. Returns false if there's no such entry.
|
|
273
|
+
def requeue_dead(id)
|
|
274
|
+
entry = redis.xrange(dead_key, id, id).first
|
|
275
|
+
return false unless entry
|
|
276
|
+
|
|
277
|
+
raw = entry.last.fetch(field)
|
|
278
|
+
redis.multi do |tx|
|
|
279
|
+
tx.xadd(stream, { field => raw, "enqueued_at" => now_ms })
|
|
280
|
+
tx.xdel(dead_key, id)
|
|
281
|
+
end
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Removes a message from the dead stream for good.
|
|
286
|
+
def delete_dead(id)
|
|
287
|
+
redis.xdel(dead_key, id).positive?
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Deletes the stream (and its group), the deferred set and the dead stream.
|
|
291
|
+
# Meant for tests.
|
|
130
292
|
def clear!
|
|
131
|
-
redis.del(stream, deferred_key)
|
|
293
|
+
redis.del(stream, deferred_key, dead_key)
|
|
132
294
|
end
|
|
133
295
|
|
|
134
296
|
private
|
|
@@ -137,11 +299,63 @@ module Queekiq
|
|
|
137
299
|
(Time.now.to_f * 1000).to_i
|
|
138
300
|
end
|
|
139
301
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
302
|
+
def as_message(message_or_payload)
|
|
303
|
+
return message_or_payload if message_or_payload.is_a?(Message)
|
|
304
|
+
|
|
305
|
+
Message.new(id: nil, payload: message_or_payload, serializer: serializer, enqueued_at: now_ms)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Writes the message into the deferred set and acks its delivery, atomically.
|
|
309
|
+
def schedule(message, delay_ms, attempt:, defers:)
|
|
310
|
+
member = [ attempt, defers, message.enqueued_at || now_ms, message.raw ].join("|")
|
|
311
|
+
redis.multi do |tx|
|
|
312
|
+
tx.zadd(deferred_key, now_ms + delay_ms, member)
|
|
313
|
+
ack_commands(tx, message.id) if message.id
|
|
144
314
|
end
|
|
145
315
|
end
|
|
316
|
+
|
|
317
|
+
def ack_commands(tx, id)
|
|
318
|
+
tx.xack(stream, group, id)
|
|
319
|
+
tx.xdel(stream, id) if @delete_on_ack
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def build_message(id, fields, deliveries: 1)
|
|
323
|
+
raw = fields.fetch(field) { raise Error, "stream entry #{id} has no #{field.inspect} field" }
|
|
324
|
+
carried = fields["attempt"] ? fields["attempt"].to_i : 1
|
|
325
|
+
Message.new(id: id, raw: raw, serializer: serializer,
|
|
326
|
+
attempt: carried + deliveries - 1,
|
|
327
|
+
defers: fields["defers"].to_i,
|
|
328
|
+
enqueued_at: fields["enqueued_at"]&.to_i)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def build_dead_message(id, fields)
|
|
332
|
+
DeadMessage.new(id: id, raw: fields[field], reason: fields["reason"],
|
|
333
|
+
error_class: fields["error_class"], error_message: fields["error_message"],
|
|
334
|
+
attempt: fields["attempt"]&.to_i, defers: fields["defers"]&.to_i,
|
|
335
|
+
enqueued_at: fields["enqueued_at"]&.to_i, dead_at: fields["dead_at"]&.to_i,
|
|
336
|
+
original_id: fields["original_id"])
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# How many times Redis has delivered this entry (XAUTOCLAIM counts each claim).
|
|
340
|
+
def delivery_count(id)
|
|
341
|
+
pending = redis.xpending(stream, group, id, id, 1).first
|
|
342
|
+
pending ? pending["count"] : 1
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def group_info
|
|
346
|
+
redis.xinfo(:groups, stream).find { |g| g["name"] == group }
|
|
347
|
+
rescue Redis::CommandError => e
|
|
348
|
+
raise unless e.message.include?("no such key")
|
|
349
|
+
|
|
350
|
+
nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def consumers
|
|
354
|
+
redis.xinfo(:consumers, stream, group).map { |c| { name: c["name"], pending: c["pending"], idle_ms: c["idle"] } }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def oldest_pending_idle_ms
|
|
358
|
+
redis.xpending(stream, group, "-", "+", 1).first&.fetch("elapsed")
|
|
359
|
+
end
|
|
146
360
|
end
|
|
147
361
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Queekiq
|
|
4
|
+
# A serializer is any object with `dump(object) -> String` and
|
|
5
|
+
# `load(String) -> object`. Queue#enqueue stores what `dump` returns;
|
|
6
|
+
# Message#payload returns what `load` makes of it.
|
|
7
|
+
module Serializers
|
|
8
|
+
# Stores payloads as plain strings (`to_s`) and hands them back untouched.
|
|
9
|
+
# This is the default and matches how the stream looks without Queekiq.
|
|
10
|
+
module Raw
|
|
11
|
+
def self.dump(object)
|
|
12
|
+
object.to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.load(string)
|
|
16
|
+
string
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Stores payloads as JSON, so hashes, arrays, numbers etc. round-trip.
|
|
21
|
+
# Hash keys come back as strings.
|
|
22
|
+
module JSON
|
|
23
|
+
def self.dump(object)
|
|
24
|
+
::JSON.generate(object)
|
|
25
|
+
rescue ::JSON::JSONError => e
|
|
26
|
+
raise SerializationError, "can't serialize #{object.class} as JSON: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.load(string)
|
|
30
|
+
::JSON.parse(string)
|
|
31
|
+
rescue ::JSON::JSONError => e
|
|
32
|
+
raise SerializationError, "payload is not valid JSON: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/queekiq/version.rb
CHANGED