falqon 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/Gemfile +40 -8
- data/README.md +107 -8
- data/bin/falqon +8 -0
- data/config/inflections.rb +3 -1
- data/lib/falqon/cli/base.rb +35 -0
- data/lib/falqon/cli/clear.rb +86 -0
- data/lib/falqon/cli/delete.rb +152 -0
- data/lib/falqon/cli/kill.rb +143 -0
- data/lib/falqon/cli/list.rb +26 -0
- data/lib/falqon/cli/refill.rb +36 -0
- data/lib/falqon/cli/revive.rb +36 -0
- data/lib/falqon/cli/schedule.rb +40 -0
- data/lib/falqon/cli/show.rb +189 -0
- data/lib/falqon/cli/stats.rb +44 -0
- data/lib/falqon/cli/status.rb +47 -0
- data/lib/falqon/cli/version.rb +14 -0
- data/lib/falqon/cli.rb +168 -0
- data/lib/falqon/concerns/hooks.rb +101 -0
- data/lib/falqon/configuration.rb +141 -0
- data/lib/falqon/connection_pool_snooper.rb +15 -0
- data/lib/falqon/data.rb +11 -0
- data/lib/falqon/error.rb +13 -0
- data/lib/falqon/identifier.rb +11 -0
- data/lib/falqon/message.rb +220 -0
- data/lib/falqon/middlewares/logger.rb +32 -0
- data/lib/falqon/queue.rb +626 -0
- data/lib/falqon/strategies/linear.rb +82 -0
- data/lib/falqon/strategies/none.rb +44 -0
- data/lib/falqon/strategy.rb +26 -0
- data/lib/falqon/sub_queue.rb +95 -0
- data/lib/falqon/sub_set.rb +91 -0
- data/lib/falqon/version.rb +7 -2
- data/lib/falqon.rb +63 -0
- data/lib/generators/falqon/install.rb +37 -0
- metadata +96 -9
data/lib/falqon/queue.rb
ADDED
@@ -0,0 +1,626 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
##
|
7
|
+
# Simple, efficient, and reliable messaging queue implementation
|
8
|
+
#
|
9
|
+
class Queue
|
10
|
+
include Hooks
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
# The name of the queue (without prefix)
|
14
|
+
sig { returns(String) }
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# The identifier of the queue (with prefix)
|
18
|
+
sig { returns(String) }
|
19
|
+
attr_reader :id
|
20
|
+
|
21
|
+
# The configured retry strategy of the queue
|
22
|
+
sig { returns(Strategy) }
|
23
|
+
attr_reader :retry_strategy
|
24
|
+
|
25
|
+
# The maximum number of retries before a message is considered failed
|
26
|
+
sig { returns(Integer) }
|
27
|
+
attr_reader :max_retries
|
28
|
+
|
29
|
+
# The delay in seconds before a message is eligible for a retry
|
30
|
+
sig { returns(Integer) }
|
31
|
+
attr_reader :retry_delay
|
32
|
+
|
33
|
+
# @!visibility private
|
34
|
+
sig { returns(ConnectionPool) }
|
35
|
+
attr_reader :redis
|
36
|
+
|
37
|
+
# @!visibility private
|
38
|
+
sig { returns(Logger) }
|
39
|
+
attr_reader :logger
|
40
|
+
|
41
|
+
# Create a new queue
|
42
|
+
#
|
43
|
+
# Create a new queue in Redis with the given name. If a queue with the same name already exists, it is reused.
|
44
|
+
# When registering a new queue, the following {Falqon::Queue::Metadata} is stored:
|
45
|
+
# - +created_at+: Timestamp of creation
|
46
|
+
# - +updated_at+: Timestamp of last update
|
47
|
+
# - +version+: Protocol version
|
48
|
+
#
|
49
|
+
# Initializing a queue with a different protocol version than the existing queue will raise a {Falqon::VersionMismatchError}.
|
50
|
+
# Currently queues are not compatible between different protocol versions, and must be deleted and recreated manually.
|
51
|
+
# In a future version, automatic migration between protocol versions may be supported.
|
52
|
+
#
|
53
|
+
# Please note that retry strategy, maximum retries, and retry delay are configured per queue instance, and are not shared between queue instances.
|
54
|
+
#
|
55
|
+
# @param name The name of the queue (without prefix)
|
56
|
+
# @param retry_strategy The retry strategy to use for failed messages
|
57
|
+
# @param max_retries The maximum number of retries before a message is considered failed
|
58
|
+
# @param retry_delay The delay in seconds before a message is eligible for a retry
|
59
|
+
# @param redis The Redis connection pool to use
|
60
|
+
# @param logger The logger to use
|
61
|
+
# @param version The protocol version to use
|
62
|
+
# @return The new queue
|
63
|
+
# @raise [Falqon::VersionMismatchError] if the protocol version of the existing queue does not match the protocol version of the new queue
|
64
|
+
#
|
65
|
+
# @example Create a new queue
|
66
|
+
# queue = Falqon::Queue.new("my_queue")
|
67
|
+
# queue.name # => "my_queue"
|
68
|
+
# queue.id # => "falqon/my_queue"
|
69
|
+
#
|
70
|
+
sig { params(name: String, retry_strategy: Symbol, max_retries: Integer, retry_delay: Integer, redis: ConnectionPool, logger: Logger, version: Integer).void }
|
71
|
+
def initialize(
|
72
|
+
name,
|
73
|
+
retry_strategy: Falqon.configuration.retry_strategy,
|
74
|
+
max_retries: Falqon.configuration.max_retries,
|
75
|
+
retry_delay: Falqon.configuration.retry_delay,
|
76
|
+
redis: Falqon.configuration.redis,
|
77
|
+
logger: Falqon.configuration.logger,
|
78
|
+
version: Falqon::PROTOCOL
|
79
|
+
)
|
80
|
+
@name = name
|
81
|
+
@id = [Falqon.configuration.prefix, name].compact.join("/")
|
82
|
+
@retry_strategy = Strategies.const_get(retry_strategy.to_s.capitalize).new(self)
|
83
|
+
@max_retries = max_retries
|
84
|
+
@retry_delay = retry_delay
|
85
|
+
@redis = redis
|
86
|
+
@logger = logger
|
87
|
+
@version = version
|
88
|
+
|
89
|
+
redis.with do |r|
|
90
|
+
queue_version = r.hget("#{id}:metadata", :version)
|
91
|
+
|
92
|
+
raise Falqon::VersionMismatchError, "Queue #{name} is using protocol version #{queue_version}, but this client is using protocol version #{version}" if queue_version && queue_version.to_i != @version
|
93
|
+
|
94
|
+
# Register the queue
|
95
|
+
r.sadd([Falqon.configuration.prefix, "queues"].compact.join(":"), name)
|
96
|
+
|
97
|
+
# Set creation and update timestamp (if not set)
|
98
|
+
r.hsetnx("#{id}:metadata", :created_at, Time.now.to_i)
|
99
|
+
r.hsetnx("#{id}:metadata", :updated_at, Time.now.to_i)
|
100
|
+
|
101
|
+
# Set protocol version
|
102
|
+
r.hsetnx("#{id}:metadata", :version, @version)
|
103
|
+
end
|
104
|
+
|
105
|
+
run_hook :initialize, :after
|
106
|
+
end
|
107
|
+
|
108
|
+
# Push data onto the tail of the queue
|
109
|
+
#
|
110
|
+
# @param data The data to push onto the queue (one or more strings)
|
111
|
+
# @return The identifier(s) of the pushed message(s)
|
112
|
+
#
|
113
|
+
# @example Push a single message
|
114
|
+
# queue = Falqon::Queue.new("my_queue")
|
115
|
+
# queue.push("Hello, world!") # => "1"
|
116
|
+
#
|
117
|
+
# @example Push multiple messages
|
118
|
+
# queue = Falqon::Queue.new("my_queue")
|
119
|
+
# queue.push("Hello, world!", "Goodbye, world!") # => ["1", "2"]
|
120
|
+
#
|
121
|
+
sig { params(data: Data).returns(T.any(Identifier, T::Array[Identifier])) }
|
122
|
+
def push(*data)
|
123
|
+
logger.debug "Pushing #{data.size} messages onto queue #{name}"
|
124
|
+
|
125
|
+
run_hook :push, :before
|
126
|
+
|
127
|
+
# Set update timestamp
|
128
|
+
redis.with { |r| r.hset("#{id}:metadata", :updated_at, Time.now.to_i) }
|
129
|
+
|
130
|
+
ids = data.map do |d|
|
131
|
+
message = Message
|
132
|
+
.new(self, data: d)
|
133
|
+
.create
|
134
|
+
|
135
|
+
# Push identifier to queue
|
136
|
+
pending.add(message.id)
|
137
|
+
|
138
|
+
# Set message status
|
139
|
+
redis.with { |r| r.hset("#{id}:metadata:#{message.id}", :status, "pending") }
|
140
|
+
|
141
|
+
# Return identifier(s)
|
142
|
+
data.size == 1 ? (return message.id) : (next message.id)
|
143
|
+
end
|
144
|
+
|
145
|
+
run_hook :push, :after
|
146
|
+
|
147
|
+
# Return identifier(s)
|
148
|
+
ids
|
149
|
+
end
|
150
|
+
|
151
|
+
# Pop data from the head of the queue
|
152
|
+
#
|
153
|
+
# This method blocks until a message is available.
|
154
|
+
#
|
155
|
+
# == Acknowledgement
|
156
|
+
#
|
157
|
+
# If a block is given, the popped data is passed to the block. If the block raises a {Falqon::Error} exception, the message is retried according to the configured retry strategy.
|
158
|
+
# If no exception is raised, the message is ackwnowledged and removed from the queue.
|
159
|
+
#
|
160
|
+
# If no block is given, the popped data is returned.
|
161
|
+
# The message is immediately acknowledged and removed from the queue.
|
162
|
+
#
|
163
|
+
# @param block A block to execute with the popped data (block-style)
|
164
|
+
# @return The popped data (return-style)
|
165
|
+
#
|
166
|
+
# @example Pop a message (return-style)
|
167
|
+
# queue = Falqon::Queue.new("my_queue")
|
168
|
+
# queue.push("Hello, world!")
|
169
|
+
# queue.pop # => "Hello, world!"
|
170
|
+
#
|
171
|
+
# @example Pop a message (block-style)
|
172
|
+
# queue = Falqon::Queue.new("my_queue")
|
173
|
+
# queue.push("Hello, world!")
|
174
|
+
# queue.pop do |data|
|
175
|
+
# puts data # => "Hello, world!"
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
sig { params(block: T.nilable(T.proc.params(data: Data).void)).returns(T.nilable(Data)) }
|
179
|
+
def pop(&block)
|
180
|
+
logger.debug "Popping message from queue #{name}"
|
181
|
+
|
182
|
+
run_hook :pop, :before
|
183
|
+
|
184
|
+
message = redis.with do |r|
|
185
|
+
# Move identifier from pending queue to processing queue
|
186
|
+
message_id = r.blmove(pending.id, processing.id, :left, :right).to_i
|
187
|
+
|
188
|
+
# Set message status
|
189
|
+
r.hset("#{id}:metadata:#{message_id}", :status, "processing")
|
190
|
+
|
191
|
+
# Set update timestamp
|
192
|
+
r.hset("#{id}:metadata", :updated_at, Time.now.to_i)
|
193
|
+
r.hset("#{id}:metadata:#{message_id}", :updated_at, Time.now.to_i)
|
194
|
+
|
195
|
+
# Increment processing counter
|
196
|
+
r.hincrby("#{id}:metadata", :processed, 1)
|
197
|
+
|
198
|
+
# Increment retry counter if message is retried
|
199
|
+
r.hincrby("#{id}:metadata", :retried, 1) if r.hget("#{id}:metadata:#{message_id}", :retries).to_i.positive?
|
200
|
+
|
201
|
+
Message.new(self, id: message_id)
|
202
|
+
end
|
203
|
+
|
204
|
+
data = message.data
|
205
|
+
|
206
|
+
yield data if block
|
207
|
+
|
208
|
+
run_hook :pop, :after
|
209
|
+
|
210
|
+
# Remove identifier from processing queue
|
211
|
+
processing.remove(message.id)
|
212
|
+
|
213
|
+
# Delete message
|
214
|
+
message.delete
|
215
|
+
|
216
|
+
data
|
217
|
+
rescue Error => e
|
218
|
+
logger.debug "Error processing message #{message.id}: #{e.message}"
|
219
|
+
|
220
|
+
# Increment failure counter
|
221
|
+
redis.with { |r| r.hincrby("#{id}:metadata", :failed, 1) }
|
222
|
+
|
223
|
+
# Retry message according to configured strategy
|
224
|
+
retry_strategy.retry(message, e)
|
225
|
+
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
|
229
|
+
# Peek at the next message in the queue
|
230
|
+
#
|
231
|
+
# Use {#range} to peek at a range of messages.
|
232
|
+
# This method does not block.
|
233
|
+
#
|
234
|
+
# @param index The index of the message to peek at
|
235
|
+
# @return The data of the peeked message
|
236
|
+
#
|
237
|
+
# @example Peek at the next message
|
238
|
+
# queue = Falqon::Queue.new("my_queue")
|
239
|
+
# queue.push("Hello, world!")
|
240
|
+
# queue.peek # => "Hello, world!"
|
241
|
+
# queue.pop # => "Hello, world!"
|
242
|
+
#
|
243
|
+
# @example Peek at the next message with an offset
|
244
|
+
# queue = Falqon::Queue.new("my_queue")
|
245
|
+
# queue.push("Hello, world!", "Goodbye, world!")
|
246
|
+
# queue.peek(1) # => "Goodbye, world!"
|
247
|
+
# queue.pop # => "Hello, world!"
|
248
|
+
#
|
249
|
+
sig { params(index: Integer).returns(T.nilable(Data)) }
|
250
|
+
def peek(index: 0)
|
251
|
+
logger.debug "Peeking at next message in queue #{name}"
|
252
|
+
|
253
|
+
run_hook :peek, :before
|
254
|
+
|
255
|
+
# Get identifier from pending queue
|
256
|
+
message_id = pending.peek(index:)
|
257
|
+
|
258
|
+
return unless message_id
|
259
|
+
|
260
|
+
run_hook :peek, :after
|
261
|
+
|
262
|
+
# Retrieve data
|
263
|
+
Message.new(self, id: message_id).data
|
264
|
+
end
|
265
|
+
|
266
|
+
# Peek at the next messages in the queue
|
267
|
+
#
|
268
|
+
# Use {#peek} to peek at a single message.
|
269
|
+
# This method does not block.
|
270
|
+
#
|
271
|
+
# @param start The start index of the range to peek at
|
272
|
+
# @param stop The stop index of the range to peek at (set to -1 to peek at all messages)
|
273
|
+
# @return The data of the peeked messages
|
274
|
+
#
|
275
|
+
# @example Peek at the next messages
|
276
|
+
# queue = Falqon::Queue.new("my_queue")
|
277
|
+
# queue.push("Hello, world!", "Goodbye, world!", "Hello again, world!")
|
278
|
+
# queue.range(start: 1, stop: 2) # => ["Goodbye, world!", "Hello again, world!"]
|
279
|
+
# queue.range(start: 1) # => ["Goodbye, world!", "Hello again, world!"]
|
280
|
+
# queue.pop # => "Hello, world!"
|
281
|
+
#
|
282
|
+
sig { params(start: Integer, stop: Integer).returns(T::Array[Data]) }
|
283
|
+
def range(start: 0, stop: -1)
|
284
|
+
logger.debug "Peeking at next messages in queue #{name}"
|
285
|
+
|
286
|
+
run_hook :range, :before
|
287
|
+
|
288
|
+
# Get identifiers from pending queue
|
289
|
+
message_ids = pending.range(start:, stop:)
|
290
|
+
|
291
|
+
return [] unless message_ids.any?
|
292
|
+
|
293
|
+
run_hook :range, :after
|
294
|
+
|
295
|
+
# Retrieve data
|
296
|
+
message_ids.map { |id| Message.new(self, id:).data }
|
297
|
+
end
|
298
|
+
|
299
|
+
# Clear the queue, removing all messages
|
300
|
+
#
|
301
|
+
# This method clears all messages from the queue, including pending, processing, scheduled, and dead messages.
|
302
|
+
# It also resets the metadata counters for processed, failed, and retried messages, but does not deregister the queue.
|
303
|
+
#
|
304
|
+
# @return The identifiers of the cleared messages
|
305
|
+
#
|
306
|
+
# @example Clear the queue
|
307
|
+
# queue = Falqon::Queue.new("my_queue")
|
308
|
+
# queue.push("Hello, world!")
|
309
|
+
# queue.clear # => ["1"]
|
310
|
+
#
|
311
|
+
sig { returns(T::Array[Identifier]) }
|
312
|
+
def clear
|
313
|
+
logger.debug "Clearing queue #{name}"
|
314
|
+
|
315
|
+
run_hook :clear, :before
|
316
|
+
|
317
|
+
# Clear all sub-queues
|
318
|
+
message_ids = pending.clear + processing.clear + scheduled.clear + dead.clear
|
319
|
+
|
320
|
+
redis.with do |r|
|
321
|
+
# Clear metadata
|
322
|
+
r.hdel("#{id}:metadata", :processed, :failed, :retried)
|
323
|
+
|
324
|
+
# Set update timestamp
|
325
|
+
r.hset("#{id}:metadata", :updated_at, Time.now.to_i)
|
326
|
+
end
|
327
|
+
|
328
|
+
run_hook :clear, :after
|
329
|
+
|
330
|
+
# Return identifiers
|
331
|
+
message_ids
|
332
|
+
end
|
333
|
+
|
334
|
+
# Delete the queue, removing all messages and deregistering the queue
|
335
|
+
#
|
336
|
+
# This method deletes the queue, removing all messages, metadata, and deregisters the queue.
|
337
|
+
#
|
338
|
+
# @example Delete the queue
|
339
|
+
# queue = Falqon::Queue.new("my_queue")
|
340
|
+
# queue.push("Hello, world!")
|
341
|
+
# queue.clear # => nil
|
342
|
+
#
|
343
|
+
sig { void }
|
344
|
+
def delete
|
345
|
+
logger.debug "Deleting queue #{name}"
|
346
|
+
|
347
|
+
run_hook :delete, :before
|
348
|
+
|
349
|
+
# Delete all sub-queues
|
350
|
+
[pending, processing, scheduled, dead]
|
351
|
+
.each(&:clear)
|
352
|
+
|
353
|
+
redis.with do |r|
|
354
|
+
# Delete metadata
|
355
|
+
r.del("#{id}:metadata")
|
356
|
+
|
357
|
+
# Deregister the queue
|
358
|
+
r.srem([Falqon.configuration.prefix, "queues"].compact.join(":"), name)
|
359
|
+
end
|
360
|
+
|
361
|
+
run_hook :delete, :after
|
362
|
+
end
|
363
|
+
|
364
|
+
# Refill the queue with messages from the processing queue
|
365
|
+
#
|
366
|
+
# This method moves all messages from the processing queue back to the pending queue (in order).
|
367
|
+
# It is useful when a worker crashes or is stopped, and messages are left in the processing queue.
|
368
|
+
#
|
369
|
+
# @return The identifiers of the refilled messages
|
370
|
+
#
|
371
|
+
# @example Refill the queue
|
372
|
+
# queue = Falqon::Queue.new("my_queue")
|
373
|
+
# queue.push("Hello, world!")
|
374
|
+
# queue.pop { Kernel.exit! }
|
375
|
+
# ...
|
376
|
+
# queue.refill # => ["1"]
|
377
|
+
#
|
378
|
+
sig { returns(T::Array[Identifier]) }
|
379
|
+
def refill
|
380
|
+
logger.debug "Refilling queue #{name}"
|
381
|
+
|
382
|
+
run_hook :refill, :before
|
383
|
+
|
384
|
+
message_ids = []
|
385
|
+
|
386
|
+
# Move all identifiers from tail of processing queue to head of pending queue
|
387
|
+
redis.with do |r|
|
388
|
+
while (message_id = r.lmove(processing.id, id, :right, :left))
|
389
|
+
# Set message status
|
390
|
+
r.hset("#{id}:metadata:#{message_id}", :status, "pending")
|
391
|
+
|
392
|
+
message_ids << message_id
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
run_hook :refill, :after
|
397
|
+
|
398
|
+
message_ids
|
399
|
+
end
|
400
|
+
|
401
|
+
# Revive the queue with messages from the dead queue
|
402
|
+
#
|
403
|
+
# This method moves all messages from the dead queue back to the pending queue (in order).
|
404
|
+
# It is useful when messages are moved to the dead queue due to repeated failures, and need to be retried.
|
405
|
+
#
|
406
|
+
# @return The identifiers of the revived messages
|
407
|
+
#
|
408
|
+
# @example Revive the queue
|
409
|
+
# queue = Falqon::Queue.new("my_queue", max_retries: 0)
|
410
|
+
# queue.push("Hello, world!")
|
411
|
+
# queue.pop { raise Falqon::Error }
|
412
|
+
# queue.revive # => ["1"]
|
413
|
+
#
|
414
|
+
sig { returns(T::Array[Identifier]) }
|
415
|
+
def revive
|
416
|
+
logger.debug "Reviving queue #{name}"
|
417
|
+
|
418
|
+
run_hook :revive, :before
|
419
|
+
|
420
|
+
message_ids = []
|
421
|
+
|
422
|
+
# Move all identifiers from tail of dead queue to head of pending queue
|
423
|
+
redis.with do |r|
|
424
|
+
while (message_id = r.lmove(dead.id, id, :right, :left))
|
425
|
+
# Set message status
|
426
|
+
r.hset("#{id}:metadata:#{message_id}", :status, "pending")
|
427
|
+
|
428
|
+
message_ids << message_id
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
run_hook :revive, :after
|
433
|
+
|
434
|
+
message_ids
|
435
|
+
end
|
436
|
+
|
437
|
+
# Schedule failed messages for retry
|
438
|
+
#
|
439
|
+
# This method moves all eligible messages from the scheduled queue back to the head of the pending queue (in order).
|
440
|
+
# Messages are eligible for a retry according to the configured retry strategy.
|
441
|
+
#
|
442
|
+
# @return The identifiers of the scheduled messages
|
443
|
+
#
|
444
|
+
# @example Schedule failed messages
|
445
|
+
# queue = Falqon::Queue.new("my_queue", max_retries: 0, retry_delay: 5, retry_strategy: :linear)
|
446
|
+
# queue.push("Hello, world!")
|
447
|
+
# queue.pop { raise Falqon::Error }
|
448
|
+
# queue.schedule # => []
|
449
|
+
# sleep 5
|
450
|
+
# queue.schedule # => ["1"]
|
451
|
+
#
|
452
|
+
sig { returns(T::Array[Identifier]) }
|
453
|
+
def schedule
|
454
|
+
logger.debug "Scheduling failed messages on queue #{name}"
|
455
|
+
|
456
|
+
run_hook :schedule, :before
|
457
|
+
|
458
|
+
message_ids = T.let([], T::Array[Identifier])
|
459
|
+
|
460
|
+
# Move all due identifiers from scheduled queue to head of pending queue
|
461
|
+
redis.with do |r|
|
462
|
+
# Select all identifiers that are due (score <= current timestamp)
|
463
|
+
# FIXME: replace with zrange(by_score: true) when https://github.com/sds/mock_redis/issues/307 is resolved
|
464
|
+
message_ids = r.zrangebyscore(scheduled.id, 0, Time.now.to_i).map(&:to_i)
|
465
|
+
|
466
|
+
logger.debug "Scheduling messages #{message_ids.join(', ')} on queue #{name}"
|
467
|
+
|
468
|
+
message_ids.each do |message_id|
|
469
|
+
# Set message status
|
470
|
+
r.hset("#{id}:metadata:#{message_id}", :status, "pending")
|
471
|
+
|
472
|
+
# Add identifier to pending queue
|
473
|
+
pending.add(message_id)
|
474
|
+
|
475
|
+
# Remove identifier from scheduled queue
|
476
|
+
scheduled.remove(message_id)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
run_hook :schedule, :after
|
481
|
+
|
482
|
+
message_ids
|
483
|
+
end
|
484
|
+
|
485
|
+
# Size of the queue
|
486
|
+
#
|
487
|
+
# @return The number of messages in the queue
|
488
|
+
#
|
489
|
+
sig { returns(Integer) }
|
490
|
+
def size
|
491
|
+
pending.size
|
492
|
+
end
|
493
|
+
|
494
|
+
# Check if the queue is empty
|
495
|
+
#
|
496
|
+
# Only the pending queue is checked for emptiness.
|
497
|
+
#
|
498
|
+
# @return Whether the queue is empty
|
499
|
+
#
|
500
|
+
sig { returns(T::Boolean) }
|
501
|
+
def empty?
|
502
|
+
size.zero?
|
503
|
+
end
|
504
|
+
|
505
|
+
# Metadata of the queue
|
506
|
+
#
|
507
|
+
# @return The metadata of the queue
|
508
|
+
# @see Falqon::Queue::Metadata
|
509
|
+
#
|
510
|
+
sig { returns Metadata }
|
511
|
+
def metadata
|
512
|
+
redis.with do |r|
|
513
|
+
Metadata
|
514
|
+
.parse(r.hgetall("#{id}:metadata"))
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Subqueue for pending messages
|
519
|
+
# @!visibility private
|
520
|
+
#
|
521
|
+
# @return The subqueue for pending messages
|
522
|
+
#
|
523
|
+
sig { returns(SubQueue) }
|
524
|
+
def pending
|
525
|
+
@pending ||= SubQueue.new(self)
|
526
|
+
end
|
527
|
+
|
528
|
+
# Subqueue for processing messages
|
529
|
+
# @!visibility private
|
530
|
+
#
|
531
|
+
# @return The subqueue for processing messages
|
532
|
+
#
|
533
|
+
sig { returns(SubQueue) }
|
534
|
+
def processing
|
535
|
+
@processing ||= SubQueue.new(self, "processing")
|
536
|
+
end
|
537
|
+
|
538
|
+
# Subqueue for scheduled messages
|
539
|
+
# @!visibility private
|
540
|
+
#
|
541
|
+
# @return The subqueue for scheduled messages
|
542
|
+
#
|
543
|
+
sig { returns(SubSet) }
|
544
|
+
def scheduled
|
545
|
+
@scheduled ||= SubSet.new(self, "scheduled")
|
546
|
+
end
|
547
|
+
|
548
|
+
# Subqueue for dead messages
|
549
|
+
# @!visibility private
|
550
|
+
#
|
551
|
+
# @return The subqueue for dead messages
|
552
|
+
#
|
553
|
+
sig { returns(SubQueue) }
|
554
|
+
def dead
|
555
|
+
@dead ||= SubQueue.new(self, "dead")
|
556
|
+
end
|
557
|
+
|
558
|
+
# @!visibility private
|
559
|
+
sig { returns(String) }
|
560
|
+
def inspect
|
561
|
+
"#<#{self.class} name=#{name.inspect} pending=#{pending.size} processing=#{processing.size} scheduled=#{scheduled.size} dead=#{dead.size}>"
|
562
|
+
end
|
563
|
+
|
564
|
+
class << self
|
565
|
+
extend T::Sig
|
566
|
+
# Get a list of all registered queues
|
567
|
+
#
|
568
|
+
# @return The queues
|
569
|
+
#
|
570
|
+
sig { returns(T::Array[Queue]) }
|
571
|
+
def all
|
572
|
+
Falqon.configuration.redis.with do |r|
|
573
|
+
r
|
574
|
+
.smembers([Falqon.configuration.prefix, "queues"].compact.join(":"))
|
575
|
+
.map { |id| new(id) }
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
# Get the number of active (registered) queues
|
580
|
+
#
|
581
|
+
# @return The number of active (registered) queues
|
582
|
+
#
|
583
|
+
sig { returns(Integer) }
|
584
|
+
def size
|
585
|
+
Falqon.configuration.redis.with do |r|
|
586
|
+
r
|
587
|
+
.scard([Falqon.configuration.prefix, "queues"].compact.join(":"))
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
##
|
593
|
+
# Queue metadata
|
594
|
+
#
|
595
|
+
class Metadata < T::Struct
|
596
|
+
# Total number of messages processed
|
597
|
+
prop :processed, Integer, default: 0
|
598
|
+
|
599
|
+
# Total number of messages failed
|
600
|
+
prop :failed, Integer, default: 0
|
601
|
+
|
602
|
+
# Total number of messages retried
|
603
|
+
prop :retried, Integer, default: 0
|
604
|
+
|
605
|
+
# Timestamp of creation
|
606
|
+
prop :created_at, Integer
|
607
|
+
|
608
|
+
# Timestamp of last update
|
609
|
+
prop :updated_at, Integer
|
610
|
+
|
611
|
+
# Protocol version
|
612
|
+
prop :version, Integer
|
613
|
+
|
614
|
+
# Parse metadata from Redis hash
|
615
|
+
#
|
616
|
+
# @!visibility private
|
617
|
+
#
|
618
|
+
def self.parse(data)
|
619
|
+
# Transform keys to symbols and values to integers
|
620
|
+
new data
|
621
|
+
.transform_keys(&:to_sym)
|
622
|
+
.transform_values(&:to_i)
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
module Strategies
|
7
|
+
##
|
8
|
+
# Retry strategy that retries a fixed number of times
|
9
|
+
#
|
10
|
+
# When a message fails to process, it is moved to the scheduled queue, and retried after a fixed delay (configured by {Falqon::Queue#retry_delay}).
|
11
|
+
# If a messages fails to process after the maximum number of retries (configured by {Falqon::Queue#max_retries}), it is marked as dead, and moved to the dead subqueue.
|
12
|
+
#
|
13
|
+
# When using the linear strategy and the retry delay is set to a non-zero value, a scheduled needs to be started to retry the messages after the configured delay.
|
14
|
+
#
|
15
|
+
# queue = Falqon::Queue.new("my_queue")
|
16
|
+
#
|
17
|
+
# # Start the watcher in a separate thread
|
18
|
+
# Thread.new { loop { queue.schedule; sleep 1 } }
|
19
|
+
#
|
20
|
+
# # Or start the watcher in a separate fiber
|
21
|
+
# Fiber
|
22
|
+
# .new { loop { queue.schedule; sleep 1 } }
|
23
|
+
# .resume
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# queue = Falqon::Queue.new("my_queue", retry_strategy: :linear, retry_delay: 60, max_retries: 3)
|
27
|
+
# queue.push("Hello, World!")
|
28
|
+
# queue.pop { raise Falqon::Error }
|
29
|
+
# queue.inspect # => #<Falqon::Queue name="my_queue" pending=0 processing=0 scheduled=1 dead=0>
|
30
|
+
# sleep 60
|
31
|
+
# queue.pop # => "Hello, World!"
|
32
|
+
#
|
33
|
+
class Linear < Strategy
|
34
|
+
# @!visibility private
|
35
|
+
sig { params(message: Message, error: Error).void }
|
36
|
+
def retry(message, error)
|
37
|
+
queue.redis.with do |r|
|
38
|
+
# Increment retry count
|
39
|
+
retries = r.hincrby("#{queue.id}:metadata:#{message.id}", :retries, 1)
|
40
|
+
|
41
|
+
# Set error metadata
|
42
|
+
r.hset(
|
43
|
+
"#{queue.id}:metadata:#{message.id}",
|
44
|
+
:retried_at, Time.now.to_i,
|
45
|
+
:retry_error, error.message,
|
46
|
+
)
|
47
|
+
|
48
|
+
r.multi do |t|
|
49
|
+
if retries < queue.max_retries || queue.max_retries == -1
|
50
|
+
if queue.retry_delay.positive?
|
51
|
+
queue.logger.debug "Scheduling message #{message.id} on queue #{queue.name} in #{queue.retry_delay} seconds (attempt #{retries})"
|
52
|
+
|
53
|
+
# Add identifier to scheduled queue
|
54
|
+
queue.scheduled.add(message.id, Time.now.to_i + queue.retry_delay)
|
55
|
+
|
56
|
+
# Set message status
|
57
|
+
t.hset("#{queue.id}:metadata:#{message.id}", :status, "scheduled")
|
58
|
+
else
|
59
|
+
queue.logger.debug "Requeuing message #{message.id} on queue #{queue.name} (attempt #{retries})"
|
60
|
+
|
61
|
+
# Add identifier back to pending queue
|
62
|
+
queue.pending.add(message.id)
|
63
|
+
|
64
|
+
# Set message status
|
65
|
+
t.hset("#{queue.id}:metadata:#{message.id}", :status, "pending")
|
66
|
+
end
|
67
|
+
else
|
68
|
+
# Kill message after max retries
|
69
|
+
message.kill
|
70
|
+
|
71
|
+
# Set message status
|
72
|
+
t.hset("#{queue.id}:metadata:#{message.id}", :status, "dead")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove identifier from processing queue
|
76
|
+
queue.processing.remove(message.id)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|