rage-rb 1.19.2 → 1.20.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/Appraisals +19 -0
  4. data/CHANGELOG.md +15 -1
  5. data/CODE_OF_CONDUCT.md +13 -17
  6. data/Gemfile +3 -0
  7. data/README.md +60 -63
  8. data/Rakefile +14 -0
  9. data/lib/rage/all.rb +3 -0
  10. data/lib/rage/cable/cable.rb +11 -7
  11. data/lib/rage/cable/channel.rb +6 -1
  12. data/lib/rage/cable/connection.rb +4 -0
  13. data/lib/rage/cable/router.rb +14 -9
  14. data/lib/rage/configuration.rb +235 -21
  15. data/lib/rage/controller/api.rb +49 -44
  16. data/lib/rage/deferred/context.rb +30 -2
  17. data/lib/rage/deferred/deferred.rb +18 -6
  18. data/lib/rage/deferred/metadata.rb +39 -0
  19. data/lib/rage/deferred/middleware_chain.rb +67 -0
  20. data/lib/rage/deferred/task.rb +45 -17
  21. data/lib/rage/events/events.rb +3 -3
  22. data/lib/rage/events/subscriber.rb +36 -25
  23. data/lib/rage/fiber.rb +33 -31
  24. data/lib/rage/fiber_scheduler.rb +6 -2
  25. data/lib/rage/logger/logger.rb +7 -1
  26. data/lib/rage/middleware/body_finalizer.rb +14 -0
  27. data/lib/rage/response.rb +10 -5
  28. data/lib/rage/rspec.rb +17 -17
  29. data/lib/rage/setup.rb +2 -2
  30. data/lib/rage/telemetry/handler.rb +131 -0
  31. data/lib/rage/telemetry/spans/await_fiber.rb +50 -0
  32. data/lib/rage/telemetry/spans/broadcast_cable_stream.rb +50 -0
  33. data/lib/rage/telemetry/spans/create_websocket_connection.rb +50 -0
  34. data/lib/rage/telemetry/spans/dispatch_fiber.rb +48 -0
  35. data/lib/rage/telemetry/spans/enqueue_deferred_task.rb +52 -0
  36. data/lib/rage/telemetry/spans/process_cable_action.rb +56 -0
  37. data/lib/rage/telemetry/spans/process_cable_connection.rb +56 -0
  38. data/lib/rage/telemetry/spans/process_controller_action.rb +56 -0
  39. data/lib/rage/telemetry/spans/process_deferred_task.rb +54 -0
  40. data/lib/rage/telemetry/spans/process_event_subscriber.rb +54 -0
  41. data/lib/rage/telemetry/spans/publish_event.rb +54 -0
  42. data/lib/rage/telemetry/spans/spawn_fiber.rb +50 -0
  43. data/lib/rage/telemetry/telemetry.rb +121 -0
  44. data/lib/rage/telemetry/tracer.rb +97 -0
  45. data/lib/rage/version.rb +1 -1
  46. data/rage.gemspec +4 -3
  47. metadata +38 -5
@@ -203,6 +203,14 @@ class Rage::Configuration
203
203
  end
204
204
  # @!endgroup
205
205
 
206
+ # @!group Telemetry Configuration
207
+ # Allows configuring telemetry settings.
208
+ # @return [Rage::Configuration::Telemetry]
209
+ def telemetry
210
+ @telemetry ||= Telemetry.new
211
+ end
212
+ # @!endgroup
213
+
206
214
  # @!group Session Configuration
207
215
  # Allows configuring session settings.
208
216
  # @return [Rage::Configuration::Session]
@@ -376,13 +384,13 @@ class Rage::Configuration
376
384
  end
377
385
  end
378
386
 
379
- class Middleware
387
+ class MiddlewareRegistry
380
388
  # @private
381
- attr_reader :middlewares
389
+ attr_reader :objects
382
390
 
383
391
  # @private
384
392
  def initialize
385
- @middlewares = [[Rage::FiberWrapper]]
393
+ @objects = []
386
394
  end
387
395
 
388
396
  # Add a new middleware to the end of the stack.
@@ -400,7 +408,8 @@ class Rage::Configuration
400
408
  # end
401
409
  # end
402
410
  def use(new_middleware, *args, &block)
