rage-rb 1.17.1 → 1.19.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/ARCHITECTURE.md +47 -0
- data/CHANGELOG.md +23 -0
- data/lib/rage/all.rb +1 -0
- data/lib/rage/application.rb +3 -25
- data/lib/rage/cable/cable.rb +2 -3
- data/lib/rage/cli.rb +101 -18
- data/lib/rage/code_loader.rb +8 -0
- data/lib/rage/configuration.rb +525 -195
- data/lib/rage/controller/api.rb +15 -3
- data/lib/rage/deferred/context.rb +48 -0
- data/lib/rage/deferred/deferred.rb +5 -1
- data/lib/rage/deferred/queue.rb +8 -8
- data/lib/rage/deferred/task.rb +29 -23
- data/lib/rage/env.rb +15 -0
- data/lib/rage/events/events.rb +140 -0
- data/lib/rage/events/subscriber.rb +174 -0
- data/lib/rage/fiber.rb +11 -2
- data/lib/rage/fiber_scheduler.rb +2 -2
- data/lib/rage/hooks.rb +1 -0
- data/lib/rage/internal.rb +53 -0
- data/lib/rage/log_processor.rb +117 -0
- data/lib/rage/logger/json_formatter.rb +37 -18
- data/lib/rage/logger/logger.rb +136 -30
- data/lib/rage/logger/text_formatter.rb +21 -2
- data/lib/rage/middleware/fiber_wrapper.rb +8 -0
- data/lib/rage/middleware/reloader.rb +6 -11
- data/lib/rage/request.rb +18 -1
- data/lib/rage/response.rb +1 -1
- data/lib/rage/router/util.rb +8 -0
- data/lib/rage/setup.rb +2 -0
- data/lib/rage/templates/config-environments-production.rb +1 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +51 -0
- data/rage.gemspec +1 -0
- metadata +22 -4
- data/OVERVIEW.md +0 -83
- data/lib/rage/deferred/metadata.rb +0 -43
data/lib/rage/controller/api.rb
CHANGED
|
@@ -9,7 +9,7 @@ class RageController::API
|
|
|
9
9
|
# returns the name of the newly defined method;
|
|
10
10
|
# rubocop:disable Layout/IndentationWidth, Layout/EndAlignment, Layout/HeredocIndentation
|
|
11
11
|
def __register_action(action)
|
|
12
|
-
raise Rage::Errors::RouterError, "The action
|
|
12
|
+
raise Rage::Errors::RouterError, "The action `#{action}` could not be found in the `#{self}` controller. This is likely due to route helpers pointing to non-existent actions in the controller. Please check your routes and ensure that all referenced actions exist." unless method_defined?(action)
|
|
13
13
|
|
|
14
14
|
around_actions_total = 0
|
|
15
15
|
|
|
@@ -153,7 +153,13 @@ class RageController::API
|
|
|
153
153
|
<<~RUBY
|
|
154
154
|
context = {}
|
|
155
155
|
append_info_to_payload(context)
|
|
156
|
-
|
|
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
|
|
157
163
|
RUBY
|
|
158
164
|
end}
|
|
159
165
|
end
|
|
@@ -448,7 +454,7 @@ class RageController::API
|
|
|
448
454
|
# Get the request object. See {Rage::Request}.
|
|
449
455
|
# @return [Rage::Request]
|
|
450
456
|
def request
|
|
451
|
-
@request ||= Rage::Request.new(@__env)
|
|
457
|
+
@request ||= Rage::Request.new(@__env, controller: self)
|
|
452
458
|
end
|
|
453
459
|
|
|
454
460
|
# Get the response object. See {Rage::Response}.
|
|
@@ -635,6 +641,12 @@ class RageController::API
|
|
|
635
641
|
!still_fresh
|
|
636
642
|
end
|
|
637
643
|
|
|
644
|
+
# Get the name of the currently executed action.
|
|
645
|
+
# @return [String] the name of the currently executed action
|
|
646
|
+
def action_name
|
|
647
|
+
@__params[:action]
|
|
648
|
+
end
|
|
649
|
+
|
|
638
650
|
# @private
|
|
639
651
|
# for comatibility with `Rails.application.routes.recognize_path`
|
|
640
652
|
def self.binary_params_for?(_)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Context for deferred tasks.
|
|
5
|
+
# The class encapsulates the context associated with a deferred task, and allows to store it without modifying the task instance.
|
|
6
|
+
#
|
|
7
|
+
class Rage::Deferred::Context
|
|
8
|
+
def self.build(task, args, kwargs, storage: nil)
|
|
9
|
+
logger = Thread.current[:rage_logger]
|
|
10
|
+
|
|
11
|
+
[
|
|
12
|
+
task,
|
|
13
|
+
args.empty? ? nil : args,
|
|
14
|
+
kwargs.empty? ? nil : kwargs,
|
|
15
|
+
nil,
|
|
16
|
+
logger&.dig(:tags),
|
|
17
|
+
logger&.dig(:context)
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.get_task(context)
|
|
22
|
+
context[0]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.get_args(context)
|
|
26
|
+
context[1]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.get_kwargs(context)
|
|
30
|
+
context[2]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.get_attempts(context)
|
|
34
|
+
context[3]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.inc_attempts(context)
|
|
38
|
+
context[3] = context[3].to_i + 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.get_log_tags(context)
|
|
42
|
+
context[4]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.get_log_context(context)
|
|
46
|
+
context[5]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -79,12 +79,16 @@ module Rage::Deferred
|
|
|
79
79
|
|
|
80
80
|
class PushTimeout < StandardError
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
# @private
|
|
84
|
+
class TaskFailed < StandardError
|
|
85
|
+
end
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
require_relative "task"
|
|
85
89
|
require_relative "queue"
|
|
86
90
|
require_relative "proxy"
|
|
87
|
-
require_relative "
|
|
91
|
+
require_relative "context"
|
|
88
92
|
require_relative "backends/disk"
|
|
89
93
|
require_relative "backends/nil"
|
|
90
94
|
|
data/lib/rage/deferred/queue.rb
CHANGED
|
@@ -10,7 +10,7 @@ class Rage::Deferred::Queue
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
# Write the task to the storage and schedule it for execution.
|
|
13
|
-
def enqueue(
|
|
13
|
+
def enqueue(context, delay: nil, delay_until: nil, task_id: nil)
|
|
14
14
|
apply_backpressure if @backpressure
|
|
15
15
|
|
|
16
16
|
publish_in, publish_at = if delay
|
|
@@ -21,14 +21,14 @@ class Rage::Deferred::Queue
|
|
|
21
21
|
[delay_until_i - current_time_i, delay_until_i] if delay_until_i > current_time_i
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
persisted_task_id = @backend.add(
|
|
25
|
-
schedule(persisted_task_id,
|
|
24
|
+
persisted_task_id = @backend.add(context, publish_at:, task_id:)
|
|
25
|
+
schedule(persisted_task_id, context, publish_in:)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Schedule the task for execution.
|
|
29
|
-
def schedule(task_id,
|
|
29
|
+
def schedule(task_id, context, publish_in: nil)
|
|
30
30
|
publish_in_ms = publish_in.to_i * 1_000 if publish_in && publish_in > 0
|
|
31
|
-
task = Rage::Deferred::
|
|
31
|
+
task = Rage::Deferred::Context.get_task(context)
|
|
32
32
|
@backlog_size += 1 unless publish_in_ms
|
|
33
33
|
|
|
34
34
|
Iodine.run_after(publish_in_ms) do
|
|
@@ -38,14 +38,14 @@ class Rage::Deferred::Queue
|
|
|
38
38
|
Fiber.schedule do
|
|
39
39
|
Iodine.task_inc!
|
|
40
40
|
|
|
41
|
-
is_completed = task.new.__perform(
|
|
41
|
+
is_completed = task.new.__perform(context)
|
|
42
42
|
|
|
43
43
|
if is_completed
|
|
44
44
|
@backend.remove(task_id)
|
|
45
45
|
else
|
|
46
|
-
attempts = Rage::Deferred::
|
|
46
|
+
attempts = Rage::Deferred::Context.inc_attempts(context)
|
|
47
47
|
if task.__should_retry?(attempts)
|
|
48
|
-
enqueue(
|
|
48
|
+
enqueue(context, delay: task.__next_retry_in(attempts), task_id:)
|
|
49
49
|
else
|
|
50
50
|
@backend.remove(task_id)
|
|
51
51
|
end
|
data/lib/rage/deferred/task.rb
CHANGED
|
@@ -41,32 +41,36 @@ module Rage::Deferred::Task
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# @private
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
def __perform(context)
|
|
45
|
+
args = Rage::Deferred::Context.get_args(context)
|
|
46
|
+
kwargs = Rage::Deferred::Context.get_kwargs(context)
|
|
47
|
+
attempts = Rage::Deferred::Context.get_attempts(context)
|
|
48
|
+
|
|
49
|
+
restore_log_info(context)
|
|
50
|
+
|
|
51
|
+
task_log_context = { task: self.class.name }
|
|
52
|
+
task_log_context[:attempt] = attempts + 1 if attempts
|
|
53
|
+
|
|
54
|
+
Rage.logger.with_context(task_log_context) do
|
|
55
|
+
perform(*args, **kwargs)
|
|
56
|
+
true
|
|
57
|
+
rescue Rage::Deferred::TaskFailed
|
|
58
|
+
false
|
|
59
|
+
rescue Exception => e
|
|
60
|
+
Rage.logger.error("Deferred task failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
61
|
+
false
|
|
49
62
|
end
|
|
50
63
|
end
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
kwargs = Rage::Deferred::Metadata.get_kwargs(metadata)
|
|
56
|
-
attempts = Rage::Deferred::Metadata.get_attempts(metadata)
|
|
57
|
-
request_id = Rage::Deferred::Metadata.get_request_id(metadata)
|
|
58
|
-
|
|
59
|
-
context = { task: self.class.name }
|
|
60
|
-
context[:attempt] = attempts + 1 if attempts
|
|
65
|
+
private def restore_log_info(context)
|
|
66
|
+
log_tags = Rage::Deferred::Context.get_log_tags(context)
|
|
67
|
+
log_context = Rage::Deferred::Context.get_log_context(context)
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Rage.logger.error("Deferred task failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
68
|
-
false
|
|
69
|
-
end
|
|
69
|
+
if log_tags.is_a?(Array)
|
|
70
|
+
Thread.current[:rage_logger] = { tags: log_tags, context: log_context }
|
|
71
|
+
elsif log_tags
|
|
72
|
+
# support the previous format where only `request_id` was passed
|
|
73
|
+
Thread.current[:rage_logger] = { tags: [log_tags], context: {} }
|
|
70
74
|
end
|
|
71
75
|
end
|
|
72
76
|
|
|
@@ -77,10 +81,12 @@ module Rage::Deferred::Task
|
|
|
77
81
|
module ClassMethods
|
|
78
82
|
def enqueue(*args, delay: nil, delay_until: nil, **kwargs)
|
|
79
83
|
Rage::Deferred.__queue.enqueue(
|
|
80
|
-
Rage::Deferred::
|
|
84
|
+
Rage::Deferred::Context.build(self, args, kwargs),
|
|
81
85
|
delay:,
|
|
82
86
|
delay_until:
|
|
83
87
|
)
|
|
88
|
+
|
|
89
|
+
nil
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
# @private
|
data/lib/rage/env.rb
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
##
|
|
4
|
+
# {Rage::Env Rage::Env} represents the current environment of the Rage application.
|
|
5
|
+
#
|
|
6
|
+
# The class automatically detects the application's current environment by checking the `RAGE_ENV`, `RAILS_ENV`, and `RACK_ENV` environment variables.
|
|
7
|
+
# It provides predicate methods to check which environment is active.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # Start the application with `rage s -e preprod`
|
|
11
|
+
# Rage.env.development? # => false
|
|
12
|
+
# Rage.env.preprod? # => true
|
|
13
|
+
#
|
|
3
14
|
class Rage::Env
|
|
4
15
|
STANDARD_ENVS = %w(development test staging production)
|
|
5
16
|
|
|
@@ -28,6 +39,10 @@ class Rage::Env
|
|
|
28
39
|
@env.to_sym
|
|
29
40
|
end
|
|
30
41
|
|
|
42
|
+
def to_str
|
|
43
|
+
@env
|
|
44
|
+
end
|
|
45
|
+
|
|
31
46
|
def to_s
|
|
32
47
|
@env
|
|
33
48
|
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# `Rage::Events` provides a lightweight event-driven system for publishing and subscribing to events.
|
|
5
|
+
# Define events as data structures, register subscriber classes, and publish events to notify all relevant subscribers.
|
|
6
|
+
# Subscribers can process events and optionally receive additional context with each event.
|
|
7
|
+
#
|
|
8
|
+
# Define an event:
|
|
9
|
+
# ```ruby
|
|
10
|
+
# UserRegistered = Data.define(:user_id)
|
|
11
|
+
# ```
|
|
12
|
+
#
|
|
13
|
+
# Define a subscriber:
|
|
14
|
+
# ```ruby
|
|
15
|
+
# class SendWelcomeEmail
|
|
16
|
+
# include Rage::Events::Subscriber
|
|
17
|
+
# subscribe_to UserRegistered
|
|
18
|
+
#
|
|
19
|
+
# def call(event)
|
|
20
|
+
# puts "Sending welcome email to user #{event.user_id}"
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# Publish an event:
|
|
26
|
+
# ```ruby
|
|
27
|
+
# Rage::Events.publish(UserRegistered.new(user_id: 1))
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
module Rage::Events
|
|
31
|
+
# Publish an event to all subscribers registered for the event's class or its ancestors.
|
|
32
|
+
# Optionally, additional context data can be provided and passed to each subscriber.
|
|
33
|
+
#
|
|
34
|
+
# @param event [Object] the event to publish
|
|
35
|
+
# @param context [Object] additional data to publish along with the event
|
|
36
|
+
# @example Publish an event
|
|
37
|
+
# Rage::Events.publish(MyEvent.new)
|
|
38
|
+
# @example Publish an event with context
|
|
39
|
+
# Rage::Events.publish(MyEvent.new, context: { published_at: Time.now })
|
|
40
|
+
def self.publish(event, context: nil)
|
|
41
|
+
handler = __event_handlers[event.class] || __build_event_handler(event.class)
|
|
42
|
+
handler.call(event, context)
|
|
43
|
+
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @private
|
|
48
|
+
def self.__registered_subscribers
|
|
49
|
+
@__registered_subscribers ||= Hash.new { |hash, key| hash[key] = [] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @private
|
|
53
|
+
def self.__register_subscriber(event_class, handler_class)
|
|
54
|
+
__registered_subscribers[event_class] << handler_class
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @private
|
|
58
|
+
def self.__get_subscribers(event_class)
|
|
59
|
+
event_class.ancestors.take_while { |klass|
|
|
60
|
+
klass != Object && klass != Data
|
|
61
|
+
}.each_with_object([]) { |klass, memo|
|
|
62
|
+
memo.concat(__registered_subscribers[klass]).uniq! if __registered_subscribers.has_key?(klass)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @private
|
|
67
|
+
def self.__event_handlers
|
|
68
|
+
@__event_handlers ||= {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @private
|
|
72
|
+
def self.__build_event_handler(event_class)
|
|
73
|
+
subscriber_calls = __get_subscribers(event_class).map do |subscriber_class|
|
|
74
|
+
subscriber_class.__register_rescue_handlers
|
|
75
|
+
|
|
76
|
+
arguments = "event"
|
|
77
|
+
|
|
78
|
+
context_type, _ = subscriber_class.instance_method(:call).parameters.find do |param_type, param_name|
|
|
79
|
+
param_name == :context || param_type == :keyrest
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if context_type
|
|
83
|
+
if context_type == :keyreq
|
|
84
|
+
arguments += ", context: context || {}"
|
|
85
|
+
else
|
|
86
|
+
arguments += ", context:"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if subscriber_class.__is_deferred
|
|
91
|
+
"#{subscriber_class}.enqueue(#{arguments})"
|
|
92
|
+
else
|
|
93
|
+
"#{subscriber_class}.new.__call(#{arguments})"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if subscriber_calls.empty?
|
|
98
|
+
->(_, _) {}
|
|
99
|
+
else
|
|
100
|
+
__event_handlers[event_class] = eval <<-RUBY
|
|
101
|
+
->(event, context) { #{subscriber_calls.join("; ")} }
|
|
102
|
+
RUBY
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @private
|
|
107
|
+
def self.__reset_subscribers
|
|
108
|
+
__registered_subscribers.clear
|
|
109
|
+
__event_handlers.clear
|
|
110
|
+
|
|
111
|
+
Rage::Events.__eager_load_subscribers
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @private
|
|
115
|
+
def self.__eager_load_subscribers
|
|
116
|
+
subscribers = Dir["#{Rage.root}/app/**/*.rb"].select do |path|
|
|
117
|
+
File.foreach(path).any? do |line|
|
|
118
|
+
line.include?("include Rage::Events::Subscriber") || line.include?("subscribe_to")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
subscribers.each do |path|
|
|
123
|
+
Rage.code_loader.load_file(path)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
rescue => e
|
|
127
|
+
puts "ERROR: Failed to load an event subscriber: #{e.class} (#{e.message})."
|
|
128
|
+
puts e.backtrace.join("\n")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
require_relative "subscriber"
|
|
133
|
+
|
|
134
|
+
if Rage.env.development?
|
|
135
|
+
if Rage.config.internal.initialized?
|
|
136
|
+
Rage::Events.__eager_load_subscribers
|
|
137
|
+
else
|
|
138
|
+
Rage.config.after_initialize { Rage::Events.__eager_load_subscribers }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Include this module in a class to make it an event subscriber.
|
|
5
|
+
#
|
|
6
|
+
# Example:
|
|
7
|
+
#
|
|
8
|
+
# ```ruby
|
|
9
|
+
# # Define an event class
|
|
10
|
+
# MyEvent = Data.define
|
|
11
|
+
#
|
|
12
|
+
# # Define the subscriber class
|
|
13
|
+
# class MySubscriber
|
|
14
|
+
# include Rage::Events::Subscriber
|
|
15
|
+
# subscribe_to MyEvent
|
|
16
|
+
#
|
|
17
|
+
# def call(event)
|
|
18
|
+
# puts "Received event: #{event.inspect}"
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # Publish an event
|
|
23
|
+
# Rage::Events.publish(MyEvent.new)
|
|
24
|
+
# ```
|
|
25
|
+
#
|
|
26
|
+
# When an event matching the specified class is published, the `call` method will be invoked with the event instance.
|
|
27
|
+
#
|
|
28
|
+
# You can also subscribe to multiple event classes:
|
|
29
|
+
#
|
|
30
|
+
# ```ruby
|
|
31
|
+
# class MySubscriber
|
|
32
|
+
# include Rage::Events::Subscriber
|
|
33
|
+
# subscribe_to EventA, EventB
|
|
34
|
+
#
|
|
35
|
+
# def call(event)
|
|
36
|
+
# puts "Received event: #{event.inspect}"
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
# ```
|
|
40
|
+
#
|
|
41
|
+
# Subscribers are executed synchronously by default. You can make a subscriber asynchronous by passing the `deferred` option:
|
|
42
|
+
#
|
|
43
|
+
# ```ruby
|
|
44
|
+
# class MySubscriber
|
|
45
|
+
# include Rage::Events::Subscriber
|
|
46
|
+
# subscribe_to MyEvent, deferred: true
|
|
47
|
+
#
|
|
48
|
+
# def call(event)
|
|
49
|
+
# puts "Received event in background: #{event.inspect}"
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
# ```
|
|
53
|
+
#
|
|
54
|
+
# Such subscriber will be executed in the background using Rage's deferred task system.
|
|
55
|
+
#
|
|
56
|
+
# You can also define custom error handling for exceptions raised during event processing using `rescue_from`:
|
|
57
|
+
#
|
|
58
|
+
# ```ruby
|
|
59
|
+
# class MySubscriber
|
|
60
|
+
# include Rage::Events::Subscriber
|
|
61
|
+
# subscribe_to MyEvent
|
|
62
|
+
#
|
|
63
|
+
# rescue_from StandardError do |exception|
|
|
64
|
+
# puts "An error occurred: #{exception.message}"
|
|
65
|
+
# end
|
|
66
|
+
# end
|
|
67
|
+
# ```
|
|
68
|
+
#
|
|
69
|
+
# @see ClassMethods
|
|
70
|
+
#
|
|
71
|
+
module Rage::Events::Subscriber
|
|
72
|
+
def self.included(handler_class)
|
|
73
|
+
handler_class.extend ClassMethods
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @private
|
|
77
|
+
def call(_)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @private
|
|
81
|
+
def __call(event, context: nil)
|
|
82
|
+
Rage.logger.with_context(self.class.__log_context) do
|
|
83
|
+
context.nil? ? call(event) : call(event, context: context.freeze)
|
|
84
|
+
rescue Exception => _e
|
|
85
|
+
e = self.class.__rescue_handlers ? __run_rescue_handlers(_e) : _e
|
|
86
|
+
|
|
87
|
+
if e
|
|
88
|
+
Rage.logger.error("Subscriber failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
89
|
+
raise Rage::Deferred::TaskFailed if self.class.__is_deferred
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
module ClassMethods
|
|
95
|
+
# @private
|
|
96
|
+
attr_accessor :__event_classes, :__is_deferred, :__log_context, :__rescue_handlers
|
|
97
|
+
|
|
98
|
+
# Subscribe the class to one or more events.
|
|
99
|
+
#
|
|
100
|
+
# @param event_classes [Class, Array<Class>] one or more event classes to subscribe to
|
|
101
|
+
# @param deferred [Boolean] whether to process events asynchronously
|
|
102
|
+
def subscribe_to(*event_classes, deferred: false)
|
|
103
|
+
@__event_classes = (@__event_classes || []) | event_classes
|
|
104
|
+
@__is_deferred = !!deferred
|
|
105
|
+
@__log_context = { subscriber: name }.freeze
|
|
106
|
+
|
|
107
|
+
@__event_classes.each do |event_class|
|
|
108
|
+
Rage::Events.__register_subscriber(event_class, self)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if @__is_deferred
|
|
112
|
+
include Rage::Deferred::Task
|
|
113
|
+
alias_method :perform, :__call
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Define exception handlers for the subscriber.
|
|
118
|
+
#
|
|
119
|
+
# @param klasses [Class, Array<Class>] one or more exception classes to handle
|
|
120
|
+
# @param with [Symbol, String] the method name to call when an exception is raised
|
|
121
|
+
# @yield [exception] optional block to handle the exception
|
|
122
|
+
# @note If you do not re-raise exceptions in deferred subscribers, the subscriber will be marked as successful and Rage will not attempt to retry it.
|
|
123
|
+
def rescue_from(*klasses, with: nil, &block)
|
|
124
|
+
unless with
|
|
125
|
+
if block_given?
|
|
126
|
+
with = Rage::Internal.define_dynamic_method(self, block)
|
|
127
|
+
else
|
|
128
|
+
raise ArgumentError, "No handler provided. Pass the `with` keyword argument or provide a block."
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if @__rescue_handlers.nil?
|
|
133
|
+
@__rescue_handlers = []
|
|
134
|
+
elsif @__rescue_handlers.frozen?
|
|
135
|
+
@__rescue_handlers = @__rescue_handlers.dup
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@__rescue_handlers.unshift([klasses, with])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @private
|
|
142
|
+
def inherited(klass)
|
|
143
|
+
klass.__rescue_handlers = @__rescue_handlers.freeze
|
|
144
|
+
klass.subscribe_to(*@__event_classes, deferred: @__is_deferred) if @__event_classes
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @private
|
|
148
|
+
def __register_rescue_handlers
|
|
149
|
+
return if method_defined?(:__run_rescue_handlers, false) || @__rescue_handlers.nil?
|
|
150
|
+
|
|
151
|
+
matcher_calls = @__rescue_handlers.map do |klasses, handler|
|
|
152
|
+
handler_call = instance_method(handler).arity == 0 ? handler : "#{handler}(exception)"
|
|
153
|
+
|
|
154
|
+
<<~RUBY
|
|
155
|
+
when #{klasses.join(", ")}
|
|
156
|
+
#{handler_call}
|
|
157
|
+
nil
|
|
158
|
+
RUBY
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
162
|
+
def __run_rescue_handlers(exception)
|
|
163
|
+
case exception
|
|
164
|
+
#{matcher_calls.join("\n")}
|
|
165
|
+
else
|
|
166
|
+
exception
|
|
167
|
+
end
|
|
168
|
+
rescue Exception => e
|
|
169
|
+
e
|
|
170
|
+
end
|
|
171
|
+
RUBY
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/rage/fiber.rb
CHANGED
|
@@ -118,6 +118,14 @@ class Fiber
|
|
|
118
118
|
"block:#{object_id}:#{@__block_channel_i}"
|
|
119
119
|
end
|
|
120
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
|
+
|
|
121
129
|
# @private
|
|
122
130
|
attr_accessor :__awaited_fileno
|
|
123
131
|
|
|
@@ -148,6 +156,7 @@ class Fiber
|
|
|
148
156
|
# @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.
|
|
149
157
|
def self.await(fibers)
|
|
150
158
|
f, fibers = Fiber.current, Array(fibers)
|
|
159
|
+
await_channel = f.__await_channel(true)
|
|
151
160
|
|
|
152
161
|
# check which fibers are alive (i.e. have yielded) and which have errored out
|
|
153
162
|
i, err, num_wait_for = 0, nil, 0
|
|
@@ -169,7 +178,7 @@ class Fiber
|
|
|
169
178
|
end
|
|
170
179
|
|
|
171
180
|
# wait on async fibers; resume right away if one of the fibers errors out
|
|
172
|
-
Iodine.subscribe(
|
|
181
|
+
Iodine.subscribe(await_channel) do |_, err|
|
|
173
182
|
if err == AWAIT_ERROR_MESSAGE
|
|
174
183
|
f.resume
|
|
175
184
|
else
|
|
@@ -179,7 +188,7 @@ class Fiber
|
|
|
179
188
|
end
|
|
180
189
|
|
|
181
190
|
Fiber.defer(-1)
|
|
182
|
-
Iodine.defer { Iodine.unsubscribe(
|
|
191
|
+
Iodine.defer { Iodine.unsubscribe(await_channel) }
|
|
183
192
|
|
|
184
193
|
# if num_wait_for is not 0 means we exited prematurely because of an error
|
|
185
194
|
if num_wait_for > 0
|
data/lib/rage/fiber_scheduler.rb
CHANGED
|
@@ -130,10 +130,10 @@ class Rage::FiberScheduler
|
|
|
130
130
|
Thread.current[:rage_logger] = logger
|
|
131
131
|
Fiber.current.__set_result(block.call)
|
|
132
132
|
# send a message for `Fiber.await` to work
|
|
133
|
-
Iodine.publish(
|
|
133
|
+
Iodine.publish(parent.__await_channel, "", Iodine::PubSub::PROCESS) if parent.alive?
|
|
134
134
|
rescue Exception => e
|
|
135
135
|
Fiber.current.__set_err(e)
|
|
136
|
-
Iodine.publish(
|
|
136
|
+
Iodine.publish(parent.__await_channel, Fiber::AWAIT_ERROR_MESSAGE, Iodine::PubSub::PROCESS) if parent.alive?
|
|
137
137
|
end
|
|
138
138
|
end
|
|
139
139
|
|
data/lib/rage/hooks.rb
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @private
|
|
4
|
+
class Rage::Internal
|
|
5
|
+
class << self
|
|
6
|
+
# Define a method based on a block.
|
|
7
|
+
# @param klass [Class] the class to define the method in
|
|
8
|
+
# @param block [Proc] the implementation of the new method
|
|
9
|
+
# @return [Symbol] the name of the newly defined method
|
|
10
|
+
def define_dynamic_method(klass, block)
|
|
11
|
+
name = dynamic_name_seed.next.join
|
|
12
|
+
klass.define_method("__rage_dynamic_#{name}", block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Define a method that will call a specified method if a condition is `true` or yield if `false`.
|
|
16
|
+
# @param klass [Class] the class to define the method in
|
|
17
|
+
# @param method_name [Symbol] the method to call if the condition is `true`
|
|
18
|
+
# @return [Symbol] the name of the newly defined method
|
|
19
|
+
def define_maybe_yield(klass, method_name)
|
|
20
|
+
name = dynamic_name_seed.next.join
|
|
21
|
+
|
|
22
|
+
klass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
23
|
+
def __rage_dynamic_#{name}(condition)
|
|
24
|
+
if condition
|
|
25
|
+
#{method_name} { yield }
|
|
26
|
+
else
|
|
27
|
+
yield
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
RUBY
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Build a string representation of keyword arguments based on the parameters expected by the method.
|
|
34
|
+
# @param method [Method, Proc] the method to build arguments for
|
|
35
|
+
# @param arguments [Hash] the arguments to include in the string representation
|
|
36
|
+
# @return [String] the string representation of the method arguments
|
|
37
|
+
def build_arguments(method, arguments)
|
|
38
|
+
expected_parameters = method.parameters
|
|
39
|
+
|
|
40
|
+
arguments.filter_map { |arg_name, arg_value|
|
|
41
|
+
if expected_parameters.any? { |param_type, param_name| param_name == arg_name || param_type == :keyrest }
|
|
42
|
+
"#{arg_name}: #{arg_value}"
|
|
43
|
+
end
|
|
44
|
+
}.join(", ")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def dynamic_name_seed
|
|
50
|
+
@dynamic_name_seed ||= ("a".."j").to_a.permutation
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|