rage-rb 0.7.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c3041038ae63ae245e261a7a2096195ab56291d236414e086646ae96a4a6d16
4
- data.tar.gz: a64817ded16716fe714310c7318d8e1d318454615da43d37055b333e76778fa2
3
+ metadata.gz: 9bb19c01aee898bea43a41450feeb655f94bde8277a4f9c18cab6b9d1accbb47
4
+ data.tar.gz: fe4806d8a2bfbb71496a720371cc8416b5283c9b8284c1a72ec46384ee234348
5
5
  SHA512:
6
- metadata.gz: 350f5150012852a3ca2532c031ce8ef28338da6d985ef66dff59095dea6b3dbbc4498826996876722bc5b7991683fed82c2fd3854313f7ed59f9fff20aaf4315
7
- data.tar.gz: 84cd798056a43c8eb0e94809f72f6b0ac602076aa904831a3a049a8ed13352cad9321329233063343b73e65c0fbf1521564b0cee6f3696c89877a0af15191c94
6
+ metadata.gz: 43d06881f297512724588c06637a29eefcb8308fc1c086a09b013c4291d4ca58cf7b2ab482b1c1276a3e4a88544a71e95289268cc896d479cb92135f31555bf8
7
+ data.tar.gz: 9af4f8816208451c18ff3b31d7d5a7e230561af93753d1ee0f8177f515b92f3dd2c47007b7f65be823ceeb69b6338683a05c57eb1a13e17cfe769b87e4ef2540
data/.yardopts CHANGED
@@ -1 +1 @@
1
- --exclude lib/rage/templates --markup markdown --no-private -o doc
1
+ --exclude lib/rage/templates --exclude lib/rage/rspec --exclude lib/rage/rails --markup markdown --no-private -o doc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2024-03-13
4
+
5
+ ### Added
6
+
7
+ - RSpec integration (#60).
8
+ - Add DNS cache (#65).
9
+ - Allow to disable the `FiberScheduler#io_write` hook (#63).
10
+
11
+ ### Fixed
12
+
13
+ - Preload fiber ID (#62).
14
+ - Release ActiveRecord connections on yield (#66).
15
+ - Logger fixes (#64).
16
+ - Fix publish calls in cluster mode (#67).
17
+
3
18
  ## [0.7.0] - 2024-01-09
4
19
 
5
20
  - Add conditional GET using `stale?` by [@tonekk](https://github.com/tonekk) (#55).
data/README.md CHANGED
@@ -150,8 +150,8 @@ Status | Changes
150
150
  :white_check_mark: | ~~Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br>&emsp;• add the `resources` route helper;<br>&emsp;• add the `namespace` route helper;~~
151
151
  :white_check_mark: | ~~Add request logging.~~
152
152
  :white_check_mark: | ~~Automatic code reloading in development with Zeitwerk.~~
153
+ :white_check_mark: | ~~Support conditional get with `etag` and `last_modified`.~~
153
154
  ⏳ | Expose the `send_data` and `send_file` methods.
154
- ⏳ | Support conditional get with `etag` and `last_modified`.
155
155
  ⏳ | Expose the `cookies` and `session` objects.
156
156
  ⏳ | Implement Iodine-based equivalent of Action Cable.
157
157
 
data/lib/rage/fiber.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Fiber
4
+ # @private
4
5
  AWAIT_ERROR_MESSAGE = "err"
5
6
 
6
7
  # @private
@@ -23,14 +24,14 @@ class Fiber
23
24
  @__err
24
25
  end
25
26
 
26
- # @private
27
- def __get_id
28
- @__rage_id ||= object_id.to_s
27
+ # @private
28
+ def __set_id
29
+ @__rage_id = object_id.to_s
29
30
  end
30
31
 
31
- # @private
32
- def __yielded?
33
- !@__rage_id.nil?
32
+ # @private
33
+ def __get_id
34
+ @__rage_id
34
35
  end
35
36
 
36
37
  # @private
@@ -49,6 +50,13 @@ class Fiber
49
50
  Fiber.yield
50
51
  end
51
52
 
53
+ # @private
54
+ # under normal circumstances, the method is a copy of `yield`, but it can be overriden to perform
55
+ # additional steps on yielding, e.g. releasing AR connections; see "lib/rage/rails.rb"
56
+ class << self
57
+ alias_method :defer, :yield
58
+ end
59
+
52
60
  # Wait on several fibers at the same time. Calling this method will automatically pause the current fiber, allowing the
53
61
  # server to process other requests. Once all fibers have completed, the current fiber will be automatically resumed.
54
62
  #
@@ -7,13 +7,14 @@ class Rage::FiberScheduler
7
7
 
8
8
  def initialize
9
9
  @root_fiber = Fiber.current
10
+ @dns_cache = {}
10
11
  end
11
12
 
12
13
  def io_wait(io, events, timeout = nil)
13
14
  f = Fiber.current
14
15
  ::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { |err| f.resume(err) }
15
16
 
16
- err = Fiber.yield
17
+ err = Fiber.defer
17
18
  if err == Errno::ETIMEDOUT::Errno
18
19
  0
19
20
  else
@@ -49,13 +50,15 @@ class Rage::FiberScheduler
49
50
  end
50
51
  end
51
52
 
52
- def io_write(io, buffer, length, offset = 0)
53
- bytes_to_write = length
54
- bytes_to_write = buffer.size if length == 0
53
+ unless ENV["RAGE_DISABLE_IO_WRITE"]
54
+ def io_write(io, buffer, length, offset = 0)
55
+ bytes_to_write = length
56
+ bytes_to_write = buffer.size if length == 0
55
57
 
56
- ::Iodine::Scheduler.write(io.fileno, buffer.get_string, bytes_to_write, offset)
58
+ ::Iodine::Scheduler.write(io.fileno, buffer.get_string, bytes_to_write, offset)
57
59
 
58
- bytes_to_write - offset
60
+ bytes_to_write - offset
61
+ end
59
62
  end
60
63
 
61
64
  def kernel_sleep(duration = nil)
@@ -77,7 +80,13 @@ class Rage::FiberScheduler
77
80
  # end
78
81
 
79
82
  def address_resolve(hostname)
80
- Resolv.getaddresses(hostname)
83
+ @dns_cache[hostname] ||= begin
84
+ ::Iodine.run_after(60_000) do
85
+ @dns_cache[hostname] = nil
86
+ end
87
+
88
+ Resolv.getaddresses(hostname)
89
+ end
81
90
  end
82
91
 
83
92
  def block(_blocker, timeout = nil)
@@ -100,7 +109,7 @@ class Rage::FiberScheduler
100
109
  end
101
110
 
102
111
  def unblock(_blocker, fiber)
103
- ::Iodine.publish(fiber.__block_channel, "")
112
+ ::Iodine.publish(fiber.__block_channel, "", Iodine::PubSub::PROCESS)
104
113
  end
105
114
 
106
115
  def fiber(&block)
@@ -109,6 +118,7 @@ class Rage::FiberScheduler
109
118
  fiber = if parent == @root_fiber
110
119
  # the fiber to wrap a request in
111
120
  Fiber.new(blocking: false) do
121
+ Fiber.current.__set_id
112
122
  Fiber.current.__set_result(block.call)
113
123
  end
114
124
  else
@@ -119,10 +129,10 @@ class Rage::FiberScheduler
119
129
  Thread.current[:rage_logger] = logger
120
130
  Fiber.current.__set_result(block.call)
121
131
  # send a message for `Fiber.await` to work
122
- Iodine.publish("await:#{parent.object_id}", "") if parent.alive?
132
+ Iodine.publish("await:#{parent.object_id}", "", Iodine::PubSub::PROCESS) if parent.alive?
123
133
  rescue Exception => e
124
134
  Fiber.current.__set_err(e)
125
- Iodine.publish("await:#{parent.object_id}", Fiber::AWAIT_ERROR_MESSAGE) if parent.alive?
135
+ Iodine.publish("await:#{parent.object_id}", Fiber::AWAIT_ERROR_MESSAGE, Iodine::PubSub::PROCESS) if parent.alive?
126
136
  end
127
137
  end
128
138
 
@@ -7,7 +7,7 @@ class Rage::JSONFormatter
7
7
  end
8
8
 
9
9
  def call(severity, timestamp, _, message)
10
- logger = Thread.current[:rage_logger]
10
+ logger = Thread.current[:rage_logger] || { tags: [], context: {} }
11
11
  tags, context = logger[:tags], logger[:context]
12
12
 
13
13
  if !context.empty?
@@ -29,6 +29,8 @@ class Rage::JSONFormatter
29
29
  tags_msg = "{\"tags\":[\"#{tags[0]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
30
30
  elsif tags.length == 2
31
31
  tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\"],\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
32
+ elsif tags.length == 0
33
+ tags_msg = "{\"timestamp\":\"#{timestamp}\",\"pid\":\"#{@pid}\",\"level\":\"#{severity}\""
32
34
  else
33
35
  tags_msg = "{\"tags\":[\"#{tags[0]}\",\"#{tags[1]}\""
34
36
  i = 2
@@ -83,7 +83,7 @@ class Rage::Logger
83
83
  # Rage.logger.info "cache miss"
84
84
  # end
85
85
  def with_context(context)
86
- old_context = Thread.current[:rage_logger][:context]
86
+ old_context = (Thread.current[:rage_logger] ||= { tags: [], context: {} })[:context]
87
87
 
88
88
  if old_context.empty? # there's nothing in the context yet
89
89
  Thread.current[:rage_logger][:context] = context
@@ -92,8 +92,6 @@ class Rage::Logger
92
92
  end
93
93
 
94
94
  yield(self)
95
- true
96
-
97
95
  ensure
98
96
  Thread.current[:rage_logger][:context] = old_context
99
97
  end
@@ -106,17 +104,20 @@ class Rage::Logger
106
104
  # Rage.logger.info "success"
107
105
  # end
108
106
  def tagged(tag)
109
- Thread.current[:rage_logger][:tags] << tag
110
-
107
+ (Thread.current[:rage_logger] ||= { tags: [], context: {} })[:tags] << tag
111
108
  yield(self)
112
- true
113
-
114
109
  ensure
115
110
  Thread.current[:rage_logger][:tags].pop
116
111
  end
117
112
 
118
113
  alias_method :with_tag, :tagged
119
114
 
115
+ def debug? = @level <= Logger::DEBUG
116
+ def error? = @level <= Logger::ERROR
117
+ def fatal? = @level <= Logger::FATAL
118
+ def info? = @level <= Logger::INFO
119
+ def warn? = @level <= Logger::WARN
120
+
120
121
  private
121
122
 
122
123
  def define_log_methods
@@ -7,7 +7,7 @@ class Rage::TextFormatter
7
7
  end
8
8
 
9
9
  def call(severity, timestamp, _, message)
10
- logger = Thread.current[:rage_logger]
10
+ logger = Thread.current[:rage_logger] || { tags: [], context: {} }
11
11
  tags, context = logger[:tags], logger[:context]
12
12
 
13
13
  if !context.empty?
@@ -29,6 +29,8 @@ class Rage::TextFormatter
29
29
  tags_msg = "[#{tags[0]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
30
30
  elsif tags.length == 2
31
31
  tags_msg = "[#{tags[0]}][#{tags[1]}] timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
32
+ elsif tags.length == 0
33
+ tags_msg = "timestamp=#{timestamp} pid=#{@pid} level=#{severity}"
32
34
  else
33
35
  tags_msg = "[#{tags[0]}][#{tags[1]}]"
34
36
  i = 2
@@ -17,7 +17,7 @@ class Rage::FiberWrapper
17
17
  @app.call(env)
18
18
  ensure
19
19
  # notify Iodine the request can now be resumed
20
- Iodine.publish(Fiber.current.__get_id, "", Iodine::PubSub::PROCESS) if Fiber.current.__yielded?
20
+ Iodine.publish(Fiber.current.__get_id, "", Iodine::PubSub::PROCESS)
21
21
  end
22
22
 
23
23
  # the fiber encountered blocking IO and yielded; instruct Iodine to pause the request
data/lib/rage/rails.rb CHANGED
@@ -30,6 +30,21 @@ if defined?(ActiveRecord)
30
30
  end
31
31
  end
32
32
 
33
+ # release ActiveRecord connections on yield
34
+ if defined?(ActiveRecord)
35
+ class Fiber
36
+ def self.defer
37
+ res = Fiber.yield
38
+
39
+ if ActiveRecord::Base.connection_pool.active_connection?
40
+ ActiveRecord::Base.connection_handler.clear_active_connections!
41
+ end
42
+
43
+ res
44
+ end
45
+ end
46
+ end
47
+
33
48
  # plug into Rails' Zeitwerk instance to reload the code
34
49
  Rails.autoloaders.main.on_setup do
35
50
  if Iodine.running?
data/lib/rage/rspec.rb ADDED
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+ require "json"
5
+
6
+ # set up environment
7
+ ENV["RAGE_ENV"] ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test"
8
+
9
+ # load the app
10
+ require "bundler/setup"
11
+ require "rage"
12
+ require_relative "#{Rage.root}/config/application"
13
+
14
+ # verify the environment
15
+ abort("The test suite is running in #{Rage.env} mode instead of 'test'!") unless Rage.env.test?
16
+
17
+ # mock fiber methods as RSpec tests don't run concurrently
18
+ class Fiber
19
+ def self.schedule(&block)
20
+ fiber = Fiber.new(blocking: true) do
21
+ Fiber.current.__set_id
22
+ Fiber.current.__set_result(block.call)
23
+ end
24
+ fiber.resume
25
+
26
+ fiber
27
+ end
28
+
29
+ def self.await(_)
30
+ # no-op
31
+ end
32
+ end
33
+
34
+ # define request helpers
35
+ module RageRequestHelpers
36
+ include Rack::Test::Methods
37
+
38
+ alias_method :response, :last_response
39
+
40
+ APP = Rack::Builder.parse_file("#{Rage.root}/config.ru").yield_self do |app|
41
+ app.is_a?(Array) ? app[0] : app
42
+ end
43
+
44
+ def app
45
+ APP
46
+ end
47
+
48
+ %w(get options head).each do |method_name|
49
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
50
+ def #{method_name}(path, params: {}, headers: {})
51
+ request("#{method_name.upcase}", path, params: params, headers: headers)
52
+ end
53
+ RUBY
54
+ end
55
+
56
+ %w(post put patch delete).each do |method_name|
57
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
58
+ def #{method_name}(path, params: {}, headers: {}, as: nil)
59
+ if as == :json
60
+ params = params.to_json
61
+ headers["content-type"] = "application/json"
62
+ end
63
+
64
+ request("#{method_name.upcase}", path, params: params, headers: headers.merge("IODINE_HAS_BODY" => !params.empty?))
65
+ end
66
+ RUBY
67
+ end
68
+
69
+ def request(method, path, params: {}, headers: {})
70
+ if headers.any?
71
+ headers = headers.transform_keys do |k|
72
+ if k.downcase == "content-type"
73
+ "CONTENT_TYPE"
74
+ elsif k.downcase == "content-length"
75
+ "CONTENT_LENGTH"
76
+ elsif k.upcase == k
77
+ k
78
+ else
79
+ "HTTP_#{k.tr("-", "_").upcase! || k}"
80
+ end
81
+ end
82
+ end
83
+
84
+ custom_request(method, path, params, headers)
85
+ end
86
+
87
+ def host!(host)
88
+ @__host = host
89
+ end
90
+
91
+ def default_host
92
+ @__host || "example.org"
93
+ end
94
+ end
95
+
96
+ # include request helpers
97
+ RSpec.configure do |config|
98
+ config.include(RageRequestHelpers, type: :request)
99
+ end
100
+
101
+ # patch MockResponse class
102
+ class Rack::MockResponse
103
+ def parsed_body
104
+ if headers["content-type"].start_with?("application/json")
105
+ JSON.parse(body)
106
+ else
107
+ body
108
+ end
109
+ end
110
+
111
+ def code
112
+ status.to_s
113
+ end
114
+
115
+ alias_method :response_code, :status
116
+ end
117
+
118
+ # define http status matcher
119
+ RSpec::Matchers.matcher :have_http_status do |expected|
120
+ codes = Rack::Utils::SYMBOL_TO_STATUS_CODE
121
+
122
+ failure_message do |response|
123
+ actual = response.status
124
+
125
+ if expected.is_a?(Integer)
126
+ "expected the response to have status code #{expected} but it was #{actual}"
127
+ elsif expected == :success
128
+ "expected the response to have a success status code (2xx) but it was #{actual}"
129
+ elsif expected == :error
130
+ "expected the response to have an error status code (5xx) but it was #{actual}"
131
+ elsif expected == :missing
132
+ "expected the response to have a missing status code (404) but it was #{actual}"
133
+ else
134
+ "expected the response to have status code :#{expected} (#{codes[expected]}) but it was :#{codes.key(actual)} (#{actual})"
135
+ end
136
+ end
137
+
138
+ failure_message_when_negated do |response|
139
+ actual = response.status
140
+
141
+ if expected.is_a?(Integer)
142
+ "expected the response not to have status code #{expected} but it was #{actual}"
143
+ elsif expected == :success
144
+ "expected the response not to have a success status code (2xx) but it was #{actual}"
145
+ elsif expected == :error
146
+ "expected the response not to have an error status code (5xx) but it was #{actual}"
147
+ elsif expected == :missing
148
+ "expected the response not to have a missing status code (404) but it was #{actual}"
149
+ else
150
+ "expected the response not to have status code :#{expected} (#{codes[expected]}) but it was :#{codes.key(actual)} (#{actual})"
151
+ end
152
+ end
153
+
154
+ match do |response|
155
+ actual = response.status
156
+
157
+ case expected
158
+ when :success
159
+ actual >= 200 && actual < 300
160
+ when :error
161
+ actual >= 500
162
+ when :missing
163
+ actual == 404
164
+ when Symbol
165
+ actual == codes.fetch(expected)
166
+ else
167
+ actual == expected
168
+ end
169
+ end
170
+ end
171
+
172
+ if defined? RSpec::Rails::Matchers
173
+ module RSpec::Rails::Matchers
174
+ def have_http_status(_)
175
+ super
176
+ end
177
+ end
178
+ end
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "0.7.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/rage.gemspec CHANGED
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency "rack", "~> 2.0"
32
32
  spec.add_dependency "rage-iodine", "~> 3.0"
33
33
  spec.add_dependency "zeitwerk", "~> 2.6"
34
+ spec.add_dependency "rack-test", "~> 2.1"
34
35
  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: 0.7.0
4
+ version: 1.0.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-01-09 00:00:00.000000000 Z
11
+ date: 2024-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '2.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-test
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.1'
69
83
  description:
70
84
  email:
71
85
  - rsamoi@icloud.com
@@ -112,6 +126,7 @@ files:
112
126
  - lib/rage/router/handler_storage.rb
113
127
  - lib/rage/router/node.rb
114
128
  - lib/rage/router/strategies/host.rb
129
+ - lib/rage/rspec.rb
115
130
  - lib/rage/setup.rb
116
131
  - lib/rage/sidekiq_session.rb
117
132
  - lib/rage/templates/Gemfile