403
- insert_after(@middlewares.length - 1, new_middleware, *args, &block)
411
+ validate!(-1, new_middleware)
412
+ @objects.insert(-1, [new_middleware, args, block])
404
413
  end
405
414
 
406
415
  # Insert a new middleware before an existing middleware in the stack.
@@ -419,11 +428,9 @@ class Rage::Configuration
419
428
  # end
420
429
  # end
421
430
  def insert_before(existing_middleware, new_middleware, *args, &block)
422
- index = find_middleware_index(existing_middleware)
423
- if index == 0 && @middlewares[0][0] == Rage::FiberWrapper
424
- puts("Warning: inserting #{new_middleware} before Rage::FiberWrapper may lead to undefined behavior.")
425
- end
426
- @middlewares = (@middlewares[0...index] + [[new_middleware, args, block]] + @middlewares[index..]).uniq(&:first)
431
+ index = find_object_index(existing_middleware)
432
+ validate!(index, new_middleware)
433
+ @objects.insert(index, [new_middleware, args, block])
427
434
  end
428
435
 
429
436
  # Insert a new middleware after an existing middleware in the stack.
@@ -441,29 +448,65 @@ class Rage::Configuration
441
448
  # end
442
449
  # end
443
450
  def insert_after(existing_middleware, new_middleware, *args, &block)
444
- index = find_middleware_index(existing_middleware)
445
- @middlewares = (@middlewares[0..index] + [[new_middleware, args, block]] + @middlewares[index + 1..]).uniq(&:first)
451
+ index = find_object_index(existing_middleware) + 1
452
+ index = 0 if @objects.empty?
453
+ validate!(index, new_middleware)
454
+ @objects.insert(index, [new_middleware, args, block])
446
455
  end
447
456
 
448
457
  # Check if a middleware is included in the stack.
449
- # @param middleware [Class, Integer] the middleware class or its index in the stack
458
+ # @param middleware [Class] the middleware class
450
459
  # @return [Boolean]
451
460
  def include?(middleware)
452
- !!find_middleware_index(middleware) rescue false
461
+ @objects.any? { |o, _, _| o == middleware }
462
+ end
463
+
464
+ # Delete a middleware from the stack.
465
+ # @param middleware [Class] the middleware class
466
+ # @example
467
+ # Rage.configure do
468
+ # config.middleware.delete Rack::Cors
469
+ # end
470
+ def delete(middleware)
471
+ @objects.reject! { |o, _, _| o == middleware }
453
472
  end
454
473
 
455
474
  private
456
475
 
457
- def find_middleware_index(middleware)
458
- if middleware.is_a?(Integer)
459
- if middleware < 0 || middleware >= @middlewares.length
460
- raise ArgumentError, "Middleware index should be in the (0...#{@middlewares.length}) range"
476
+ def find_object_index(object)
477
+ if object.is_a?(Integer)
478
+ if @objects[object] || object == 0
479
+ object
480
+ else
481
+ raise ArgumentError, "Could not find middleware at index #{object}"
461
482
  end
462
- middleware
463
483
  else
464
- @middlewares.index { |m, _, _| m == middleware }.tap do |i|
465
- raise ArgumentError, "Couldn't find #{middleware} in the middleware stack" unless i
466
- end
484
+ index = @objects.index { |o, _, _| o == object }
485
+ raise ArgumentError, "Could not find `#{object}` in the middleware registry" unless index
486
+ index
487
+ end
488
+ end
489
+
490
+ def validate!(_, _)
491
+ end
492
+ end
493
+
494
+ # See {Rage::Configuration::MiddlewareRegistry Rage::Configuration::MiddlewareRegistry} for details on available methods.
495
+ class Middleware < MiddlewareRegistry
496
+ # @private
497
+ alias_method :middlewares, :objects
498
+
499
+ # @private
500
+ def initialize
501
+ super
502
+ @objects = [[Rage::FiberWrapper]]
503
+ end
504
+
505
+ private
506
+
507
+ def validate!(index, middleware)
508
+ if index == 0 && @objects[0][0] == Rage::FiberWrapper
509
+ puts "WARNING: inserting the `#{middleware}` middleware before `Rage::FiberWrapper` may cause undefined behavior."
467
510
  end
468
511
  end
469
512
  end
