raptor 0.21.0 → 0.22.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 +6 -0
- data/README.md +37 -71
- data/docs/raptor-vs-puma.md +12 -16
- data/lib/raptor/cluster.rb +1 -0
- data/lib/raptor/detached_body.rb +293 -0
- data/lib/raptor/http1.rb +86 -16
- data/lib/raptor/http2.rb +98 -36
- data/lib/raptor/reactor.rb +856 -33
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +1 -0
- data/sig/generated/raptor/detached_body.rbs +127 -0
- data/sig/generated/raptor/http1.rbs +22 -4
- data/sig/generated/raptor/http2.rbs +41 -13
- data/sig/generated/raptor/reactor.rbs +320 -13
- metadata +3 -1
data/lib/raptor/http1.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "tempfile"
|
|
|
8
8
|
require "atomic-ruby/atomic_boolean"
|
|
9
9
|
require "rack"
|
|
10
10
|
|
|
11
|
+
require_relative "detached_body"
|
|
11
12
|
require_relative "http"
|
|
12
13
|
require_relative "raptor_http"
|
|
13
14
|
require_relative "thread_locals"
|
|
@@ -97,6 +98,28 @@ module Raptor
|
|
|
97
98
|
false
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
# Encodes one HTTP/1.1 response chunk.
|
|
102
|
+
#
|
|
103
|
+
# @rbs (String chunk) -> String
|
|
104
|
+
def self.encode_chunk(chunk)
|
|
105
|
+
HttpParser.chunked_encode(String.new, chunk)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Encodes the final HTTP/1.1 response chunk and its trailers.
|
|
109
|
+
#
|
|
110
|
+
# @rbs (Hash[String, String | Array[String]] trailers) -> String
|
|
111
|
+
def self.encode_trailers(trailers)
|
|
112
|
+
normalized = trailers.each_with_object({}) do |(key, value), headers|
|
|
113
|
+
name = key.match?(/[A-Z]/) ? key.downcase : key
|
|
114
|
+
next if name.start_with?(RACK_HEADER_PREFIX) || name == "status"
|
|
115
|
+
|
|
116
|
+
headers[name] = value
|
|
117
|
+
end
|
|
118
|
+
response = +"0\r\n"
|
|
119
|
+
HttpParser.format_headers(response, normalized)
|
|
120
|
+
response << "\r\n"
|
|
121
|
+
end
|
|
122
|
+
|
|
100
123
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
101
124
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
102
125
|
# plus populated `:env` and `:body`; malformed or oversized input flips
|
|
@@ -492,7 +515,7 @@ module Raptor
|
|
|
492
515
|
#
|
|
493
516
|
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
494
517
|
def process_client(socket, id, env, parse_data, body, reactor, thread_pool, request_count, remote_addr, url_scheme)
|
|
495
|
-
keep_alive = process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
518
|
+
keep_alive = process_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
496
519
|
eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) if keep_alive
|
|
497
520
|
end
|
|
498
521
|
|
|
@@ -501,20 +524,22 @@ module Raptor
|
|
|
501
524
|
# for another request.
|
|
502
525
|
#
|
|
503
526
|
# @param socket [TCPSocket] the client socket
|
|
527
|
+
# @param id [Integer] unique client identifier
|
|
504
528
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
505
529
|
# @param parse_data [Hash] metadata from the parsing pass
|
|
506
530
|
# @param body [String, nil] decoded request body
|
|
531
|
+
# @param reactor [Reactor] the reactor managing the client connection
|
|
507
532
|
# @param request_count [Integer] number of requests handled on this connection
|
|
508
533
|
# @param remote_addr [String] client IP address
|
|
509
534
|
# @param url_scheme [String] "http" or "https"
|
|
510
535
|
# @return [Boolean] true if the connection should be kept alive
|
|
511
536
|
#
|
|
512
|
-
# @rbs (TCPSocket socket, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
513
|
-
def process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
537
|
+
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
538
|
+
def process_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
514
539
|
if @clean_fiber_locals
|
|
515
|
-
Fiber.new { perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme) }.resume
|
|
540
|
+
Fiber.new { perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme) }.resume
|
|
516
541
|
else
|
|
517
|
-
perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
542
|
+
perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
518
543
|
end
|
|
519
544
|
ensure
|
|
520
545
|
ThreadLocals.clear if @clean_thread_locals
|
|
@@ -522,13 +547,14 @@ module Raptor
|
|
|
522
547
|
|
|
523
548
|
# Calls the Rack app and writes its response for one request.
|
|
524
549
|
#
|
|
525
|
-
# @rbs (TCPSocket socket, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
526
|
-
def perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
550
|
+
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
551
|
+
def perform_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
527
552
|
rack_env = nil
|
|
528
553
|
status = nil
|
|
529
554
|
headers = nil
|
|
530
555
|
hijacked = false
|
|
531
556
|
keep_alive = false
|
|
557
|
+
detached = false
|
|
532
558
|
response_started = false
|
|
533
559
|
|
|
534
560
|
begin
|
|
@@ -540,16 +566,40 @@ module Raptor
|
|
|
540
566
|
body.close if body.respond_to?(:close)
|
|
541
567
|
else
|
|
542
568
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
569
|
+
no_body = rack_env[Rack::REQUEST_METHOD] == "HEAD" || (status >= 100 && status < 200) || status == 204 || status == 304
|
|
570
|
+
detached = body.is_a?(DetachedBody) && !no_body
|
|
571
|
+
if detached
|
|
572
|
+
chunked = rack_env[Rack::SERVER_PROTOCOL] == HTTP_11
|
|
573
|
+
keep_alive = chunked && keep_alive?(rack_env, request_count)
|
|
574
|
+
response_started = true
|
|
575
|
+
write_detached_response(socket, status, headers, chunked: chunked, keep_alive: keep_alive)
|
|
576
|
+
finished = proc do |reason|
|
|
577
|
+
error = IOError.new("detached response closed") unless reason == :closed
|
|
578
|
+
write_access_log(rack_env, status, "-", remote_addr) if @access_log_io
|
|
579
|
+
Http.call_response_finished(rack_env, status, headers, error)
|
|
580
|
+
end
|
|
581
|
+
reactor.attach_http1_body(
|
|
582
|
+
socket,
|
|
583
|
+
id,
|
|
584
|
+
body,
|
|
585
|
+
{chunked: chunked, keep_alive: keep_alive, request_count: request_count, remote_addr: remote_addr, url_scheme: url_scheme},
|
|
586
|
+
finished
|
|
587
|
+
)
|
|
588
|
+
else
|
|
589
|
+
streaming = body.respond_to?(:call) && !body.respond_to?(:each)
|
|
590
|
+
keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
|
|
591
|
+
response_size = response_size(headers, body) if @access_log_io && !hijacked
|
|
592
|
+
response_started = true
|
|
593
|
+
write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
|
|
594
|
+
body.finish(:closed) if body.is_a?(DetachedBody)
|
|
595
|
+
end
|
|
548
596
|
end
|
|
549
597
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
598
|
+
unless detached
|
|
599
|
+
write_access_log(rack_env, status, response_size, remote_addr) if @access_log_io && !hijacked
|
|
600
|
+
Http.call_response_finished(rack_env, status, headers, nil)
|
|
601
|
+
end
|
|
602
|
+
keep_alive && !hijacked && !detached
|
|
553
603
|
rescue => error
|
|
554
604
|
keep_alive = false
|
|
555
605
|
handle_app_error(socket, rack_env, status, headers, error, response_started: response_started, hijacked: hijacked)
|
|
@@ -557,7 +607,7 @@ module Raptor
|
|
|
557
607
|
rack_input = rack_env && rack_env[Rack::RACK_INPUT]
|
|
558
608
|
rack_input.close! rescue nil if rack_input.respond_to?(:close!)
|
|
559
609
|
|
|
560
|
-
unless hijacked || keep_alive
|
|
610
|
+
unless hijacked || keep_alive || detached
|
|
561
611
|
socket.close rescue nil
|
|
562
612
|
end
|
|
563
613
|
end
|
|
@@ -666,9 +716,11 @@ module Raptor
|
|
|
666
716
|
|
|
667
717
|
keep_alive = process_request(
|
|
668
718
|
socket,
|
|
719
|
+
id,
|
|
669
720
|
env,
|
|
670
721
|
parse_data,
|
|
671
722
|
body,
|
|
723
|
+
reactor,
|
|
672
724
|
request_count,
|
|
673
725
|
remote_addr,
|
|
674
726
|
url_scheme
|
|
@@ -895,6 +947,24 @@ module Raptor
|
|
|
895
947
|
socket_write(socket, response)
|
|
896
948
|
end
|
|
897
949
|
|
|
950
|
+
# Starts a detached response, chunked on HTTP/1.1 and ended by closing
|
|
951
|
+
# the connection on HTTP/1.0.
|
|
952
|
+
#
|
|
953
|
+
# @rbs (TCPSocket socket, Integer status, Hash[String, String | Array[String]] headers, chunked: bool, keep_alive: bool) -> void
|
|
954
|
+
def write_detached_response(socket, status, headers, chunked:, keep_alive:)
|
|
955
|
+
validate_status(status)
|
|
956
|
+
headers = normalize_headers(headers)
|
|
957
|
+
validate_headers(headers, status, false)
|
|
958
|
+
headers.delete(Rack::CONTENT_LENGTH)
|
|
959
|
+
headers[Rack::TRANSFER_ENCODING] = TRANSFER_ENCODING_CHUNKED if chunked
|
|
960
|
+
headers["connection"] = keep_alive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE
|
|
961
|
+
|
|
962
|
+
response = build_status_line(chunked ? HTTP_11 : HTTP_10, status)
|
|
963
|
+
HttpParser.format_headers(response, headers)
|
|
964
|
+
response << "\r\n"
|
|
965
|
+
socket_write(socket, response)
|
|
966
|
+
end
|
|
967
|
+
|
|
898
968
|
# Writes a complete HTTP response to the socket.
|
|
899
969
|
#
|
|
900
970
|
# @param socket [TCPSocket] the client socket to write to
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "atomic-ruby/atomic_boolean"
|
|
|
8
8
|
require "atomic-ruby/atomic_condition_variable"
|
|
9
9
|
require "rack"
|
|
10
10
|
|
|
11
|
+
require_relative "detached_body"
|
|
11
12
|
require_relative "http"
|
|
12
13
|
require_relative "raptor_http2"
|
|
13
14
|
require_relative "thread_locals"
|
|
@@ -18,13 +19,12 @@ module Raptor
|
|
|
18
19
|
class Http2
|
|
19
20
|
RESPONSE_TRAILERS = "raptor.response_trailers"
|
|
20
21
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
22
|
+
# Queues concurrent frame writes while the reactor remains the sole
|
|
23
|
+
# owner of the connection socket.
|
|
23
24
|
#
|
|
24
25
|
class Writer
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# @rbs @state: Atom
|
|
26
|
+
# @rbs @reactor: Reactor?
|
|
27
|
+
# @rbs @connection_id: Integer?
|
|
28
28
|
# @rbs @write_timeout: Integer
|
|
29
29
|
|
|
30
30
|
# Creates a new Writer.
|
|
@@ -34,45 +34,46 @@ module Raptor
|
|
|
34
34
|
#
|
|
35
35
|
# @rbs (write_timeout: Integer) -> void
|
|
36
36
|
def initialize(write_timeout:)
|
|
37
|
-
@
|
|
37
|
+
@reactor = nil
|
|
38
|
+
@connection_id = nil
|
|
38
39
|
@write_timeout = write_timeout
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
42
|
+
# Routes subsequent frame writes through the connection's reactor.
|
|
43
|
+
#
|
|
44
|
+
# @param reactor [Reactor] the reactor that owns the connection
|
|
45
|
+
# @param connection_id [Integer] unique connection identifier
|
|
46
|
+
# @return [void]
|
|
47
|
+
#
|
|
48
|
+
# @rbs (Reactor reactor, Integer connection_id) -> void
|
|
49
|
+
def attach(reactor, connection_id)
|
|
50
|
+
@reactor = reactor
|
|
51
|
+
@connection_id = connection_id
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Queues frames for the reactor to write. Before attachment, writes
|
|
55
|
+
# directly so the writer remains usable during connection setup.
|
|
43
56
|
#
|
|
44
57
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
45
|
-
# @param frames [Array<String
|
|
58
|
+
# @param frames [Array<String>, nil] frame bytes to write in order
|
|
46
59
|
# @return [void]
|
|
47
60
|
#
|
|
48
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
|
|
61
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Array[String]? frames) -> void
|
|
49
62
|
def write_frames(socket, frames)
|
|
50
63
|
return if !frames || frames.empty?
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
frames
|
|
57
|
-
else
|
|
58
|
-
claimed = false
|
|
59
|
-
current + frames
|
|
60
|
-
end
|
|
65
|
+
if @reactor
|
|
66
|
+
@reactor.write_http2_frames(@connection_id, frames)
|
|
67
|
+
else
|
|
68
|
+
Http.socket_write(socket, frames.join, timeout: @write_timeout) rescue nil
|
|
61
69
|
end
|
|
70
|
+
end
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
pending = current
|
|
69
|
-
current.empty? ? IDLE : []
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
break if pending.empty?
|
|
73
|
-
|
|
74
|
-
Http.socket_write(socket, pending.join, timeout: @write_timeout) rescue nil
|
|
75
|
-
end
|
|
72
|
+
# Attaches a response body that outlives its application thread.
|
|
73
|
+
#
|
|
74
|
+
# @rbs (Integer stream_id, DetachedBody body, ^(Symbol) -> void finished) -> bool
|
|
75
|
+
def attach_body(stream_id, body, finished)
|
|
76
|
+
@reactor.attach_http2_body(@connection_id, stream_id, body, finished)
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
79
|
|
|
@@ -133,6 +134,30 @@ module Raptor
|
|
|
133
134
|
end
|
|
134
135
|
end
|
|
135
136
|
|
|
137
|
+
# Reserves outbound capacity without waiting for a window update.
|
|
138
|
+
#
|
|
139
|
+
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
140
|
+
# @param max_bytes [Integer] the largest size the caller would like to send
|
|
141
|
+
# @return [Integer] available bytes, or 0 when flow control is exhausted
|
|
142
|
+
#
|
|
143
|
+
# @rbs (Integer stream_id, Integer max_bytes) -> Integer
|
|
144
|
+
def try_acquire(stream_id, max_bytes)
|
|
145
|
+
check(stream_id)
|
|
146
|
+
initial = @initial_stream_window.value
|
|
147
|
+
stream_window = @stream_windows.value[stream_id] || initial
|
|
148
|
+
capped = [max_bytes, MAX_FRAME_SIZE, stream_window].min
|
|
149
|
+
return 0 unless capped.positive?
|
|
150
|
+
|
|
151
|
+
granted = reserve_connection(capped)
|
|
152
|
+
if granted.positive?
|
|
153
|
+
@stream_windows.swap do |windows|
|
|
154
|
+
current = windows[stream_id] || initial
|
|
155
|
+
windows.merge(stream_id => current - granted)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
granted
|
|
159
|
+
end
|
|
160
|
+
|
|
136
161
|
# Increments the connection-level send window by `increment` bytes.
|
|
137
162
|
#
|
|
138
163
|
# @param increment [Integer] the byte count to add
|
|
@@ -900,10 +925,14 @@ module Raptor
|
|
|
900
925
|
rounds = 0
|
|
901
926
|
loop do
|
|
902
927
|
reactor.acknowledge_http2_ping(result[:id], result[:ping_acknowledgements])
|
|
903
|
-
result[:cancelled_streams]&.each
|
|
928
|
+
result[:cancelled_streams]&.each do |stream_id|
|
|
929
|
+
flow_control.cancel_stream(stream_id)
|
|
930
|
+
reactor.cancel_http2_body(result[:id], stream_id)
|
|
931
|
+
end
|
|
904
932
|
|
|
905
|
-
if flow_control && (result[:window_updates] || result[:peer_initial_window_size])
|
|
933
|
+
if flow_control && (result[:window_updates]&.any? || result[:peer_initial_window_size])
|
|
906
934
|
apply_flow_control_updates(flow_control, result)
|
|
935
|
+
reactor.resume_http2_bodies(result[:id])
|
|
907
936
|
end
|
|
908
937
|
|
|
909
938
|
writer.write_frames(socket, result[:outgoing_frames])
|
|
@@ -1025,11 +1054,26 @@ module Raptor
|
|
|
1025
1054
|
status = nil
|
|
1026
1055
|
response_headers = nil
|
|
1027
1056
|
response_started = false
|
|
1057
|
+
detached = false
|
|
1028
1058
|
|
|
1029
1059
|
flow_control.check(stream_id)
|
|
1030
1060
|
env = build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr: remote_addr)
|
|
1031
1061
|
status, response_headers, response_body = @app.call(env)
|
|
1032
1062
|
|
|
1063
|
+
no_body = env[Rack::REQUEST_METHOD] == "HEAD" || (status >= 100 && status < 200) || status == 204 || status == 304
|
|
1064
|
+
if response_body.is_a?(DetachedBody) && !no_body
|
|
1065
|
+
finished = proc do |reason|
|
|
1066
|
+
error = StreamClosedError.new unless reason == :closed
|
|
1067
|
+
write_access_log(env, status, "-", remote_addr) if @access_log_io
|
|
1068
|
+
Http.call_response_finished(env, status, response_headers, error)
|
|
1069
|
+
flow_control.discard_stream(stream_id)
|
|
1070
|
+
end
|
|
1071
|
+
detached = write_http2_detached_response(socket, writer, flow_control, stream_id, status, response_headers, response_body, finished) do
|
|
1072
|
+
response_started = true
|
|
1073
|
+
end
|
|
1074
|
+
return
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1033
1077
|
response_size = write_http2_response(
|
|
1034
1078
|
socket,
|
|
1035
1079
|
writer,
|
|
@@ -1041,7 +1085,11 @@ module Raptor
|
|
|
1041
1085
|
trailers: env[RESPONSE_TRAILERS],
|
|
1042
1086
|
request_method: env[Rack::REQUEST_METHOD]
|
|
1043
1087
|
) { response_started = true }
|
|
1044
|
-
|
|
1088
|
+
if response_body.is_a?(DetachedBody)
|
|
1089
|
+
response_body.finish(:closed)
|
|
1090
|
+
elsif response_body.respond_to?(:close)
|
|
1091
|
+
response_body.close
|
|
1092
|
+
end
|
|
1045
1093
|
write_access_log(env, status, response_size, remote_addr) if @access_log_io
|
|
1046
1094
|
Http.call_response_finished(env, status, response_headers, nil)
|
|
1047
1095
|
rescue StreamClosedError => error
|
|
@@ -1060,7 +1108,21 @@ module Raptor
|
|
|
1060
1108
|
raise
|
|
1061
1109
|
end
|
|
1062
1110
|
ensure
|
|
1063
|
-
flow_control.discard_stream(stream_id) if flow_control
|
|
1111
|
+
flow_control.discard_stream(stream_id) if flow_control && !detached
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Starts a detached HTTP/2 response after writing its response headers.
|
|
1115
|
+
#
|
|
1116
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, DetachedBody body, ^(Symbol) -> void finished) { () -> void } -> bool
|
|
1117
|
+
def write_http2_detached_response(socket, writer, flow_control, stream_id, status, headers, body, finished)
|
|
1118
|
+
flow_control.check(stream_id)
|
|
1119
|
+
parser = Http2Parser.new
|
|
1120
|
+
encoded = parser.encode_response_headers(status, headers)
|
|
1121
|
+
writer.write_frames(socket, [parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded)])
|
|
1122
|
+
yield
|
|
1123
|
+
attached = writer.attach_body(stream_id, body, finished)
|
|
1124
|
+
write_http2_reset_stream(socket, writer, stream_id, ERROR_REFUSED_STREAM) unless attached
|
|
1125
|
+
true
|
|
1064
1126
|
end
|
|
1065
1127
|
|
|
1066
1128
|
# Writes a Rack response as HTTP/2 frames to the socket, partitioning
|