rage-rb 1.25.1 → 1.26.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/Appraisals +0 -4
- data/CHANGELOG.md +26 -0
- data/Gemfile +1 -0
- data/Rakefile +4 -1
- data/lib/rage/configuration.rb +75 -1
- data/lib/rage/controller/api.rb +2 -13
- data/lib/rage/cookies.rb +2 -2
- data/lib/rage/daemon.rb +240 -0
- data/lib/rage/deferred/backends/disk.rb +8 -4
- data/lib/rage/ext/active_record/connection_pool.rb +133 -22
- data/lib/rage/extension.rb +27 -0
- data/lib/rage/fiber_scheduler.rb +5 -0
- data/lib/rage/openapi/parsers/ext/blueprinter.rb +51 -74
- data/lib/rage/request.rb +25 -9
- data/lib/rage/router/constrainer.rb +1 -1
- data/lib/rage/setup.rb +1 -0
- data/lib/rage/templates/db-templates/mysql/config-database.yml +1 -1
- data/lib/rage/templates/db-templates/postgresql/config-database.yml +1 -1
- data/lib/rage/templates/db-templates/sqlite3/config-database.yml +1 -1
- data/lib/rage/templates/db-templates/trilogy/config-database.yml +1 -1
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +8 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ad60525da974b198db62787784804444e9ad8cb768119a2eeea7f45aefaa93f
|
|
4
|
+
data.tar.gz: 8bac8c6ac175b486e61bf64a1ca574e590e14bcbacbec0e0da6de59d905de3b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bd4700b5265c58df651b026c546a2cbe452577bd5f225cdbdfd4f0334d857c024d3d22743fed4db4d98be3eb71ad30e656106ecae80c55cbbc4f0883f0ae619
|
|
7
|
+
data.tar.gz: 3033c718ad816739c81ff79fa825f662943ea02c9aeade6cd6c5bfb68be6800e9fa79c06d57c3430f623db9f0b41e015e921711bcdf407014e4c0fb62c96ba20
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.26.0] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- [OpenAPI] Add support for blueprint inheritance. Child blueprints now inherit fields from parent blueprints.
|
|
8
|
+
- [OpenAPI] Refactor `Rage::OpenAPI::Parsers::Ext::Blueprinter` using live reflection instead of Prism AST traversal to simplify schema extraction
|
|
9
|
+
- Connection pool improvements (#301).
|
|
10
|
+
- [OpenAPI] Add Blueprinter association parsing with cycle detection (#291)
|
|
11
|
+
- [OpenAPI] Add support for the `view:` option in Blueprinter parser.
|
|
12
|
+
- Add `Rage::Daemon` (#339).
|
|
13
|
+
- Enable non-blocking process monitoring (#341).
|
|
14
|
+
- Allow to suppress FiberWrapper warnings (#347).
|
|
15
|
+
- Add the Extension API (#344).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- [CI] Check all changed PR file pages when requiring changelog updates.
|
|
20
|
+
- [API] Ignore `If-Modified-Since` when `If-None-Match` is present.
|
|
21
|
+
- [API] Use weak comparison for `If-None-Match` validation.
|
|
22
|
+
- [Deferred] Ignore missing temp files during async disk storage cleanup.
|
|
23
|
+
- [Request] Treat IPv6 literals as non-domain hosts.
|
|
24
|
+
- [Router] Fall back to `SERVER_NAME` when deriving exact host constraints.
|
|
25
|
+
- [Cookies] Use request host fallback when resolving cookie domains.
|
|
26
|
+
- [Cookies] Match array-based cookie domains case-insensitively.
|
|
27
|
+
- [API] Reject invalid status symbols in `head` and `render`.
|
|
28
|
+
|
|
3
29
|
## [1.25.1] - 2026-06-08
|
|
4
30
|
|
|
5
31
|
### Fixed
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
|
@@ -17,6 +17,9 @@ task :appraise do |_, args|
|
|
|
17
17
|
puts ">> Appraising #{ext_version}"
|
|
18
18
|
|
|
19
19
|
gem_name = ext_version.sub(/_\d+(_\d+)*$/, "")
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
Pathname.new("spec/ext").join(gem_name).glob("*_spec.rb").each do |spec|
|
|
22
|
+
system("bundle exec appraisal #{ext_version} rspec #{spec}", exception: true)
|
|
23
|
+
end
|
|
21
24
|
end
|
|
22
25
|
end
|
data/lib/rage/configuration.rb
CHANGED
|
@@ -289,6 +289,14 @@ class Rage::Configuration
|
|
|
289
289
|
end
|
|
290
290
|
# @!endgroup
|
|
291
291
|
|
|
292
|
+
# @!group Daemons
|
|
293
|
+
# Allows configuring daemon settings.
|
|
294
|
+
# @return [Rage::Configuration::Daemons]
|
|
295
|
+
def daemons
|
|
296
|
+
@daemons ||= Daemons.new
|
|
297
|
+
end
|
|
298
|
+
# @!endgroup
|
|
299
|
+
|
|
292
300
|
# @private
|
|
293
301
|
def pubsub
|
|
294
302
|
@pubsub ||= PubSub.new
|
|
@@ -630,12 +638,36 @@ class Rage::Configuration
|
|
|
630
638
|
def initialize
|
|
631
639
|
super
|
|
632
640
|
@objects = [[Rage::FiberWrapper]]
|
|
641
|
+
@suppress_fiber_wrapper_warning = false
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Suppress warnings when inserting middleware before {Rage::FiberWrapper}.
|
|
645
|
+
#
|
|
646
|
+
# By default, Rage warns when middleware is inserted before {Rage::FiberWrapper} because such middleware
|
|
647
|
+
# runs outside the fiber-based request context. This means the middleware won't benefit from Rage's
|
|
648
|
+
# non-blocking I/O scheduling and must handle the custom response format.
|
|
649
|
+
#
|
|
650
|
+
# Use this method when you intentionally want middleware to run outside the request fiber, for example,
|
|
651
|
+
# to serve static assets without the overhead of creating a fiber for each request.
|
|
652
|
+
#
|
|
653
|
+
# @yield The block within which middleware insertion warnings are suppressed
|
|
654
|
+
# @example Suppress warnings when inserting a static file server
|
|
655
|
+
# Rage.configure do
|
|
656
|
+
# config.middleware.allow_outside_request_fiber! do
|
|
657
|
+
# config.middleware.insert_before 0, MyStaticFileServer
|
|
658
|
+
# end
|
|
659
|
+
# end
|
|
660
|
+
def allow_outside_request_fiber!
|
|
661
|
+
@suppress_fiber_wrapper_warning = true
|
|
662
|
+
yield
|
|
663
|
+
ensure
|
|
664
|
+
@suppress_fiber_wrapper_warning = false
|
|
633
665
|
end
|
|
634
666
|
|
|
635
667
|
private
|
|
636
668
|
|
|
637
669
|
def validate!(index, middleware)
|
|
638
|
-
if index == 0 && @objects[0][0] == Rage::FiberWrapper
|
|
670
|
+
if index == 0 && @objects[0][0] == Rage::FiberWrapper && !@suppress_fiber_wrapper_warning
|
|
639
671
|
puts "WARNING: inserting the `#{middleware}` middleware before `Rage::FiberWrapper` may cause undefined behavior."
|
|
640
672
|
end
|
|
641
673
|
end
|
|
@@ -1115,6 +1147,48 @@ class Rage::Configuration
|
|
|
1115
1147
|
end
|
|
1116
1148
|
end
|
|
1117
1149
|
|
|
1150
|
+
class Daemons
|
|
1151
|
+
# @private
|
|
1152
|
+
def initialize
|
|
1153
|
+
@klasses = []
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# @private
|
|
1157
|
+
attr_reader :klasses
|
|
1158
|
+
|
|
1159
|
+
# Register a new daemon.
|
|
1160
|
+
# @param daemon [Class]
|
|
1161
|
+
# @example
|
|
1162
|
+
# Rage.configure do
|
|
1163
|
+
# config.daemons << FileWatcher
|
|
1164
|
+
# end
|
|
1165
|
+
def <<(daemon)
|
|
1166
|
+
validate!(daemon)
|
|
1167
|
+
@klasses << daemon
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
alias_method :push, :<<
|
|
1171
|
+
|
|
1172
|
+
# Remove a registered daemon.
|
|
1173
|
+
# @param daemon [Class]
|
|
1174
|
+
# @example
|
|
1175
|
+
# Rage.configure do
|
|
1176
|
+
# config.daemons.delete(FileWatcher)
|
|
1177
|
+
# end
|
|
1178
|
+
def delete(daemon)
|
|
1179
|
+
validate!(daemon)
|
|
1180
|
+
@klasses.delete(daemon)
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
private
|
|
1184
|
+
|
|
1185
|
+
def validate!(klass)
|
|
1186
|
+
if !klass.is_a?(Class) || !klass.ancestors.include?(Rage::Daemon)
|
|
1187
|
+
raise ArgumentError, "Cannot add `#{klass}` as a daemon; should inherit `Rage::Daemon`"
|
|
1188
|
+
end
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1118
1192
|
# @private
|
|
1119
1193
|
class PubSub
|
|
1120
1194
|
attr_reader :adapter
|
data/lib/rage/controller/api.rb
CHANGED
|
@@ -499,13 +499,7 @@ class RageController::API
|
|
|
499
499
|
@__status = 200
|
|
500
500
|
end
|
|
501
501
|
|
|
502
|
-
if status
|
|
503
|
-
@__status = if status.is_a?(Symbol)
|
|
504
|
-
::Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
|
|
505
|
-
else
|
|
506
|
-
status
|
|
507
|
-
end
|
|
508
|
-
end
|
|
502
|
+
@__status = ::Rack::Utils.status_code(status) if status
|
|
509
503
|
|
|
510
504
|
if sse
|
|
511
505
|
raise ArgumentError, "Cannot render both a standard body and an SSE stream." unless @__body.empty?
|
|
@@ -531,12 +525,7 @@ class RageController::API
|
|
|
531
525
|
# head 429
|
|
532
526
|
def head(status)
|
|
533
527
|
@__rendered = true
|
|
534
|
-
|
|
535
|
-
@__status = if status.is_a?(Symbol)
|
|
536
|
-
::Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
|
|
537
|
-
else
|
|
538
|
-
status
|
|
539
|
-
end
|
|
528
|
+
@__status = ::Rack::Utils.status_code(status)
|
|
540
529
|
end
|
|
541
530
|
|
|
542
531
|
# Set response headers.
|
data/lib/rage/cookies.rb
CHANGED
|
@@ -201,14 +201,14 @@ class Rage::Cookies
|
|
|
201
201
|
end
|
|
202
202
|
|
|
203
203
|
if (domain = value[:domain])
|
|
204
|
-
host = @env
|
|
204
|
+
host = Rack::Request.new(@env).host
|
|
205
205
|
|
|
206
206
|
processed_domain = if domain.is_a?(String)
|
|
207
207
|
domain
|
|
208
208
|
elsif domain == :all
|
|
209
209
|
DomainName(host).domain
|
|
210
210
|
elsif domain.is_a?(Array)
|
|
211
|
-
|
|
211
|
+
domain.find { |candidate| candidate.casecmp?(host) } if host
|
|
212
212
|
end
|
|
213
213
|
end
|
|
214
214
|
|
data/lib/rage/daemon.rb
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# `Rage::Daemon` is an abstraction for running long-lived background processes alongside your application.
|
|
5
|
+
# Use daemons for tasks that need to run continuously, such as listening to message queues, consuming streaming APIs,
|
|
6
|
+
# or maintaining persistent connections to external services.
|
|
7
|
+
#
|
|
8
|
+
# The framework automatically manages daemon lifecycle:
|
|
9
|
+
# - Daemons are started when the server boots
|
|
10
|
+
# - If a daemon exits or crashes, Rage restarts it with exponential backoff
|
|
11
|
+
# - If a worker process dies, Rage restarts the daemon in another worker
|
|
12
|
+
# - On server shutdown, daemons are gracefully stopped
|
|
13
|
+
#
|
|
14
|
+
# ## Defining a Daemon
|
|
15
|
+
#
|
|
16
|
+
# Create a daemon by subclassing `Rage::Daemon` and implementing the {#perform} method:
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# class RedisListener < Rage::Daemon
|
|
20
|
+
# def perform
|
|
21
|
+
# redis = Redis.new
|
|
22
|
+
#
|
|
23
|
+
# redis.subscribe("notifications") do |on|
|
|
24
|
+
# on.message do |channel, message|
|
|
25
|
+
# Rage::Cable.broadcast("notifications", message)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
# ```
|
|
31
|
+
#
|
|
32
|
+
# ## Registering Daemons
|
|
33
|
+
#
|
|
34
|
+
# Register daemons in your application configuration:
|
|
35
|
+
#
|
|
36
|
+
# ```ruby
|
|
37
|
+
# # config/application.rb
|
|
38
|
+
# Rage.configure do
|
|
39
|
+
# config.daemons << RedisListener
|
|
40
|
+
# end
|
|
41
|
+
# ```
|
|
42
|
+
#
|
|
43
|
+
# ## Daemon Scope
|
|
44
|
+
#
|
|
45
|
+
# By default, daemons run in exactly one worker process per server. Use {scope scope} to change this behavior:
|
|
46
|
+
#
|
|
47
|
+
# ```ruby
|
|
48
|
+
# class MetricsCollector < Rage::Daemon
|
|
49
|
+
# scope :worker
|
|
50
|
+
#
|
|
51
|
+
# def perform
|
|
52
|
+
# # runs in every worker process
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
# ```
|
|
56
|
+
#
|
|
57
|
+
# ## Stopping a Daemon
|
|
58
|
+
#
|
|
59
|
+
# Return {Stop} from {#perform} to explicitly stop a daemon without triggering a restart:
|
|
60
|
+
#
|
|
61
|
+
# ```ruby
|
|
62
|
+
# class BatchProcessor < Rage::Daemon
|
|
63
|
+
# def perform
|
|
64
|
+
# while (batch = Batch.next_pending)
|
|
65
|
+
# process(batch)
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# Stop # all batches processed, stop the daemon
|
|
69
|
+
# end
|
|
70
|
+
# end
|
|
71
|
+
# ```
|
|
72
|
+
#
|
|
73
|
+
# ## Cleanup
|
|
74
|
+
#
|
|
75
|
+
# Implement the `cleanup` method to release resources when a daemon stops or restarts:
|
|
76
|
+
#
|
|
77
|
+
# ```ruby
|
|
78
|
+
# class StreamConsumer < Rage::Daemon
|
|
79
|
+
# def perform
|
|
80
|
+
# @connection = StreamingAPI.connect
|
|
81
|
+
# @connection.each { |event| process(event) }
|
|
82
|
+
# end
|
|
83
|
+
#
|
|
84
|
+
# def cleanup
|
|
85
|
+
# @connection&.close
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
# ```
|
|
89
|
+
#
|
|
90
|
+
# The `cleanup` method is called:
|
|
91
|
+
# - When {#perform} returns normally
|
|
92
|
+
# - When {#perform} raises an exception (before restart)
|
|
93
|
+
# - When the server is shutting down
|
|
94
|
+
#
|
|
95
|
+
class Rage::Daemon
|
|
96
|
+
# A sentinel value that can be returned from {#perform} to explicitly stop the daemon.
|
|
97
|
+
# When returned, the daemon will not be restarted.
|
|
98
|
+
# @example
|
|
99
|
+
# def perform
|
|
100
|
+
# return Stop if shutdown_requested?
|
|
101
|
+
# # ...
|
|
102
|
+
# end
|
|
103
|
+
Stop = Object.new
|
|
104
|
+
|
|
105
|
+
INITIAL_BACKOFF = 0.1
|
|
106
|
+
private_constant :INITIAL_BACKOFF
|
|
107
|
+
|
|
108
|
+
MAX_BACKOFF = 30
|
|
109
|
+
private_constant :MAX_BACKOFF
|
|
110
|
+
|
|
111
|
+
BACKOFF_RESET_INTERVAL = 30
|
|
112
|
+
private_constant :BACKOFF_RESET_INTERVAL
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
# @private
|
|
116
|
+
attr_accessor :__level
|
|
117
|
+
|
|
118
|
+
# Configures the scope at which the daemon runs.
|
|
119
|
+
#
|
|
120
|
+
# By default, daemons run in one worker process per server (`:node` scope).
|
|
121
|
+
# Use this method to change the scope when a different behavior is needed.
|
|
122
|
+
#
|
|
123
|
+
# @param level [Symbol] the scope level
|
|
124
|
+
# @option level :node One instance per server (default). Rage uses IPC to coordinate
|
|
125
|
+
# which worker runs the daemon. If that worker dies, the daemon restarts in another worker.
|
|
126
|
+
# @option level :worker One instance per worker process. Use this when the daemon performs
|
|
127
|
+
# work that needs to run in parallel across workers.
|
|
128
|
+
#
|
|
129
|
+
# @example Running in every worker
|
|
130
|
+
# class MetricsCollector < Rage::Daemon
|
|
131
|
+
# scope :worker
|
|
132
|
+
#
|
|
133
|
+
# def perform
|
|
134
|
+
# # runs in every worker process
|
|
135
|
+
# end
|
|
136
|
+
# end
|
|
137
|
+
def scope(level)
|
|
138
|
+
unless level == :worker || level == :node
|
|
139
|
+
raise ArgumentError, "Invalid scope level: #{level.inspect}. Valid options are :node and :worker."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
@__level = level
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @private
|
|
146
|
+
def inherited(klass)
|
|
147
|
+
klass.__level = __level
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @private
|
|
151
|
+
def __perform
|
|
152
|
+
if __level.nil? || __level == :node
|
|
153
|
+
Rage::Internal.pick_a_worker(purpose: "daemon-#{name}") { __execute }
|
|
154
|
+
else
|
|
155
|
+
__execute
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def __execute
|
|
162
|
+
Iodine.on_state(:start_shutdown) do
|
|
163
|
+
@__stopping = true
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
Fiber.schedule do
|
|
167
|
+
backoff = INITIAL_BACKOFF
|
|
168
|
+
|
|
169
|
+
Rage.logger.with_context(daemon: name) do
|
|
170
|
+
loop do
|
|
171
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
172
|
+
|
|
173
|
+
begin
|
|
174
|
+
instance = new
|
|
175
|
+
result = instance.perform
|
|
176
|
+
break if result.equal?(Stop) || @__stopping
|
|
177
|
+
Rage.logger.warn("Daemon exited, restarting...")
|
|
178
|
+
rescue => e
|
|
179
|
+
break if @__stopping
|
|
180
|
+
Rage.logger.error("Daemon failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
181
|
+
Rage::Errors.report(e)
|
|
182
|
+
ensure
|
|
183
|
+
__cleanup(instance)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# reset backoff if ran successfully for a while
|
|
187
|
+
backoff = INITIAL_BACKOFF if Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at > BACKOFF_RESET_INTERVAL
|
|
188
|
+
sleep(backoff / 2 + rand * backoff / 2)
|
|
189
|
+
backoff = (backoff * 2).clamp(INITIAL_BACKOFF, MAX_BACKOFF)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end # class << self
|
|
194
|
+
|
|
195
|
+
def __cleanup(instance)
|
|
196
|
+
instance.cleanup if instance.respond_to?(:cleanup)
|
|
197
|
+
rescue => e
|
|
198
|
+
Rage.logger.error("Cleanup hook failed with exception: #{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
|
|
199
|
+
Rage::Errors.report(e)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The main entry point for the daemon's work.
|
|
204
|
+
#
|
|
205
|
+
# Implement this method to define what the daemon does. The method should contain
|
|
206
|
+
# the daemon's main loop or blocking operation. When this method returns normally,
|
|
207
|
+
# Rage logs a warning and restarts the daemon. Return {Stop} to explicitly stop
|
|
208
|
+
# the daemon without triggering a restart.
|
|
209
|
+
#
|
|
210
|
+
# @return [Object] return {Stop} to stop the daemon; any other return value triggers a restart
|
|
211
|
+
# @example Listening to a Redis channel
|
|
212
|
+
# def perform
|
|
213
|
+
# redis = Redis.new
|
|
214
|
+
# redis.subscribe("events") do |on|
|
|
215
|
+
# on.message { |_, msg| handle(msg) }
|
|
216
|
+
# end
|
|
217
|
+
# end
|
|
218
|
+
# @example Polling a queue
|
|
219
|
+
# def perform
|
|
220
|
+
# loop do
|
|
221
|
+
# message = queue.pop
|
|
222
|
+
# process(message)
|
|
223
|
+
# end
|
|
224
|
+
# end
|
|
225
|
+
# @example Processing until complete
|
|
226
|
+
# def perform
|
|
227
|
+
# while (item = fetch_next_item)
|
|
228
|
+
# process(item)
|
|
229
|
+
# end
|
|
230
|
+
#
|
|
231
|
+
# Stop
|
|
232
|
+
# end
|
|
233
|
+
def perform
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
Iodine.on_state(:on_start) do
|
|
238
|
+
Fiber.set_scheduler(Rage::FiberScheduler.new) unless Fiber.scheduler
|
|
239
|
+
Rage.config.daemons.klasses.each { |klass| klass.__perform }
|
|
240
|
+
end
|
|
@@ -241,8 +241,7 @@ class Rage::Deferred::Backends::Disk
|
|
|
241
241
|
|
|
242
242
|
# delete the old storage ensuring the copied data has already been written to disk
|
|
243
243
|
Iodine.run_after(@fsync_frequency) do
|
|
244
|
-
old_storage
|
|
245
|
-
File.unlink(old_storage.path)
|
|
244
|
+
cleanup_storage(old_storage)
|
|
246
245
|
end
|
|
247
246
|
end
|
|
248
247
|
|
|
@@ -267,8 +266,13 @@ class Rage::Deferred::Backends::Disk
|
|
|
267
266
|
end
|
|
268
267
|
|
|
269
268
|
Iodine.run_after(@fsync_frequency) do
|
|
270
|
-
storage
|
|
271
|
-
File.unlink(storage.path)
|
|
269
|
+
cleanup_storage(storage)
|
|
272
270
|
end
|
|
273
271
|
end
|
|
272
|
+
|
|
273
|
+
def cleanup_storage(storage)
|
|
274
|
+
path = storage.path
|
|
275
|
+
storage.close
|
|
276
|
+
File.unlink(path) if File.exist?(path)
|
|
277
|
+
end
|
|
274
278
|
end
|
|
@@ -11,7 +11,11 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
11
11
|
@arr << el
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def unshift(el)
|
|
15
|
+
@arr.unshift(el)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def pop
|
|
15
19
|
nil
|
|
16
20
|
end
|
|
17
21
|
|
|
@@ -59,18 +63,27 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
59
63
|
# in the format of { Fiber => Connection }
|
|
60
64
|
@__in_use = {}
|
|
61
65
|
|
|
62
|
-
# a list of all DB connections that are currently idle
|
|
63
|
-
@__connections = build_new_connections
|
|
64
|
-
|
|
65
66
|
# how long a fiber can wait for a connection to become available
|
|
66
67
|
@__checkout_timeout = checkout_timeout
|
|
67
68
|
|
|
68
69
|
# how long a connection can be idle for before disconnecting
|
|
69
70
|
@__idle_timeout = respond_to?(:db_config) ? db_config.idle_timeout : @idle_timeout
|
|
70
71
|
|
|
72
|
+
# pool will maintain at least this many connections
|
|
73
|
+
@__min_connections = respond_to?(:min_connections) ? min_connections : 0
|
|
74
|
+
|
|
75
|
+
# connections older than this are automatically disconnected
|
|
76
|
+
@__max_age = respond_to?(:max_age) ? max_age : Float::INFINITY
|
|
77
|
+
|
|
78
|
+
# seconds between keepalive pings on idle connections
|
|
79
|
+
@__keepalive = respond_to?(:keepalive) ? keepalive : nil
|
|
80
|
+
|
|
71
81
|
# how often should we check for fibers that wait for a connection for too long
|
|
72
82
|
@__timeout_worker_frequency = 0.5
|
|
73
83
|
|
|
84
|
+
# a list of all DB connections that are currently idle
|
|
85
|
+
@__connections = build_new_connections
|
|
86
|
+
|
|
74
87
|
# reject fibers that wait for a connection for more than `@__checkout_timeout`
|
|
75
88
|
Iodine.run_every((@__timeout_worker_frequency * 1_000).to_i) do
|
|
76
89
|
if @__blocked.length > 0
|
|
@@ -105,6 +118,8 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
105
118
|
end
|
|
106
119
|
|
|
107
120
|
@release_connection_channel = "ext:ar-connection-released:#{object_id}"
|
|
121
|
+
@__owner_thread = Thread.current
|
|
122
|
+
@__background_maintenance_disabled = (ActiveRecord.version < Gem::Version.create("8.1"))
|
|
108
123
|
|
|
109
124
|
# resume blocked fibers once connections become available
|
|
110
125
|
Iodine.subscribe(@release_connection_channel) do
|
|
@@ -118,6 +133,14 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
118
133
|
Iodine.on_state(:on_finish) do
|
|
119
134
|
Iodine.unsubscribe(@release_connection_channel)
|
|
120
135
|
end
|
|
136
|
+
|
|
137
|
+
Iodine.run_every((db_config.reaping_frequency * 1_000).to_i) do
|
|
138
|
+
reap
|
|
139
|
+
flush
|
|
140
|
+
disconnected = retire_old_connections
|
|
141
|
+
keep_alive
|
|
142
|
+
preconnect if disconnected
|
|
143
|
+
end
|
|
121
144
|
end
|
|
122
145
|
|
|
123
146
|
# Returns true if there is an open connection being used for the current fiber.
|
|
@@ -127,12 +150,10 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
127
150
|
|
|
128
151
|
# Retrieve the connection associated with the current fiber, or obtain one if necessary.
|
|
129
152
|
def connection
|
|
130
|
-
@__in_use[Fiber.current] ||= @__connections.
|
|
131
|
-
|
|
132
|
-
@__blocked[fiber] = blocked_since
|
|
153
|
+
@__in_use[Fiber.current] ||= @__connections.pop || begin
|
|
154
|
+
@__blocked[Fiber.current] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
133
155
|
Fiber.yield
|
|
134
|
-
|
|
135
|
-
@__connections.shift
|
|
156
|
+
@__connections.pop
|
|
136
157
|
end
|
|
137
158
|
end
|
|
138
159
|
|
|
@@ -149,45 +170,125 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
149
170
|
|
|
150
171
|
# Recover lost connections for the pool.
|
|
151
172
|
def reap
|
|
152
|
-
|
|
173
|
+
return unless Thread.current == @__owner_thread
|
|
153
174
|
|
|
154
|
-
|
|
175
|
+
dead = nil
|
|
176
|
+
|
|
177
|
+
@__in_use.delete_if do |fiber, conn|
|
|
155
178
|
unless fiber.alive?
|
|
179
|
+
(dead ||= []) << [fiber, conn]
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
dead&.each do |fiber, conn|
|
|
185
|
+
Fiber.schedule do
|
|
156
186
|
if conn.active?
|
|
157
187
|
conn.reset!
|
|
158
|
-
|
|
188
|
+
@__in_use[fiber] = conn
|
|
189
|
+
release_connection(fiber)
|
|
159
190
|
else
|
|
160
|
-
@__in_use.delete(fiber)
|
|
161
191
|
conn.disconnect!
|
|
162
192
|
__remove__(conn)
|
|
163
193
|
self.automatic_reconnect = true
|
|
164
|
-
@__connections
|
|
194
|
+
@__connections.unshift(*build_new_connections(1))
|
|
165
195
|
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
|
166
196
|
end
|
|
167
197
|
end
|
|
168
198
|
end
|
|
169
|
-
|
|
170
|
-
if crashed_fibers
|
|
171
|
-
crashed_fibers.each { |fiber| release_connection(fiber) }
|
|
172
|
-
end
|
|
173
199
|
end
|
|
174
200
|
|
|
175
201
|
# Disconnect all connections that have been idle for at least
|
|
176
202
|
# `minimum_idle` seconds. Connections currently checked out, or that were
|
|
177
203
|
# checked in less than `minimum_idle` seconds ago, are unaffected.
|
|
178
204
|
def flush(minimum_idle = @__idle_timeout)
|
|
179
|
-
return if minimum_idle.nil? || @__connections.length == 0
|
|
205
|
+
return if Thread.current != @__owner_thread || minimum_idle.nil? || @__connections.length == 0
|
|
180
206
|
|
|
181
|
-
current_time
|
|
182
|
-
|
|
207
|
+
current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
208
|
+
i = @__connections.length - 1
|
|
209
|
+
connections_to_preserve = @__min_connections
|
|
210
|
+
|
|
211
|
+
while i >= 0
|
|
183
212
|
conn = @__connections[i]
|
|
213
|
+
|
|
214
|
+
if connections_to_preserve > 0 && conn.connected? && conn.verified?
|
|
215
|
+
connections_to_preserve -= 1
|
|
216
|
+
i -= 1
|
|
217
|
+
next
|
|
218
|
+
end
|
|
219
|
+
|
|
184
220
|
if conn.__idle_since && current_time - conn.__idle_since >= minimum_idle
|
|
185
221
|
conn.__idle_since = nil
|
|
186
222
|
conn.__needs_reconnect = true
|
|
187
223
|
conn.disconnect!
|
|
188
224
|
end
|
|
225
|
+
i -= 1
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Disconnect connections exceeding `max_age`
|
|
230
|
+
def retire_old_connections(max_age = @__max_age)
|
|
231
|
+
return false if max_age.nil? || max_age.infinite?
|
|
232
|
+
|
|
233
|
+
i, disconnected = 0, false
|
|
234
|
+
|
|
235
|
+
while i < @__connections.length
|
|
236
|
+
conn = @__connections[i]
|
|
237
|
+
if (conn.connection_age || 0) >= conn.pool_jitter(max_age)
|
|
238
|
+
conn.disconnect!
|
|
239
|
+
disconnected = true
|
|
240
|
+
end
|
|
189
241
|
i += 1
|
|
190
242
|
end
|
|
243
|
+
|
|
244
|
+
disconnected
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Ping idle connections to prevent firewall/server timeouts
|
|
248
|
+
def keep_alive(threshold = @__keepalive)
|
|
249
|
+
return if threshold.nil? || @__connections.length == 0
|
|
250
|
+
|
|
251
|
+
to_ping = nil
|
|
252
|
+
|
|
253
|
+
@__connections.delete_if do |conn|
|
|
254
|
+
if (conn.seconds_since_last_activity || 0) >= conn.pool_jitter(threshold)
|
|
255
|
+
(to_ping ||= []) << conn
|
|
256
|
+
true
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
to_ping&.each do |conn|
|
|
261
|
+
Fiber.schedule do
|
|
262
|
+
if conn.active?
|
|
263
|
+
@__connections << conn
|
|
264
|
+
else
|
|
265
|
+
conn.disconnect!
|
|
266
|
+
@__connections.unshift(conn)
|
|
267
|
+
end
|
|
268
|
+
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Proactively establish DB connections
|
|
274
|
+
def preconnect
|
|
275
|
+
return if @__connections.length == 0 || @__background_maintenance_disabled
|
|
276
|
+
|
|
277
|
+
active_connections_count = @__in_use.length + @__connections.count { |conn| conn.connected? && conn.verified? }
|
|
278
|
+
|
|
279
|
+
while @__min_connections - active_connections_count > 0
|
|
280
|
+
i = @__connections.rindex { |conn| !conn.connected? || !conn.verified? }
|
|
281
|
+
return unless i
|
|
282
|
+
|
|
283
|
+
Fiber.schedule do
|
|
284
|
+
conn = @__connections.delete_at(i)
|
|
285
|
+
conn.connect! rescue nil
|
|
286
|
+
@__connections << conn
|
|
287
|
+
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
active_connections_count += 1
|
|
291
|
+
end
|
|
191
292
|
end
|
|
192
293
|
|
|
193
294
|
# Disconnect all currently idle connections. Connections currently checked out are unaffected.
|
|
@@ -234,6 +335,8 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
234
335
|
# Raises `ActiveRecord::ExclusiveConnectionTimeoutError` if unable to gain ownership of all
|
|
235
336
|
# connections in the pool within a timeout interval (default duration is `checkout_timeout * 2` seconds).
|
|
236
337
|
def disconnect(raise_on_acquisition_timeout = true, disconnect_attempts = 0)
|
|
338
|
+
return if @__connections.is_a?(BlackHoleList)
|
|
339
|
+
|
|
237
340
|
# allow request fibers to release connections, but block from acquiring new ones
|
|
238
341
|
if disconnect_attempts == 0
|
|
239
342
|
@__connections = BlackHoleList.new(@__connections)
|
|
@@ -288,6 +391,10 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
288
391
|
connection
|
|
289
392
|
end
|
|
290
393
|
|
|
394
|
+
def maintainable?
|
|
395
|
+
false
|
|
396
|
+
end
|
|
397
|
+
|
|
291
398
|
# Check in a database connection back into the pool, indicating that you no longer need this connection.
|
|
292
399
|
def checkin(conn)
|
|
293
400
|
fiber = @__in_use.key(conn)
|
|
@@ -328,8 +435,12 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
|
328
435
|
private
|
|
329
436
|
|
|
330
437
|
def build_new_connections(num_connections = size)
|
|
331
|
-
(1..num_connections).map do
|
|
438
|
+
connections = (1..num_connections).map do
|
|
332
439
|
__checkout__.tap { |conn| conn.__idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
333
440
|
end
|
|
441
|
+
|
|
442
|
+
Iodine.defer { preconnect }
|
|
443
|
+
|
|
444
|
+
connections
|
|
334
445
|
end
|
|
335
446
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @private
|
|
4
|
+
# This needs much more work to make it public, but it's already enough for inertia-rage.
|
|
5
|
+
class Rage::Extension
|
|
6
|
+
class << self
|
|
7
|
+
def initializer(id = name, before: nil, after: nil, &block)
|
|
8
|
+
__initializers[id] << block
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def configure(&block)
|
|
12
|
+
__configurations << block
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def before_server_start(&block)
|
|
16
|
+
Iodine.on_state(:pre_start, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def __initializers
|
|
20
|
+
@@initializers ||= Hash.new { |h, k| h[k] = [] }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def __configurations
|
|
24
|
+
@@configurations ||= []
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/rage/fiber_scheduler.rb
CHANGED
|
@@ -152,6 +152,11 @@ class Rage::FiberScheduler
|
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# Wait for a specified process.
|
|
156
|
+
def process_wait(pid, flags)
|
|
157
|
+
Thread.new { Process::Status.wait(pid, flags) }.value
|
|
158
|
+
end
|
|
159
|
+
|
|
155
160
|
# Create and schedule a new non-blocking fiber, handling request and user-spawned fibers differently.
|
|
156
161
|
def fiber(&block)
|
|
157
162
|
parent = Fiber.current
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Rage::OpenAPI::Parsers::Ext::Blueprinter
|
|
4
|
+
class InvalidViewError < StandardError; end
|
|
5
|
+
|
|
4
6
|
def initialize(namespace: Object, root: Rage::OpenAPI::Nodes::Root.new, **)
|
|
5
7
|
@namespace = namespace
|
|
6
8
|
@root = root
|
|
9
|
+
@parsing_stack = Set.new
|
|
7
10
|
end
|
|
8
11
|
|
|
9
12
|
def known_definition?(str)
|
|
@@ -14,97 +17,71 @@ class Rage::OpenAPI::Parsers::Ext::Blueprinter
|
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def parse(klass_str)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def __parse(klass_str)
|
|
22
|
-
is_collection, klass_str, _ = Rage::OpenAPI.__parse_serializer_args(klass_str)
|
|
20
|
+
is_collection, raw_klass_str, serializer_options = Rage::OpenAPI.__parse_serializer_args(klass_str)
|
|
21
|
+
klass = @namespace.const_get(raw_klass_str)
|
|
22
|
+
schema = build_schema(klass, is_collection, serializer_options)
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
visitor = Visitor.new(self, is_collection)
|
|
29
|
-
ast.value.accept(visitor)
|
|
24
|
+
if @root.schema_registry.key?(raw_klass_str)
|
|
25
|
+
@root.schema_registry[raw_klass_str] = is_collection ? schema["items"] : schema
|
|
26
|
+
end
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
schema
|
|
29
|
+
rescue InvalidViewError => e
|
|
30
|
+
Rage::OpenAPI.__log_warn e.message
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
attr_accessor :symbols, :keywords, :strings
|
|
33
|
+
private
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@strings = []
|
|
40
|
-
@keywords = {}
|
|
41
|
-
end
|
|
42
|
-
end
|
|
35
|
+
def build_schema(klass, is_collection, serializer_options = nil)
|
|
36
|
+
@parsing_stack.add(klass.name)
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
view_name = serializer_options&.key?(:view) ? serializer_options[:view] : :default
|
|
39
|
+
reflections = klass.reflections
|
|
40
|
+
view = reflections[view_name]
|
|
41
|
+
raise InvalidViewError, "invalid view #{view_name}" unless view
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
identifier_fields = extract_fields(reflections[:identifier])
|
|
44
|
+
default_fields = extract_fields(view)
|
|
45
|
+
association_fields = extract_associations(view)
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
@schema = {}
|
|
53
|
-
@segment = @schema
|
|
54
|
-
@identifier = {}
|
|
55
|
-
end
|
|
47
|
+
@parsing_stack.delete(klass.name)
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
result = { "type" => "object" }
|
|
49
|
+
schema = identifier_fields.merge(default_fields.merge(association_fields).sort.to_h)
|
|
59
50
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
result = { "type" => "object" }
|
|
52
|
+
result["properties"] = schema if schema.any?
|
|
53
|
+
result = { "type" => "array", "items" => result } if is_collection
|
|
54
|
+
result
|
|
55
|
+
end
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
def extract_fields(view)
|
|
58
|
+
view.fields.each_with_object({}) do |(_, field), properties|
|
|
59
|
+
properties[field.display_name.to_s] = { "type" => "string" }
|
|
67
60
|
end
|
|
61
|
+
end
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@segment[context.strings.first] = { "type" => "string" } if context.strings.first
|
|
83
|
-
else
|
|
84
|
-
context.symbols.each { |symbol| @segment[symbol] = { "type" => "string" } }
|
|
85
|
-
context.strings.each { |string| @segment[string] = { "type" => "string" } }
|
|
86
|
-
end
|
|
63
|
+
def extract_associations(view)
|
|
64
|
+
view.associations.each_with_object({}) do |(_, association), properties|
|
|
65
|
+
blueprint = association.blueprint
|
|
66
|
+
name, display_name = association.name.to_s, association.display_name.to_s
|
|
67
|
+
is_collection = collection_association?(name)
|
|
68
|
+
|
|
69
|
+
item_schema = if blueprint.is_a?(Proc)
|
|
70
|
+
{ "type" => "object" }
|
|
71
|
+
elsif @parsing_stack.include?(blueprint.name)
|
|
72
|
+
@root.schema_registry[blueprint.name] ||= nil
|
|
73
|
+
{ "$ref" => "#/components/schemas/#{blueprint.name}" }
|
|
74
|
+
else
|
|
75
|
+
build_schema(blueprint, false)
|
|
87
76
|
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def visit_assoc_node(node)
|
|
91
|
-
@context.keywords[node.key.value] = node.value.unescaped
|
|
92
|
-
end
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
@context.symbols << node.value
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def visit_string_node(node)
|
|
99
|
-
@context.strings << node.unescaped
|
|
78
|
+
properties[display_name] = is_collection ? { "type" => "array", "items" => item_schema } : item_schema
|
|
100
79
|
end
|
|
80
|
+
end
|
|
101
81
|
|
|
102
|
-
|
|
82
|
+
def collection_association?(name)
|
|
83
|
+
return true unless name.respond_to?(:singularize)
|
|
103
84
|
|
|
104
|
-
|
|
105
|
-
@context = VisitorContext.new
|
|
106
|
-
yield
|
|
107
|
-
@context
|
|
108
|
-
end
|
|
85
|
+
name.singularize != name
|
|
109
86
|
end
|
|
110
87
|
end
|
data/lib/rage/request.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ipaddr"
|
|
3
4
|
require "time"
|
|
4
5
|
|
|
5
6
|
class Rage::Request
|
|
@@ -168,16 +169,18 @@ class Rage::Request
|
|
|
168
169
|
# request.fresh?(last_modified: Time.utc(2023, 12, 15))
|
|
169
170
|
# request.fresh?(etag: "123")
|
|
170
171
|
def fresh?(etag:, last_modified:)
|
|
172
|
+
request_if_none_match = if_none_match
|
|
173
|
+
request_not_modified_since = if_not_modified_since
|
|
174
|
+
|
|
171
175
|
# Always render response when no freshness information
|
|
172
176
|
# is provided in the request.
|
|
173
|
-
return false unless
|
|
177
|
+
return false unless request_if_none_match || request_not_modified_since
|
|
174
178
|
|
|
175
|
-
|
|
176
|
-
requested_etags:
|
|
177
|
-
|
|
178
|
-
request_not_modified_since:
|
|
179
|
-
|
|
180
|
-
)
|
|
179
|
+
if request_if_none_match
|
|
180
|
+
etag_matches?(requested_etags: request_if_none_match, response_etag: etag)
|
|
181
|
+
else
|
|
182
|
+
not_modified?(request_not_modified_since: request_not_modified_since, response_last_modified: last_modified)
|
|
183
|
+
end
|
|
181
184
|
end
|
|
182
185
|
|
|
183
186
|
# Get the domain part of the request.
|
|
@@ -245,7 +248,14 @@ class Rage::Request
|
|
|
245
248
|
end
|
|
246
249
|
|
|
247
250
|
def named_host?(host)
|
|
248
|
-
!
|
|
251
|
+
host && !ip_host?(host)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def ip_host?(host)
|
|
255
|
+
IP_HOST_REGEXP.match?(host) || IPAddr.new(host.delete_prefix("[").delete_suffix("]"))
|
|
256
|
+
true
|
|
257
|
+
rescue IPAddr::InvalidAddressError
|
|
258
|
+
false
|
|
249
259
|
end
|
|
250
260
|
|
|
251
261
|
def if_none_match
|
|
@@ -263,8 +273,10 @@ class Rage::Request
|
|
|
263
273
|
|
|
264
274
|
return true if requested_etags.empty?
|
|
265
275
|
return false if response_etag.nil?
|
|
276
|
+
return true if requested_etags.include?("*")
|
|
266
277
|
|
|
267
|
-
|
|
278
|
+
response_etag = weak_etag(response_etag)
|
|
279
|
+
requested_etags.any? { |requested_etag| weak_etag(requested_etag) == response_etag }
|
|
268
280
|
end
|
|
269
281
|
|
|
270
282
|
def not_modified?(request_not_modified_since:, response_last_modified:)
|
|
@@ -274,6 +286,10 @@ class Rage::Request
|
|
|
274
286
|
request_not_modified_since >= response_last_modified
|
|
275
287
|
end
|
|
276
288
|
|
|
289
|
+
private def weak_etag(etag)
|
|
290
|
+
etag.delete_prefix("W/")
|
|
291
|
+
end
|
|
292
|
+
|
|
277
293
|
# @private
|
|
278
294
|
class Headers
|
|
279
295
|
HTTP = "HTTP_"
|
|
@@ -69,7 +69,7 @@ class Rage::Router::Constrainer
|
|
|
69
69
|
# Optimization: inline the derivation for the common built in constraints
|
|
70
70
|
if !strategy.custom?
|
|
71
71
|
if key == :host
|
|
72
|
-
lines << " host: env['HTTP_HOST'.freeze]&.sub(/:\\d+\\z/, ''.freeze),"
|
|
72
|
+
lines << " host: env['HTTP_HOST'.freeze]&.sub(/:\\d+\\z/, ''.freeze) || env['SERVER_NAME'.freeze],"
|
|
73
73
|
else
|
|
74
74
|
raise ArgumentError, "unknown non-custom strategy for compiling constraint derivation function"
|
|
75
75
|
end
|
data/lib/rage/setup.rb
CHANGED
|
@@ -7,6 +7,7 @@ rescue LoadError
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
# Run application initializers
|
|
10
|
+
Rage::Extension.__initializers.values.flatten.each(&:call) if Rage::Extension.subclasses.any?
|
|
10
11
|
Dir["#{Rage.root}/config/initializers/**/*.rb"].each { |initializer| load(initializer) }
|
|
11
12
|
|
|
12
13
|
require "rage/ext/setup"
|
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
|
@@ -84,6 +84,12 @@ module Rage
|
|
|
84
84
|
# config.log_level = :debug
|
|
85
85
|
# end
|
|
86
86
|
def self.configure(&)
|
|
87
|
+
if Rage::Extension.subclasses.any? && !@__extension_config_applied
|
|
88
|
+
Rage::Extension.__configurations.each { |block| config.instance_eval(&block) }
|
|
89
|
+
config.__finalize
|
|
90
|
+
@__extension_config_applied = true
|
|
91
|
+
end
|
|
92
|
+
|
|
87
93
|
config.instance_eval(&)
|
|
88
94
|
config.__finalize
|
|
89
95
|
end
|
|
@@ -198,6 +204,7 @@ module Rage
|
|
|
198
204
|
autoload :Deferred, "rage/deferred/deferred"
|
|
199
205
|
autoload :Events, "rage/events/events"
|
|
200
206
|
autoload :PubSub, "rage/pubsub/pubsub"
|
|
207
|
+
autoload :Daemon, "rage/daemon"
|
|
201
208
|
end
|
|
202
209
|
|
|
203
210
|
module RageController
|
|
@@ -206,3 +213,4 @@ end
|
|
|
206
213
|
|
|
207
214
|
require_relative "rage/env"
|
|
208
215
|
require_relative "rage/internal"
|
|
216
|
+
require_relative "rage/extension"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rage-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Samoilov
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '5.
|
|
46
|
+
version: '5.5'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '5.
|
|
53
|
+
version: '5.5'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: zeitwerk
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -158,6 +158,7 @@ files:
|
|
|
158
158
|
- lib/rage/controller/api.rb
|
|
159
159
|
- lib/rage/controller/renderers.rb
|
|
160
160
|
- lib/rage/cookies.rb
|
|
161
|
+
- lib/rage/daemon.rb
|
|
161
162
|
- lib/rage/deferred/backends/disk.rb
|
|
162
163
|
- lib/rage/deferred/backends/nil.rb
|
|
163
164
|
- lib/rage/deferred/context.rb
|
|
@@ -174,6 +175,7 @@ files:
|
|
|
174
175
|
- lib/rage/events/subscriber.rb
|
|
175
176
|
- lib/rage/ext/active_record/connection_pool.rb
|
|
176
177
|
- lib/rage/ext/setup.rb
|
|
178
|
+
- lib/rage/extension.rb
|
|
177
179
|
- lib/rage/fiber.rb
|
|
178
180
|
- lib/rage/fiber_scheduler.rb
|
|
179
181
|
- lib/rage/hooks.rb
|