rage-rb 1.28.2 → 1.29.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 +15 -0
- data/Gemfile +1 -1
- data/lib/rage/cable/cable.rb +84 -24
- data/lib/rage/cable/channel.rb +15 -9
- data/lib/rage/cable/connection.rb +62 -1
- data/lib/rage/cable/protocols/actioncable_v1_json.rb +36 -15
- data/lib/rage/cable/protocols/base.rb +18 -10
- data/lib/rage/cable/protocols/raw_web_socket_json.rb +22 -10
- data/lib/rage/cable/remote_disconnect.rb +69 -0
- data/lib/rage/cable/router.rb +42 -29
- data/lib/rage/code_loader.rb +1 -1
- data/lib/rage/configuration.rb +20 -19
- data/lib/rage/cookies.rb +11 -1
- data/lib/rage/daemon.rb +6 -5
- data/lib/rage/deferred/backends/disk.rb +215 -91
- data/lib/rage/deferred/backends/nil.rb +2 -2
- data/lib/rage/deferred/dead_task.rb +96 -0
- data/lib/rage/deferred/dead_tasks.rb +44 -0
- data/lib/rage/deferred/deferred.rb +28 -1
- data/lib/rage/extension.rb +8 -0
- data/lib/rage/fiber.rb +1 -1
- data/lib/rage/fiber_scheduler.rb +53 -39
- data/lib/rage/internal.rb +4 -0
- data/lib/rage/middleware/origin_validator.rb +35 -12
- data/lib/rage/pubsub/pubsub.rb +146 -1
- data/lib/rage/router/dsl.rb +4 -1
- data/lib/rage/rspec.rb +1 -6
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +6 -1
- metadata +6 -3
|
@@ -10,8 +10,10 @@ require "zlib"
|
|
|
10
10
|
# * `remove_task` - called when a task has to be removed from the storage;
|
|
11
11
|
# * `pending_tasks` - the method should iterate over the underlying storage and return a list of tasks to replay;
|
|
12
12
|
# * `add_dead_task` - called when a task has exhausted its retries or aborted them;
|
|
13
|
-
# * `
|
|
14
|
-
#
|
|
13
|
+
# * `each_dead_task` - yields one record for each task ID that exists when traversal starts;
|
|
14
|
+
# if multiple records have the same task ID, it yields the newest record;
|
|
15
|
+
# records added during traversal are not included;
|
|
16
|
+
# * `find_dead_task` - returns the newest dead-task record with the exact task ID;
|
|
15
17
|
# * `remove_dead_tasks` - permanently delete dead tasks;
|
|
16
18
|
#
|
|
17
19
|
class Rage::Deferred::Backends::Disk
|
|
@@ -53,19 +55,23 @@ class Rage::Deferred::Backends::Disk
|
|
|
53
55
|
@dead_tasks_storage.add(task_id, context, exception, task_class:, attempts:)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
# Yield one record for each task ID that exists when traversal starts.
|
|
59
|
+
# If multiple records have the same task ID, yield the newest record.
|
|
60
|
+
# Do not include records that Rage adds during traversal.
|
|
61
|
+
# Yield records from oldest to newest.
|
|
62
|
+
# @yieldparam record [Hash] a dead-task record
|
|
63
|
+
# @return [Rage::Deferred::Backends::Disk::DeadTasksStorage]
|
|
64
|
+
# @private
|
|
65
|
+
def each_dead_task(&block)
|
|
66
|
+
@dead_tasks_storage.each(&block)
|
|
62
67
|
end
|
|
63
68
|
|
|
64
|
-
# Return
|
|
65
|
-
# @param id [String] the
|
|
66
|
-
# @return [Hash, nil]
|
|
69
|
+
# Return the newest dead-task record with the exact task ID.
|
|
70
|
+
# @param id [String] the task ID
|
|
71
|
+
# @return [Hash, nil] the matching record, or nil if there is no matching record
|
|
72
|
+
# @private
|
|
67
73
|
def find_dead_task(id)
|
|
68
|
-
@dead_tasks_storage.
|
|
74
|
+
@dead_tasks_storage.find_by_id(id)
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
# Permanently delete dead-lettered tasks.
|
|
@@ -421,25 +427,76 @@ class Rage::Deferred::Backends::Disk
|
|
|
421
427
|
end
|
|
422
428
|
end
|
|
423
429
|
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
430
|
+
# Yield each selected dead-task record from oldest to newest.
|
|
431
|
+
#
|
|
432
|
+
# The method locks the store and opens the current file.
|
|
433
|
+
# It sets a read boundary after the last complete record.
|
|
434
|
+
# It then releases the lock.
|
|
435
|
+
# A record that Rage adds after this step is not part of the traversal.
|
|
436
|
+
#
|
|
437
|
+
# The method scans each complete record before it yields the first record.
|
|
438
|
+
# For each task ID, it selects the newest record that has a valid frame.
|
|
439
|
+
#
|
|
440
|
+
# The method decodes one selected record immediately before it yields that record.
|
|
441
|
+
# It does not decode the next record while the caller processes the current record.
|
|
442
|
+
# Each call uses independent traversal state.
|
|
443
|
+
#
|
|
444
|
+
# @yieldparam record [Hash] a decoded dead-task record
|
|
445
|
+
# @return [self]
|
|
428
446
|
# @raise [Rage::Deferred::DeadTasksLockTimeout] if the store cannot be locked
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
447
|
+
# @raise [SystemCallError] when an operation on the selected storage file fails
|
|
448
|
+
def each
|
|
449
|
+
with_snapshot do |storage, snapshot_end|
|
|
450
|
+
records_index(storage, snapshot_end).each do |task_id, (offset, length)|
|
|
451
|
+
storage.seek(offset, IO::SEEK_SET)
|
|
452
|
+
|
|
453
|
+
begin
|
|
454
|
+
record = Marshal.load(storage.read(length).undump)
|
|
455
|
+
rescue ArgumentError, NameError, TypeError => e
|
|
456
|
+
puts "ERROR: Can't deserialize the dead-lettered task with id #{task_id}: (#{e.class}) #{e.message}"
|
|
457
|
+
next
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# yield outside the `rescue`, so that an error raised by the caller's block is not swallowed
|
|
461
|
+
yield record.merge(id: task_id)
|
|
462
|
+
end
|
|
433
463
|
|
|
434
|
-
|
|
464
|
+
self
|
|
465
|
+
end
|
|
435
466
|
end
|
|
436
467
|
|
|
437
|
-
#
|
|
438
|
-
#
|
|
439
|
-
#
|
|
468
|
+
# Return the newest record with the exact task ID.
|
|
469
|
+
#
|
|
470
|
+
# Scan all complete records in the fixed snapshot.
|
|
471
|
+
# Do not decode a candidate record during the scan.
|
|
472
|
+
# After the scan, decode only the newest matching record.
|
|
473
|
+
# @param id [String] the persisted task ID
|
|
474
|
+
# @return [Hash, nil] the decoded record, or nil if there is no matching record
|
|
440
475
|
# @raise [Rage::Deferred::DeadTasksLockTimeout] if the store cannot be locked
|
|
441
|
-
|
|
442
|
-
|
|
476
|
+
# @raise [SystemCallError] when an operation on the selected storage file fails
|
|
477
|
+
def find_by_id(id)
|
|
478
|
+
with_snapshot do |storage, snapshot_end|
|
|
479
|
+
serialized_record_location = nil
|
|
480
|
+
each_framed_record(storage, snapshot_end) do |task_id, offset, length|
|
|
481
|
+
next unless task_id == id
|
|
482
|
+
|
|
483
|
+
serialized_record_location = [offset, length]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
next unless serialized_record_location
|
|
487
|
+
|
|
488
|
+
offset, length = serialized_record_location
|
|
489
|
+
storage.seek(offset, IO::SEEK_SET)
|
|
490
|
+
|
|
491
|
+
begin
|
|
492
|
+
record = Marshal.load(storage.read(length).undump)
|
|
493
|
+
rescue ArgumentError, NameError, TypeError => e
|
|
494
|
+
puts "ERROR: Can't deserialize the dead-lettered task with id #{id}: (#{e.class}) #{e.message}"
|
|
495
|
+
next
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
record.merge(id: id)
|
|
499
|
+
end
|
|
443
500
|
end
|
|
444
501
|
|
|
445
502
|
# Permanently delete the records with the given ids.
|
|
@@ -459,7 +516,7 @@ class Rage::Deferred::Backends::Disk
|
|
|
459
516
|
File.open(@tmp_storage_path, File::WRONLY | File::CREAT | File::TRUNC | File::BINARY, 0o644) do |tmp|
|
|
460
517
|
File.open(@storage_path, File::RDONLY | File::BINARY) do |storage|
|
|
461
518
|
storage.each_line do |entry|
|
|
462
|
-
id =
|
|
519
|
+
id, = framed_record(entry.chomp)
|
|
463
520
|
|
|
464
521
|
if id.nil?
|
|
465
522
|
next # drop corrupted records; they can neither be listed nor deleted otherwise
|
|
@@ -489,95 +546,153 @@ class Rage::Deferred::Backends::Disk
|
|
|
489
546
|
|
|
490
547
|
private
|
|
491
548
|
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
|
|
495
|
-
|
|
549
|
+
# Index the newest valid payload location for each task ID.
|
|
550
|
+
# If the index contains the duplicate task ID, the method removes the ID before it adds the newer record.
|
|
551
|
+
# This operation moves the ID to the position of its newest record.
|
|
552
|
+
# Hash insertion order then gives the required order from oldest to newest.
|
|
553
|
+
# @param storage [File] the descriptor for the fixed snapshot
|
|
554
|
+
# @param snapshot_end [Integer] the byte boundary after its last complete record
|
|
555
|
+
# @return [Hash{String => Array<(Integer, Integer)>}] ordered serialized-payload locations
|
|
556
|
+
def records_index(storage, snapshot_end)
|
|
557
|
+
records_index = {}
|
|
558
|
+
each_framed_record(storage, snapshot_end) do |task_id, offset, length|
|
|
559
|
+
records_index.delete(task_id)
|
|
560
|
+
records_index[task_id] = [offset, length].freeze
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
records_index
|
|
496
564
|
end
|
|
497
565
|
|
|
498
|
-
#
|
|
499
|
-
#
|
|
566
|
+
# Scan each complete record up to the fixed snapshot boundary.
|
|
567
|
+
# Silently skip a malformed frame or an invalid CRC.
|
|
568
|
+
# Do not decode or inspect a serialized payload.
|
|
569
|
+
# @param storage [File] the descriptor for the fixed snapshot
|
|
570
|
+
# @param snapshot_end [Integer] the byte boundary after its last complete record
|
|
571
|
+
# @yieldparam task_id [String] the task ID from the storage frame
|
|
572
|
+
# @yieldparam offset [Integer] the serialized payload's byte offset
|
|
573
|
+
# @yieldparam length [Integer] the serialized payload's byte length
|
|
500
574
|
# @return [void]
|
|
501
|
-
|
|
575
|
+
# @private
|
|
576
|
+
def each_framed_record(storage, snapshot_end)
|
|
577
|
+
position = 0
|
|
578
|
+
storage.seek(0, IO::SEEK_SET)
|
|
579
|
+
|
|
580
|
+
while position < snapshot_end
|
|
581
|
+
offset = position
|
|
582
|
+
entry = storage.gets
|
|
583
|
+
break unless entry
|
|
584
|
+
|
|
585
|
+
position += entry.bytesize
|
|
586
|
+
length = entry.bytesize - 1
|
|
587
|
+
entry = entry.byteslice(0, length)
|
|
588
|
+
task_id, serialized_record = framed_record(entry)
|
|
589
|
+
next unless task_id
|
|
590
|
+
|
|
591
|
+
serialized_record_offset = offset + length - serialized_record.bytesize
|
|
592
|
+
yield task_id, serialized_record_offset, serialized_record.bytesize
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Lock the store, open the live file, and find the last complete record.
|
|
597
|
+
# Release the lock before the block scans, decodes, or yields a record.
|
|
598
|
+
# Keep the file descriptor open while the block runs.
|
|
599
|
+
# The descriptor continues to identify the same file if another operation replaces the live path.
|
|
600
|
+
# Close the descriptor when the block exits.
|
|
601
|
+
# If the block and `File#close` both raise exceptions, the method re-raises the block's exception.
|
|
602
|
+
# @yieldparam storage [File] the descriptor for the fixed snapshot
|
|
603
|
+
# @yieldparam snapshot_end [Integer] the fixed end position of the last complete record
|
|
604
|
+
# @yieldreturn [Object] the result from the read operation
|
|
605
|
+
# @return [Object] the block result
|
|
606
|
+
def with_snapshot
|
|
607
|
+
storage = nil
|
|
608
|
+
active_exception = nil
|
|
609
|
+
|
|
610
|
+
begin
|
|
611
|
+
snapshot_end = with_lock("read tasks from") do
|
|
612
|
+
storage = File.open(@storage_path, File::RDONLY | File::BINARY)
|
|
613
|
+
complete_record_end(storage)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
yield storage, snapshot_end
|
|
617
|
+
rescue Exception => e
|
|
618
|
+
active_exception = e
|
|
619
|
+
raise
|
|
620
|
+
ensure
|
|
621
|
+
begin
|
|
622
|
+
storage.close if storage && !storage.closed?
|
|
623
|
+
rescue
|
|
624
|
+
raise unless active_exception
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Return the byte position after the last complete record.
|
|
630
|
+
# A complete record ends with a newline.
|
|
631
|
+
# If the file has an incomplete tail, scan backwards in fixed-size chunks.
|
|
632
|
+
# Do not parse a record during this scan.
|
|
633
|
+
# Do not change the file.
|
|
634
|
+
# Return zero if the file has no complete record.
|
|
635
|
+
# @param storage [File] an open dead-task file
|
|
636
|
+
# @return [Integer] the byte boundary after the last complete record
|
|
637
|
+
def complete_record_end(storage)
|
|
502
638
|
storage.seek(0, IO::SEEK_END)
|
|
503
639
|
end_position = storage.pos
|
|
504
|
-
return if end_position == 0
|
|
640
|
+
return 0 if end_position == 0
|
|
505
641
|
|
|
506
642
|
storage.seek(-1, IO::SEEK_END)
|
|
507
|
-
return if storage.read(1) == "\n"
|
|
643
|
+
return end_position if storage.read(1) == "\n"
|
|
508
644
|
|
|
509
645
|
position = end_position
|
|
510
|
-
truncate_at = 0
|
|
511
|
-
|
|
512
646
|
while position > 0
|
|
513
647
|
chunk_start = [position - TAIL_SCAN_CHUNK_SIZE, 0].max
|
|
514
648
|
storage.seek(chunk_start, IO::SEEK_SET)
|
|
515
649
|
chunk = storage.read(position - chunk_start)
|
|
516
650
|
|
|
517
651
|
if (newline_index = chunk.rindex("\n"))
|
|
518
|
-
|
|
519
|
-
break
|
|
652
|
+
return chunk_start + newline_index + 1
|
|
520
653
|
end
|
|
521
654
|
|
|
522
655
|
position = chunk_start
|
|
523
656
|
end
|
|
524
657
|
|
|
525
|
-
|
|
658
|
+
0
|
|
526
659
|
end
|
|
527
660
|
|
|
528
|
-
#
|
|
529
|
-
#
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
# the same task can be dead-lettered more than once if the worker crashed
|
|
546
|
-
# before the task was removed from the write-ahead log
|
|
547
|
-
result.delete(id)
|
|
548
|
-
result[id] = entry
|
|
549
|
-
end
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
if corrupted_count != 0
|
|
553
|
-
puts "WARNING: Detected #{corrupted_count} corrupted dead-lettered task(s)"
|
|
554
|
-
end
|
|
661
|
+
# Validate the storage frame.
|
|
662
|
+
# Return its task ID and serialized payload.
|
|
663
|
+
# Return `nil` for a malformed frame, invalid CRC, unsupported operation, or empty task ID.
|
|
664
|
+
# Do not decode the payload.
|
|
665
|
+
# @param entry [String] one complete physical record without the newline
|
|
666
|
+
# @return [Array<(String, String)>, nil]
|
|
667
|
+
def framed_record(entry)
|
|
668
|
+
return unless entry.bytesize > ENTRY_CRC_HEX_WIDTH + 1
|
|
669
|
+
return unless entry.getbyte(ENTRY_CRC_HEX_WIDTH) == 58 # `:`
|
|
670
|
+
|
|
671
|
+
signature = entry.byteslice(0, ENTRY_CRC_HEX_WIDTH)
|
|
672
|
+
payload = entry.byteslice((ENTRY_CRC_HEX_WIDTH + 1)..)
|
|
673
|
+
operation_prefix = "#{ENTRY_OP}:"
|
|
674
|
+
return unless payload.start_with?(operation_prefix)
|
|
675
|
+
return unless signature == Zlib.crc32(payload).to_s(16).rjust(ENTRY_CRC_HEX_WIDTH, "0")
|
|
555
676
|
|
|
556
|
-
|
|
557
|
-
|
|
677
|
+
id_start = operation_prefix.bytesize
|
|
678
|
+
separator_index = payload.index(":", id_start)
|
|
679
|
+
return unless separator_index && separator_index > id_start
|
|
558
680
|
|
|
559
|
-
|
|
560
|
-
_, _, _, serialized_record = entry.split(":", 4)
|
|
561
|
-
Marshal.load(serialized_record.undump)
|
|
562
|
-
rescue ArgumentError, NameError, TypeError => e
|
|
563
|
-
puts "ERROR: Can't deserialize the dead-lettered task with id #{id}: (#{e.class}) #{e.message}"
|
|
564
|
-
nil
|
|
565
|
-
end
|
|
681
|
+
[payload.byteslice(id_start...separator_index), payload.byteslice((separator_index + 1)..)]
|
|
566
682
|
end
|
|
567
683
|
|
|
568
|
-
#
|
|
569
|
-
# @
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
signature, payload = entry[0...ENTRY_CRC_HEX_WIDTH], entry[(ENTRY_CRC_HEX_WIDTH + 1)..]
|
|
575
|
-
return if signature.nil? || payload.nil? || !payload.start_with?("#{ENTRY_OP}:")
|
|
576
|
-
return unless signature == Zlib.crc32(payload).to_s(16).rjust(ENTRY_CRC_HEX_WIDTH, "0")
|
|
684
|
+
# Call `fsync` on the storage directory after file creation or replacement.
|
|
685
|
+
# @return [void]
|
|
686
|
+
def sync_storage_directory
|
|
687
|
+
File.open(@storage_path.dirname, File::RDONLY, &:fsync)
|
|
688
|
+
end
|
|
577
689
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
690
|
+
# Remove bytes after the last complete record. An interrupted append can leave these bytes.
|
|
691
|
+
# @param storage [File] the store opened for reading and writing
|
|
692
|
+
# @return [void]
|
|
693
|
+
def repair_torn_tail(storage)
|
|
694
|
+
truncate_at = complete_record_end(storage)
|
|
695
|
+
storage.truncate(truncate_at) if truncate_at < storage.size
|
|
581
696
|
end
|
|
582
697
|
|
|
583
698
|
# Serialize a task record as a checksummed, newline-delimited entry.
|
|
@@ -611,12 +726,21 @@ class Rage::Deferred::Backends::Disk
|
|
|
611
726
|
end
|
|
612
727
|
|
|
613
728
|
@locked = true
|
|
729
|
+
active_exception = nil
|
|
614
730
|
|
|
615
731
|
begin
|
|
616
732
|
yield
|
|
733
|
+
rescue Exception => e
|
|
734
|
+
active_exception = e
|
|
735
|
+
raise
|
|
617
736
|
ensure
|
|
618
|
-
|
|
619
|
-
|
|
737
|
+
begin
|
|
738
|
+
@lock_file.flock(File::LOCK_UN)
|
|
739
|
+
rescue
|
|
740
|
+
raise unless active_exception
|
|
741
|
+
ensure
|
|
742
|
+
@locked = false
|
|
743
|
+
end
|
|
620
744
|
end
|
|
621
745
|
end
|
|
622
746
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Provides information about one dead task.
|
|
5
|
+
#
|
|
6
|
+
# A dead task is a deferred task that is no longer retried.
|
|
7
|
+
# Creating this object does not load the task class.
|
|
8
|
+
# The first call to {#args} or {#kwargs} deserializes the execution context.
|
|
9
|
+
class Rage::Deferred::DeadTask
|
|
10
|
+
# @return [String] the ID that Rage assigned when it enqueued the task
|
|
11
|
+
attr_reader :id
|
|
12
|
+
|
|
13
|
+
# @return [String] the name of the task class
|
|
14
|
+
attr_reader :task_class
|
|
15
|
+
|
|
16
|
+
# @return [Integer] the number of processing attempts
|
|
17
|
+
attr_reader :attempts
|
|
18
|
+
|
|
19
|
+
# @return [Time] the time when Rage enqueued the task
|
|
20
|
+
attr_reader :enqueued_at
|
|
21
|
+
|
|
22
|
+
# @return [Time] the time when the last attempt failed
|
|
23
|
+
attr_reader :failed_at
|
|
24
|
+
|
|
25
|
+
# @return [String] the class name of the last exception
|
|
26
|
+
attr_reader :exception_class
|
|
27
|
+
|
|
28
|
+
# @return [String] the message from the last exception
|
|
29
|
+
attr_reader :exception_message
|
|
30
|
+
|
|
31
|
+
# @return [Array<String>] the frozen backtrace from the last exception
|
|
32
|
+
attr_reader :backtrace
|
|
33
|
+
|
|
34
|
+
# @private
|
|
35
|
+
def initialize(record)
|
|
36
|
+
@id = record[:id].dup.freeze
|
|
37
|
+
@task_class = record[:task_class].dup.freeze
|
|
38
|
+
@attempts = record[:attempts]
|
|
39
|
+
@enqueued_at = Time.at(record[:enqueued_at])
|
|
40
|
+
@failed_at = Time.at(record[:failed_at])
|
|
41
|
+
|
|
42
|
+
@exception_class = record[:exception_class].dup.freeze
|
|
43
|
+
@exception_message = record[:exception_message].dup.freeze
|
|
44
|
+
@backtrace = record[:backtrace].map { |line| line.dup.freeze }.freeze
|
|
45
|
+
@__context = record[:context].dup.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return the original positional arguments.
|
|
49
|
+
#
|
|
50
|
+
# On the first call, the method deserializes the execution context.
|
|
51
|
+
# The method freezes all objects in the decoded data.
|
|
52
|
+
# After a successful call, the method returns the same Array on each later call.
|
|
53
|
+
# If the stored value is `nil`, the method returns a frozen empty Array.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array] frozen positional arguments
|
|
56
|
+
# @raise [Rage::Deferred::DeadTaskContextDeserializationError] when the context cannot be decoded
|
|
57
|
+
def args
|
|
58
|
+
decode_context unless defined?(@__args)
|
|
59
|
+
@__args
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Return the original keyword arguments.
|
|
63
|
+
#
|
|
64
|
+
# On the first call, the method deserializes the execution context.
|
|
65
|
+
# The method freezes all objects in the decoded data.
|
|
66
|
+
# After a successful call, the method returns the same Hash on each later call.
|
|
67
|
+
# If the stored value is `nil`, the method returns a frozen empty Hash.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] frozen keyword arguments
|
|
70
|
+
# @raise [Rage::Deferred::DeadTaskContextDeserializationError] when the context cannot be decoded
|
|
71
|
+
def kwargs
|
|
72
|
+
decode_context unless defined?(@__kwargs)
|
|
73
|
+
@__kwargs
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# @private
|
|
79
|
+
def decode_context
|
|
80
|
+
context = Marshal.load(@__context, freeze: true)
|
|
81
|
+
args = Rage::Deferred::Context.get_args(context)
|
|
82
|
+
kwargs = Rage::Deferred::Context.get_kwargs(context)
|
|
83
|
+
args = [].freeze if args.nil?
|
|
84
|
+
kwargs = {}.freeze if kwargs.nil?
|
|
85
|
+
|
|
86
|
+
@__args = args
|
|
87
|
+
@__kwargs = kwargs
|
|
88
|
+
rescue => e
|
|
89
|
+
error = Rage::Deferred::DeadTaskContextDeserializationError.new(
|
|
90
|
+
"Could not deserialize execution context for dead task #{@id}"
|
|
91
|
+
)
|
|
92
|
+
raise error, cause: e
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private_class_method :new
|
|
96
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Provides access to deferred tasks that Rage no longer retries.
|
|
5
|
+
#
|
|
6
|
+
# {Rage::Deferred.dead_tasks} returns this collection.
|
|
7
|
+
class Rage::Deferred::DeadTasks
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
# @private
|
|
11
|
+
def initialize(backend)
|
|
12
|
+
@backend = backend
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Yield each dead task from oldest to newest.
|
|
16
|
+
#
|
|
17
|
+
# Without a block, the method returns a new Enumerator.
|
|
18
|
+
#
|
|
19
|
+
# @yieldparam dead_task [Rage::Deferred::DeadTask] information about one dead task
|
|
20
|
+
# @return [Enumerator, self] a new Enumerator without a block, or this collection with a block
|
|
21
|
+
def each
|
|
22
|
+
return enum_for(__method__) unless block_given?
|
|
23
|
+
|
|
24
|
+
@backend.each_dead_task do |record|
|
|
25
|
+
yield Rage::Deferred::DeadTask.send(:new, record)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Return the dead task with the exact persisted ID.
|
|
32
|
+
#
|
|
33
|
+
# The ID must be a String. The method does not convert other values to a String.
|
|
34
|
+
#
|
|
35
|
+
# @param id [String] the exact persisted task ID
|
|
36
|
+
# @return [Rage::Deferred::DeadTask, nil] the matching task, or nil
|
|
37
|
+
# @raise [TypeError] when the ID is not a String
|
|
38
|
+
def find_by_id(id)
|
|
39
|
+
raise TypeError, "dead task id must be a String" unless id.is_a?(String)
|
|
40
|
+
|
|
41
|
+
record = @backend.find_dead_task(id)
|
|
42
|
+
Rage::Deferred::DeadTask.send(:new, record) if record
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -51,6 +51,24 @@ module Rage::Deferred
|
|
|
51
51
|
Rage::Deferred::Proxy.new(instance, delay:, delay_until:)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Return the collection of tasks that Rage no longer retries.
|
|
55
|
+
#
|
|
56
|
+
# The method returns the same collection object on each call.
|
|
57
|
+
# The collection uses the same backend object as the deferred queue.
|
|
58
|
+
#
|
|
59
|
+
# @example List dead tasks
|
|
60
|
+
# Rage::Deferred.dead_tasks.each do |task|
|
|
61
|
+
# puts "#{task.id}: #{task.exception_message}"
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# @example Find one dead task
|
|
65
|
+
# task = Rage::Deferred.dead_tasks.find_by_id("1735689600-12345-1")
|
|
66
|
+
#
|
|
67
|
+
# @return [Rage::Deferred::DeadTasks]
|
|
68
|
+
def self.dead_tasks
|
|
69
|
+
@__dead_tasks ||= Rage::Deferred::DeadTasks.new(__backend)
|
|
70
|
+
end
|
|
71
|
+
|
|
54
72
|
# @private
|
|
55
73
|
def self.__backend
|
|
56
74
|
@__backend ||= Rage.config.deferred.backend
|
|
@@ -100,11 +118,20 @@ module Rage::Deferred
|
|
|
100
118
|
class PushTimeout < StandardError
|
|
101
119
|
end
|
|
102
120
|
|
|
103
|
-
#
|
|
121
|
+
# Raise this error when the dead-task store cannot be locked after all retries.
|
|
104
122
|
class DeadTasksLockTimeout < StandardError
|
|
105
123
|
end
|
|
124
|
+
|
|
125
|
+
# Raise this error when a dead task's execution context cannot be deserialized.
|
|
126
|
+
#
|
|
127
|
+
# The error message identifies the dead task.
|
|
128
|
+
# `Exception#cause` contains the original deserialization error.
|
|
129
|
+
class DeadTaskContextDeserializationError < StandardError
|
|
130
|
+
end
|
|
106
131
|
end
|
|
107
132
|
|
|
133
|
+
require_relative "dead_task"
|
|
134
|
+
require_relative "dead_tasks"
|
|
108
135
|
require_relative "task"
|
|
109
136
|
require_relative "scheduler"
|
|
110
137
|
require_relative "queue"
|
data/lib/rage/extension.rb
CHANGED
|
@@ -16,6 +16,10 @@ class Rage::Extension
|
|
|
16
16
|
Iodine.on_state(:pre_start, &block)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def routes(&block)
|
|
20
|
+
__routes << block
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
def __initializers
|
|
20
24
|
@@initializers ||= Hash.new { |h, k| h[k] = [] }
|
|
21
25
|
end
|
|
@@ -23,5 +27,9 @@ class Rage::Extension
|
|
|
23
27
|
def __configurations
|
|
24
28
|
@@configurations ||= []
|
|
25
29
|
end
|
|
30
|
+
|
|
31
|
+
def __routes
|
|
32
|
+
@@routes ||= []
|
|
33
|
+
end
|
|
26
34
|
end
|
|
27
35
|
end
|
data/lib/rage/fiber.rb
CHANGED
|
@@ -111,7 +111,7 @@ class Fiber
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# @private
|
|
114
|
-
attr_accessor :__awaited_fileno, :__wait_generation, :
|
|
114
|
+
attr_accessor :__awaited_fileno, :__wait_generation, :__await_channel
|
|
115
115
|
|
|
116
116
|
# @private
|
|
117
117
|
# pause a fiber and resume in the next iteration of the event loop
|