falqon 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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