rage-rb 1.22.1 → 1.24.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 +42 -0
- data/CONTRIBUTING.md +240 -0
- data/README.md +2 -1
- data/lib/rage/all.rb +1 -0
- 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 +229 -27
- data/lib/rage/controller/api.rb +17 -33
- data/lib/rage/controller/renderers.rb +47 -0
- data/lib/rage/deferred/backends/disk.rb +19 -3
- data/lib/rage/deferred/deferred.rb +7 -0
- data/lib/rage/deferred/metadata.rb +9 -1
- data/lib/rage/deferred/queue.rb +5 -4
- data/lib/rage/deferred/scheduler.rb +25 -0
- data/lib/rage/deferred/task.rb +90 -9
- data/lib/rage/errors.rb +86 -0
- data/lib/rage/events/subscriber.rb +6 -1
- data/lib/rage/internal.rb +45 -0
- data/lib/rage/logger/logger.rb +1 -1
- data/lib/rage/middleware/fiber_wrapper.rb +1 -0
- data/lib/rage/openapi/builder.rb +1 -1
- data/lib/rage/openapi/converter.rb +48 -3
- data/lib/rage/openapi/nodes/method.rb +2 -1
- data/lib/rage/openapi/nodes/root.rb +2 -1
- data/lib/rage/openapi/openapi.rb +12 -1
- data/lib/rage/openapi/parser.rb +73 -2
- data/lib/rage/openapi/parsers/ext/alba.rb +35 -6
- data/lib/rage/openapi/parsers/request.rb +2 -2
- data/lib/rage/openapi/parsers/response.rb +2 -2
- data/lib/rage/openapi/parsers/yaml.rb +27 -5
- data/lib/rage/params_parser.rb +2 -2
- data/lib/rage/{cable → pubsub}/adapters/redis.rb +43 -23
- data/lib/rage/pubsub/pubsub.rb +25 -0
- data/lib/rage/rails.rb +16 -0
- data/lib/rage/router/README.md +1 -1
- data/lib/rage/router/dsl.rb +72 -10
- data/lib/rage/sse/application.rb +31 -2
- data/lib/rage/sse/sse.rb +96 -0
- data/lib/rage/sse/stream.rb +78 -0
- data/lib/rage/telemetry/spans/broadcast_sse_stream.rb +50 -0
- data/lib/rage/telemetry/spans/process_sse_stream.rb +1 -0
- data/lib/rage/telemetry/telemetry.rb +2 -1
- data/lib/rage/telemetry/tracer.rb +1 -0
- data/lib/rage/uploaded_file.rb +3 -7
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +8 -1
- metadata +9 -4
- data/lib/rage/cable/adapters/base.rb +0 -16
data/lib/rage/configuration.rb
CHANGED
|
@@ -139,6 +139,52 @@ class Rage::Configuration
|
|
|
139
139
|
def after_initialize(&block)
|
|
140
140
|
push_hook(block, :after_initialize)
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
# Register a custom renderer that generates overloads `render` on all controllers.
|
|
144
|
+
# The block receives the object passed to `render` together with any additional keyword arguments.
|
|
145
|
+
# The code inside the block is executed in the context of the controller instance, so you can access all usual controller methods in it.
|
|
146
|
+
# The return value of the block is used as the response body.
|
|
147
|
+
#
|
|
148
|
+
# @param name [Symbol, String] the name of the renderer
|
|
149
|
+
# @param block [Proc] the rendering logic. The block is executed in the controller's context and its return value becomes the response body
|
|
150
|
+
# @raise [ArgumentError] if no block is given or if a renderer with the same name is already registered
|
|
151
|
+
#
|
|
152
|
+
# @example Register an ERB renderer
|
|
153
|
+
# Rage.configure do
|
|
154
|
+
# config.renderer(:erb) do |path, trim_mode: nil|
|
|
155
|
+
# headers["content-type"] = "text/html"
|
|
156
|
+
# template = File.read("app/views/#{path}.html.erb")
|
|
157
|
+
#
|
|
158
|
+
# ERB.new(template, trim_mode:).result(binding)
|
|
159
|
+
# end
|
|
160
|
+
# end
|
|
161
|
+
# @example Use in a controller
|
|
162
|
+
# class ReportsController < RageController::API
|
|
163
|
+
# def index
|
|
164
|
+
# render erb: "reports/index"
|
|
165
|
+
# end
|
|
166
|
+
# end
|
|
167
|
+
# @example Pass arguments
|
|
168
|
+
# class ReportsController < RageController::API
|
|
169
|
+
# def index
|
|
170
|
+
# render erb: "reports/index", trim_mode: "%<>"
|
|
171
|
+
# end
|
|
172
|
+
# end
|
|
173
|
+
# @example Set response status
|
|
174
|
+
# class ReportsController < RageController::API
|
|
175
|
+
# def index
|
|
176
|
+
# render erb: "reports/index", status: 202
|
|
177
|
+
# end
|
|
178
|
+
# end
|
|
179
|
+
def renderer(name, &block)
|
|
180
|
+
@renderers ||= {}
|
|
181
|
+
raise ArgumentError, "renderer requires a block" unless block_given?
|
|
182
|
+
name = name.to_sym
|
|
183
|
+
if @renderers.key?(name)
|
|
184
|
+
raise ArgumentError, "a renderer named :#{name} is already registered"
|
|
185
|
+
end
|
|
186
|
+
@renderers[name] = RendererEntry.new(block)
|
|
187
|
+
end
|
|
142
188
|
# @!endgroup
|
|
143
189
|
|
|
144
190
|
# @!group Middleware Configuration
|
|
@@ -173,6 +219,14 @@ class Rage::Configuration
|
|
|
173
219
|
end
|
|
174
220
|
# @!endgroup
|
|
175
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
|
+
|
|
176
230
|
# @!group OpenAPI Configuration
|
|
177
231
|
# Allows configuring OpenAPI settings.
|
|
178
232
|
# @return [Rage::Configuration::OpenAPI]
|
|
@@ -219,6 +273,19 @@ class Rage::Configuration
|
|
|
219
273
|
end
|
|
220
274
|
# @!endgroup
|
|
221
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
|
+
# @private
|
|
285
|
+
def pubsub
|
|
286
|
+
@pubsub ||= PubSub.new
|
|
287
|
+
end
|
|
288
|
+
|
|
222
289
|
# @private
|
|
223
290
|
def internal
|
|
224
291
|
@internal ||= Internal.new
|
|
@@ -327,6 +394,60 @@ class Rage::Configuration
|
|
|
327
394
|
end
|
|
328
395
|
end
|
|
329
396
|
|
|
397
|
+
class ErrorReporters
|
|
398
|
+
# @private
|
|
399
|
+
def initialize
|
|
400
|
+
@objects = []
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# @private
|
|
404
|
+
def objects
|
|
405
|
+
@objects.dup
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Add a new error reporter.
|
|
409
|
+
# Error reporters should respond to `#call` and accept one of:
|
|
410
|
+
# - `call(exception)`
|
|
411
|
+
# - `call(exception, context: {})`
|
|
412
|
+
#
|
|
413
|
+
# @param reporter [#call]
|
|
414
|
+
# @return [self]
|
|
415
|
+
# @example
|
|
416
|
+
# Rage.configure do
|
|
417
|
+
# config.error_reporters << SentryReporter.new
|
|
418
|
+
# end
|
|
419
|
+
def <<(reporter)
|
|
420
|
+
validate_input!(reporter)
|
|
421
|
+
return self if @objects.include?(reporter)
|
|
422
|
+
|
|
423
|
+
@objects << reporter
|
|
424
|
+
Rage::Errors.__send__(:__register_reporter, reporter)
|
|
425
|
+
|
|
426
|
+
self
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
alias_method :push, :<<
|
|
430
|
+
|
|
431
|
+
# Remove an error reporter.
|
|
432
|
+
# @param reporter [#call] the reporter to remove
|
|
433
|
+
# @example
|
|
434
|
+
# reporter = SentryReporter.new
|
|
435
|
+
# Rage.configure do
|
|
436
|
+
# config.error_reporters.delete(reporter)
|
|
437
|
+
# end
|
|
438
|
+
def delete(reporter)
|
|
439
|
+
deleted = @objects.delete(reporter)
|
|
440
|
+
Rage::Errors.__send__(:__unregister_reporter, reporter) if deleted
|
|
441
|
+
deleted
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
def validate_input!(reporter)
|
|
447
|
+
raise ArgumentError, "error reporter must respond to #call" unless reporter.respond_to?(:call)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
330
451
|
class Server
|
|
331
452
|
# @!attribute port
|
|
332
453
|
# Specify the port the server will listen on.
|
|
@@ -581,33 +702,6 @@ class Rage::Configuration
|
|
|
581
702
|
end
|
|
582
703
|
end
|
|
583
704
|
end
|
|
584
|
-
|
|
585
|
-
# @private
|
|
586
|
-
def config
|
|
587
|
-
@config ||= begin
|
|
588
|
-
config_file = Rage.root.join("config/cable.yml")
|
|
589
|
-
|
|
590
|
-
if config_file.exist?
|
|
591
|
-
yaml = ERB.new(config_file.read).result
|
|
592
|
-
YAML.safe_load(yaml, aliases: true, symbolize_names: true)[Rage.env.to_sym] || {}
|
|
593
|
-
else
|
|
594
|
-
{}
|
|
595
|
-
end
|
|
596
|
-
end
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
# @private
|
|
600
|
-
def adapter_config
|
|
601
|
-
config.except(:adapter)
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
# @private
|
|
605
|
-
def adapter
|
|
606
|
-
case config[:adapter]
|
|
607
|
-
when "redis"
|
|
608
|
-
Rage::Cable::Adapters::Redis.new(adapter_config)
|
|
609
|
-
end
|
|
610
|
-
end
|
|
611
705
|
end
|
|
612
706
|
|
|
613
707
|
class PublicFileServer
|
|
@@ -656,6 +750,46 @@ class Rage::Configuration
|
|
|
656
750
|
# @private
|
|
657
751
|
def initialize
|
|
658
752
|
@configured = false
|
|
753
|
+
@schedule_blocks = []
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Schedule a periodic task to run at a fixed interval.
|
|
757
|
+
# @example
|
|
758
|
+
# Rage.configure do
|
|
759
|
+
# config.deferred.schedule do
|
|
760
|
+
# every 5.minutes, task: ClearCache
|
|
761
|
+
# end
|
|
762
|
+
# end
|
|
763
|
+
def schedule(&block)
|
|
764
|
+
@schedule_blocks << block
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# @private
|
|
768
|
+
# Evaluates all stored schedule blocks and returns the collected tasks.
|
|
769
|
+
# Called at boot time after all app constants are loaded.
|
|
770
|
+
def scheduled_tasks
|
|
771
|
+
@schedule_blocks.flat_map do |block|
|
|
772
|
+
dsl = ScheduleDSL.new
|
|
773
|
+
dsl.instance_eval(&block)
|
|
774
|
+
dsl.tasks
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# @private
|
|
779
|
+
class ScheduleDSL
|
|
780
|
+
attr_reader :tasks
|
|
781
|
+
|
|
782
|
+
def initialize
|
|
783
|
+
@tasks = []
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# Registers a task to run on a fixed interval (in seconds)
|
|
787
|
+
def every(interval, task:)
|
|
788
|
+
unless task.is_a?(Class) && task.include?(Rage::Deferred::Task)
|
|
789
|
+
raise ArgumentError, "#{task} must be a class that includes Rage::Deferred::Task"
|
|
790
|
+
end
|
|
791
|
+
@tasks << { interval:, task: }
|
|
792
|
+
end
|
|
659
793
|
end
|
|
660
794
|
|
|
661
795
|
# Returns the backend instance used by `Rage::Deferred`.
|
|
@@ -937,6 +1071,49 @@ class Rage::Configuration
|
|
|
937
1071
|
attr_accessor :key
|
|
938
1072
|
end
|
|
939
1073
|
|
|
1074
|
+
class Router
|
|
1075
|
+
# @!attribute form_actions
|
|
1076
|
+
# Enable the automatic generation of `new` and `edit` routes via resource helpers.
|
|
1077
|
+
# @return [Boolean]
|
|
1078
|
+
# @example Enable form actions
|
|
1079
|
+
# Rage.configure do
|
|
1080
|
+
# config.router.form_actions = true
|
|
1081
|
+
# end
|
|
1082
|
+
attr_accessor :form_actions
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
# @private
|
|
1086
|
+
class PubSub
|
|
1087
|
+
attr_reader :adapter
|
|
1088
|
+
|
|
1089
|
+
def initialize
|
|
1090
|
+
@adapter = if config.any?
|
|
1091
|
+
case config[:adapter]
|
|
1092
|
+
when "redis"
|
|
1093
|
+
Rage::PubSub::Adapters::Redis.new(adapter_config)
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def config
|
|
1099
|
+
@config ||= begin
|
|
1100
|
+
config_file = Rage.root.join("config/pubsub.yml")
|
|
1101
|
+
config_file = Rage.root.join("config/cable.yml") unless config_file.exist?
|
|
1102
|
+
|
|
1103
|
+
config = if config_file.exist?
|
|
1104
|
+
yaml = ERB.new(config_file.read).result
|
|
1105
|
+
YAML.safe_load(yaml, aliases: true, symbolize_names: true)&.dig(Rage.env.to_sym)
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
config || {}
|
|
1109
|
+
end
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
def adapter_config
|
|
1113
|
+
config.except(:adapter)
|
|
1114
|
+
end
|
|
1115
|
+
end
|
|
1116
|
+
|
|
940
1117
|
# @private
|
|
941
1118
|
class Internal
|
|
942
1119
|
attr_accessor :rails_mode
|
|
@@ -999,7 +1176,32 @@ class Rage::Configuration
|
|
|
999
1176
|
end
|
|
1000
1177
|
|
|
1001
1178
|
Rage::Telemetry.__setup(@telemetry.handlers_map) if @telemetry
|
|
1179
|
+
|
|
1180
|
+
__define_custom_renderers if @renderers
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
# @private
|
|
1184
|
+
class RendererEntry
|
|
1185
|
+
attr_reader :block
|
|
1186
|
+
|
|
1187
|
+
def initialize(block)
|
|
1188
|
+
@block = block
|
|
1189
|
+
@applied = false
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
def applied? = @applied
|
|
1193
|
+
def applied! = (@applied = true)
|
|
1194
|
+
end
|
|
1195
|
+
private_constant :RendererEntry
|
|
1196
|
+
|
|
1197
|
+
def __define_custom_renderers
|
|
1198
|
+
@renderers.each do |name, entry|
|
|
1199
|
+
next if entry.applied?
|
|
1200
|
+
RageController::API.__register_renderer(name, entry.block)
|
|
1201
|
+
entry.applied!
|
|
1202
|
+
end
|
|
1002
1203
|
end
|
|
1204
|
+
private :__define_custom_renderers
|
|
1003
1205
|
end
|
|
1004
1206
|
|
|
1005
1207
|
# @!parse [ruby]
|
data/lib/rage/controller/api.rb
CHANGED
|
@@ -186,29 +186,9 @@ class RageController::API
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
# @private
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
189
|
+
def __register_renderer(name, block)
|
|
190
|
+
prepend(RageController::Renderers) unless ancestors.include?(RageController::Renderers)
|
|
191
|
+
RageController::Renderers.__register(name, block)
|
|
212
192
|
end
|
|
213
193
|
|
|
214
194
|
############
|
|
@@ -234,7 +214,7 @@ class RageController::API
|
|
|
234
214
|
def rescue_from(*klasses, with: nil, &block)
|
|
235
215
|
unless with
|
|
236
216
|
if block_given?
|
|
237
|
-
with = define_dynamic_method(block)
|
|
217
|
+
with = Rage::Internal.define_dynamic_method(self, block)
|
|
238
218
|
else
|
|
239
219
|
raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
|
|
240
220
|
end
|
|
@@ -308,7 +288,7 @@ class RageController::API
|
|
|
308
288
|
# end
|
|
309
289
|
def around_action(action_name = nil, **opts, &block)
|
|
310
290
|
action = prepare_action_params(action_name, **opts, &block)
|
|
311
|
-
action.merge!(around: true, wrapper: define_maybe_yield(action[:name]))
|
|
291
|
+
action.merge!(around: true, wrapper: Rage::Internal.define_maybe_yield(self, action[:name]))
|
|
312
292
|
|
|
313
293
|
if @__before_actions && @__before_actions.frozen?
|
|
314
294
|
@__before_actions = @__before_actions.dup
|
|
@@ -423,7 +403,7 @@ class RageController::API
|
|
|
423
403
|
# used by `before_action` and `after_action`
|
|
424
404
|
def prepare_action_params(action_name = nil, **opts, &block)
|
|
425
405
|
if block_given?
|
|
426
|
-
action_name = define_dynamic_method(block)
|
|
406
|
+
action_name = Rage::Internal.define_dynamic_method(self, block)
|
|
427
407
|
elsif action_name.nil?
|
|
428
408
|
raise ArgumentError, "No handler provided. Pass the `action_name` parameter or provide a block."
|
|
429
409
|
end
|
|
@@ -438,18 +418,21 @@ class RageController::API
|
|
|
438
418
|
unless: _unless
|
|
439
419
|
}
|
|
440
420
|
|
|
441
|
-
action[:if] = define_dynamic_method(action[:if]) if action[:if].is_a?(Proc)
|
|
442
|
-
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)
|
|
443
423
|
|
|
444
424
|
action
|
|
445
425
|
end
|
|
446
426
|
end # class << self
|
|
447
427
|
|
|
428
|
+
DEFAULT_CONTENT_TYPE = "application/json; charset=utf-8"
|
|
429
|
+
private_constant :DEFAULT_CONTENT_TYPE
|
|
430
|
+
|
|
448
431
|
# @private
|
|
449
432
|
def initialize(env, params)
|
|
450
433
|
@__env = env
|
|
451
434
|
@__params = params
|
|
452
|
-
@__status, @__headers, @__body = 204, { "content-type" =>
|
|
435
|
+
@__status, @__headers, @__body = 204, { "content-type" => DEFAULT_CONTENT_TYPE }, []
|
|
453
436
|
@__rendered = false
|
|
454
437
|
end
|
|
455
438
|
|
|
@@ -480,10 +463,10 @@ class RageController::API
|
|
|
480
463
|
@session ||= Rage::Session.new(cookies)
|
|
481
464
|
end
|
|
482
465
|
|
|
483
|
-
# Send a response to the client.
|
|
466
|
+
# Send a response to the client. Keywords corresponding to custom renderers (see {Rage::Configuration#renderer}) will be delegated automatically.
|
|
484
467
|
#
|
|
485
|
-
# @param json [String,
|
|
486
|
-
# @param plain [
|
|
468
|
+
# @param json [String, #to_json] send a json response to the client; objects will be serialized automatically
|
|
469
|
+
# @param plain [#to_s] send a text response to the client
|
|
487
470
|
# @param sse [#each, Proc, #to_json] send an SSE response to the client
|
|
488
471
|
# @param status [Integer, Symbol] set a response status
|
|
489
472
|
# @example Render a JSON object
|
|
@@ -508,7 +491,8 @@ class RageController::API
|
|
|
508
491
|
@__body << if json
|
|
509
492
|
json.is_a?(String) ? json : json.to_json
|
|
510
493
|
else
|
|
511
|
-
@__headers["content-type"]
|
|
494
|
+
ct = @__headers["content-type"]
|
|
495
|
+
@__headers["content-type"] = "text/plain; charset=utf-8" if ct.nil? || ct == DEFAULT_CONTENT_TYPE
|
|
512
496
|
plain.to_s
|
|
513
497
|
end
|
|
514
498
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# This module overloads the `render` method on {RageController::API RageController::API} to enable the usage of custom renderers defined using {Rage::Configuration#renderer}.
|
|
5
|
+
#
|
|
6
|
+
module RageController::Renderers
|
|
7
|
+
# @private
|
|
8
|
+
def self.prepended(_)
|
|
9
|
+
@__renderers = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @private
|
|
13
|
+
# rubocop:disable Layout/IndentationWidth, Layout/EndAlignment, Layout/HeredocIndentation
|
|
14
|
+
def self.__register(name, block)
|
|
15
|
+
@__renderers[name] = Rage::Internal.define_dynamic_method(self, block)
|
|
16
|
+
|
|
17
|
+
render_args = @__renderers.keys.map { |key| "#{key}: nil" }.join(", ")
|
|
18
|
+
|
|
19
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
20
|
+
def render(#{render_args}, status: nil, **)
|
|
21
|
+
raise "Render was called multiple times in this action." if @__rendered
|
|
22
|
+
|
|
23
|
+
active_renderers = []
|
|
24
|
+
#{@__renderers.keys.map { |key| "active_renderers << :#{key} if #{key}" }.join("\n")}
|
|
25
|
+
|
|
26
|
+
return super(status:, **) if active_renderers.empty?
|
|
27
|
+
|
|
28
|
+
if active_renderers.size > 1
|
|
29
|
+
raise Rage::Errors::AmbiguousRenderError, "Only one renderer can be used per 'render' call, but multiple were provided: \#{active_renderers.join(", ")}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result = case active_renderers.first
|
|
33
|
+
#{@__renderers.map do |renderer_name, method_name|
|
|
34
|
+
<<~RUBY
|
|
35
|
+
when :#{renderer_name}
|
|
36
|
+
#{method_name}(#{renderer_name}, **)
|
|
37
|
+
RUBY
|
|
38
|
+
end.join("\n")}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return if @__rendered
|
|
42
|
+
render plain: result.to_s, status: (status || 200)
|
|
43
|
+
end
|
|
44
|
+
RUBY
|
|
45
|
+
end
|
|
46
|
+
# rubocop:enable all
|
|
47
|
+
end
|
|
@@ -44,8 +44,24 @@ class Rage::Deferred::Backends::Disk
|
|
|
44
44
|
@recovered_storages = storage_files[1..] if storage_files.length > 1
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
|
|
47
|
+
# include recovered storages from crashed/previous workers
|
|
48
|
+
all_storages = [@storage, *@recovered_storages].compact
|
|
49
|
+
|
|
50
|
+
# find the highest task timestamp across all storage files
|
|
51
|
+
storage_file_max_timestamp = all_storages.map do |storage|
|
|
52
|
+
max_timestamp = 0
|
|
53
|
+
storage.tap(&:rewind).each_line(chomp: true) do |entry|
|
|
54
|
+
next unless entry[9...12] == "add"
|
|
55
|
+
timestamp = entry[13..].split("-").first.to_i
|
|
56
|
+
max_timestamp = timestamp if timestamp > max_timestamp
|
|
57
|
+
end
|
|
58
|
+
max_timestamp
|
|
59
|
+
end.max.to_i
|
|
60
|
+
|
|
61
|
+
# apply Lamport IR2(b) From time, clocks and the ordering of
|
|
62
|
+
# events in a distributed system to guard against clock skew
|
|
63
|
+
task_id_seed = [Time.now.to_i, storage_file_max_timestamp].max + 1
|
|
64
|
+
|
|
49
65
|
@task_id_base, @task_id_i = "#{task_id_seed}-#{Process.pid}", 0
|
|
50
66
|
Iodine.run_every(1_000) do
|
|
51
67
|
task_id_seed += 1
|
|
@@ -117,7 +133,7 @@ class Rage::Deferred::Backends::Disk
|
|
|
117
133
|
# `@recovered_storages` will only be present if the server has previously crashed and left
|
|
118
134
|
# some storage files behind, or if the new cluster is started with fewer workers than before;
|
|
119
135
|
# TLDR: this code is expected to execute very rarely
|
|
120
|
-
@recovered_storages.each { |storage| recover_tasks(storage) }
|
|
136
|
+
@recovered_storages.each { |storage| recover_tasks(storage.tap(&:rewind)) }
|
|
121
137
|
end
|
|
122
138
|
|
|
123
139
|
tasks = {}
|
|
@@ -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"
|
|
@@ -27,7 +27,15 @@ class Rage::Deferred::Metadata
|
|
|
27
27
|
# @return [Boolean] `true` if a failure will schedule another attempt, `false` otherwise
|
|
28
28
|
def will_retry?
|
|
29
29
|
task = Rage::Deferred::Context.get_task(context)
|
|
30
|
-
task.
|
|
30
|
+
!!task.__next_retry_in(attempts, nil)
|
|
31
|
+
end
|
|
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)
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
private
|
data/lib/rage/deferred/queue.rb
CHANGED
|
@@ -38,14 +38,15 @@ class Rage::Deferred::Queue
|
|
|
38
38
|
Fiber.schedule do
|
|
39
39
|
Iodine.task_inc!
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
result = task.new.__perform(context)
|
|
42
42
|
|
|
43
|
-
if
|
|
43
|
+
if result == true
|
|
44
44
|
@backend.remove(task_id)
|
|
45
45
|
else
|
|
46
46
|
attempts = Rage::Deferred::Context.inc_attempts(context)
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
retry_in = task.__next_retry_in(attempts, result)
|
|
48
|
+
if retry_in
|
|
49
|
+
enqueue(context, delay: retry_in, task_id:)
|
|
49
50
|
else
|
|
50
51
|
@backend.remove(task_id)
|
|
51
52
|
end
|
|
@@ -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
|