raptor 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/Dockerfile +7 -1
- data/README.md +184 -69
- data/docs/assets/profile_picture.png +0 -0
- data/docs/assets/startup_output.svg +37 -0
- data/docs/raptor-vs-puma.md +105 -429
- data/docs/talk.md +262 -169
- data/ext/raptor_http/raptor_http.c +82 -123
- data/lib/rackup/handler/raptor.rb +2 -1
- data/lib/raptor/cli.rb +1 -0
- data/lib/raptor/cluster.rb +85 -39
- data/lib/raptor/control_server.rb +2 -0
- data/lib/raptor/http.rb +0 -1
- data/lib/raptor/http1.rb +52 -30
- data/lib/raptor/http2.rb +46 -13
- data/lib/raptor/plugin.rb +98 -0
- data/lib/raptor/reactor.rb +102 -72
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +18 -5
- data/sig/generated/raptor/http.rbs +0 -2
- data/sig/generated/raptor/http1.rbs +21 -13
- data/sig/generated/raptor/http2.rbs +12 -9
- data/sig/generated/raptor/plugin.rbs +52 -0
- data/sig/generated/raptor/reactor.rbs +31 -16
- data/sig/generated/raptor.rbs +1 -1
- metadata +6 -2
data/lib/raptor/http1.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Raptor
|
|
|
43
43
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
44
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
45
45
|
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
46
|
+
NOT_IMPLEMENTED_RESPONSE = "HTTP/1.1 501 Not Implemented\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
46
47
|
CHUNKED_TERMINATOR = "0\r\n\r\n"
|
|
47
48
|
|
|
48
49
|
CONNECTION_CLOSE = "close"
|
|
@@ -75,31 +76,29 @@ module Raptor
|
|
|
75
76
|
!http_host || http_host.empty? || http_host.include?(",")
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
81
|
-
# paired with a `Content-Length`; or a `Content-Length` containing any
|
|
82
|
-
# non-digit character.
|
|
79
|
+
# Rejects ambiguous message framing per RFC 9112 section 6.3 and
|
|
80
|
+
# transfer codings other than a single `chunked`.
|
|
83
81
|
#
|
|
84
82
|
# @param env [Hash] the Rack environment after header parsing
|
|
85
|
-
# @return [
|
|
83
|
+
# @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
|
|
86
84
|
#
|
|
87
|
-
# @rbs (Hash[String, untyped] env) ->
|
|
88
|
-
def self.
|
|
85
|
+
# @rbs (Hash[String, untyped] env) -> Symbol?
|
|
86
|
+
def self.framing_error(env)
|
|
89
87
|
transfer_encoding = env[HTTP_TRANSFER_ENCODING]
|
|
90
88
|
content_length = env[Http::CONTENT_LENGTH]
|
|
91
89
|
|
|
92
90
|
if transfer_encoding
|
|
93
|
-
return
|
|
91
|
+
return :malformed if content_length
|
|
94
92
|
|
|
95
|
-
encodings = transfer_encoding.downcase.split(",").map(&:strip)
|
|
96
|
-
return
|
|
97
|
-
return
|
|
93
|
+
encodings = transfer_encoding.downcase.split(",", -1).map(&:strip)
|
|
94
|
+
return :malformed if encodings.any?(&:empty?) || encodings.last != TRANSFER_ENCODING_CHUNKED
|
|
95
|
+
return :malformed if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
|
|
96
|
+
return :unsupported_encoding if encodings.size > 1
|
|
98
97
|
elsif content_length
|
|
99
|
-
return
|
|
98
|
+
return :malformed unless content_length.match?(/\A[0-9]+\z/)
|
|
100
99
|
end
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
nil
|
|
103
102
|
end
|
|
104
103
|
|
|
105
104
|
# Returns any pipelined bytes following the request, or nil if none remain.
|
|
@@ -144,9 +143,9 @@ module Raptor
|
|
|
144
143
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
145
144
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
146
145
|
# plus populated `:env` and `:body`, and records any bytes after it in
|
|
147
|
-
# the parse data's `:pipelined
|
|
148
|
-
# `:
|
|
149
|
-
# so the reactor can wait for more.
|
|
146
|
+
# the parse data's `:pipelined`. Rejected input sets `:malformed`,
|
|
147
|
+
# `:too_large`, or `:unsupported_encoding`; incomplete input returns
|
|
148
|
+
# the state so the reactor can wait for more.
|
|
150
149
|
#
|
|
151
150
|
# @param data [Hash] the current parse state
|
|
152
151
|
# @param env_template [Hash] the Rack env template to seed the request with
|
|
@@ -170,8 +169,9 @@ module Raptor
|
|
|
170
169
|
parse_data[:parse_count] += 1
|
|
171
170
|
|
|
172
171
|
message = if parser.finished?
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
error = invalid_host?(env) ? :malformed : framing_error(env)
|
|
173
|
+
if error
|
|
174
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, error => true)
|
|
175
175
|
elsif parser.has_body?
|
|
176
176
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
177
177
|
|
|
@@ -325,6 +325,9 @@ module Raptor
|
|
|
325
325
|
when :malformed
|
|
326
326
|
reject_malformed(socket)
|
|
327
327
|
return
|
|
328
|
+
when :unsupported_encoding
|
|
329
|
+
reject_unsupported_encoding(socket)
|
|
330
|
+
return
|
|
328
331
|
end
|
|
329
332
|
|
|
330
333
|
thread_pool << proc do
|
|
@@ -354,6 +357,12 @@ module Raptor
|
|
|
354
357
|
return
|
|
355
358
|
end
|
|
356
359
|
|
|
360
|
+
if parsed_request[:unsupported_encoding]
|
|
361
|
+
socket = reactor.remove(parsed_request[:id])
|
|
362
|
+
reject_unsupported_encoding(socket) if socket
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
|
|
357
366
|
if parsed_request[:complete]
|
|
358
367
|
socket = reactor.remove(parsed_request[:id])
|
|
359
368
|
request_count = (parsed_request[:request_count] || 0) + 1
|
|
@@ -444,6 +453,9 @@ module Raptor
|
|
|
444
453
|
when :malformed
|
|
445
454
|
reject_malformed(socket)
|
|
446
455
|
return
|
|
456
|
+
when :unsupported_encoding
|
|
457
|
+
reject_unsupported_encoding(socket)
|
|
458
|
+
return
|
|
447
459
|
end
|
|
448
460
|
|
|
449
461
|
request_count += 1
|
|
@@ -530,11 +542,13 @@ module Raptor
|
|
|
530
542
|
# @param parser [HttpParser] the parser holding the finished parse state
|
|
531
543
|
# @param nread [Integer] the byte offset where the body begins in `buffer`
|
|
532
544
|
# @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
|
|
533
|
-
# `:too_large`, or `:
|
|
545
|
+
# `:too_large`, `:malformed`, or `:unsupported_encoding`
|
|
534
546
|
#
|
|
535
547
|
# @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
|
|
536
548
|
def extract_body(buffer, env, parse_data, parser, nread)
|
|
537
|
-
return :malformed if Http1.invalid_host?(env)
|
|
549
|
+
return :malformed if Http1.invalid_host?(env)
|
|
550
|
+
error = Http1.framing_error(env)
|
|
551
|
+
return error if error
|
|
538
552
|
|
|
539
553
|
unless parser.has_body?
|
|
540
554
|
parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread)
|
|
@@ -809,6 +823,17 @@ module Raptor
|
|
|
809
823
|
socket.close rescue nil
|
|
810
824
|
end
|
|
811
825
|
|
|
826
|
+
# Writes a 501 response and closes the socket.
|
|
827
|
+
#
|
|
828
|
+
# @param socket [TCPSocket] the client socket
|
|
829
|
+
# @return [void]
|
|
830
|
+
#
|
|
831
|
+
# @rbs (TCPSocket socket) -> void
|
|
832
|
+
def reject_unsupported_encoding(socket)
|
|
833
|
+
socket.write(NOT_IMPLEMENTED_RESPONSE) rescue nil
|
|
834
|
+
socket.close rescue nil
|
|
835
|
+
end
|
|
836
|
+
|
|
812
837
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
813
838
|
#
|
|
814
839
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
@@ -855,7 +880,6 @@ module Raptor
|
|
|
855
880
|
end
|
|
856
881
|
|
|
857
882
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
858
|
-
env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
|
|
859
883
|
|
|
860
884
|
behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
|
|
861
885
|
env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
|
|
@@ -890,7 +914,8 @@ module Raptor
|
|
|
890
914
|
|
|
891
915
|
# Builds the `rack.input` IO object for the request body. Returns an
|
|
892
916
|
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
893
|
-
# for larger bodies
|
|
917
|
+
# for larger bodies. The complete body is already buffered at this point,
|
|
918
|
+
# so spooling does not bound peak receive memory.
|
|
894
919
|
#
|
|
895
920
|
# @param body [String, nil] decoded request body
|
|
896
921
|
# @return [IO] an IO-like object positioned at the start of the body
|
|
@@ -934,13 +959,10 @@ module Raptor
|
|
|
934
959
|
def keep_alive?(env, request_count)
|
|
935
960
|
return false if request_count >= @max_keepalive_requests
|
|
936
961
|
|
|
937
|
-
|
|
962
|
+
connection_tokens = env[HTTP_CONNECTION].to_s.downcase.split(",").map(&:strip)
|
|
963
|
+
return false if connection_tokens.include?(CONNECTION_CLOSE)
|
|
938
964
|
|
|
939
|
-
|
|
940
|
-
!connection_header&.casecmp?(CONNECTION_CLOSE)
|
|
941
|
-
else
|
|
942
|
-
connection_header&.casecmp?(CONNECTION_KEEPALIVE) || false
|
|
943
|
-
end
|
|
965
|
+
env[Rack::SERVER_PROTOCOL] == HTTP_11 || connection_tokens.include?(CONNECTION_KEEPALIVE)
|
|
944
966
|
end
|
|
945
967
|
|
|
946
968
|
# Sends an HTTP 103 Early Hints response, skipping any entries with
|
|
@@ -1159,7 +1181,7 @@ module Raptor
|
|
|
1159
1181
|
#
|
|
1160
1182
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
|
|
1161
1183
|
def write_full_response(socket, response, headers, body, http_version)
|
|
1162
|
-
if body.respond_to?(:call)
|
|
1184
|
+
if body.respond_to?(:call) && !body.respond_to?(:each)
|
|
1163
1185
|
HttpParser.format_headers(response, headers)
|
|
1164
1186
|
response << "\r\n"
|
|
1165
1187
|
socket_write(socket, response)
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -455,7 +455,7 @@ module Raptor
|
|
|
455
455
|
# updated connection state along with any outgoing protocol frames and
|
|
456
456
|
# completed stream requests.
|
|
457
457
|
#
|
|
458
|
-
# @param data [Hash] the connection state including buffer and
|
|
458
|
+
# @param data [Hash] the connection state including buffer, HPACK table and body-size limit
|
|
459
459
|
# @return [Hash] updated state with outgoing_frames and completed_requests
|
|
460
460
|
#
|
|
461
461
|
# @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
|
|
@@ -474,6 +474,7 @@ module Raptor
|
|
|
474
474
|
preface_received = data[:http2_preface_received] || false
|
|
475
475
|
last_client_stream_id = data[:http2_last_client_stream_id] || 0
|
|
476
476
|
pending_headers = data[:http2_pending_headers]
|
|
477
|
+
max_body_size = data[:max_body_size]
|
|
477
478
|
goaway_error = nil
|
|
478
479
|
|
|
479
480
|
unless preface_received
|
|
@@ -586,12 +587,29 @@ module Raptor
|
|
|
586
587
|
stream = streams[stream_id]
|
|
587
588
|
next unless stream
|
|
588
589
|
|
|
590
|
+
if stream[:too_large]
|
|
591
|
+
streams.delete(stream_id) if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
592
|
+
next
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
body = stream[:body] || ""
|
|
596
|
+
if max_body_size && body.bytesize + chunk.bytesize > max_body_size
|
|
597
|
+
headers = parser.encode_headers([[":status", "413"], ["content-length", "0"]])
|
|
598
|
+
outgoing_frames << parser.build_frame(:headers, FLAG_END_HEADERS | FLAG_END_STREAM, stream_id, headers)
|
|
599
|
+
if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
600
|
+
streams.delete(stream_id)
|
|
601
|
+
else
|
|
602
|
+
streams[stream_id] = { too_large: true }
|
|
603
|
+
end
|
|
604
|
+
next
|
|
605
|
+
end
|
|
606
|
+
|
|
589
607
|
stream_window_used = (stream[:window_used] || 0) + frame[:length]
|
|
590
608
|
if stream_window_used >= DEFAULT_WINDOW_SIZE / 2 && frame[:flags].nobits?(FLAG_END_STREAM)
|
|
591
609
|
outgoing_frames << parser.build_frame(:window_update, 0, stream_id, [stream_window_used].pack("N"))
|
|
592
610
|
stream_window_used = 0
|
|
593
611
|
end
|
|
594
|
-
stream = stream.merge(body:
|
|
612
|
+
stream = stream.merge(body: body + chunk, window_used: stream_window_used)
|
|
595
613
|
|
|
596
614
|
if frame[:flags].anybits?(FLAG_END_STREAM)
|
|
597
615
|
stream_headers = stream[:headers] || []
|
|
@@ -714,11 +732,13 @@ module Raptor
|
|
|
714
732
|
outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
|
|
715
733
|
elsif trailers
|
|
716
734
|
stream = streams.delete(stream_id)
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
735
|
+
unless stream[:too_large]
|
|
736
|
+
completed_requests << {
|
|
737
|
+
stream_id: stream_id,
|
|
738
|
+
headers: stream[:headers],
|
|
739
|
+
body: stream[:body] || ""
|
|
740
|
+
}
|
|
741
|
+
end
|
|
722
742
|
else
|
|
723
743
|
streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
|
|
724
744
|
end
|
|
@@ -783,6 +803,7 @@ module Raptor
|
|
|
783
803
|
id: data[:id],
|
|
784
804
|
protocol: :http2,
|
|
785
805
|
buffer: buffer || "",
|
|
806
|
+
max_body_size: data[:max_body_size],
|
|
786
807
|
hpack_table: hpack_table,
|
|
787
808
|
http2_streams: streams,
|
|
788
809
|
http2_window: connection_window,
|
|
@@ -806,6 +827,7 @@ module Raptor
|
|
|
806
827
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
807
828
|
# @rbs @server_port_string: String
|
|
808
829
|
# @rbs @write_timeout: Integer
|
|
830
|
+
# @rbs @max_body_size: Integer?
|
|
809
831
|
# @rbs @access_log_io: IO?
|
|
810
832
|
# @rbs @clean_thread_locals: bool
|
|
811
833
|
# @rbs @clean_fiber_locals: bool
|
|
@@ -819,6 +841,7 @@ module Raptor
|
|
|
819
841
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
820
842
|
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
821
843
|
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
844
|
+
# @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
822
845
|
# @param http2_options [Hash] HTTP/2-specific settings
|
|
823
846
|
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
824
847
|
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
@@ -832,6 +855,7 @@ module Raptor
|
|
|
832
855
|
@app = app
|
|
833
856
|
@server_port_string = server_port.to_s.freeze
|
|
834
857
|
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
858
|
+
@max_body_size = connection_options[:max_body_size]
|
|
835
859
|
@access_log_io = access_log_io
|
|
836
860
|
@clean_thread_locals = clean_thread_locals
|
|
837
861
|
@clean_fiber_locals = clean_fiber_locals
|
|
@@ -902,6 +926,7 @@ module Raptor
|
|
|
902
926
|
initial_state = {
|
|
903
927
|
id: id,
|
|
904
928
|
protocol: :http2,
|
|
929
|
+
max_body_size: @max_body_size,
|
|
905
930
|
remote_addr: remote_addr,
|
|
906
931
|
url_scheme: url_scheme
|
|
907
932
|
}
|
|
@@ -1140,11 +1165,6 @@ module Raptor
|
|
|
1140
1165
|
trailers: env[RESPONSE_TRAILERS],
|
|
1141
1166
|
request_method: env[Rack::REQUEST_METHOD]
|
|
1142
1167
|
) { response_started = true }
|
|
1143
|
-
if response_body.is_a?(DetachedBody)
|
|
1144
|
-
response_body.finish(:closed)
|
|
1145
|
-
elsif response_body.respond_to?(:close)
|
|
1146
|
-
response_body.close
|
|
1147
|
-
end
|
|
1148
1168
|
write_access_log(env, status, response_size, remote_addr) if @access_log_io
|
|
1149
1169
|
Http.call_response_finished(env, status, response_headers, nil)
|
|
1150
1170
|
rescue StreamClosedError => error
|
|
@@ -1230,6 +1250,12 @@ module Raptor
|
|
|
1230
1250
|
stream.close
|
|
1231
1251
|
|
|
1232
1252
|
stream.bytes_written.to_s
|
|
1253
|
+
ensure
|
|
1254
|
+
if body.is_a?(DetachedBody)
|
|
1255
|
+
body.finish($! ? :cancelled : :closed)
|
|
1256
|
+
elsif body.respond_to?(:close)
|
|
1257
|
+
body.close
|
|
1258
|
+
end
|
|
1233
1259
|
end
|
|
1234
1260
|
|
|
1235
1261
|
# Sends an HTTP 103 Early Hints response.
|
|
@@ -1298,6 +1324,7 @@ module Raptor
|
|
|
1298
1324
|
# @rbs (Array[[String, String]] headers, String body, TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
|
|
1299
1325
|
def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
|
|
1300
1326
|
env = {}
|
|
1327
|
+
underscored_headers = nil
|
|
1301
1328
|
|
|
1302
1329
|
headers.each do |name, value|
|
|
1303
1330
|
if name.start_with?(":")
|
|
@@ -1316,6 +1343,13 @@ module Raptor
|
|
|
1316
1343
|
env[Http::CONTENT_LENGTH] = value
|
|
1317
1344
|
else
|
|
1318
1345
|
rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
1346
|
+
if name.include?("_")
|
|
1347
|
+
next if env.key?(rack_key) && !underscored_headers&.key?(rack_key)
|
|
1348
|
+
|
|
1349
|
+
(underscored_headers ||= {})[rack_key] = true
|
|
1350
|
+
elsif underscored_headers&.delete(rack_key)
|
|
1351
|
+
env.delete(rack_key)
|
|
1352
|
+
end
|
|
1319
1353
|
if (existing = env[rack_key])
|
|
1320
1354
|
separator = rack_key == Rack::HTTP_COOKIE ? "; " : ", "
|
|
1321
1355
|
env[rack_key] = "#{existing}#{separator}#{value}"
|
|
@@ -1345,7 +1379,6 @@ module Raptor
|
|
|
1345
1379
|
|
|
1346
1380
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
1347
1381
|
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
1348
|
-
env[Http::HTTP_VERSION] = SERVER_PROTOCOL
|
|
1349
1382
|
env[RESPONSE_TRAILERS] = {}
|
|
1350
1383
|
|
|
1351
1384
|
populate_server_name_and_port(env)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Raptor
|
|
5
|
+
# An extension, loaded from `raptor/plugin/<name>` or
|
|
6
|
+
# registered explicitly by application code.
|
|
7
|
+
#
|
|
8
|
+
class Plugin
|
|
9
|
+
class UnknownPlugin < LoadError; end
|
|
10
|
+
|
|
11
|
+
# @rbs self.@registry: Hash[String, singleton(Plugin)]
|
|
12
|
+
@registry = {}
|
|
13
|
+
|
|
14
|
+
# Registers a plugin class under a name.
|
|
15
|
+
#
|
|
16
|
+
# @rbs (String | Symbol name, singleton(Plugin) plugin) -> void
|
|
17
|
+
def self.register(name, plugin)
|
|
18
|
+
@registry[name.to_s] = plugin
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Defines and registers a plugin without a named subclass.
|
|
22
|
+
#
|
|
23
|
+
# @rbs (String | Symbol name) { () -> void } -> singleton(Plugin)
|
|
24
|
+
def self.create(name, &block)
|
|
25
|
+
plugin = Class.new(self, &block)
|
|
26
|
+
register(name, plugin)
|
|
27
|
+
plugin
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Loads a registered plugin, requiring its conventional path if needed.
|
|
31
|
+
#
|
|
32
|
+
# @rbs (String | Symbol name) -> Plugin
|
|
33
|
+
def self.load(name)
|
|
34
|
+
name = name.to_s
|
|
35
|
+
path = "raptor/plugin/#{name}"
|
|
36
|
+
unless @registry.key?(name)
|
|
37
|
+
begin
|
|
38
|
+
require path
|
|
39
|
+
rescue LoadError => error
|
|
40
|
+
raise UnknownPlugin, "Unable to find plugin #{name.inspect}" if error.path == path
|
|
41
|
+
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
plugin = @registry[name]
|
|
47
|
+
raise UnknownPlugin, "#{path} did not register plugin #{name.inspect}" unless plugin
|
|
48
|
+
|
|
49
|
+
plugin.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @rbs @threads: Array[Thread]?
|
|
53
|
+
|
|
54
|
+
# Starts in the master before workers fork. Override to open resources
|
|
55
|
+
# and append callbacks to the cluster's lifecycle hook arrays.
|
|
56
|
+
#
|
|
57
|
+
# @rbs (Cluster cluster) -> void
|
|
58
|
+
def start(cluster)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Stops in the master after workers exit, including before hot restart.
|
|
62
|
+
# Also called if startup fails, so resources may be only partly opened.
|
|
63
|
+
#
|
|
64
|
+
# @rbs () -> void
|
|
65
|
+
def stop
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Runs a block on a master-owned thread. Raptor stops and joins these
|
|
69
|
+
# threads after calling `stop`; use `ensure` in the block for cleanup.
|
|
70
|
+
#
|
|
71
|
+
# @rbs () { () -> void } -> Thread
|
|
72
|
+
def in_background(&block)
|
|
73
|
+
@threads ||= []
|
|
74
|
+
@threads << Thread.new do
|
|
75
|
+
Thread.current.name = "Plugin #{self.class}"
|
|
76
|
+
block.call
|
|
77
|
+
end
|
|
78
|
+
@threads.last
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Calls the plugin's stop callback and joins its background threads.
|
|
82
|
+
#
|
|
83
|
+
# @rbs () -> void
|
|
84
|
+
def shutdown
|
|
85
|
+
stop
|
|
86
|
+
ensure
|
|
87
|
+
threads = @threads || []
|
|
88
|
+
threads.each(&:kill)
|
|
89
|
+
error = nil
|
|
90
|
+
threads.each do |thread|
|
|
91
|
+
thread.join
|
|
92
|
+
rescue StandardError => thread_error
|
|
93
|
+
error ||= thread_error
|
|
94
|
+
end
|
|
95
|
+
raise error if error
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|