rage-rb 1.9.0 → 1.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -1
- data/README.md +3 -2
- data/lib/rage/cable/channel.rb +3 -5
- data/lib/rage/cli.rb +155 -9
- data/lib/rage/configuration.rb +22 -2
- data/lib/rage/controller/api.rb +11 -7
- data/lib/rage/ext/active_record/connection_pool.rb +57 -7
- data/lib/rage/ext/setup.rb +72 -9
- data/lib/rage/fiber.rb +41 -1
- data/lib/rage/fiber_scheduler.rb +3 -3
- data/lib/rage/logger/logger.rb +1 -1
- data/lib/rage/setup.rb +2 -2
- data/lib/rage/tasks.rb +33 -0
- data/lib/rage/templates/Rakefile +1 -0
- data/lib/rage/templates/config-application.rb +3 -0
- data/lib/rage/templates/config-environments-production.rb +2 -2
- data/lib/rage/templates/db-templates/app-models-application_record.rb +3 -0
- data/lib/rage/templates/db-templates/db-seeds.rb +9 -0
- data/lib/rage/templates/db-templates/mysql/config-database.yml +24 -0
- data/lib/rage/templates/db-templates/postgresql/config-database.yml +21 -0
- data/lib/rage/templates/db-templates/sqlite3/config-database.yml +19 -0
- data/lib/rage/templates/db-templates/trilogy/config-database.yml +24 -0
- data/lib/rage/templates/lib-tasks-.keep +0 -0
- data/lib/rage/templates/model-template/model.rb +2 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +10 -3
- data/rage.gemspec +2 -1
- metadata +27 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87a1aecc1f62917581dee82c9efe0d56cebc69046500eeaf7f2e73aa7f35f809
|
4
|
+
data.tar.gz: 6e890b8641f214b2cfbebc2fd56bc1f5a32f2f8c2281cbf8f62d3a607d05c4ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c659a925cd991c383714d48e803a93edec1703c5e99aacd73d7c4748be00e77ec8077f6c86ec31c13c8fab373c9ef22bc11fec647b0bc233abec710967a827f4
|
7
|
+
data.tar.gz: a0085405ad42e00730128e942b72abf9944f9344d2e9d5b178b393f8f019b517b430a0b3bf3ac7dd8b8e575392e4f79f7c0cc8531093be3d8100a549f13583f2
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.10.1] - 2024-09-17
|
4
|
+
|
5
|
+
### Fixed
|
6
|
+
|
7
|
+
- Patch AR pool even if `Rake` is defined (#105).
|
8
|
+
|
9
|
+
## [1.10.0] - 2024-09-16
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- Enable Rage Connection Pool by default (#103).
|
14
|
+
- Allow to preconfigure the app for selected database (#104).
|
15
|
+
|
16
|
+
### Added
|
17
|
+
|
18
|
+
- Add `version` and `middleware` CLI commands (#99).
|
19
|
+
|
3
20
|
## [1.9.0] - 2024-08-24
|
4
21
|
|
5
22
|
### Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -59,6 +59,7 @@ Also, see the following integration guides:
|
|
59
59
|
|
60
60
|
- [Rails integration](https://github.com/rage-rb/rage/wiki/Rails-integration)
|
61
61
|
- [RSpec integration](https://github.com/rage-rb/rage/wiki/RSpec-integration)
|
62
|
+
- [WebSockets guide](https://github.com/rage-rb/rage/wiki/WebSockets-guide)
|
62
63
|
|
63
64
|
If you are a first-time contributor, make sure to check the [overview doc](https://github.com/rage-rb/rage/blob/master/OVERVIEW.md) that shows how Rage's core components interact with each other.
|
64
65
|
|
@@ -154,7 +155,7 @@ end
|
|
154
155
|
```ruby
|
155
156
|
class BenchmarksController < ApplicationController
|
156
157
|
def show
|
157
|
-
render json: World.find(rand(10_000))
|
158
|
+
render json: World.find(rand(1..10_000))
|
158
159
|
end
|
159
160
|
end
|
160
161
|
```
|
@@ -172,8 +173,8 @@ Status | Changes
|
|
172
173
|
:white_check_mark: | ~~Automatic code reloading in development with Zeitwerk.~~
|
173
174
|
:white_check_mark: | ~~Support conditional get with `etag` and `last_modified`.~~
|
174
175
|
:white_check_mark: | ~~Expose the `cookies` and `session` objects.~~
|
176
|
+
:white_check_mark: | ~~Implement Iodine-based equivalent of Action Cable.~~
|
175
177
|
⏳ | Expose the `send_data` and `send_file` methods.
|
176
|
-
⏳ | Implement Iodine-based equivalent of Action Cable.
|
177
178
|
|
178
179
|
## Development
|
179
180
|
|
data/lib/rage/cable/channel.rb
CHANGED
@@ -135,7 +135,7 @@ class Rage::Cable::Channel
|
|
135
135
|
end
|
136
136
|
|
137
137
|
is_subscribing = action_name == :subscribed
|
138
|
-
|
138
|
+
should_release_connections = Rage.config.internal.should_manually_release_ar_connections?
|
139
139
|
|
140
140
|
method_name = class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
141
141
|
def __run_#{action_name}(data)
|
@@ -163,12 +163,10 @@ class Rage::Cable::Channel
|
|
163
163
|
#{periodic_timers_chunk}
|
164
164
|
#{rescue_handlers_chunk}
|
165
165
|
|
166
|
-
#{if
|
166
|
+
#{if should_release_connections
|
167
167
|
<<~RUBY
|
168
168
|
ensure
|
169
|
-
|
170
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
171
|
-
end
|
169
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
172
170
|
RUBY
|
173
171
|
end}
|
174
172
|
end
|
data/lib/rage/cli.rb
CHANGED
@@ -2,17 +2,57 @@
|
|
2
2
|
|
3
3
|
require "thor"
|
4
4
|
require "rack"
|
5
|
+
require "rage/version"
|
5
6
|
|
6
7
|
module Rage
|
8
|
+
class CLICodeGenerator < Thor
|
9
|
+
include Thor::Actions
|
10
|
+
|
11
|
+
def self.source_root
|
12
|
+
File.expand_path("templates", __dir__)
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "migration NAME", "Generate a new migration."
|
16
|
+
def migration(name = nil)
|
17
|
+
return help("migration") if name.nil?
|
18
|
+
|
19
|
+
setup
|
20
|
+
Rake::Task["db:new_migration"].invoke(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "model NAME", "Generate a new model."
|
24
|
+
def model(name = nil)
|
25
|
+
return help("model") if name.nil?
|
26
|
+
|
27
|
+
setup
|
28
|
+
migration("create_#{name.pluralize}")
|
29
|
+
@model_name = name.classify
|
30
|
+
template("model-template/model.rb", "app/models/#{name.singularize.underscore}.rb")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def setup
|
36
|
+
@setup ||= begin
|
37
|
+
require "rake"
|
38
|
+
load "Rakefile"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
7
43
|
class CLI < Thor
|
8
44
|
def self.exit_on_failure?
|
9
45
|
true
|
10
46
|
end
|
11
47
|
|
12
48
|
desc "new PATH", "Create a new application."
|
13
|
-
|
49
|
+
option :database, aliases: "-d", desc: "Preconfigure for selected database.", enum: %w(mysql trilogy postgresql sqlite3)
|
50
|
+
option :help, aliases: "-h", desc: "Show this message."
|
51
|
+
def new(path = nil)
|
52
|
+
return help("new") if options.help? || path.nil?
|
53
|
+
|
14
54
|
require "rage/all"
|
15
|
-
|
55
|
+
CLINewAppGenerator.start([path, options[:database]])
|
16
56
|
end
|
17
57
|
|
18
58
|
desc "s", "Start the app server."
|
@@ -115,6 +155,57 @@ module Rage
|
|
115
155
|
IRB.start
|
116
156
|
end
|
117
157
|
|
158
|
+
desc "middleware", "List Rack middleware stack enabled for the application"
|
159
|
+
def middleware
|
160
|
+
environment
|
161
|
+
|
162
|
+
Rage.config.middleware.middlewares.each do |middleware|
|
163
|
+
say "use #{middleware.first.name}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
desc "version", "Return the current version of the framework"
|
168
|
+
def version
|
169
|
+
puts Rage::VERSION
|
170
|
+
end
|
171
|
+
|
172
|
+
map "generate" => :g
|
173
|
+
desc "g TYPE", "Generate new code."
|
174
|
+
subcommand "g", CLICodeGenerator
|
175
|
+
|
176
|
+
map "--tasks" => :tasks
|
177
|
+
desc "--tasks", "See the list of available tasks."
|
178
|
+
def tasks
|
179
|
+
require "io/console"
|
180
|
+
|
181
|
+
tasks = linked_rake_tasks
|
182
|
+
return if tasks.empty?
|
183
|
+
|
184
|
+
_, max_width = IO.console.winsize
|
185
|
+
max_task_name = tasks.max_by { |task| task.name.length }.name.length + 2
|
186
|
+
max_comment = max_width - max_task_name - 8
|
187
|
+
|
188
|
+
tasks.each do |task|
|
189
|
+
comment = task.comment.length <= max_comment ? task.comment : "#{task.comment[0...max_comment - 5]}..."
|
190
|
+
puts sprintf("rage %-#{max_task_name}s # %s", task.name, comment)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def method_missing(method_name, *, &)
|
195
|
+
set_env({})
|
196
|
+
|
197
|
+
if respond_to?(method_name)
|
198
|
+
Rake::Task[method_name].invoke
|
199
|
+
else
|
200
|
+
suggestions = linked_rake_tasks.map(&:name)
|
201
|
+
raise UndefinedCommandError.new(method_name.to_s, suggestions, nil)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def respond_to_missing?(method_name, include_private = false)
|
206
|
+
linked_rake_tasks.any? { |task| task.name == method_name.to_s } || super
|
207
|
+
end
|
208
|
+
|
118
209
|
private
|
119
210
|
|
120
211
|
def environment
|
@@ -126,30 +217,85 @@ module Rage
|
|
126
217
|
end
|
127
218
|
|
128
219
|
def set_env(options)
|
129
|
-
|
220
|
+
if options[:environment]
|
221
|
+
ENV["RAGE_ENV"] = ENV["RAILS_ENV"] = options[:environment]
|
222
|
+
elsif ENV["RAGE_ENV"]
|
223
|
+
ENV["RAILS_ENV"] = ENV["RAGE_ENV"]
|
224
|
+
elsif ENV["RAILS_ENV"]
|
225
|
+
ENV["RAGE_ENV"] = ENV["RAILS_ENV"]
|
226
|
+
else
|
227
|
+
ENV["RAGE_ENV"] = ENV["RAILS_ENV"] = "development"
|
228
|
+
end
|
229
|
+
end
|
130
230
|
|
131
|
-
|
132
|
-
|
133
|
-
|
231
|
+
def linked_rake_tasks
|
232
|
+
require "rake"
|
233
|
+
Rake::TaskManager.record_task_metadata = true
|
234
|
+
load "Rakefile"
|
235
|
+
|
236
|
+
Rake::Task.tasks.select { |task| !task.comment.nil? && task.name.start_with?("db:") }
|
134
237
|
end
|
135
238
|
end
|
136
239
|
|
137
|
-
class
|
240
|
+
class CLINewAppGenerator < Thor::Group
|
138
241
|
include Thor::Actions
|
139
242
|
argument :path, type: :string
|
243
|
+
argument :database, type: :string, required: false
|
140
244
|
|
141
245
|
def self.source_root
|
142
246
|
File.expand_path("templates", __dir__)
|
143
247
|
end
|
144
248
|
|
249
|
+
def setup
|
250
|
+
@use_database = !database.nil?
|
251
|
+
end
|
252
|
+
|
145
253
|
def create_directory
|
146
254
|
empty_directory(path)
|
147
255
|
end
|
148
256
|
|
149
257
|
def copy_files
|
150
|
-
|
258
|
+
inject_templates
|
259
|
+
end
|
260
|
+
|
261
|
+
def install_database
|
262
|
+
return unless @use_database
|
263
|
+
|
264
|
+
@app_name = path.tr("-", "_").downcase
|
265
|
+
append_to_file "#{path}/Gemfile", <<~RUBY
|
266
|
+
|
267
|
+
gem "#{get_db_gem_name}"
|
268
|
+
gem "activerecord"
|
269
|
+
gem "standalone_migrations", require: false
|
270
|
+
RUBY
|
271
|
+
|
272
|
+
inject_templates("db-templates")
|
273
|
+
inject_templates("db-templates/#{database}")
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
def inject_templates(from = nil)
|
279
|
+
root = "#{self.class.source_root}/#{from}"
|
280
|
+
|
281
|
+
Dir.glob("*", base: root).each do |template|
|
282
|
+
next if File.directory?("#{root}/#{template}")
|
283
|
+
|
151
284
|
*template_path_parts, template_name = template.split("-")
|
152
|
-
template(
|
285
|
+
template("#{root}/#{template}", [path, *template_path_parts, template_name].join("/"))
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def get_db_gem_name
|
290
|
+
case database
|
291
|
+
when "mysql"
|
292
|
+
"mysql2"
|
293
|
+
when "trilogy"
|
294
|
+
"trilogy"
|
295
|
+
when "postgresql"
|
296
|
+
"pg"
|
297
|
+
when "sqlite3"
|
298
|
+
"sqlite3"
|
153
299
|
end
|
154
300
|
end
|
155
301
|
end
|
data/lib/rage/configuration.rb
CHANGED
@@ -130,9 +130,13 @@
|
|
130
130
|
#
|
131
131
|
# > Disables the `io_write` hook to fix the ["zero-length iov"](https://bugs.ruby-lang.org/issues/19640) error on Ruby < 3.3.
|
132
132
|
#
|
133
|
-
# •
|
133
|
+
# • _RAGE_DISABLE_AR_POOL_PATCH_
|
134
134
|
#
|
135
|
-
# >
|
135
|
+
# > Disables the `ActiveRecord::ConnectionPool` patch and makes Rage use the original ActiveRecord implementation.
|
136
|
+
#
|
137
|
+
# • _RAGE_DISABLE_AR_WEAK_CONNECTIONS_
|
138
|
+
#
|
139
|
+
# > Instructs Rage to not reuse Active Record connections between different fibers.
|
136
140
|
#
|
137
141
|
class Rage::Configuration
|
138
142
|
attr_accessor :logger
|
@@ -264,6 +268,22 @@ class Rage::Configuration
|
|
264
268
|
class Internal
|
265
269
|
attr_accessor :rails_mode
|
266
270
|
|
271
|
+
def patch_ar_pool?
|
272
|
+
!ENV["RAGE_DISABLE_AR_POOL_PATCH"] && !Rage.env.test?
|
273
|
+
end
|
274
|
+
|
275
|
+
# whether we should manually release AR connections;
|
276
|
+
# AR 7.2+ uses `with_connection` internaly, so we only need to do this for older versions;
|
277
|
+
def should_manually_release_ar_connections?
|
278
|
+
defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.2.0")
|
279
|
+
end
|
280
|
+
|
281
|
+
# whether we should manually reconnect closed AR connections;
|
282
|
+
# AR 7.1+ does this automatically while executing the query;
|
283
|
+
def should_manually_restore_ar_connections?
|
284
|
+
defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("7.1.0")
|
285
|
+
end
|
286
|
+
|
267
287
|
def inspect
|
268
288
|
"#<#{self.class.name}>"
|
269
289
|
end
|
data/lib/rage/controller/api.rb
CHANGED
@@ -76,8 +76,6 @@ class RageController::API
|
|
76
76
|
""
|
77
77
|
end
|
78
78
|
|
79
|
-
activerecord_loaded = defined?(::ActiveRecord)
|
80
|
-
|
81
79
|
wrap_parameters_chunk = if __wrap_parameters_key
|
82
80
|
<<~RUBY
|
83
81
|
wrap_key = self.class.__wrap_parameters_key
|
@@ -95,9 +93,12 @@ class RageController::API
|
|
95
93
|
RUBY
|
96
94
|
end
|
97
95
|
|
96
|
+
query_cache_enabled = defined?(::ActiveRecord)
|
97
|
+
should_release_connections = Rage.config.internal.should_manually_release_ar_connections?
|
98
|
+
|
98
99
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
99
100
|
def __run_#{action}
|
100
|
-
#{if
|
101
|
+
#{if query_cache_enabled
|
101
102
|
<<~RUBY
|
102
103
|
ActiveRecord::Base.connection_pool.enable_query_cache!
|
103
104
|
RUBY
|
@@ -119,12 +120,15 @@ class RageController::API
|
|
119
120
|
#{rescue_handlers_chunk}
|
120
121
|
|
121
122
|
ensure
|
122
|
-
#{if
|
123
|
+
#{if query_cache_enabled
|
123
124
|
<<~RUBY
|
124
125
|
ActiveRecord::Base.connection_pool.disable_query_cache!
|
125
|
-
|
126
|
-
|
127
|
-
|
126
|
+
RUBY
|
127
|
+
end}
|
128
|
+
|
129
|
+
#{if should_release_connections
|
130
|
+
<<~RUBY
|
131
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
128
132
|
RUBY
|
129
133
|
end}
|
130
134
|
|
@@ -24,11 +24,30 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
# reconnect closed connections on checkout;
|
28
|
+
# only included with `Rage.config.should_manually_restore_ar_connections?`
|
29
|
+
module ConnectionWithVerify
|
30
|
+
def connection
|
31
|
+
conn = super
|
32
|
+
|
33
|
+
if conn.__needs_reconnect
|
34
|
+
conn.reconnect!
|
35
|
+
conn.__needs_reconnect = false
|
36
|
+
end
|
37
|
+
|
38
|
+
conn
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if Rage.config.internal.should_manually_restore_ar_connections?
|
42
|
+
prepend ConnectionWithVerify
|
43
|
+
end
|
44
|
+
|
27
45
|
def self.extended(instance)
|
28
46
|
instance.class.alias_method :__checkout__, :checkout
|
29
47
|
instance.class.alias_method :__remove__, :remove
|
30
48
|
|
31
49
|
ActiveRecord::ConnectionAdapters::AbstractAdapter.attr_accessor(:__idle_since)
|
50
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.attr_accessor(:__needs_reconnect)
|
32
51
|
end
|
33
52
|
|
34
53
|
def __init_rage_extension
|
@@ -47,7 +66,7 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
47
66
|
@__checkout_timeout = checkout_timeout
|
48
67
|
|
49
68
|
# how long a connection can be idle for before disconnecting
|
50
|
-
@__idle_timeout =
|
69
|
+
@__idle_timeout = respond_to?(:db_config) ? db_config.idle_timeout : @idle_timeout
|
51
70
|
|
52
71
|
# how often should we check for fibers that wait for a connection for too long
|
53
72
|
@__timeout_worker_frequency = 0.5
|
@@ -65,8 +84,30 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
65
84
|
end
|
66
85
|
end
|
67
86
|
|
87
|
+
# monitor connections health
|
88
|
+
if Rage.config.internal.should_manually_restore_ar_connections?
|
89
|
+
Iodine.run_every(1_000) do
|
90
|
+
i = 0
|
91
|
+
while i < @__connections.length
|
92
|
+
conn = @__connections[i]
|
93
|
+
|
94
|
+
unless conn.__needs_reconnect
|
95
|
+
needs_reconnect = !conn.active? rescue true
|
96
|
+
if needs_reconnect
|
97
|
+
conn.__needs_reconnect = true
|
98
|
+
conn.disconnect!
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
i += 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
@release_connection_channel = "ext:ar-connection-released:#{object_id}"
|
108
|
+
|
68
109
|
# resume blocked fibers once connections become available
|
69
|
-
Iodine.subscribe(
|
110
|
+
Iodine.subscribe(@release_connection_channel) do
|
70
111
|
if @__blocked.length > 0 && @__connections.length > 0
|
71
112
|
f, _ = @__blocked.shift
|
72
113
|
f.resume
|
@@ -75,7 +116,7 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
75
116
|
|
76
117
|
# unsubscribe on shutdown
|
77
118
|
Iodine.on_state(:on_finish) do
|
78
|
-
Iodine.unsubscribe(
|
119
|
+
Iodine.unsubscribe(@release_connection_channel)
|
79
120
|
end
|
80
121
|
end
|
81
122
|
|
@@ -100,7 +141,7 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
100
141
|
if (conn = @__in_use.delete(owner))
|
101
142
|
conn.__idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
102
143
|
@__connections << conn
|
103
|
-
Iodine.publish(
|
144
|
+
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
104
145
|
end
|
105
146
|
|
106
147
|
conn
|
@@ -108,20 +149,27 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
108
149
|
|
109
150
|
# Recover lost connections for the pool.
|
110
151
|
def reap
|
152
|
+
crashed_fibers = nil
|
153
|
+
|
111
154
|
@__in_use.each do |fiber, conn|
|
112
155
|
unless fiber.alive?
|
113
156
|
if conn.active?
|
114
157
|
conn.reset!
|
115
|
-
|
158
|
+
(crashed_fibers ||= []) << fiber
|
116
159
|
else
|
117
160
|
@__in_use.delete(fiber)
|
118
161
|
conn.disconnect!
|
119
162
|
__remove__(conn)
|
163
|
+
self.automatic_reconnect = true
|
120
164
|
@__connections += build_new_connections(1)
|
121
|
-
Iodine.publish(
|
165
|
+
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS) if @__blocked.length > 0
|
122
166
|
end
|
123
167
|
end
|
124
168
|
end
|
169
|
+
|
170
|
+
if crashed_fibers
|
171
|
+
crashed_fibers.each { |fiber| release_connection(fiber) }
|
172
|
+
end
|
125
173
|
end
|
126
174
|
|
127
175
|
# Disconnect all connections that have been idle for at least
|
@@ -135,6 +183,7 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
135
183
|
conn = @__connections[i]
|
136
184
|
if conn.__idle_since && current_time - conn.__idle_since >= minimum_idle
|
137
185
|
conn.__idle_since = nil
|
186
|
+
conn.__needs_reconnect = true
|
138
187
|
conn.disconnect!
|
139
188
|
end
|
140
189
|
i += 1
|
@@ -212,11 +261,12 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
212
261
|
end
|
213
262
|
|
214
263
|
# create a new pool
|
264
|
+
self.automatic_reconnect = true
|
215
265
|
@__connections = build_new_connections
|
216
266
|
|
217
267
|
# notify blocked fibers that there are new connections available
|
218
268
|
[@__blocked.length, @__connections.length].min.times do
|
219
|
-
Iodine.publish(
|
269
|
+
Iodine.publish(@release_connection_channel, "", Iodine::PubSub::PROCESS)
|
220
270
|
end
|
221
271
|
end
|
222
272
|
|
data/lib/rage/ext/setup.rb
CHANGED
@@ -1,19 +1,45 @@
|
|
1
|
+
if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("6")
|
2
|
+
fail "Rage is only compatible with Active Record 6+. Detected Active Record version: #{ActiveRecord.version}."
|
3
|
+
end
|
4
|
+
|
1
5
|
# set ActiveSupport isolation level
|
2
6
|
if defined?(ActiveSupport::IsolatedExecutionState)
|
3
7
|
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
4
8
|
end
|
5
9
|
|
10
|
+
# patch Active Record 6.0 to accept the role argument
|
11
|
+
if defined?(ActiveRecord) && ActiveRecord.version < Gem::Version.create("6.1")
|
12
|
+
%i(active_connections? connection_pool_list clear_active_connections!).each do |m|
|
13
|
+
ActiveRecord::Base.connection_handler.define_singleton_method(m) do |_ = nil|
|
14
|
+
super()
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
6
19
|
# release ActiveRecord connections on yield
|
7
|
-
if defined?(ActiveRecord) &&
|
8
|
-
|
9
|
-
|
10
|
-
|
20
|
+
if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
|
21
|
+
if ENV["RAGE_DISABLE_AR_WEAK_CONNECTIONS"]
|
22
|
+
unless Rage.config.internal.should_manually_release_ar_connections?
|
23
|
+
puts "WARNING: The RAGE_DISABLE_AR_WEAK_CONNECTIONS setting does not have any effect with Active Record 7.2+"
|
24
|
+
end
|
25
|
+
elsif Rage.config.internal.should_manually_release_ar_connections?
|
26
|
+
class Fiber
|
27
|
+
def self.defer(fileno)
|
28
|
+
f = Fiber.current
|
29
|
+
f.__awaited_fileno = fileno
|
11
30
|
|
12
|
-
|
13
|
-
|
14
|
-
|
31
|
+
res = Fiber.yield
|
32
|
+
|
33
|
+
if ActiveRecord::Base.connection_handler.active_connections?(:all)
|
34
|
+
Iodine.defer do
|
35
|
+
if fileno != f.__awaited_fileno || !f.alive?
|
36
|
+
ActiveRecord::Base.connection_handler.connection_pool_list(:all).each { |pool| pool.release_connection(f) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
15
40
|
|
16
|
-
|
41
|
+
res
|
42
|
+
end
|
17
43
|
end
|
18
44
|
end
|
19
45
|
end
|
@@ -30,7 +56,44 @@ if defined?(ActiveRecord::ConnectionAdapters::ConnectionPool)
|
|
30
56
|
end
|
31
57
|
end
|
32
58
|
|
59
|
+
# connect to the database in standalone mode
|
60
|
+
database_url, database_file = ENV["DATABASE_URL"], Rage.root.join("config/database.yml")
|
61
|
+
if defined?(ActiveRecord) && !Rage.config.internal.rails_mode && (database_url || database_file.exist?)
|
62
|
+
# transform database URL to an object
|
63
|
+
database_url_config = if database_url.nil?
|
64
|
+
{}
|
65
|
+
elsif ActiveRecord.version >= Gem::Version.create("6.1.0")
|
66
|
+
ActiveRecord::Base.configurations
|
67
|
+
ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.new(database_url).to_hash
|
68
|
+
else
|
69
|
+
ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(database_url).to_hash
|
70
|
+
end
|
71
|
+
database_url_config.transform_keys!(&:to_s)
|
72
|
+
|
73
|
+
# load config/database.yml
|
74
|
+
if database_file.exist?
|
75
|
+
database_file_config = begin
|
76
|
+
require "yaml"
|
77
|
+
require "erb"
|
78
|
+
YAML.safe_load(ERB.new(database_file.read).result, aliases: true)
|
79
|
+
end
|
80
|
+
|
81
|
+
# merge database URL config into the file config (only if we have one database)
|
82
|
+
database_file_config.transform_values! do |env_config|
|
83
|
+
env_config.all? { |_, v| v.is_a?(Hash) } ? env_config : env_config.merge(database_url_config)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
ActiveRecord::Base.configurations = database_file_config || { Rage.env.to_s => database_url_config }
|
88
|
+
ActiveRecord::Base.establish_connection(Rage.env.to_sym)
|
89
|
+
|
90
|
+
unless defined?(Rake)
|
91
|
+
ActiveRecord::Base.logger = Rage.logger if Rage.logger.debug?
|
92
|
+
ActiveRecord::Base.connection_pool.with_connection {} # validate the connection
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
33
96
|
# patch `ActiveRecord::ConnectionPool`
|
34
|
-
if defined?(ActiveRecord) &&
|
97
|
+
if defined?(ActiveRecord) && Rage.config.internal.patch_ar_pool?
|
35
98
|
Rage.patch_active_record_connection_pool
|
36
99
|
end
|
data/lib/rage/fiber.rb
CHANGED
@@ -39,6 +39,43 @@
|
|
39
39
|
# Many developers see fibers as "lightweight threads" that should be used in conjunction with fiber pools, the same way we use thread pools for threads.<br>
|
40
40
|
# Instead, it makes sense to think of fibers as regular Ruby objects. We don't use a pool of arrays when we need to create an array - we create a new object and let Ruby and the GC do their job.<br>
|
41
41
|
# Same applies to fibers. Feel free to create as many fibers as you need on demand.
|
42
|
+
#
|
43
|
+
# ## Active Record Connections
|
44
|
+
#
|
45
|
+
# Let's consider the following controller, where we update a record in the database:
|
46
|
+
#
|
47
|
+
# ```ruby
|
48
|
+
# class UsersController < RageController::API
|
49
|
+
# def update
|
50
|
+
# User.update!(params[:id], email: params[:email])
|
51
|
+
# render status: :ok
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# ```
|
55
|
+
#
|
56
|
+
# The `User.update!` call here checks out an Active Record connection, and Rage will automatically check it back in once the action is completed. So far so good!
|
57
|
+
#
|
58
|
+
# Let's consider another example:
|
59
|
+
#
|
60
|
+
# ```ruby
|
61
|
+
# require "net/http"
|
62
|
+
#
|
63
|
+
# class UsersController < RageController::API
|
64
|
+
# def update
|
65
|
+
# User.update!(params[:id], email: params[:email]) # takes 5ms
|
66
|
+
# Net::HTTP.post_form(URI("https://mailing.service/update"), { user_id: params[:id] }) # takes 50ms
|
67
|
+
# render status: :ok
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
# ```
|
71
|
+
#
|
72
|
+
# Here, we've added another step: once the record is updated, we will send a request to update the user's data in the mailing list service.
|
73
|
+
#
|
74
|
+
# However, in this case, we want to release the Active Record connection before the action is completed. You can see that we need the connection only for the `User.update!` call.
|
75
|
+
# The next 50ms the code will spend waiting for the HTTP request to finish, and if we don't release the Active Record connection right away, other fibers won't be able to use it.
|
76
|
+
#
|
77
|
+
# Active Record 7.2 handles this case by using [#with_connection](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html#method-i-with_connection) internally.
|
78
|
+
# With older Active Record versions, Rage handles this case on its own by keeping track of blocking calls and releasing Active Record connections between them.
|
42
79
|
class Fiber
|
43
80
|
# @private
|
44
81
|
AWAIT_ERROR_MESSAGE = "err"
|
@@ -81,6 +118,9 @@ class Fiber
|
|
81
118
|
"block:#{object_id}:#{@__block_channel_i}"
|
82
119
|
end
|
83
120
|
|
121
|
+
# @private
|
122
|
+
attr_accessor :__awaited_fileno
|
123
|
+
|
84
124
|
# @private
|
85
125
|
# pause a fiber and resume in the next iteration of the event loop
|
86
126
|
def self.pause
|
@@ -138,7 +178,7 @@ class Fiber
|
|
138
178
|
end
|
139
179
|
end
|
140
180
|
|
141
|
-
Fiber.
|
181
|
+
Fiber.defer(-1)
|
142
182
|
Iodine.defer { Iodine.unsubscribe("await:#{f.object_id}") }
|
143
183
|
|
144
184
|
# if num_wait_for is not 0 means we exited prematurely because of an error
|
data/lib/rage/fiber_scheduler.rb
CHANGED
@@ -14,9 +14,9 @@ class Rage::FiberScheduler
|
|
14
14
|
f = Fiber.current
|
15
15
|
::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { |err| f.resume(err) }
|
16
16
|
|
17
|
-
err = Fiber.defer
|
18
|
-
if err
|
19
|
-
|
17
|
+
err = Fiber.defer(io.fileno)
|
18
|
+
if err && err < 0
|
19
|
+
err
|
20
20
|
else
|
21
21
|
events
|
22
22
|
end
|
data/lib/rage/logger/logger.rb
CHANGED
data/lib/rage/setup.rb
CHANGED
@@ -9,9 +9,9 @@ end
|
|
9
9
|
# Run application initializers
|
10
10
|
Dir["#{Rage.root}/config/initializers/**/*.rb"].each { |initializer| load(initializer) }
|
11
11
|
|
12
|
+
require "rage/ext/setup"
|
13
|
+
|
12
14
|
# Load application classes
|
13
15
|
Rage.code_loader.setup
|
14
16
|
|
15
17
|
require_relative "#{Rage.root}/config/routes"
|
16
|
-
|
17
|
-
require "rage/ext/setup"
|
data/lib/rage/tasks.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
begin
|
2
|
+
require "standalone_migrations"
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
class Rage::Tasks
|
7
|
+
class << self
|
8
|
+
def init
|
9
|
+
load_db_tasks if defined?(StandaloneMigrations)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def load_db_tasks
|
15
|
+
StandaloneMigrations::Configurator.prepend(Module.new do
|
16
|
+
def configuration_file
|
17
|
+
@path ||= begin
|
18
|
+
@__tempfile = Tempfile.new
|
19
|
+
@__tempfile.write <<~YAML
|
20
|
+
config:
|
21
|
+
database: config/database.yml
|
22
|
+
YAML
|
23
|
+
@__tempfile.close
|
24
|
+
|
25
|
+
@__tempfile.path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end)
|
29
|
+
|
30
|
+
StandaloneMigrations::Tasks.load_tasks
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/rage/templates/Rakefile
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
Rage.configure do
|
2
2
|
# Specify the number of server processes to run. Defaults to number of CPU cores.
|
3
|
-
# config.server.workers_count = ENV.fetch("WEB_CONCURRENCY", 1)
|
3
|
+
# config.server.workers_count = ENV.fetch("WEB_CONCURRENCY", 1).to_i
|
4
4
|
|
5
5
|
# Specify the port the server will listen on.
|
6
6
|
config.server.port = 3000
|
7
7
|
|
8
8
|
# Specify the logger
|
9
|
-
config.logger = Rage::Logger.new(
|
9
|
+
config.logger = Rage::Logger.new(STDOUT)
|
10
10
|
config.log_level = Logger::INFO
|
11
11
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# This file should ensure the existence of records required to run the application in every environment (production,
|
2
|
+
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
|
3
|
+
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
8
|
+
# MovieGenre.find_or_create_by!(name: genre_name)
|
9
|
+
# end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
##
|
2
|
+
# MySQL. Versions 5.5.8 and up are supported.
|
3
|
+
#
|
4
|
+
default: &default
|
5
|
+
adapter: mysql2
|
6
|
+
encoding: utf8mb4
|
7
|
+
pool: <%%= ENV.fetch("DB_MAX_CONNECTIONS") { 5 } %>
|
8
|
+
username: root
|
9
|
+
password:
|
10
|
+
socket: /tmp/mysql.sock
|
11
|
+
|
12
|
+
development:
|
13
|
+
<<: *default
|
14
|
+
database: <%= @app_name %>_development
|
15
|
+
|
16
|
+
test:
|
17
|
+
<<: *default
|
18
|
+
database: <%= @app_name %>_test
|
19
|
+
|
20
|
+
production:
|
21
|
+
<<: *default
|
22
|
+
database: <%= @app_name %>_production
|
23
|
+
username: <%= @app_name %>
|
24
|
+
password: <%%= ENV["<%= @app_name.upcase %>_DATABASE_PASSWORD"] %>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
##
|
2
|
+
# PostgreSQL. Versions 9.3 and up are supported.
|
3
|
+
#
|
4
|
+
default: &default
|
5
|
+
adapter: postgresql
|
6
|
+
encoding: unicode
|
7
|
+
pool: <%%= ENV.fetch("DB_MAX_CONNECTIONS") { 5 } %>
|
8
|
+
|
9
|
+
development:
|
10
|
+
<<: *default
|
11
|
+
database: <%= @app_name %>_development
|
12
|
+
|
13
|
+
test:
|
14
|
+
<<: *default
|
15
|
+
database: <%= @app_name %>_test
|
16
|
+
|
17
|
+
production:
|
18
|
+
<<: *default
|
19
|
+
database: <%= @app_name %>_production
|
20
|
+
username: <%= @app_name %>
|
21
|
+
password: <%%= ENV["<%= @app_name.upcase %>_DATABASE_PASSWORD"] %>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
##
|
2
|
+
# SQLite. Versions 3.8.0 and up are supported.
|
3
|
+
#
|
4
|
+
default: &default
|
5
|
+
adapter: sqlite3
|
6
|
+
pool: <%%= ENV.fetch("DB_MAX_CONNECTIONS") { 5 } %>
|
7
|
+
timeout: 5000
|
8
|
+
|
9
|
+
development:
|
10
|
+
<<: *default
|
11
|
+
database: storage/development.sqlite3
|
12
|
+
|
13
|
+
test:
|
14
|
+
<<: *default
|
15
|
+
database: storage/test.sqlite3
|
16
|
+
|
17
|
+
production:
|
18
|
+
<<: *default
|
19
|
+
database: storage/production.sqlite3
|
@@ -0,0 +1,24 @@
|
|
1
|
+
##
|
2
|
+
# MySQL. Versions 5.5.8 and up are supported.
|
3
|
+
#
|
4
|
+
default: &default
|
5
|
+
adapter: trilogy
|
6
|
+
encoding: utf8mb4
|
7
|
+
pool: <%%= ENV.fetch("DB_MAX_THREADS") { 5 } %>
|
8
|
+
username: root
|
9
|
+
password:
|
10
|
+
socket: /tmp/mysql.sock
|
11
|
+
|
12
|
+
development:
|
13
|
+
<<: *default
|
14
|
+
database: <%= @app_name %>_development
|
15
|
+
|
16
|
+
test:
|
17
|
+
<<: *default
|
18
|
+
database: <%= @app_name %>_test
|
19
|
+
|
20
|
+
production:
|
21
|
+
<<: *default
|
22
|
+
database: <%= @app_name %>_production
|
23
|
+
username: <%= @app_name %>
|
24
|
+
password: <%%= ENV["<%= @app_name.upcase %>_DATABASE_PASSWORD"] %>
|
File without changes
|
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
@@ -63,10 +63,12 @@ module Rage
|
|
63
63
|
patch = proc do
|
64
64
|
is_connected = ActiveRecord::Base.connection_pool rescue false
|
65
65
|
if is_connected
|
66
|
-
puts "INFO: Patching ActiveRecord::ConnectionPool"
|
66
|
+
Iodine.on_state(:pre_start) { puts "INFO: Patching ActiveRecord::ConnectionPool" }
|
67
67
|
Iodine.on_state(:on_start) do
|
68
|
-
ActiveRecord::Base.
|
69
|
-
|
68
|
+
ActiveRecord::Base.connection_handler.connection_pool_list(:all).each do |pool|
|
69
|
+
pool.extend(Rage::Ext::ActiveRecord::ConnectionPool)
|
70
|
+
pool.__init_rage_extension
|
71
|
+
end
|
70
72
|
end
|
71
73
|
else
|
72
74
|
puts "WARNING: DB connection is not established - can't patch ActiveRecord::ConnectionPool"
|
@@ -80,6 +82,10 @@ module Rage
|
|
80
82
|
end
|
81
83
|
end
|
82
84
|
|
85
|
+
def self.load_tasks
|
86
|
+
Rage::Tasks.init
|
87
|
+
end
|
88
|
+
|
83
89
|
# @private
|
84
90
|
def self.with_middlewares(app, middlewares)
|
85
91
|
middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
|
@@ -111,6 +117,7 @@ module Rage
|
|
111
117
|
end
|
112
118
|
end
|
113
119
|
|
120
|
+
autoload :Tasks, "rage/tasks"
|
114
121
|
autoload :Cookies, "rage/cookies"
|
115
122
|
autoload :Session, "rage/session"
|
116
123
|
autoload :Cable, "rage/cable/cable"
|
data/rage.gemspec
CHANGED
@@ -29,7 +29,8 @@ Gem::Specification.new do |spec|
|
|
29
29
|
|
30
30
|
spec.add_dependency "thor", "~> 1.0"
|
31
31
|
spec.add_dependency "rack", "~> 2.0"
|
32
|
-
spec.add_dependency "rage-iodine", "~>
|
32
|
+
spec.add_dependency "rage-iodine", "~> 4.0"
|
33
33
|
spec.add_dependency "zeitwerk", "~> 2.6"
|
34
34
|
spec.add_dependency "rack-test", "~> 2.1"
|
35
|
+
spec.add_dependency "rake", ">= 12.0"
|
35
36
|
end
|
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.10.1
|
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-
|
11
|
+
date: 2024-09-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '4.0'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '4.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: zeitwerk
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '2.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '12.0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '12.0'
|
83
97
|
description:
|
84
98
|
email:
|
85
99
|
- rsamoi@icloud.com
|
@@ -145,6 +159,7 @@ files:
|
|
145
159
|
- lib/rage/session.rb
|
146
160
|
- lib/rage/setup.rb
|
147
161
|
- lib/rage/sidekiq_session.rb
|
162
|
+
- lib/rage/tasks.rb
|
148
163
|
- lib/rage/templates/Gemfile
|
149
164
|
- lib/rage/templates/Rakefile
|
150
165
|
- lib/rage/templates/app-controllers-application_controller.rb
|
@@ -155,8 +170,16 @@ files:
|
|
155
170
|
- lib/rage/templates/config-initializers-.keep
|
156
171
|
- lib/rage/templates/config-routes.rb
|
157
172
|
- lib/rage/templates/config.ru
|
173
|
+
- lib/rage/templates/db-templates/app-models-application_record.rb
|
174
|
+
- lib/rage/templates/db-templates/db-seeds.rb
|
175
|
+
- lib/rage/templates/db-templates/mysql/config-database.yml
|
176
|
+
- lib/rage/templates/db-templates/postgresql/config-database.yml
|
177
|
+
- lib/rage/templates/db-templates/sqlite3/config-database.yml
|
178
|
+
- lib/rage/templates/db-templates/trilogy/config-database.yml
|
158
179
|
- lib/rage/templates/lib-.keep
|
180
|
+
- lib/rage/templates/lib-tasks-.keep
|
159
181
|
- lib/rage/templates/log-.keep
|
182
|
+
- lib/rage/templates/model-template/model.rb
|
160
183
|
- lib/rage/templates/public-.keep
|
161
184
|
- lib/rage/uploaded_file.rb
|
162
185
|
- lib/rage/version.rb
|