@@ -730,6 +773,48 @@ class Rage::Configuration
730
773
  @backpressure = Backpressure.new(high_water_mark, low_water_mark, timeout)
731
774
  end
732
775
 
776
+ # Allows configuring middleware used by `Rage::Deferred`. See {MiddlewareRegistry} for details on available methods.
777
+ # @example
778
+ # Rage.configure do
779
+ # config.deferred.enqueue_middleware.use MyEnqueueMiddleware
780
+ # config.deferred.enqueue_middleware.insert_before MyEnqueueMiddleware, MyLoggingMiddleware
781
+ # end
782
+ class Middleware < Rage::Configuration::MiddlewareRegistry
783
+ private
784
+
785
+ def validate!(_, middleware)
786
+ unless middleware.is_a?(Class)
787
+ raise ArgumentError, "Deferred middleware has to be a class"
788
+ end
789
+
790
+ unless middleware.method_defined?(:call)
791
+ raise ArgumentError, "Deferred middleware has to implement the `#call` method"
792
+ end
793
+ end
794
+ end
795
+
796
+ # Configure enqueue middleware used by `Rage::Deferred`.
797
+ # See {EnqueueMiddlewareInterface} for details on the arguments passed to the middleware.
798
+ # @return [Rage::Configuration::Deferred::Middleware]
799
+ # @example
800
+ # Rage.configure do
801
+ # config.deferred.enqueue_middleware.use MyCustomMiddleware
802
+ # end
803
+ def enqueue_middleware
804
+ @enqueue_middleware ||= Middleware.new
805
+ end
806
+
807
+ # Configure perform middleware used by `Rage::Deferred`.
808
+ # See {PerformMiddlewareInterface} for details on the arguments passed to the middleware.
809
+ # @return [Rage::Configuration::Deferred::Middleware]
810
+ # @example
811
+ # Rage.configure do
812
+ # config.deferred.perform_middleware.use MyCustomMiddleware
813
+ # end
814
+ def perform_middleware
815
+ @perform_middleware ||= Middleware.new
816
+ end
817
+
733
818
  # @private
734
819
  def default_disk_storage_path
735
820
  Pathname.new("storage")
@@ -781,6 +866,65 @@ class Rage::Configuration
781
866
  end
782
867
  end
783
868
 
869
+ # The class allows configuring telemetry handlers. See {MiddlewareRegistry} for details on available methods.
870
+ # @example
871
+ # Rage.configure do
872
+ # config.telemetry.use MyTelemetryHandler.new
873
+ # end
874
+ # @see Rage::Configuration::MiddlewareRegistry
875
+ # @see Rage::Telemetry
876
+ class Telemetry < MiddlewareRegistry
877
+ # @private
878
+ # @return [Hash{String => Array<Rage::Telemetry::HandlerRef>}] a map of span IDs to handler references
879
+ def handlers_map
880
+ @objects.map(&:first).each_with_object({}) do |handler, memo|
881
+ handlers_map = handler.is_a?(Class) ? handler.handlers_map : handler.class.handlers_map
882
+
883
+ handlers_map.each do |span_id, handler_methods|
884
+ handler_refs = handler_methods.map do |handler_method|
885
+ Rage::Telemetry::HandlerRef[handler, handler_method]
886
+ end
887
+
888
+ if memo[span_id]
889
+ memo[span_id] += handler_refs
890
+ else
891
+ memo[span_id] = handler_refs
892
+ end
893
+ end
894
+ end
895
+ end
896
+
897
+ private
898
+
899
+ def validate!(_, handler)
900
+ is_handler = if handler.is_a?(Class)
901
+ handler.ancestors.include?(Rage::Telemetry::Handler)
902
+ else
903
+ handler.is_a?(Rage::Telemetry::Handler)
904
+ end
905
+
906
+ unless is_handler
907
+ raise ArgumentError, "Cannot add `#{handler}` as a telemetry handler; should inherit `Rage::Telemetry::Handler`"
908
+ end
909
+
910
+ handlers_map = if handler.is_a?(Class)
911
+ handler.handlers_map
912
+ else
913
+ handler.class.handlers_map
914
+ end
915
+
916
+ unless handlers_map&.any?
917
+ raise ArgumentError, "Telemetry handler `#{handler}` does not define any handlers"
918
+ end
919
+
920
+ handlers_map.values.reduce(&:+).each do |handler_method|
921
+ unless handler.respond_to?(handler_method)
922
+ raise ArgumentError, "Telemetry handler `#{handler}` does not implement the `#{handler_method}` handler method"
923
+ end
924
+ end
925
+ end
926
+ end
927
+
784
928
  class Session
