falqon 0.0.1 → 0.1.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.
@@ -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