ruby-object-framework 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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/API.md +78 -0
  3. data/README.md +250 -0
  4. data/THIRD_PARTY_NOTICES.md +45 -0
  5. data/examples/guides/01_plain_use_case.rb +13 -0
  6. data/examples/guides/02_composed_application.rb +25 -0
  7. data/examples/guides/03_order_domain.rb +14 -0
  8. data/examples/guides/04_scoped_resources.rb +38 -0
  9. data/examples/guides/05_mapping.rb +39 -0
  10. data/examples/guides/06_associations.rb +49 -0
  11. data/examples/guides/07_http.rb +17 -0
  12. data/examples/guides/08_errors.rb +20 -0
  13. data/examples/guides/09_sqlite.rb +38 -0
  14. data/examples/guides/10_grpc.rb +26 -0
  15. data/examples/guides/11_ractor.rb +15 -0
  16. data/examples/guides/12_box.rb +11 -0
  17. data/examples/guides/13_testing.rb +24 -0
  18. data/examples/guides/manifest.yml +62 -0
  19. data/examples/orders/application.rb +51 -0
  20. data/examples/orders/contracts.rb +19 -0
  21. data/examples/orders/domain.rb +35 -0
  22. data/examples/orders/export_proto.rb +5 -0
  23. data/examples/orders/grpc_client.py +24 -0
  24. data/examples/orders/grpc_server.rb +23 -0
  25. data/examples/orders/server.rb +18 -0
  26. data/lib/object_framework/application.rb +136 -0
  27. data/lib/object_framework/capabilities.rb +59 -0
  28. data/lib/object_framework/errors.rb +85 -0
  29. data/lib/object_framework/execution.rb +184 -0
  30. data/lib/object_framework/grpc/messages.rb +30 -0
  31. data/lib/object_framework/grpc/server.rb +264 -0
  32. data/lib/object_framework/grpc/service.rb +55 -0
  33. data/lib/object_framework/http/io.rb +127 -0
  34. data/lib/object_framework/http/presentation.rb +55 -0
  35. data/lib/object_framework/http/protocol.rb +209 -0
  36. data/lib/object_framework/http/router.rb +80 -0
  37. data/lib/object_framework/http/server.rb +279 -0
  38. data/lib/object_framework/http2/connection.rb +399 -0
  39. data/lib/object_framework/http2/frame.rb +21 -0
  40. data/lib/object_framework/http2/hpack.rb +206 -0
  41. data/lib/object_framework/http2/hpack_tables.rb +333 -0
  42. data/lib/object_framework/http2/inbound.rb +75 -0
  43. data/lib/object_framework/memory/storage.rb +97 -0
  44. data/lib/object_framework/persistence/mapping.rb +150 -0
  45. data/lib/object_framework/persistence/query.rb +97 -0
  46. data/lib/object_framework/persistence/session.rb +173 -0
  47. data/lib/object_framework/persistence/transaction.rb +54 -0
  48. data/lib/object_framework/protobuf.rb +268 -0
  49. data/lib/object_framework/sqlite/compiler.rb +77 -0
  50. data/lib/object_framework/sqlite/schema.rb +95 -0
  51. data/lib/object_framework/sqlite/storage.rb +220 -0
  52. data/lib/object_framework/sqlite.rb +6 -0
  53. data/lib/object_framework/version.rb +5 -0
  54. data/lib/object_framework.rb +31 -0
  55. metadata +105 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module HTTP