785
929
  # @!attribute key
786
930
  # Specify the name of the session cookie.
@@ -847,6 +991,13 @@ class Rage::Configuration
847
991
  Rage.__log_processor.add_custom_tags(@log_tags.objects)
848
992
  @logger.dynamic_tags = Rage.__log_processor.dynamic_tags
849
993
  end
994
+
995
+ if defined?(::Rack::Events) && middleware.include?(::Rack::Events)
996
+ middleware.delete(Rage::BodyFinalizer)
997
+ middleware.insert_before(::Rack::Events, Rage::BodyFinalizer)
998
+ end
999
+
1000
+ Rage::Telemetry.__setup if @telemetry
850
1001
  end
851
1002
  end
852
1003
 
@@ -882,3 +1033,66 @@ end
882
1033
  # def call(severity:, tags:, context:, message:, request_info:)
883
1034
  # end
884
1035
  # end
1036
+
1037
+ # @!parse [ruby]
1038
+ # # @note This class does not exist at runtime and is used for documentation purposes only. Do not inherit your middleware classes from it.
1039
+ # class EnqueueMiddlewareInterface
1040
+ # # Called whenever a deferred task is enqueued.
1041
+ # #
1042
+ # # The middleware is expected to call `yield` to pass control to the next middleware in the stack. If the middleware does not call `yield`, the task will not be enqueued.
1043
+ # #
1044
+ # # Rage automatically detects which parameters your middleware's `#call` method accepts, and only passes those parameters. You can omit any of the described parameters in your implementation.
1045
+ # #
1046
+ # # @param task_class [Class] the deferred task class
1047
+ # # @param delay [Integer, nil] the delay in seconds before the task is executed
1048
+ # # @param delay_until [Time, Integer, nil] the time at which the task should be executed
1049
+ # # @param phase [:enqueue] the middleware phase. Useful for middlewares that are shared between enqueue and perform phases
1050
+ # # @param args [Array] the positional arguments passed to the task
1051
+ # # @param kwargs [Hash] the keyword arguments passed to the task
1052
+ # # @param context [Hash] the context is serialized together with the task and allows passing data between middlewares without exposing it to the task itself
1053
+ # # @example
1054
+ # # class EncryptArgumentsMiddleware
1055
+ # # def call(args:, kwargs:)
1056
+ # # args.map! { |arg| MyEncryptionSDK.encrypt(arg) }
1057
+ # # kwargs.transform_values! { |value| MyEncryptionSDK.encrypt(value) }
1058
+ # #
1059
+ # # yield
1060
+ # # end
1061
+ # # end
1062
+ # def call(task_class:, delay:, delay_until:, phase:, args:, kwargs:, context:)
1063
+ # end
1064
+ # end
1065
+
1066
+ # @!parse [ruby]
1067
+ # # @note This class does not exist at runtime and is used for documentation purposes only. Do not inherit your middleware classes from it.
1068
+ # class PerformMiddlewareInterface
1069
+ # # Called whenever a deferred task is performed.
1070
+ # #
1071
+ # # The middleware is expected to call `yield` to pass control to the next middleware in the stack. If the middleware does not call `yield`, the task will not be performed.
1072
+ # #
1073
+ # # Rage automatically detects which parameters your middleware's `#call` method accepts, and only passes those parameters. You can omit any of the described parameters in your implementation.
1074
+ # #
1075
+ # # @param task_class [Class] the deferred task class
1076
+ # # @param task [Rage::Deferred::Task] the deferred task instance
1077
+ # # @param phase [:perform] the middleware phase. Useful for middlewares that are shared between enqueue and perform phases
1078
+ # # @param args [Array] the positional arguments passed to the task
1079
+ # # @param kwargs [Hash] the keyword arguments passed to the task
1080
+ # # @param context [Hash] the context is serialized together with the task and allows passing data between middlewares without exposing it to the task itself
1081
+ # # @example
1082
+ # # class DecryptArgumentsMiddleware
1083
+ # # def call(args:, kwargs:)
1084
+ # # args.map! { |arg| MyEncryptionSDK.decrypt(arg) }
1085
+ # # kwargs.transform_values! { |value| MyEncryptionSDK.decrypt(value) }
1086
+ # #
1087
+ # # yield
1088
+ # #
1089
+ # # rescue
1090
+ # # # Re-encrypt the arguments in case of an error
1091
+ # # args.map! { |arg| MyEncryptionSDK.encrypt(arg) }
1092
+ # # kwargs.transform_values! { |value| MyEncryptionSDK.encrypt(value) }
1093
+ # # raise
1094
+ # # end
1095
+ # # end
1096
+ # def call(task_class:, task:, phase:, args:, kwargs:, context:)
1097
+ # end
1098
+ # end
@@ -112,56 +112,58 @@ class RageController::API
112
112
 
