rage-rb 1.2.1 → 1.3.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 +12 -0
- data/lib/rage/cli.rb +1 -1
- data/lib/rage/configuration.rb +12 -0
- data/lib/rage/controller/api.rb +1 -1
- data/lib/rage/ext/active_record/connection_pool.rb +277 -0
- data/lib/rage/ext/setup.rb +36 -0
- data/lib/rage/rails.rb +5 -29
- data/lib/rage/rspec.rb +1 -1
- data/lib/rage/setup.rb +2 -0
- data/lib/rage/templates/Gemfile +1 -1
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +28 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cef03d55f96279c7e6d451dc7ce0abce04024a7ca30d07e9c48c0d054c530bb6
|
4
|
+
data.tar.gz: c399b5b3d060aee5367c2e3064435649bbb8eba7dcb2bf2bd215730a698f69d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc5578e1e51d557a8f30729b0eff9a41d4a651e62c837af53aff3b3f50caee2b827be47a239aeadd47f2e9ff15ff8263f35a032634c1fdb738cb2b2c71787780
|
7
|
+
data.tar.gz: 28a70f5c33752ea33dd1474bc4e481ba7b56fd76541544f838226c9b5ace3758b80d9a93b5947c1ea8cbf9374cbae204e9c0e6c5ff40f2adcbc40be3208f5597
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.3.0] - 2024-04-17
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
- Introduce the `ActiveRecord::ConnectionPool` patch (#78).
|
8
|
+
|
9
|
+
## [1.2.2] - 2024-04-03
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Correctly determine Rage env (#77).
|
14
|
+
|
3
15
|
## [1.2.1] - 2024-04-03
|
4
16
|
|
5
17
|
### Fixed
|
data/lib/rage/cli.rb
CHANGED
@@ -124,7 +124,7 @@ module Rage
|
|
124
124
|
end
|
125
125
|
|
126
126
|
def set_env(options)
|
127
|
-
ENV["RAGE_ENV"] = options[:environment]
|
127
|
+
ENV["RAGE_ENV"] = options[:environment] if options[:environment]
|
128
128
|
end
|
129
129
|
end
|
130
130
|
|
data/lib/rage/configuration.rb
CHANGED
@@ -85,6 +85,18 @@
|
|
85
85
|
#
|
86
86
|
# > Specifies connection timeout.
|
87
87
|
#
|
88
|
+
# # Transient Settings
|
89
|
+
#
|
90
|
+
# The settings described in this section should be configured using **environment variables** and are either temporary or will become the default in the future.
|
91
|
+
#
|
92
|
+
# • _RAGE_DISABLE_IO_WRITE_
|
93
|
+
#
|
94
|
+
# > Disables the `io_write` hook to fix the ["zero-length iov"](https://bugs.ruby-lang.org/issues/19640) error on Ruby < 3.3.
|
95
|
+
#
|
96
|
+
# • _RAGE_PATCH_AR_POOL_
|
97
|
+
#
|
98
|
+
# > Enables the `ActiveRecord::ConnectionPool` patch to optimize database connection management. Use it to increase throughput under high load.
|
99
|
+
#
|
88
100
|
class Rage::Configuration
|
89
101
|
attr_accessor :logger
|
90
102
|
attr_reader :log_formatter, :log_level
|
data/lib/rage/controller/api.rb
CHANGED
@@ -0,0 +1,277 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rage::Ext::ActiveRecord::ConnectionPool
|
4
|
+
# items can be added but not removed
|
5
|
+
class BlackHoleList
|
6
|
+
def initialize(arr)
|
7
|
+
@arr = arr
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(el)
|
11
|
+
@arr << el
|
12
|
+
end
|
13
|
+
|
14
|
+
def shift
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def length
|
19
|
+
0
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_a
|
23
|
+
@arr
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.extended(instance)
|
28
|
+
instance.class.alias_method :__checkout__, :checkout
|
29
|
+
instance.class.alias_method :__remove__, :remove
|
30
|
+
|
31
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.attr_accessor(:__idle_since)
|
32
|
+
end
|
33
|
+
|
34
|
+
def __init_rage_extension
|
35
|
+
# a map of fibers that are currently waiting for a
|
36
|
+
# connection in the format of { Fiber => timestamp }
|
37
|
+
@__blocked = {}
|
38
|
+
|
39
|
+
# a map of fibers that are currently hodling connections
|
40
|
+
# in the format of { Fiber => Connection }
|
41
|
+
@__in_use = {}
|
42
|
+
|
43
|
+
# a list of all DB connections that are currently idle
|
44
|
+
@__connections = build_new_connections
|
45
|
+
|
46
|
+
# how long a fiber can wait for a connection to become available
|
47
|
+
@__checkout_timeout = checkout_timeout
|
48
|
+
|
49
|
+
# how long a connection can be idle for before disconnecting
|
50
|
+
@__idle_timeout = reaper.frequency
|
51
|
+
|
52
|
+
# how often should we check for fibers that wait for a connection for too long
|
53
|
+
@__timeout_worker_frequency = 0.5
|
54
|
+
|
55
|
+
# reject fibers that wait for a connection for more than `@__checkout_timeout`
|
56
|
+
Iodine.run_every((@__timeout_worker_frequency * 1_000).to_i) do
|
57
|
+
if @__blocked.length > 0
|
58
|
+
current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
59
|
+
@__blocked.each do |fiber, blocked_since|
|
60
|
+
if (current_time - blocked_since) > @__checkout_timeout
|
61
|
+
@__blocked.delete(fiber)
|
62
|
+
fiber.raise(ActiveRecord::ConnectionTimeoutError, "could not obtain a connection from the pool within #{@__checkout_timeout} seconds; all pooled connections were in use")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# resume blocked fibers once connections become available
|
69
|
+
Iodine.subscribe("ext:ar-connection-released") do
|
70
|
+
if @__blocked.length > 0 && @__connections.length > 0
|
71
|
+
f, _ = @__blocked.shift
|
72
|
+
f.resume
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# unsubscribe on shutdown
|
77
|
+
Iodine.on_state(:on_finish) do
|
78
|
+
Iodine.unsubscribe("ext:ar-connection-released")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns true if there is an open connection being used for the current fiber.
|
83
|
+
def active_connection?
|
84
|
+
@__in_use[Fiber.current]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Retrieve the connection associated with the current fiber, or obtain one if necessary.
|
88
|
+
def connection
|
89
|
+
@__in_use[Fiber.current] ||= @__connections.shift || begin
|
90
|
+
fiber, blocked_since = Fiber.current, Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
91
|
+
@__blocked[fiber] = blocked_since
|
92
|
+
Fiber.yield
|
93
|
+
|
94
|
+
@__connections.shift
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Signal that the fiber is finished with the current connection and it can be returned to the pool.
|
99
|
+
def release_connection(owner = Fiber.current)
|
100
|
+
if conn = @__in_use.delete(owner)
|
101
|
+
conn.__idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
102
|
+
@__connections << conn
|
103
|
+
Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
104
|
+
end
|
105
|
+
|
106
|
+
conn
|
107
|
+
end
|
108
|
+
|
109
|
+
# Recover lost connections for the pool.
|
110
|
+
def reap
|
111
|
+
@__in_use.each do |fiber, conn|
|
112
|
+
unless fiber.alive?
|
113
|
+
if conn.active?
|
114
|
+
conn.reset!
|
115
|
+
release_connection(fiber)
|
116
|
+
else
|
117
|
+
@__in_use.delete(fiber)
|
118
|
+
conn.disconnect!
|
119
|
+
__remove__(conn)
|
120
|
+
@__connections += build_new_connections(1)
|
121
|
+
Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Disconnect all connections that have been idle for at least
|
128
|
+
# `minimum_idle` seconds. Connections currently checked out, or that were
|
129
|
+
# checked in less than `minimum_idle` seconds ago, are unaffected.
|
130
|
+
def flush(minimum_idle = @__idle_timeout)
|
131
|
+
return if minimum_idle.nil? || @__connections.length == 0
|
132
|
+
|
133
|
+
current_time, i = Process.clock_gettime(Process::CLOCK_MONOTONIC), 0
|
134
|
+
while i < @__connections.length
|
135
|
+
conn = @__connections[i]
|
136
|
+
if conn.__idle_since && current_time - conn.__idle_since >= minimum_idle
|
137
|
+
conn.__idle_since = nil
|
138
|
+
conn.disconnect!
|
139
|
+
end
|
140
|
+
i += 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Disconnect all currently idle connections. Connections currently checked out are unaffected.
|
145
|
+
def flush!
|
146
|
+
reap
|
147
|
+
flush(-1)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Yields a connection from the connection pool to the block.
|
151
|
+
def with_connection
|
152
|
+
yield connection
|
153
|
+
ensure
|
154
|
+
release_connection
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns an array containing the connections currently in the pool.
|
158
|
+
def connections
|
159
|
+
@__connections.to_a
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns true if a connection has already been opened.
|
163
|
+
def connected?
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return connection pool's usage statistic.
|
168
|
+
def stat
|
169
|
+
{
|
170
|
+
size: size,
|
171
|
+
connections: size,
|
172
|
+
busy: @__in_use.count { |fiber, _| fiber.alive? },
|
173
|
+
dead: @__in_use.count { |fiber, _| !fiber.alive? },
|
174
|
+
idle: @__connections.length,
|
175
|
+
waiting: @__blocked.length,
|
176
|
+
checkout_timeout: @__checkout_timeout
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
# Disconnects all connections in the pool, and clears the pool.
|
181
|
+
# Raises `ActiveRecord::ExclusiveConnectionTimeoutError` if unable to gain ownership of all
|
182
|
+
# connections in the pool within a timeout interval (default duration is `checkout_timeout * 2` seconds).
|
183
|
+
def disconnect(raise_on_acquisition_timeout = true, disconnect_attempts = 0)
|
184
|
+
# allow request fibers to release connections, but block from acquiring new ones
|
185
|
+
if disconnect_attempts == 0
|
186
|
+
@__connections = BlackHoleList.new(@__connections)
|
187
|
+
end
|
188
|
+
|
189
|
+
# if some connections are in use, we will wait for up to `@__checkout_timeout * 2` seconds
|
190
|
+
if @__in_use.length > 0 && disconnect_attempts <= @__checkout_timeout * 4
|
191
|
+
Iodine.run_after(500) { disconnect(raise_on_acquisition_timeout, disconnect_attempts + 1) }
|
192
|
+
return
|
193
|
+
end
|
194
|
+
|
195
|
+
pool_connections = @__connections.to_a
|
196
|
+
|
197
|
+
# check if there are still some connections in use
|
198
|
+
if @__in_use.length > 0
|
199
|
+
raise(ActiveRecord::ExclusiveConnectionTimeoutError, "could not obtain ownership of all database connections") if raise_on_acquisition_timeout
|
200
|
+
pool_connections += @__in_use.values
|
201
|
+
@__in_use.clear
|
202
|
+
end
|
203
|
+
|
204
|
+
# disconnect all connections
|
205
|
+
pool_connections.each do |conn|
|
206
|
+
conn.disconnect!
|
207
|
+
__remove__(conn)
|
208
|
+
end
|
209
|
+
|
210
|
+
# create a new pool
|
211
|
+
@__connections = build_new_connections
|
212
|
+
|
213
|
+
# notify blocked fibers that there are new connections available
|
214
|
+
[@__blocked.length, @__connections.length].min.times do
|
215
|
+
Iodine.publish("ext:ar-connection-released", "", Iodine::PubSub::PROCESS)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Disconnects all connections in the pool, and clears the pool.
|
220
|
+
# The pool first tries to gain ownership of all connections. If unable to
|
221
|
+
# do so within a timeout interval (default duration is `checkout_timeout * 2` seconds),
|
222
|
+
# then the pool is forcefully disconnected without any regard for other connection owning fibers.
|
223
|
+
def disconnect!
|
224
|
+
disconnect(false)
|
225
|
+
end
|
226
|
+
|
227
|
+
# Check out a database connection from the pool, indicating that you want
|
228
|
+
# to use it. You should call #checkin when you no longer need this.
|
229
|
+
def checkout(_ = nil)
|
230
|
+
connection
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check in a database connection back into the pool, indicating that you no longer need this connection.
|
234
|
+
def checkin(conn)
|
235
|
+
fiber = @__in_use.key(conn)
|
236
|
+
release_connection(fiber)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Remove a connection from the connection pool. The connection will
|
240
|
+
# remain open and active but will no longer be managed by this pool.
|
241
|
+
def remove(conn)
|
242
|
+
__remove__(conn)
|
243
|
+
@__in_use.delete_if { |_, c| c == conn }
|
244
|
+
@__connections.delete(conn)
|
245
|
+
end
|
246
|
+
|
247
|
+
def clear_reloadable_connections(raise_on_acquisition_timeout = true)
|
248
|
+
disconnect(raise_on_acquisition_timeout)
|
249
|
+
end
|
250
|
+
|
251
|
+
def clear_reloadable_connections!
|
252
|
+
disconnect(false)
|
253
|
+
end
|
254
|
+
|
255
|
+
def num_waiting_in_queue
|
256
|
+
@__blocked.length
|
257
|
+
end
|
258
|
+
|
259
|
+
# Discards all connections in the pool (even if they're currently in use!),
|
260
|
+
# along with the pool itself. Any further interaction with the pool is undefined.
|
261
|
+
def discard!
|
262
|
+
@__discarded = true
|
263
|
+
(@__connections + @__in_use.values).each { |conn| conn.discard! }
|
264
|
+
end
|
265
|
+
|
266
|
+
def discarded?
|
267
|
+
!!@__discarded
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
272
|
+
def build_new_connections(num_connections = size)
|
273
|
+
(1..num_connections).map do
|
274
|
+
__checkout__.tap { |conn| conn.__idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# set ActiveSupport isolation level
|
2
|
+
if defined?(ActiveSupport::IsolatedExecutionState)
|
3
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
4
|
+
end
|
5
|
+
|
6
|
+
# release ActiveRecord connections on yield
|
7
|
+
if defined?(ActiveRecord)
|
8
|
+
class Fiber
|
9
|
+
def self.defer
|
10
|
+
res = Fiber.yield
|
11
|
+
|
12
|
+
if ActiveRecord::Base.connection_pool.active_connection?
|
13
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
14
|
+
end
|
15
|
+
|
16
|
+
res
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# make `ActiveRecord::ConnectionPool` work correctly with fibers
|
22
|
+
if defined?(ActiveRecord::ConnectionAdapters::ConnectionPool)
|
23
|
+
ActiveRecord::ConnectionAdapters::ConnectionPool
|
24
|
+
module ActiveRecord::ConnectionAdapters
|
25
|
+
class ConnectionPool
|
26
|
+
def connection_cache_key(_)
|
27
|
+
Fiber.current
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# patch `ActiveRecord::ConnectionPool`
|
34
|
+
if defined?(ActiveRecord) && ENV["RAGE_PATCH_AR_POOL"]
|
35
|
+
Rage.patch_active_record_connection_pool
|
36
|
+
end
|
data/lib/rage/rails.rb
CHANGED
@@ -11,34 +11,6 @@ Iodine.patch_rack
|
|
11
11
|
# configure the framework
|
12
12
|
Rage.config.internal.rails_mode = true
|
13
13
|
|
14
|
-
# patch ActiveRecord's connection pool
|
15
|
-
if defined?(ActiveRecord)
|
16
|
-
Rails.configuration.after_initialize do
|
17
|
-
module ActiveRecord::ConnectionAdapters
|
18
|
-
class ConnectionPool
|
19
|
-
def connection_cache_key(_)
|
20
|
-
Fiber.current
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# release ActiveRecord connections on yield
|
28
|
-
if defined?(ActiveRecord)
|
29
|
-
class Fiber
|
30
|
-
def self.defer
|
31
|
-
res = Fiber.yield
|
32
|
-
|
33
|
-
if ActiveRecord::Base.connection_pool.active_connection?
|
34
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
35
|
-
end
|
36
|
-
|
37
|
-
res
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
14
|
# plug into Rails' Zeitwerk instance to reload the code
|
43
15
|
Rails.autoloaders.main.on_setup do
|
44
16
|
if Iodine.running?
|
@@ -71,6 +43,10 @@ end
|
|
71
43
|
Rails.configuration.after_initialize do
|
72
44
|
if Rails.logger && !Rage.logger
|
73
45
|
rails_logdev = Rails.logger.instance_variable_get(:@logdev)
|
74
|
-
Rage.
|
46
|
+
Rage.configure do
|
47
|
+
config.logger = Rage::Logger.new(rails_logdev.dev) if rails_logdev.is_a?(Logger::LogDevice)
|
48
|
+
end
|
75
49
|
end
|
76
50
|
end
|
51
|
+
|
52
|
+
require "rage/ext/setup"
|
data/lib/rage/rspec.rb
CHANGED
data/lib/rage/setup.rb
CHANGED
data/lib/rage/templates/Gemfile
CHANGED
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
@@ -28,7 +28,7 @@ module Rage
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.env
|
31
|
-
@__env ||= Rage::Env.new(ENV["RAGE_ENV"])
|
31
|
+
@__env ||= Rage::Env.new(ENV["RAGE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
|
32
32
|
end
|
33
33
|
|
34
34
|
def self.groups
|
@@ -71,10 +71,37 @@ module Rage
|
|
71
71
|
@code_loader ||= Rage::CodeLoader.new
|
72
72
|
end
|
73
73
|
|
74
|
+
def self.patch_active_record_connection_pool
|
75
|
+
patch = proc do
|
76
|
+
is_connected = ActiveRecord::Base.connection_pool rescue false
|
77
|
+
if is_connected
|
78
|
+
puts "INFO: Patching ActiveRecord::ConnectionPool"
|
79
|
+
Iodine.on_state(:on_start) do
|
80
|
+
ActiveRecord::Base.connection_pool.extend(Rage::Ext::ActiveRecord::ConnectionPool)
|
81
|
+
ActiveRecord::Base.connection_pool.__init_rage_extension
|
82
|
+
end
|
83
|
+
else
|
84
|
+
puts "WARNING: DB connection is not established - can't patch ActiveRecord::ConnectionPool"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
if Rage.config.internal.rails_mode
|
89
|
+
Rails.configuration.after_initialize(&patch)
|
90
|
+
else
|
91
|
+
patch.call
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
74
95
|
module Router
|
75
96
|
module Strategies
|
76
97
|
end
|
77
98
|
end
|
99
|
+
|
100
|
+
module Ext
|
101
|
+
module ActiveRecord
|
102
|
+
autoload :ConnectionPool, "rage/ext/active_record/connection_pool"
|
103
|
+
end
|
104
|
+
end
|
78
105
|
end
|
79
106
|
|
80
107
|
module RageController
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rage-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Samoilov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -107,6 +107,8 @@ files:
|
|
107
107
|
- lib/rage/controller/api.rb
|
108
108
|
- lib/rage/env.rb
|
109
109
|
- lib/rage/errors.rb
|
110
|
+
- lib/rage/ext/active_record/connection_pool.rb
|
111
|
+
- lib/rage/ext/setup.rb
|
110
112
|
- lib/rage/fiber.rb
|
111
113
|
- lib/rage/fiber_scheduler.rb
|
112
114
|
- lib/rage/logger/json_formatter.rb
|