5
+ Response = Data.define(:status, :headers, :body)
6
+
7
+ class JSONPresenter
8
+ def call(value)
9
+ Response.new(status: 200, headers: { "Content-Type" => "application/json" }, body: JSON.generate(json_value(value)))
10
+ end
11
+
12
+ private
13
+
14
+ def json_value(value)
15
+ case value
16
+ when Data then json_value(value.to_h)
17
+ when Hash then value.to_h { |key, item| [key, json_value(item)] }
18
+ when Array then value.map { |item| json_value(item) }
19
+ else value
20
+ end
21
+ end
22
+ end
23
+
24
+ class ProblemPresenter
25
+ STATUSES = {
26
+ bad_request: 400, invalid_input: 422, unauthenticated: 401, forbidden: 403,
27
+ not_found: 404, conflict: 409, duplicate: 409, transaction_conflict: 409,
28
+ rate_limited: 429, capacity: 503, unavailable: 503, deadline: 503,
29
+ cancelled: 503, unimplemented: 501, internal: 500
30
+ }.freeze
31
+
32
+ def initialize(registry:)
33
+ @registry = registry
34
+ end
35
+
36
+ def call(exception, context:)
37
+ description = @registry.resolve(exception)
38
+ status = exception.is_a?(Error) ? exception.class::STATUS : STATUSES.fetch(description.category)
39
+ problem = description.details.transform_keys(&:to_s).merge(
40
+ "type" => "urn:object-framework:error:#{description.code}", "title" => description.title,
41
+ "status" => status, "instance" => "urn:uuid:#{context.operation_id}",
42
+ "code" => description.code, "operation_id" => context.operation_id
43
+ )
44
+ headers = { "Content-Type" => "application/problem+json" }
45
+ headers.merge!(exception.headers) if exception.is_a?(Error)
46
+ Response.new(status:, headers:, body: JSON.generate(problem))
47
+ end
48
+
49
+ def self.fallback(context:)
50
+ body = '{"type":"urn:object-framework:error:internal_error","title":"Internal server error","status":500,"code":"internal_error","operation_id":"' + context.operation_id + '","instance":"urn:uuid:' + context.operation_id + '"}'
51
+ Response.new(status: 500, headers: { "Content-Type" => "application/problem+json" }, body:)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "json"
5
+
6
+ module ObjectFramework
7
+ module HTTP
8
+ class Error < StandardError
9
+ attr_reader :headers
10
+ def initialize(message = nil, headers: {})
11
+ super(message || self.class::TITLE)
12
+ @headers = headers
13
+ end
14
+ end
15
+
16
+ ERROR_TYPES = {
17
+ BadRequest: [400, "Bad request"], MethodNotAllowed: [405, "Method not allowed"],
18
+ RequestTimeout: [408, "Request timeout"], PayloadTooLarge: [413, "Payload too large"],
19
+ URITooLong: [414, "Request target too long"], UnsupportedMediaType: [415, "Unsupported media type"],
20
+ ExpectationFailed: [417, "Expectation failed"], HeadersTooLarge: [431, "Request headers too large"],
21
+ NotImplemented: [501, "Transfer coding not implemented"], VersionNotSupported: [505, "HTTP version not supported"]
22
+ }.freeze
23
+ ERROR_TYPES.each do |name, (status, title)|
24
+ type = Class.new(Error)
25
+ type.const_set(:STATUS, status)
26
+ type.const_set(:TITLE, title.freeze)
27
+ const_set(name, type)
28
+ end
29
+
30
+ def self.register_errors(registry)
31
+ ERROR_TYPES.each_key do |name|
32
+ type = const_get(name)
33
+ code = name.to_s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
34
+ registry.register(type, category: :bad_request, code: "http_#{code}", title: type::TITLE)
35
+ end
36
+ end
37
+
38
+ Head = Data.define(:method, :path, :query, :headers, :length, :chunked, :keep_alive)
39
+ Input = Data.define(:params, :query, :body)
40
+
41
+ class Parser
42
+ TOKEN = /\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\z/
43
+ attr_reader :line_limit, :header_limit, :body_limit
44
+
45
+ def initialize(line_limit: 8 * 1024, header_limit: 32 * 1024, body_limit: 1024 * 1024)
46
+ @line_limit, @header_limit, @body_limit = line_limit, header_limit, body_limit
47
+ raise ConfigurationError, "HTTP limits must be positive integers" unless [line_limit, header_limit, body_limit].all? { |limit| limit.is_a?(Integer) && limit.positive? }
48
+ end
49
+
50
+ def parse_head(raw)
51
+ raise BadRequest unless raw.end_with?("\r\n\r\n")
52
+ lines = raw.b.split("\r\n", -1)
53
+ request_line = lines.shift
54
+ raise URITooLong if request_line.bytesize > line_limit
55
+ raise HeadersTooLarge if raw.bytesize - request_line.bytesize - 2 > header_limit
56
+ method, target, version = request_line.split(" ", -1)
57
+ raise BadRequest unless method && TOKEN.match?(method) && target && version && request_line == "#{method} #{target} #{version}"
58
+ raise VersionNotSupported unless version == "HTTP/1.1"
59
+ raise BadRequest if target.match?(/[\x00-\x20\x7f-\xff#]/n)
60
+ headers = {}
61
+ lines[0...-2].each do |line|
62
+ name, value = parse_field(line)
63
+ if headers.key?(name)
64
+ raise BadRequest if %w[host content-length transfer-encoding].include?(name)
65
+ headers[name] = "#{headers[name]}, #{value}"
66
+ else
67
+ headers[name] = value
68
+ end
69
+ end
70
+ host = headers["host"]
71
+ raise BadRequest unless host && /\A(?:\[[0-9a-fA-F:.]+\]|[A-Za-z0-9.-]+)(?::\d{1,5})?\z/.match?(host)
72
+ raise BadRequest if headers.key?("content-length") && headers.key?("transfer-encoding")
73
+ length = 0
74
+ if headers.key?("content-length")
75
+ raise BadRequest unless /\A[0-9]+\z/.match?(headers["content-length"])
76
+ length = Integer(headers["content-length"], 10)
77
+ raise PayloadTooLarge if length > body_limit
78
+ end
79
+ encoding = headers["transfer-encoding"]&.downcase
80
+ if encoding && encoding != "chunked"
81
+ raise BadRequest if encoding.split(",").map(&:strip).count("chunked") != 1 || !encoding.end_with?("chunked")
82
+ raise NotImplemented
83
+ end
84
+ expect = headers["expect"]
85
+ raise ExpectationFailed if expect && expect.downcase != "100-continue"
86
+ path, query = parse_target(method, target)
87
+ Head.new(method:, path:, query:, headers: Values.detach(headers), length:, chunked: encoding == "chunked",
88
+ keep_alive: !headers.fetch("connection", "").downcase.split(",").map(&:strip).include?("close"))
89
+ end
90
+
91
+ def parse_field(line)
92
+ name, value = line.split(":", 2)
93
+ raise BadRequest unless name && TOKEN.match?(name) && value && !value.match?(/[\x00-\x08\x0a-\x1f\x7f]/)
94
+ [name.downcase, value.strip]
95
+ end
96
+
97
+ def read_body(reader, head, deadline:)
98
+ return reader.read_exact(head.length, deadline:) unless head.chunked
99
+ body, trailer_size = +"".b, 0
100
+ loop do
101
+ line = reader.read_line(limit: line_limit, deadline:)
102
+ size, extension = line.split(";", 2)
103
+ raise BadRequest unless /\A[0-9a-fA-F]+\z/.match?(size)
104
+ if extension && !/\A(?:[!#$%&'*+.^_`|~0-9A-Za-z-]+(?:=(?:[!#$%&'*+.^_`|~0-9A-Za-z-]+|"(?:[^"\\\r\n]|\\[\x20-\x7e])*"))?)(?:;[!#$%&'*+.^_`|~0-9A-Za-z-]+(?:=(?:[!#$%&'*+.^_`|~0-9A-Za-z-]+|"(?:[^"\\\r\n]|\\[\x20-\x7e])*"))?)*\z/.match?(extension)
105
+ raise BadRequest
106
+ end
107
+ count = Integer(size, 16)
108
+ raise PayloadTooLarge if body.bytesize + count > body_limit
109
+ if count.zero?
110
+ loop do
111
+ trailer = reader.read_line(limit: header_limit, deadline:)
112
+ trailer_size += trailer.bytesize + 2
113
+ raise HeadersTooLarge if trailer_size > header_limit
114
+ break if trailer.empty?
115
+ name, = parse_field(trailer)
116
+ raise BadRequest if %w[content-length transfer-encoding host connection content-type authorization].include?(name)
117
+ end
118
+ return body
119
+ end
120
+ body << reader.read_exact(count, deadline:)
121
+ raise BadRequest unless reader.read_exact(2, deadline:) == "\r\n"
122
+ end
123
+ rescue EOFError
124
+ raise BadRequest
125
+ end
126
+
127
+ private
128
+
129
+ def parse_target(method, target)
130
+ return ["*", ""] if method == "OPTIONS" && target == "*"
131
+ uri = URI.parse(target)
132
+ if uri.absolute?
133
+ raise BadRequest unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo
134
+ path = uri.path.empty? ? "/" : uri.path
135
+ else
136
+ raise BadRequest unless target.start_with?("/") && !target.start_with?("//")
137
+ path = uri.path
138
+ end
139
+ raise BadRequest if target.match?(/%(?![0-9a-fA-F]{2})/)
140
+ [path, uri.query || ""]
141
+ rescue URI::InvalidURIError
142
+ raise BadRequest
143
+ end
144
+ end
145
+
146
+ class JSONDecoder
147
+ def call(body, content_type: nil)
148
+ return {} if body.empty?
149
+ raise UnsupportedMediaType unless content_type&.split(";", 2)&.first&.strip&.downcase == "application/json"
150
+ value = JSON.parse(body.dup.force_encoding(Encoding::UTF_8))
151
+ raise InvalidInput, "Expected a JSON object" unless value.is_a?(Hash)
152
+ value
153
+ rescue JSON::ParserError, EncodingError
154
+ raise BadRequest
155
+ end
156
+ end
157
+
158
+ Parameter = Data.define(:name, :source, :key, :type, :required)
159
+
160
+ class Parameters
161
+ def initialize(&declaration)
162
+ @fields = []
163
+ instance_eval(&declaration) if declaration
164
+ @fields.freeze
165
+ freeze
166
+ end
167
+
168
+ def field(name, source: :body, key: name, type: String, required: true)
169
+ raise ConfigurationError, "Invalid parameter source" unless %i[path query body].include?(source)
170
+ raise ConfigurationError, "Duplicate parameter" if @fields.any? { |field| field.name == name }
171
+ @fields << Parameter.new(name:, source:, key:, type:, required:)
172
+ end
173
+
174
+ def normalize(input)
175
+ @fields.each_with_object({}) do |field, result|
176
+ source = input.public_send(field.source == :path ? :params : field.source)
177
+ key = field.source == :path ? field.key.to_sym : field.key.to_s
178
+ unless source.key?(key)
179
+ raise InvalidInput, "Missing required parameter" if field.required
180
+ next
181
+ end
182
+ value = source.fetch(key)
183
+ if field.source != :body
184
+ value = Integer(value, 10) if field.type == Integer
185
+ value = Float(value) if field.type == Float
186
+ end
187
+ raise InvalidInput, "Invalid parameter type" unless value.is_a?(field.type)
188
+ result[field.name] = value
189
+ end
190
+ rescue ArgumentError, TypeError
191
+ raise InvalidInput, "Invalid parameter value"
192
+ end
193
+ end
194
+
195
+ class Controller
196
+ def initialize(application:, operation:, parameters: Parameters.new)
197
+ @application, @operation, @parameters = application, operation, parameters
198
+ end
199
+
200
+ def call(input, context:)
201
+ @application.invoke(@operation, context:, **@parameters.normalize(input))
202
+ end
203
+
204
+ def validate!
205
+ @application.validate_operation!(@operation)
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module HTTP
5
+ Route = Data.define(:method, :pattern, :controller, :expression, :names)
6
+ Match = Data.define(:controller, :params, :allow)
7
+
8
+ class Router
9
+ def initialize
10
+ @controllers, @routes = {}, []
11
+ end
12
+
13
+ def controller(name, instance)
14
+ mutable!
15
+ raise ConfigurationError, "Duplicate controller: #{name}" if @controllers.key?(name)
16
+ @controllers[name] = instance
17
+ self
18
+ end
19
+
20
+ def route(method, pattern, controller:)
21
+ mutable!
22
+ method = method.to_s.upcase
23
+ raise ConfigurationError, "Invalid route" unless Parser::TOKEN.match?(method) && pattern.start_with?("/") && !pattern.include?("?")
24
+ names = []
25
+ expression = pattern.split("/", -1).map do |part|
26
+ if part.start_with?(":")
27
+ name = Persistence::Query.identifier(part.delete_prefix(":"))
28
+ raise ConfigurationError, "Duplicate route parameter" if names.include?(name)
29
+ names << name
30
+ "([^/]+)"
31
+ else
32
+ Regexp.escape(part)
33
+ end
34
+ end.join("/")
35
+ raise ConfigurationError, "Duplicate route" if @routes.any? { |route| route.method == method && route.expression.source == "\\A#{expression}\\z" }
36
+ @routes << Route.new(method:, pattern: pattern.dup.freeze, controller:, expression: /\A#{expression}\z/, names: names.freeze)
37
+ self
38
+ end
39
+
40
+ def validate!
41
+ @routes.each do |route|
42
+ controller = @controllers.fetch(route.controller) { raise ConfigurationError, "Unknown route controller" }
43
+ raise ConfigurationError, "Controller must implement call" unless controller.respond_to?(:call)
44
+ controller.validate! if controller.respond_to?(:validate!)
45
+ end
46
+ @controllers.freeze
47
+ @routes.sort_by! { |route| route.names.size } unless @routes.frozen?
48
+ @routes.freeze
49
+ self
50
+ end
51
+
52
+ def resolve(method, path)
53
+ matching = path == "*" ? @routes : @routes.select { |route| route.expression.match?(path) }
54
+ raise NotFound, "Route not found" if matching.empty?
55
+ allow = (matching.map(&:method) + ["OPTIONS"]).uniq
56
+ allow << "HEAD" if allow.include?("GET")
57
+ allow.sort!
58
+ return Match.new(controller: nil, params: {}, allow:) if method == "OPTIONS"
59
+ route = matching.find { |candidate| candidate.method == method }
60
+ route ||= matching.find { |candidate| candidate.method == "GET" } if method == "HEAD"
61
+ raise MethodNotAllowed.new(headers: { "Allow" => allow.join(", ") }) unless route
62
+ captures = route.expression.match(path).captures
63
+ params = route.names.zip(captures).to_h.transform_values do |value|
64
+ decoded = URI.decode_www_form_component(value.gsub("+", "%2B"))
65
+ raise BadRequest unless decoded.valid_encoding? && !decoded.match?(/[\x00-\x1f\x7f]/)
66
+ decoded
67
+ end
68
+ Match.new(controller: @controllers.fetch(route.controller), params:, allow:)
69
+ rescue ArgumentError
70
+ raise BadRequest
71
+ end
72
+
73
+ private
74
+
75
+ def mutable!
76
+ raise ConfigurationError, "Router is finalized" if @routes.frozen?
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "openssl"
5
+
6
+ module ObjectFramework
7
+ module HTTP
8
+ Connection = Struct.new(:io, :buffer, :fiber, :context, keyword_init: true) do
9
+ def close
10
+ io.close unless io.closed?
11
+ rescue IOError, SystemCallError
12
+ nil
13
+ end
14
+ end
15
+
16
+ class Server
17
+ attr_reader :port, :name
18
+
19
+ def initialize(application:, router:, host: "127.0.0.1", port: 0, name: :http,
20
+ parser: Parser.new, presenter: JSONPresenter.new, problem_presenter: nil,
21
+ max_requests: 128, max_connections: 1024, header_timeout: 10,
22
+ body_timeout: 30, execution_timeout: 30, idle_timeout: 30,
23
+ write_timeout: 30, shutdown_timeout: 5, tls_context: nil,
24
+ writer_factory: ->(reader) { Writer.new(reader:) })
25
+ @application, @router, @host, @port, @name = application, router, host, port, name
26
+ @parser, @presenter, @problem_presenter = parser, presenter, problem_presenter
27
+ @max_requests, @max_connections = max_requests, max_connections
28
+ @header_timeout, @body_timeout, @execution_timeout = header_timeout, body_timeout, execution_timeout
29
+ @idle_timeout, @write_timeout, @shutdown_timeout = idle_timeout, write_timeout, shutdown_timeout
30
+ @tls_context, @writer_factory = tls_context, writer_factory
31
+ @mutex, @workers, @admitted_count = Mutex.new, {}, 0
32
+ @completed = Queue.new
33
+ @connections, @waiting = {}.compare_by_identity, {}.compare_by_identity
34
+ end
35
+
36
+ def requires; []; end
37
+
38
+ def configure(application)
39
+ HTTP.register_errors(application.errors)
40
+ @problem_presenter ||= ProblemPresenter.new(registry: application.errors)
41
+ end
42
+
43
+ def validate!
44
+ @router.validate!
45
+ unless [@max_requests, @max_connections].all? { |value| value.is_a?(Integer) && value.positive? }
46
+ raise ConfigurationError, "Admission limits must be positive integers"
47
+ end
48
+ unless [@header_timeout, @body_timeout, @execution_timeout, @idle_timeout, @write_timeout, @shutdown_timeout].all? { |value| value.is_a?(Numeric) && value.positive? }
49
+ raise ConfigurationError, "HTTP timeouts must be positive"
50
+ end
51
+ if @tls_context
52
+ raise ConfigurationError, "TLS requires an application certificate and private key" unless @tls_context.cert && @tls_context.key
53
+ @tls_context.setup
54
+ end
55
+ end
56
+
57
+ def start
58
+ @listener = TCPServer.new(@host, @port)
59
+ @port = @listener.local_address.ip_port
60
+ @wake_reader, @wake_writer = IO.pipe
61
+ @running = true
62
+ @dispatcher = Thread.new { dispatch }
63
+ self
64
+ rescue Exception
65
+ @listener&.close
66
+ @wake_reader&.close
67
+ @wake_writer&.close
68
+ raise
69
+ end
70
+
71
+ def active_count; @mutex.synchronize { @workers.size }; end
72
+ def admitted_count; @mutex.synchronize { @admitted_count }; end
73
+ def connection_count; @connections.size; end
74
+
75
+ def stop
76
+ return unless @dispatcher
77
+ @running = false
78
+ wake
79
+ @dispatcher.join
80
+ workers = @mutex.synchronize do
81
+ @workers.each_value { |context, connection| context.cancellation.cancel; connection.close }
82
+ @workers.keys
83
+ end
84
+ deadline = Timing.now + @shutdown_timeout
85
+ workers.each { |thread| thread.join([deadline - Timing.now, 0].max) }
86
+ @wake_reader.close
87
+ @wake_writer.close
88
+ @dispatcher = nil
89
+ end
90
+
91
+ private
92
+
93
+ def dispatch
94
+ while @running
95
+ complete_requests
96
+ now = Timing.now
97
+ @waiting.dup.each { |connection, (_, deadline)| resume_connection(connection) if deadline <= now }
98
+ reads = [@listener, @wake_reader] + @waiting.select { |_, (mode, _)| mode == :read }.keys.map(&:io)
99
+ writes = @waiting.select { |_, (mode, _)| mode == :write }.keys.map(&:io)
100
+ deadline = @waiting.values.map(&:last).min
101
+ timeout = deadline ? [[deadline - Timing.now, 0].max, 0.5].min : 0.5
102
+ readable, writable = IO.select(reads, writes, nil, timeout)
103
+ next unless readable
104
+ drain_wake if readable.delete(@wake_reader)
105
+ accept_connections if readable.delete(@listener)
106
+ ready = readable + writable
107
+ @waiting.keys.each { |connection| resume_connection(connection) if ready.include?(connection.io) }
108
+ end
109
+ ensure
110
+ @connections.each_key(&:close)
111
+ @connections.clear
112
+ @waiting.clear
113
+ @listener.close unless @listener.closed?
114
+ end
115
+
116
+ def accept_connections
117
+ loop do
118
+ socket = @listener.accept_nonblock(exception: false)
119
+ break if socket == :wait_readable
120
+ if @connections.size >= @max_connections
121
+ socket.close
122
+ next
123
+ end
124
+ io = @tls_context ? OpenSSL::SSL::SSLSocket.new(socket, @tls_context) : socket
125
+ io.sync_close = true if @tls_context
126
+ connection = Connection.new(io:, buffer: +"".b, context: ExecutionContext.new)
127
+ @connections[connection] = true
128
+ connection.fiber = connection_fiber(connection)
129
+ resume_connection(connection)
130
+ end
131
+ end
132
+
133
+ def connection_fiber(connection)
134
+ Fiber.new do
135
+ reader = Reader.new(io: connection.io, buffer: connection.buffer,
136
+ waiter: ->(mode, deadline) { Fiber.yield([mode, deadline]) })
137
+ begin
138
+ handshake(connection, reader) if @tls_context
139
+ idle = false
140
+ loop do
141
+ connection.context = ExecutionContext.new
142
+ head = reader.read_head(@parser, header_timeout: @header_timeout, idle_timeout: @idle_timeout, idle:)
143
+ raise CapacityExceeded if Fiber.yield([:request, head]) == :rejected
144
+ idle = true
145
+ end
146
+ rescue EOFError
147
+ nil
148
+ rescue StandardError => error
149
+ @application.reporter.report(error, context: connection.context)
150
+ response = problem(error, context: connection.context)
151
+ Writer.new(reader:).call(response, keep_alive: false, deadline: Timing.now + @write_timeout)
152
+ end
153
+ nil
154
+ end
155
+ end
156
+
157
+ def handshake(connection, reader)
158
+ deadline = Timing.now + @header_timeout
159
+ loop do
160
+ raise RequestTimeout if Timing.now >= deadline
161
+ result = connection.io.accept_nonblock(exception: false)
162
+ break unless %i[wait_readable wait_writable].include?(result)
163
+ reader.waiter.call(result == :wait_readable ? :read : :write, deadline)
164
+ end
165
+ end
166
+
167
+ def resume_connection(connection, value = nil)
168
+ @waiting.delete(connection)
169
+ result = connection.fiber.resume(value)
170
+ if result && result.first == :request
171
+ resume_connection(connection, :rejected) unless admit(connection, result.last)
172
+ elsif result
173
+ @waiting[connection] = result
174
+ else
175
+ forget(connection)
176
+ end
177
+ rescue StandardError => error
178
+ @application.reporter.report(error, context: connection.context)
179
+ forget(connection)
180
+ end
181
+
182
+ def admit(connection, head)
183
+ @mutex.synchronize do
184
+ return false if @workers.size >= @max_requests
185
+ @admitted_count += 1
186
+ thread = Thread.new { serve(connection, head) }
187
+ @workers[thread] = [connection.context, connection]
188
+ end
189
+ true
190
+ end
191
+
192
+ def serve(connection, head)
193
+ context = connection.context
194
+ reader = Reader.new(io: connection.io, buffer: connection.buffer)
195
+ writer = nil
196
+ keep_alive, body_read = false, false
197
+ begin
198
+ writer = @writer_factory.call(reader)
199
+ reader.write("HTTP/1.1 100 Continue\r\n\r\n", deadline: Timing.now + @write_timeout) if head.headers["expect"]
200
+ body = @parser.read_body(reader, head, deadline: Timing.now + @body_timeout)
201
+ body_read = true
202
+ context.cancellation.deadline = Timing.now + @execution_timeout
203
+ context.cancellation.check!
204
+ match = @router.resolve(head.method, head.path)
205
+ if head.method == "OPTIONS"
206
+ response = Response.new(status: 204, headers: { "Allow" => match.allow.join(", ") }, body: "")
207
+ else
208
+ input = Input.new(params: match.params, query: query_parameters(head.query),
209
+ body: JSONDecoder.new.call(body, content_type: head.headers["content-type"]))
210
+ result = match.controller.call(input, context:)
211
+ context.cancellation.check!
212
+ response = @presenter.call(result)
213
+ end
214
+ writer.call(response, head: head.method == "HEAD", keep_alive: head.keep_alive, deadline: Timing.now + @write_timeout)
215
+ keep_alive = head.keep_alive
216
+ rescue StandardError => error
217
+ @application.reporter.report(error, context:)
218
+ writer ||= Writer.new(reader:)
219
+ unless writer.started
220
+ begin
221
+ writer.call(problem(error, context:), head: head.method == "HEAD", keep_alive: body_read && head.keep_alive, deadline: Timing.now + @write_timeout)
222
+ keep_alive = body_read && head.keep_alive
223
+ rescue StandardError => secondary
224
+ @application.reporter.report(secondary, context:, secondary: true)
225
+ keep_alive = false
226
+ end
227
+ end
228
+ ensure
229
+ @mutex.synchronize { @workers.delete(Thread.current) }
230
+ reuse = keep_alive && @running
231
+ connection.close unless reuse
232
+ @completed << [reuse ? :reuse : :close, connection]
233
+ wake
234
+ end
235
+ end
236
+
237
+ def query_parameters(query)
238
+ URI.decode_www_form(query).each_with_object({}) do |(key, value), result|
239
+ result[key] = result.key?(key) ? Array(result[key]) + [value] : value
240
+ end
241
+ rescue ArgumentError
242
+ raise BadRequest
243
+ end
244
+
245
+ def problem(error, context:)
246
+ @problem_presenter.call(error, context:)
247
+ rescue StandardError => secondary
248
+ @application.reporter.report(secondary, context:, secondary: true)
249
+ ProblemPresenter.fallback(context:)
250
+ end
251
+
252
+ def complete_requests
253
+ until @completed.empty?
254
+ action, connection = @completed.pop
255
+ action == :reuse && @running ? resume_connection(connection) : forget(connection)
256
+ end
257
+ end
258
+
259
+ def forget(connection)
260
+ @waiting.delete(connection)
261
+ @connections.delete(connection)
262
+ connection.close
263
+ end
264
+
265
+ def wake
266
+ @wake_writer&.write_nonblock(".", exception: false)
267
+ rescue IOError, SystemCallError
268
+ nil
269
+ end
270
+
271
+ def drain_wake
272
+ loop do
273
+ result = @wake_reader.read_nonblock(1024, exception: false)
274
+ break if result == :wait_readable || result.nil?
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end