113
113
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
114
114
  def __run_#{action}
115
- #{if query_cache_enabled
116
- <<~RUBY
117
- ActiveRecord::Base.connection_pool.enable_query_cache!
118
- RUBY
119
- end}
115
+ Rage::Telemetry.tracer.span_controller_action_process(controller: self, params: @__params) do
116
+ #{if query_cache_enabled
117
+ <<~RUBY
118
+ ActiveRecord::Base.connection_pool.enable_query_cache!
119
+ RUBY
120
+ end}
120
121
 
121
- #{wrap_parameters_chunk}
122
- #{before_actions_chunk}
123
- #{action} unless @__before_callback_rendered
124
- #{around_actions_end_chunk}
122
+ #{wrap_parameters_chunk}
123
+ #{before_actions_chunk}
124
+ #{action} unless @__before_callback_rendered
125
+ #{around_actions_end_chunk}
125
126
 
126
- #{if !after_actions_chunk.empty?
127
- <<~RUBY
128
- unless @__before_callback_rendered
129
- @__rendered = true
130
- #{after_actions_chunk}
131
- end
132
- RUBY
133
- end}
127
+ #{if !after_actions_chunk.empty?
128
+ <<~RUBY
129
+ unless @__before_callback_rendered
130
+ @__rendered = true
131
+ #{after_actions_chunk}
132
+ end
133
+ RUBY
134
+ end}
134
135
 
135
- [@__status, @__headers, @__body]
136
+ [@__status, @__headers, @__body]
136
137
 
137
- #{rescue_handlers_chunk}
138
+ #{rescue_handlers_chunk}
138
139
 
139
- ensure
140
- #{if query_cache_enabled
141
- <<~RUBY
142
- ActiveRecord::Base.connection_pool.disable_query_cache!
143
- RUBY
144
- end}
140
+ ensure
141
+ #{if query_cache_enabled
142
+ <<~RUBY
143
+ ActiveRecord::Base.connection_pool.disable_query_cache!
144
+ RUBY
145
+ end}
145
146
 
146
- #{if should_release_connections
147
- <<~RUBY
148
- ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
149
- RUBY
150
- end}
147
+ #{if should_release_connections
148
+ <<~RUBY
149
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
150
+ RUBY
151
+ end}
151
152
 
152
- #{if method_defined?(:append_info_to_payload) || private_method_defined?(:append_info_to_payload)
153
- <<~RUBY
154
- context = {}
155
- append_info_to_payload(context)
156
-
157
- log_context = Thread.current[:rage_logger][:context]
158
- if log_context.empty?
159
- Thread.current[:rage_logger][:context] = context
160
- else
161
- Thread.current[:rage_logger][:context] = log_context.merge(context)
162
- end
163
- RUBY
164
- end}
153
+ #{if method_defined?(:append_info_to_payload) || private_method_defined?(:append_info_to_payload)
154
+ <<~RUBY
155
+ context = {}
156
+ append_info_to_payload(context)
157
+
158
+ log_context = Thread.current[:rage_logger][:context]
159
+ if log_context.empty?
160
+ Thread.current[:rage_logger][:context] = context
161
+ else
162
+ Thread.current[:rage_logger][:context] = log_context.merge(context)
163
+ end
164
+ RUBY
165
+ end}
166
+ end
165
167
  end
166
168
  RUBY
167
169
  end
@@ -451,6 +453,9 @@ class RageController::API
451
453
  @__rendered = false
