rage-rb 1.23.0 → 1.25.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 +38 -0
- data/CONTRIBUTING.md +240 -0
- data/README.md +66 -123
- data/lib/rage/application.rb +1 -0
- data/lib/rage/cable/cable.rb +20 -15
- data/lib/rage/cable/channel.rb +2 -1
- data/lib/rage/configuration.rb +166 -29
- data/lib/rage/controller/api.rb +10 -34
- data/lib/rage/cookies.rb +1 -1
- data/lib/rage/deferred/deferred.rb +7 -0
- data/lib/rage/deferred/metadata.rb +8 -0
- data/lib/rage/deferred/scheduler.rb +25 -0
- data/lib/rage/deferred/task.rb +19 -5
- data/lib/rage/errors.rb +83 -0
- data/lib/rage/events/subscriber.rb +6 -1
- data/lib/rage/fiber.rb +14 -23
- data/lib/rage/fiber_scheduler.rb +51 -6
- data/lib/rage/internal.rb +15 -6
- data/lib/rage/middleware/fiber_wrapper.rb +1 -0
- data/lib/rage/openapi/builder.rb +1 -1
- data/lib/rage/openapi/converter.rb +5 -1
- data/lib/rage/openapi/nodes/method.rb +2 -1
- data/lib/rage/openapi/nodes/root.rb +2 -1
- data/lib/rage/openapi/openapi.rb +33 -1
- data/lib/rage/openapi/parser.rb +73 -2
- data/lib/rage/openapi/parsers/ext/alba.rb +30 -2
- data/lib/rage/openapi/parsers/ext/blueprinter.rb +110 -0
- data/lib/rage/openapi/parsers/request.rb +2 -2
- data/lib/rage/openapi/parsers/response.rb +3 -2
- data/lib/rage/openapi/parsers/yaml.rb +27 -5
- data/lib/rage/params_parser.rb +2 -2
- data/lib/rage/pubsub/adapters/redis.rb +2 -1
- data/lib/rage/router/constrainer.rb +1 -1
- data/lib/rage/router/dsl.rb +7 -2
- data/lib/rage/sse/application.rb +1 -0
- data/lib/rage/telemetry/tracer.rb +1 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +6 -0
- metadata +6 -4
- data/lib/rage/cable/adapters/base.rb +0 -16
- data/lib/rage/cable/adapters/redis.rb +0 -128
data/lib/rage/configuration.rb
CHANGED
|
@@ -219,6 +219,14 @@ class Rage::Configuration
|
|
|
219
219
|
end
|
|
220
220
|
# @!endgroup
|
|
221
221
|
|
|
222
|
+
# @!group Error Reporter Configuration
|
|
223
|
+
# Allows configuring error reporters.
|
|
224
|
+
# @return [Rage::Configuration::ErrorReporters]
|
|
225
|
+
def error_reporters
|
|
226
|
+
@error_reporters ||= ErrorReporters.new
|
|
227
|
+
end
|
|
228
|
+
# @!endgroup
|
|
229
|
+
|
|
222
230
|
# @!group OpenAPI Configuration
|
|
223
231
|
# Allows configuring OpenAPI settings.
|
|
224
232
|
# @return [Rage::Configuration::OpenAPI]
|
|
@@ -265,6 +273,22 @@ class Rage::Configuration
|
|
|
265
273
|
end
|
|
266
274
|
# @!endgroup
|
|
267
275
|
|
|
276
|
+
# @!group Router Configuration
|
|
277
|
+
# Allows configuring router settings.
|
|
278
|
+
# @return [Rage::Configuration::Router]
|
|
279
|
+
def router
|
|
280
|
+
@router ||= Router.new
|
|
281
|
+
end
|
|
282
|
+
# @!endgroup
|
|
283
|
+
|
|
284
|
+
# @!group Blocking Operation Pool Configuration
|
|
285
|
+
# Allows configuring the thread pool for offloading native calls.
|
|
286
|
+
# @return [Rage::Configuration::BlockingOperationPool]
|
|
287
|
+
def blocking_operation_pool
|
|
288
|
+
@blocking_operation_pool ||= BlockingOperationPool.new
|
|
289
|
+
end
|
|
290
|
+
# @!endgroup
|
|
291
|
+
|
|
268
292
|
# @private
|
|
269
293
|
def pubsub
|
|
270
294
|
@pubsub ||= PubSub.new
|
|
@@ -278,7 +302,7 @@ class Rage::Configuration
|
|
|
278
302
|
# @private
|
|
279
303
|
def run_after_initialize!
|
|
280
304
|
run_hooks_for!(:after_initialize, self)
|
|
281
|
-
__finalize
|
|
305
|
+
__finalize(true)
|
|
282
306
|
end
|
|
283
307
|
|
|
284
308
|
class LogContext
|
|
@@ -378,6 +402,60 @@ class Rage::Configuration
|
|
|
378
402
|
end
|
|
379
403
|
end
|
|
380
404
|
|
|
405
|
+
class ErrorReporters
|
|
406
|
+
# @private
|
|
407
|
+
def initialize
|
|
408
|
+
@objects = []
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# @private
|
|
412
|
+
def objects
|
|
413
|
+
@objects.dup
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Add a new error reporter.
|
|
417
|
+
# Error reporters should respond to `#call` and accept one of:
|
|
418
|
+
# - `call(exception)`
|
|
419
|
+
# - `call(exception, context: {})`
|
|
420
|
+
#
|
|
421
|
+
# @param reporter [#call]
|
|
422
|
+
# @return [self]
|
|
423
|
+
# @example
|
|
424
|
+
# Rage.configure do
|
|
425
|
+
# config.error_reporters << SentryReporter.new
|
|
426
|
+
# end
|
|
427
|
+
def <<(reporter)
|
|
428
|
+
validate_input!(reporter)
|
|
429
|
+
return self if @objects.include?(reporter)
|
|
430
|
+
|
|
431
|
+
@objects << reporter
|
|
432
|
+
Rage::Errors.__send__(:__register_reporter, reporter)
|
|
433
|
+
|
|
434
|
+
self
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
alias_method :push, :<<
|
|
438
|
+
|
|
439
|
+
# Remove an error reporter.
|
|
440
|
+
# @param reporter [#call] the reporter to remove
|
|
441
|
+
# @example
|
|
442
|
+
# reporter = SentryReporter.new
|
|
443
|
+
# Rage.configure do
|
|
444
|
+
# config.error_reporters.delete(reporter)
|
|
445
|
+
# end
|
|
446
|
+
def delete(reporter)
|
|
447
|
+
deleted = @objects.delete(reporter)
|
|
448
|
+
Rage::Errors.__send__(:__unregister_reporter, reporter) if deleted
|
|
449
|
+
deleted
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
private
|
|
453
|
+
|
|
454
|
+
def validate_input!(reporter)
|
|
455
|
+
raise ArgumentError, "error reporter must respond to #call" unless reporter.respond_to?(:call)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
381
459
|
class Server
|
|
382
460
|
# @!attribute port
|
|
383
461
|
# Specify the port the server will listen on.
|
|
@@ -632,33 +710,6 @@ class Rage::Configuration
|
|
|
632
710
|
end
|
|
633
711
|
end
|
|
634
712
|
end
|
|
635
|
-
|
|
636
|
-
# @private
|
|
637
|
-
def config
|
|
638
|
-
@config ||= begin
|
|
639
|
-
config_file = Rage.root.join("config/cable.yml")
|
|
640
|
-
|
|
641
|
-
if config_file.exist?
|
|
642
|
-
yaml = ERB.new(config_file.read).result
|
|
643
|
-
YAML.safe_load(yaml, aliases: true, symbolize_names: true)[Rage.env.to_sym] || {}
|
|
644
|
-
else
|
|
645
|
-
{}
|
|
646
|
-
end
|
|
647
|
-
end
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
# @private
|
|
651
|
-
def adapter_config
|
|
652
|
-
config.except(:adapter)
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
# @private
|
|
656
|
-
def adapter
|
|
657
|
-
case config[:adapter]
|
|
658
|
-
when "redis"
|
|
659
|
-
Rage::Cable::Adapters::Redis.new(adapter_config)
|
|
660
|
-
end
|
|
661
|
-
end
|
|
662
713
|
end
|
|
663
714
|
|
|
664
715
|
class PublicFileServer
|
|
@@ -707,6 +758,46 @@ class Rage::Configuration
|
|
|
707
758
|
# @private
|
|
708
759
|
def initialize
|
|
709
760
|
@configured = false
|
|
761
|
+
@schedule_blocks = []
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Schedule a periodic task to run at a fixed interval.
|
|
765
|
+
# @example
|
|
766
|
+
# Rage.configure do
|
|
767
|
+
# config.deferred.schedule do
|
|
768
|
+
# every 5.minutes, task: ClearCache
|
|
769
|
+
# end
|
|
770
|
+
# end
|
|
771
|
+
def schedule(&block)
|
|
772
|
+
@schedule_blocks << block
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# @private
|
|
776
|
+
# Evaluates all stored schedule blocks and returns the collected tasks.
|
|
777
|
+
# Called at boot time after all app constants are loaded.
|
|
778
|
+
def scheduled_tasks
|
|
779
|
+
@schedule_blocks.flat_map do |block|
|
|
780
|
+
dsl = ScheduleDSL.new
|
|
781
|
+
dsl.instance_eval(&block)
|
|
782
|
+
dsl.tasks
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# @private
|
|
787
|
+
class ScheduleDSL
|
|
788
|
+
attr_reader :tasks
|
|
789
|
+
|
|
790
|
+
def initialize
|
|
791
|
+
@tasks = []
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Registers a task to run on a fixed interval (in seconds)
|
|
795
|
+
def every(interval, task:)
|
|
796
|
+
unless task.is_a?(Class) && task.include?(Rage::Deferred::Task)
|
|
797
|
+
raise ArgumentError, "#{task} must be a class that includes Rage::Deferred::Task"
|
|
798
|
+
end
|
|
799
|
+
@tasks << { interval:, task: }
|
|
800
|
+
end
|
|
710
801
|
end
|
|
711
802
|
|
|
712
803
|
# Returns the backend instance used by `Rage::Deferred`.
|
|
@@ -988,6 +1079,42 @@ class Rage::Configuration
|
|
|
988
1079
|
attr_accessor :key
|
|
989
1080
|
end
|
|
990
1081
|
|
|
1082
|
+
class Router
|
|
1083
|
+
# @!attribute form_actions
|
|
1084
|
+
# Enable the automatic generation of `new` and `edit` routes via resource helpers.
|
|
1085
|
+
# @return [Boolean]
|
|
1086
|
+
# @example Enable form actions
|
|
1087
|
+
# Rage.configure do
|
|
1088
|
+
# config.router.form_actions = true
|
|
1089
|
+
# end
|
|
1090
|
+
attr_accessor :form_actions
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
class BlockingOperationPool
|
|
1094
|
+
# @!attribute enabled
|
|
1095
|
+
# Enable a background thread pool for offloading native calls that can be executed outside the GVL, freeing
|
|
1096
|
+
# the main server thread to continue processing other requests. This acts as a preemption mechanism: native calls
|
|
1097
|
+
# won't stall requests, as the OS will context-switch between the server thread and the worker threads.
|
|
1098
|
+
# Defaults to `false`.
|
|
1099
|
+
# @return [Boolean]
|
|
1100
|
+
# @example Enable the thread pool
|
|
1101
|
+
# Rage.configure do
|
|
1102
|
+
# config.blocking_operation_pool.enabled = true
|
|
1103
|
+
# end
|
|
1104
|
+
#
|
|
1105
|
+
# @!attribute size
|
|
1106
|
+
# Specify the number of threads in the pool. Defaults to `1`. A single thread is sufficient in most cases
|
|
1107
|
+
# because the pool's goal is context switching, not parallelization.
|
|
1108
|
+
# @return [Integer]
|
|
1109
|
+
attr_accessor :enabled, :size
|
|
1110
|
+
|
|
1111
|
+
# @private
|
|
1112
|
+
def initialize
|
|
1113
|
+
@enabled = false
|
|
1114
|
+
@size = 1
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
|
|
991
1118
|
# @private
|
|
992
1119
|
class PubSub
|
|
993
1120
|
attr_reader :adapter
|
|
@@ -1004,6 +1131,7 @@ class Rage::Configuration
|
|
|
1004
1131
|
def config
|
|
1005
1132
|
@config ||= begin
|
|
1006
1133
|
config_file = Rage.root.join("config/pubsub.yml")
|
|
1134
|
+
config_file = Rage.root.join("config/cable.yml") unless config_file.exist?
|
|
1007
1135
|
|
|
1008
1136
|
config = if config_file.exist?
|
|
1009
1137
|
yaml = ERB.new(config_file.read).result
|
|
@@ -1053,7 +1181,7 @@ class Rage::Configuration
|
|
|
1053
1181
|
end
|
|
1054
1182
|
|
|
1055
1183
|
# @private
|
|
1056
|
-
def __finalize
|
|
1184
|
+
def __finalize(before_boot = false)
|
|
1057
1185
|
if @logger
|
|
1058
1186
|
@logger.formatter = @log_formatter if @log_formatter
|
|
1059
1187
|
@logger.level = @log_level if @log_level
|
|
@@ -1075,6 +1203,15 @@ class Rage::Configuration
|
|
|
1075
1203
|
@logger.dynamic_tags = Rage.__log_processor.dynamic_tags
|
|
1076
1204
|
end
|
|
1077
1205
|
|
|
1206
|
+
if before_boot && @blocking_operation_pool&.enabled
|
|
1207
|
+
if defined?(Rage::FiberScheduler::BlockingOperationWait)
|
|
1208
|
+
Iodine.on_state(:pre_start) { puts "INFO: Using blocking operation pool" }
|
|
1209
|
+
Rage::FiberScheduler.include(Rage::FiberScheduler::BlockingOperationWait)
|
|
1210
|
+
else
|
|
1211
|
+
puts "WARNING: Blocking operation pool is not supported on Ruby #{RUBY_VERSION}"
|
|
1212
|
+
end
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1078
1215
|
if defined?(::Rack::Events) && middleware.include?(::Rack::Events)
|
|
1079
1216
|
middleware.delete(Rage::BodyFinalizer)
|
|
1080
1217
|
middleware.insert_before(::Rack::Events, Rage::BodyFinalizer)
|
data/lib/rage/controller/api.rb
CHANGED
|
@@ -185,32 +185,6 @@ class RageController::API
|
|
|
185
185
|
klass.__wrap_parameters_options = __wrap_parameters_options
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
-
# @private
|
|
189
|
-
@@__dynamic_name_seed = ("a".."i").to_a.permutation
|
|
190
|
-
|
|
191
|
-
# @private
|
|
192
|
-
# define a method based on a block
|
|
193
|
-
def define_dynamic_method(block)
|
|
194
|
-
name = @@__dynamic_name_seed.next.join
|
|
195
|
-
define_method("__rage_dynamic_#{name}", block)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# @private
|
|
199
|
-
# define a method that will call a specified method if a condition is `true` or yield if `false`
|
|
200
|
-
def define_maybe_yield(method_name)
|
|
201
|
-
name = @@__dynamic_name_seed.next.join
|
|
202
|
-
|
|
203
|
-
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
204
|
-
def __rage_dynamic_#{name}(condition)
|
|
205
|
-
if condition
|
|
206
|
-
#{method_name} { yield }
|
|
207
|
-
else
|
|
208
|
-
yield
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
RUBY
|
|
212
|
-
end
|
|
213
|
-
|
|
214
188
|
# @private
|
|
215
189
|
def __register_renderer(name, block)
|
|
216
190
|
prepend(RageController::Renderers) unless ancestors.include?(RageController::Renderers)
|
|
@@ -240,7 +214,7 @@ class RageController::API
|
|
|
240
214
|
def rescue_from(*klasses, with: nil, &block)
|
|
241
215
|
unless with
|
|
242
216
|
if block_given?
|
|
243
|
-
with = define_dynamic_method(block)
|
|
217
|
+
with = Rage::Internal.define_dynamic_method(self, block)
|
|
244
218
|
else
|
|
245
219
|
raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
|
|
246
220
|
end
|
|
@@ -314,7 +288,7 @@ class RageController::API
|
|
|
314
288
|
# end
|
|
315
289
|
def around_action(action_name = nil, **opts, &block)
|
|
316
290
|
action = prepare_action_params(action_name, **opts, &block)
|
|
317
|
-
action.merge!(around: true, wrapper: define_maybe_yield(action[:name]))
|
|
291
|
+
action.merge!(around: true, wrapper: Rage::Internal.define_maybe_yield(self, action[:name]))
|
|
318
292
|
|
|
319
293
|
if @__before_actions && @__before_actions.frozen?
|
|
320
294
|
@__before_actions = @__before_actions.dup
|
|
@@ -429,7 +403,7 @@ class RageController::API
|
|
|
429
403
|
# used by `before_action` and `after_action`
|
|
430
404
|
def prepare_action_params(action_name = nil, **opts, &block)
|
|
431
405
|
if block_given?
|
|
432
|
-
action_name = define_dynamic_method(block)
|
|
406
|
+
action_name = Rage::Internal.define_dynamic_method(self, block)
|
|
433
407
|
elsif action_name.nil?
|
|
434
408
|
raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
|
|
435
409
|
end
|
|
@@ -444,8 +418,8 @@ class RageController::API
|
|
|
444
418
|
unless: _unless
|
|
445
419
|
}
|
|
446
420
|
|
|
447
|
-
action[:if] = define_dynamic_method(action[:if]) if action[:if].is_a?(Proc)
|
|
448
|
-
action[:unless] = define_dynamic_method(action[:unless]) if action[:unless].is_a?(Proc)
|
|
421
|
+
action[:if] = Rage::Internal.define_dynamic_method(self, action[:if]) if action[:if].is_a?(Proc)
|
|
422
|
+
action[:unless] = Rage::Internal.define_dynamic_method(self, action[:unless]) if action[:unless].is_a?(Proc)
|
|
449
423
|
|
|
450
424
|
action
|
|
451
425
|
end
|
|
@@ -584,13 +558,13 @@ class RageController::API
|
|
|
584
558
|
def authenticate_with_http_token
|
|
585
559
|
auth_header = @__env["HTTP_AUTHORIZATION"]
|
|
586
560
|
|
|
587
|
-
payload = if auth_header&.start_with?("Bearer")
|
|
561
|
+
payload = if auth_header&.start_with?("Bearer ")
|
|
588
562
|
auth_header[7..]
|
|
589
|
-
elsif auth_header&.start_with?("Token")
|
|
563
|
+
elsif auth_header&.start_with?("Token ")
|
|
590
564
|
auth_header[6..]
|
|
591
565
|
end
|
|
592
566
|
|
|
593
|
-
return
|
|
567
|
+
return if payload.nil? || payload.empty?
|
|
594
568
|
|
|
595
569
|
token = if payload.start_with?("token=")
|
|
596
570
|
payload[6..]
|
|
@@ -601,6 +575,8 @@ class RageController::API
|
|
|
601
575
|
token.delete_prefix!('"')
|
|
602
576
|
token.delete_suffix!('"')
|
|
603
577
|
|
|
578
|
+
return if token.empty?
|
|
579
|
+
|
|
604
580
|
yield token
|
|
605
581
|
end
|
|
606
582
|
|
data/lib/rage/cookies.rb
CHANGED
|
@@ -82,10 +82,16 @@ module Rage::Deferred
|
|
|
82
82
|
)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# @private
|
|
86
|
+
def self.__start_scheduler
|
|
87
|
+
Rage::Deferred::Scheduler.start(Rage.config.deferred.scheduled_tasks)
|
|
88
|
+
end
|
|
89
|
+
|
|
85
90
|
# @private
|
|
86
91
|
def self.__initialize
|
|
87
92
|
__middleware_chain
|
|
88
93
|
__load_tasks
|
|
94
|
+
__start_scheduler
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
module Backends
|
|
@@ -96,6 +102,7 @@ module Rage::Deferred
|
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
require_relative "task"
|
|
105
|
+
require_relative "scheduler"
|
|
99
106
|
require_relative "queue"
|
|
100
107
|
require_relative "proxy"
|
|
101
108
|
require_relative "context"
|
|
@@ -30,6 +30,14 @@ class Rage::Deferred::Metadata
|
|
|
30
30
|
!!task.__next_retry_in(attempts, nil)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Returns the number of seconds until the next retry, or `nil` if no retry will occur.
|
|
34
|
+
# The result is memoized per attempt so that the value reported here matches what the queue uses to schedule the retry.
|
|
35
|
+
# @return [Numeric, nil] retry delay in seconds, or `nil` if the task won't be retried
|
|
36
|
+
def will_retry_in
|
|
37
|
+
task = Rage::Deferred::Context.get_task(context)
|
|
38
|
+
task.__next_retry_in(attempts, nil)
|
|
39
|
+
end
|
|
40
|
+
|
|
33
41
|
private
|
|
34
42
|
|
|
35
43
|
def context
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @private
|
|
4
|
+
class Rage::Deferred::Scheduler
|
|
5
|
+
def self.start(tasks)
|
|
6
|
+
return if tasks.empty?
|
|
7
|
+
|
|
8
|
+
Rage::Internal.pick_a_worker(purpose: "deferred-scheduler") do
|
|
9
|
+
puts("INFO: #{Process.pid} is managing scheduled tasks.") if Rage.logger.info?
|
|
10
|
+
register_timers(tasks)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.register_timers(tasks)
|
|
15
|
+
tasks.each do |entry|
|
|
16
|
+
interval = (entry[:interval] * 1000).to_i
|
|
17
|
+
|
|
18
|
+
if Rage.env.development?
|
|
19
|
+
Iodine.run_every(interval) { Object.const_get(entry[:task].name).enqueue }
|
|
20
|
+
else
|
|
21
|
+
Iodine.run_every(interval) { entry[:task].enqueue }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/rage/deferred/task.rb
CHANGED
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
# ```
|
|
32
32
|
#
|
|
33
33
|
module Rage::Deferred::Task
|
|
34
|
-
MAX_ATTEMPTS =
|
|
34
|
+
MAX_ATTEMPTS = 20
|
|
35
35
|
private_constant :MAX_ATTEMPTS
|
|
36
36
|
|
|
37
|
-
BACKOFF_INTERVAL = 5
|
|
38
|
-
private_constant :BACKOFF_INTERVAL
|
|
39
|
-
|
|
40
37
|
# @private
|
|
41
38
|
CONTEXT_KEY = :__rage_deferred_execution_context
|
|
42
39
|
|
|
40
|
+
# @private
|
|
41
|
+
RETRY_IN_CACHE_VAR = :@__rage_deferred_retry_in
|
|
42
|
+
private_constant :RETRY_IN_CACHE_VAR
|
|
43
|
+
|
|
43
44
|
def perform
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -80,6 +81,8 @@ module Rage::Deferred::Task
|
|
|
80
81
|
|
|
81
82
|
true
|
|
82
83
|
rescue Exception => e
|
|
84
|
+
Rage::Errors.report(e)
|
|
85
|
+
|
|
83
86
|
unless respond_to?(:__deferred_suppress_exception_logging?, true) && __deferred_suppress_exception_logging?
|
|
84
87
|
Rage.logger.with_context(task_log_context) do
|
|
85
88
|
Rage.logger.error("Deferred task failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
@@ -175,6 +178,17 @@ module Rage::Deferred::Task
|
|
|
175
178
|
|
|
176
179
|
# @private
|
|
177
180
|
def __next_retry_in(attempts, exception)
|
|
181
|
+
f = Fiber.current
|
|
182
|
+
|
|
183
|
+
if f.instance_variable_defined?(RETRY_IN_CACHE_VAR)
|
|
184
|
+
f.instance_variable_get(RETRY_IN_CACHE_VAR)
|
|
185
|
+
else
|
|
186
|
+
f.instance_variable_set(RETRY_IN_CACHE_VAR, __calculate_next_retry_in(attempts, exception))
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @private
|
|
191
|
+
def __calculate_next_retry_in(attempts, exception)
|
|
178
192
|
max = @__max_retries || MAX_ATTEMPTS
|
|
179
193
|
return if attempts > max
|
|
180
194
|
|
|
@@ -191,7 +205,7 @@ module Rage::Deferred::Task
|
|
|
191
205
|
|
|
192
206
|
# @private
|
|
193
207
|
def __default_backoff(attempt)
|
|
194
|
-
rand(
|
|
208
|
+
(attempt**4) + 10 + (rand(15) * attempt)
|
|
195
209
|
end
|
|
196
210
|
end
|
|
197
211
|
end
|
data/lib/rage/errors.rb
CHANGED
|
@@ -1,4 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rage::Errors
|
|
4
|
+
ReporterEntry = Struct.new(:reporter, :method_name)
|
|
5
|
+
private_constant :ReporterEntry
|
|
6
|
+
|
|
7
|
+
@reporters = []
|
|
8
|
+
@next_reporter_id = 0
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Forward an exception to all registered reporters.
|
|
12
|
+
#
|
|
13
|
+
# @param exception [Exception]
|
|
14
|
+
# @param context [Hash]
|
|
15
|
+
# @return [nil]
|
|
16
|
+
def report(exception, context: {})
|
|
17
|
+
return if @reporters.empty?
|
|
18
|
+
return if exception.instance_variable_defined?(:@_rage_error_reported)
|
|
19
|
+
|
|
20
|
+
ensure_backtrace(exception)
|
|
21
|
+
|
|
22
|
+
@reporters.each do |entry|
|
|
23
|
+
__send__(entry.method_name, entry.reporter, exception, context)
|
|
24
|
+
rescue => e
|
|
25
|
+
Rage.logger.error("Error reporter #{entry.reporter.class} failed while reporting #{exception.class}: #{e.class} (#{e.message})")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
exception.instance_variable_set(:@_rage_error_reported, true) unless exception.frozen?
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @private
|
|
34
|
+
def __register_reporter(reporter)
|
|
35
|
+
raise ArgumentError, "error reporter must respond to #call" unless reporter.respond_to?(:call)
|
|
36
|
+
|
|
37
|
+
reporter_id = @next_reporter_id
|
|
38
|
+
@next_reporter_id += 1
|
|
39
|
+
method_name = :"__report_#{reporter_id}"
|
|
40
|
+
|
|
41
|
+
arguments = Rage::Internal.build_arguments(
|
|
42
|
+
reporter.method(:call),
|
|
43
|
+
{ context: "context" }
|
|
44
|
+
)
|
|
45
|
+
call_arguments = arguments.empty? ? "" : ", #{arguments}"
|
|
46
|
+
|
|
47
|
+
singleton_class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
48
|
+
def #{method_name}(reporter, exception, context)
|
|
49
|
+
reporter.call(exception#{call_arguments})
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
|
|
53
|
+
@reporters << ReporterEntry.new(reporter, method_name)
|
|
54
|
+
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @private
|
|
59
|
+
def __unregister_reporter(reporter)
|
|
60
|
+
@reporters.delete_if do |entry|
|
|
61
|
+
next false unless entry.reporter == reporter
|
|
62
|
+
|
|
63
|
+
singleton_class.remove_method(entry.method_name) if singleton_class.method_defined?(entry.method_name)
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def ensure_backtrace(exception)
|
|
73
|
+
return if exception.frozen?
|
|
74
|
+
return unless exception.backtrace.nil?
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
raise exception
|
|
78
|
+
rescue exception.class
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private :__register_reporter, :__unregister_reporter
|
|
83
|
+
end
|
|
84
|
+
|
|
2
85
|
class BadRequest < StandardError
|
|
3
86
|
end
|
|
4
87
|
|
|
@@ -90,7 +90,12 @@ module Rage::Events::Subscriber
|
|
|
90
90
|
Rage.logger.with_context(self.class.__log_context) do
|
|
91
91
|
Rage.logger.error("Subscriber failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
92
92
|
end
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
if self.class.__is_deferred
|
|
95
|
+
raise e
|
|
96
|
+
else
|
|
97
|
+
Rage::Errors.report(e)
|
|
98
|
+
end
|
|
94
99
|
end
|
|
95
100
|
|
|
96
101
|
private
|
data/lib/rage/fiber.rb
CHANGED
|
@@ -111,23 +111,7 @@ class Fiber
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# @private
|
|
114
|
-
|
|
115
|
-
@__block_channel_i ||= 0
|
|
116
|
-
@__block_channel_i += 1 if force
|
|
117
|
-
|
|
118
|
-
"block:#{object_id}:#{@__block_channel_i}"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# @private
|
|
122
|
-
def __await_channel(force = false)
|
|
123
|
-
@__fiber_channel_i ||= 0
|
|
124
|
-
@__fiber_channel_i += 1 if force
|
|
125
|
-
|
|
126
|
-
"await:#{object_id}:#{@__fiber_channel_i}"
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# @private
|
|
130
|
-
attr_accessor :__awaited_fileno
|
|
114
|
+
attr_accessor :__awaited_fileno, :__wait_generation, :__block_channel, :__await_channel
|
|
131
115
|
|
|
132
116
|
# @private
|
|
133
117
|
# pause a fiber and resume in the next iteration of the event loop
|
|
@@ -156,7 +140,10 @@ class Fiber
|
|
|
156
140
|
# @note This method should only be used when multiple fibers have to be processed in parallel. There's no need to use `Fiber.await` for single IO calls.
|
|
157
141
|
def self.await(fibers)
|
|
158
142
|
f, fibers = Fiber.current, Array(fibers)
|
|
159
|
-
|
|
143
|
+
|
|
144
|
+
f.__wait_generation ||= 0
|
|
145
|
+
gen = (f.__wait_generation += 1)
|
|
146
|
+
channel = f.__await_channel = "await:#{f.object_id}:#{gen}"
|
|
160
147
|
|
|
161
148
|
Rage::Telemetry.tracer.span_core_fiber_await(fibers:) do
|
|
162
149
|
# check which fibers are alive (i.e. have yielded) and which have errored out
|
|
@@ -179,17 +166,21 @@ class Fiber
|
|
|
179
166
|
end
|
|
180
167
|
|
|
181
168
|
# wait on async fibers; resume right away if one of the fibers errors out
|
|
182
|
-
Iodine.subscribe(
|
|
183
|
-
if err == AWAIT_ERROR_MESSAGE
|
|
184
|
-
|
|
169
|
+
Iodine.subscribe(channel) do |_, err|
|
|
170
|
+
done = if err == AWAIT_ERROR_MESSAGE
|
|
171
|
+
true
|
|
185
172
|
else
|
|
186
173
|
num_wait_for -= 1
|
|
187
|
-
|
|
174
|
+
num_wait_for == 0
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if done
|
|
178
|
+
Iodine.defer { Iodine.unsubscribe(channel) }
|
|
179
|
+
f.resume if f.alive? && gen == f.__wait_generation
|
|
188
180
|
end
|
|
189
181
|
end
|
|
190
182
|
|
|
191
183
|
Fiber.defer(-1)
|
|
192
|
-
Iodine.defer { Iodine.unsubscribe(await_channel) }
|
|
193
184
|
|
|
194
185
|
# if num_wait_for is not 0 means we exited prematurely because of an error
|
|
195
186
|
if num_wait_for > 0
|