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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/CONTRIBUTING.md +240 -0
  4. data/README.md +2 -1
  5. data/lib/rage/all.rb +1 -0
  6. data/lib/rage/application.rb +1 -0
  7. data/lib/rage/cable/cable.rb +20 -15
  8. data/lib/rage/cable/channel.rb +2 -1
  9. data/lib/rage/configuration.rb +229 -27
  10. data/lib/rage/controller/api.rb +17 -33
  11. data/lib/rage/controller/renderers.rb +47 -0
  12. data/lib/rage/deferred/backends/disk.rb +19 -3
  13. data/lib/rage/deferred/deferred.rb +7 -0
  14. data/lib/rage/deferred/metadata.rb +9 -1
  15. data/lib/rage/deferred/queue.rb +5 -4
  16. data/lib/rage/deferred/scheduler.rb +25 -0
  17. data/lib/rage/deferred/task.rb +90 -9
  18. data/lib/rage/errors.rb +86 -0
  19. data/lib/rage/events/subscriber.rb +6 -1
  20. data/lib/rage/internal.rb +45 -0
  21. data/lib/rage/logger/logger.rb +1 -1
  22. data/lib/rage/middleware/fiber_wrapper.rb +1 -0
  23. data/lib/rage/openapi/builder.rb +1 -1
  24. data/lib/rage/openapi/converter.rb +48 -3
  25. data/lib/rage/openapi/nodes/method.rb +2 -1
  26. data/lib/rage/openapi/nodes/root.rb +2 -1
  27. data/lib/rage/openapi/openapi.rb +12 -1
  28. data/lib/rage/openapi/parser.rb +73 -2
  29. data/lib/rage/openapi/parsers/ext/alba.rb +35 -6
  30. data/lib/rage/openapi/parsers/request.rb +2 -2
  31. data/lib/rage/openapi/parsers/response.rb +2 -2
  32. data/lib/rage/openapi/parsers/yaml.rb +27 -5
  33. data/lib/rage/params_parser.rb +2 -2
  34. data/lib/rage/{cable → pubsub}/adapters/redis.rb +43 -23
  35. data/lib/rage/pubsub/pubsub.rb +25 -0
  36. data/lib/rage/rails.rb +16 -0
  37. data/lib/rage/router/README.md +1 -1
  38. data/lib/rage/router/dsl.rb +72 -10
  39. data/lib/rage/sse/application.rb +31 -2
  40. data/lib/rage/sse/sse.rb +96 -0
  41. data/lib/rage/sse/stream.rb +78 -0
  42. data/lib/rage/telemetry/spans/broadcast_sse_stream.rb +50 -0
  43. data/lib/rage/telemetry/spans/process_sse_stream.rb +1 -0
  44. data/lib/rage/telemetry/telemetry.rb +2 -1
  45. data/lib/rage/telemetry/tracer.rb +1 -0
  46. data/lib/rage/uploaded_file.rb +3 -7
  47. data/lib/rage/version.rb +1 -1
  48. data/lib/rage-rb.rb +8 -1
  49. metadata +9 -4
  50. data/lib/rage/cable/adapters/base.rb +0 -16
@@ -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]
@@ -186,29 +186,9 @@ class RageController::API
186
186
  end
187
187
 
188
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
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" => "application/json; charset=utf-8" }, []
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, Object] send a json response to the client; objects like arrays will be serialized automatically
486
- # @param plain [String] send a text response to the client
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"] = "text/plain; charset=utf-8"
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
- # create seed value for the task IDs
48
- task_id_seed = Time.now.to_i # TODO: ensure timestamps in the file are not higher
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.__should_retry?(attempts)
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
@@ -38,14 +38,15 @@ class Rage::Deferred::Queue
38
38
  Fiber.schedule do
39
39
  Iodine.task_inc!
40
40
 
41
- is_completed = task.new.__perform(context)
41
+ result = task.new.__perform(context)
42
42
 
43
- if is_completed
43
+ if result == true
44
44
  @backend.remove(task_id)
45
45
  else
46
46
  attempts = Rage::Deferred::Context.inc_attempts(context)
47
- if task.__should_retry?(attempts)
48
- enqueue(context, delay: task.__next_retry_in(attempts), task_id:)
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