452
454
  end
453
455
 
456
+ # @private
457
+ attr_reader :__env, :__status, :__headers, :__body
458
+
454
459
  # Get the request object. See {Rage::Request}.
455
460
  # @return [Rage::Request]
456
461
  def request
@@ -460,7 +465,7 @@ class RageController::API
460
465
  # Get the response object. See {Rage::Response}.
461
466
  # @return [Rage::Response]
462
467
  def response
463
- @response ||= Rage::Response.new(@__headers, @__body)
468
+ @response ||= Rage::Response.new(self)
464
469
  end
465
470
 
466
471
  # Get the cookie object. See {Rage::Cookies}.
@@ -5,7 +5,7 @@
5
5
  # The class encapsulates the context associated with a deferred task, and allows to store it without modifying the task instance.
6
6
  #
7
7
  class Rage::Deferred::Context
8
- def self.build(task, args, kwargs, storage: nil)
8
+ def self.build(task, args, kwargs)
9
9
  logger = Thread.current[:rage_logger]
10
10
 
11
11
  [
@@ -14,35 +14,63 @@ class Rage::Deferred::Context
14
14
  kwargs.empty? ? nil : kwargs,
15
15
  nil,
16
16
  logger&.dig(:tags),
17
- logger&.dig(:context)
17
+ logger&.dig(:context),
18
+ nil
18
19
  ]
19
20
  end
20
21
 
22
+ # @return [Class] the task class
21
23
  def self.get_task(context)
22
24
  context[0]
23
25
  end
24
26
 
27
+ # @return [Array, nil] arguments the task was enqueued with
25
28
  def self.get_args(context)
26
29
  context[1]
27
30
  end
28
31
 
32
+ # @return [Array] arguments the task was enqueued with, creating it if it does not exist
33
+ def self.get_or_create_args(context)
34
+ context[1] ||= []
35
+ end
36
+
37
+ # @return [Hash, nil] keyword arguments the task was enqueued with
29
38
  def self.get_kwargs(context)
30
39
  context[2]
31
40
  end
32
41
 
42
+ # @return [Hash] keyword arguments the task was enqueued with, creating it if it does not exist
43
+ def self.get_or_create_kwargs(context)
44
+ context[2] ||= {}
45
+ end
46
+
47
+ # @return [Integer, nil] number of attempts made to process the task
33
48
  def self.get_attempts(context)
34
49
  context[3]
35
50
  end
36
51
 
52
+ # Increments the number of attempts made to process the task
37
53
  def self.inc_attempts(context)
38
54
  context[3] = context[3].to_i + 1
39
55
  end
40
56
 
57
+ # @return [Array, nil] log tags associated with the task
41
58
  def self.get_log_tags(context)
42
59
  context[4]
43
60
  end
44
61
 
62
+ # @return [Hash, nil] log context associated with the task
45
63
  def self.get_log_context(context)
46
64
  context[5]
47
65
  end
66
+
67
+ # @return [Hash, nil] user context associated with the task
68
+ def self.get_user_context(context)
69
+ context[6]
70
+ end
71
+
72
+ # @return [Hash] user context associated with the task, creating it if it does not exist
73
+ def self.get_or_create_user_context(context)
74
+ context[6] ||= {}
75
+ end
48
76
  end
@@ -74,14 +74,24 @@ module Rage::Deferred
74
74
  end
75
75
  end
76
76
 
77
- module Backends
77
+ # @private
78
+ def self.__middleware_chain
79
+ @__middleware_chain ||= MiddlewareChain.new(
80
+ enqueue_middleware: Rage.config.deferred.enqueue_middleware.objects,
81
+ perform_middleware: Rage.config.deferred.perform_middleware.objects
82
+ )
78
83
  end
79
84
 
80
- class PushTimeout < StandardError
85
+ # @private
86
+ def self.__initialize
87
+ __middleware_chain
88
+ __load_tasks
81
89
  end
82
90
 
83
- # @private
84
- class TaskFailed < StandardError
91
+ module Backends
92
+ end
93
+
94
+ class PushTimeout < StandardError
85
95
  end
86
96
  end
87
97
 
@@ -89,11 +99,13 @@ require_relative "task"
89
99
  require_relative "queue"
90
100
  require_relative "proxy"
91
101
  require_relative "context"
