rage-rb 0.7.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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