102
+ require_relative "metadata"
103
+ require_relative "middleware_chain"
92
104
  require_relative "backends/disk"
93
105
  require_relative "backends/nil"
94
106
 
95
107
  if Iodine.running?
96
- Rage::Deferred.__load_tasks
108
+ Rage::Deferred.__initialize
97
109
  else
98
- Iodine.on_state(:on_start) { Rage::Deferred.__load_tasks }
110
+ Iodine.on_state(:on_start) { Rage::Deferred.__initialize }
99
111
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Provides metadata about the current deferred task execution.
5
+ #
6
+ class Rage::Deferred::Metadata
7
+ class << self
8
+ # Returns the current attempt number.
9
+ # @return [Integer] the current attempt number (1 for the first run)
10
+ def attempts
11
+ Rage::Deferred::Context.get_attempts(context).to_i + 1
12
+ end
13
+
14
+ # Returns the number of retries that have occurred for the current task.
15
+ # @return [Integer] the number of retries (0 on first run, 1+ on retries)
16
+ def retries
17
+ attempts - 1
18
+ end
19
+
20
+ # Checks whether this is a retry execution.
21
+ # @return [Boolean] `true` if this is a retry, `false` if this is the first run
22
+ def retrying?
23
+ attempts > 1
24
+ end
25
+
26
+ # Checks whether the task will be retried if the current execution fails.
27
+ # @return [Boolean] `true` if a failure will schedule another attempt, `false` otherwise
28
+ def will_retry?
29
+ task = Rage::Deferred::Context.get_task(context)
30
+ task.__should_retry?(attempts)
31
+ end
32
+
33
+ private
34
+
35
+ def context
36
+ Fiber[Rage::Deferred::Task::CONTEXT_KEY]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rage::Deferred::MiddlewareChain
4
+ def initialize(enqueue_middleware:, perform_middleware:)
5
+ @enqueue_middleware = enqueue_middleware
6
+ @perform_middleware = perform_middleware
7
+
8
+ build_enqueue_chain!
9
+ build_perform_chain!
10
+ end
11
+
12
+ private
13
+
14
+ def build_enqueue_chain!
15
+ raw_arguments = {
16
+ phase: ":enqueue",
17
+ args: "Rage::Deferred::Context.get_or_create_args(context)",
18
+ kwargs: "Rage::Deferred::Context.get_or_create_kwargs(context)",
19
+ context: "Rage::Deferred::Context.get_or_create_user_context(context)",
20
+ task_class: "Rage::Deferred::Context.get_task(context)",
21
+ delay: "delay",
22
+ delay_until: "delay_until"
23
+ }
24
+
25
+ self.class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
26
+ def with_enqueue_middleware(context, delay:, delay_until:)
27
+ #{build_middleware_chain(:@enqueue_middleware, raw_arguments)}
28
+ end
29
+ RUBY
30
+ end
31
+
32
+ def build_perform_chain!
33
+ raw_arguments = {
34
+ phase: ":perform",
35
+ args: "Rage::Deferred::Context.get_or_create_args(context)",
36
+ kwargs: "Rage::Deferred::Context.get_or_create_kwargs(context)",
37
+ context: "Rage::Deferred::Context.get_or_create_user_context(context)",
38
+ task_class: "task.class",
39
+ task: "task"
40
+ }
41
+
42
+ self.class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
43
+ def with_perform_middleware(context, task:)
44
+ #{build_middleware_chain(:@perform_middleware, raw_arguments)}
45
+ end
46
+ RUBY
47
+ end
48
+
49
+ def build_middleware_chain(middlewares_var, raw_arguments)
50
+ middlewares = instance_variable_get(middlewares_var)
51
+ i = middlewares.length
52
+
53
+ middlewares.reverse.inject("yield") do |memo, middleware_with_args|
54
+ middleware, _, _ = middleware_with_args
55
+ arguments = Rage::Internal.build_arguments(middleware.instance_method(:call), raw_arguments)
56
+ i -= 1
57
+
58
+ <<~RUBY
59
+ middleware, args, block = #{middlewares_var}[#{i}]
60
+
61
+ middleware.new(*args, &block).call(#{arguments}) do
62
+ #{memo}
63
+ end
64
+ RUBY
65
+ end
66
+ end
67
+ end