tp2 0.19.3 → 0.20.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: e0b5eac74e3e54f7dfbad1af92a110aa755c3e5ebe18229879c734baaa24e8cf
4
- data.tar.gz: ca1bf493c839517f11db5b0d346958c44bfd1aa194dbbd18e2300e9d843e8d9b
3
+ metadata.gz: f50f04dad197bd317793e48f2f46e90f87c0ddf0f65c98d99c57cc8e29a24f66
4
+ data.tar.gz: 287838481d4b0e8150c021368cf709543331b5d4b0e87bb37b335c750c8ae267
5
5
  SHA512:
6
- metadata.gz: bab1d252cbe06291116e6ddbb8e973e8a0b0acefd055acd52290d371629dc85abb6a4f4f5339a13bdbf20b2f1647b53517f30695ea356b0a5f9dcc30d01cb360
7
- data.tar.gz: 6c22dd2ebdb82c883f6805f2f0d456e76f5b86d84e57d2a1dfc3c8b66dbd2c36710e4f98d91607b6e73301390b3fbab078dda1e9a571cd3f832ed9e8b2efe2e7
6
+ metadata.gz: 1597b8856cdb6adf327652d5e161d7f8b9965358b9898ebbf2f8a5eeb5af994e3d9110b09093867ae651c9237dd4f3f31b10d7bb2a8d6e0854cb5379980d8e7d
7
+ data.tar.gz: 9f85ecb5c04107b8db550a7996429d39e39baab2efbcf3a355cfcb756ba6be45fb009a9da63ab2cf44fbda4bd2bec15b5187a65e867c97ac6d8b6aeeb7479832
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest]
11
- ruby: [3.4, 'head']
11
+ ruby: ['4.0', 'head']
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # 0.20.0 2026-01-28
2
+
3
+ - Update Qeweney
4
+ - Fix lack of sendv (for kernels < 6.17)
5
+
6
+ # 0.19.4 2026-01-26
7
+
8
+ - Fix and make graceful_shutdown idempotent
9
+ - Update dependencies
10
+
1
11
  # 0.19.3 2025-12-10
2
12
 
3
13
  - Update UringMachine
data/Gemfile CHANGED
@@ -1,12 +1,12 @@
1
- source 'https://rubygems.org'
1
+ source 'https://gem.coop'
2
2
 
3
3
  gemspec name: 'tp2'
4
4
 
5
5
  group :development do
6
- gem 'listen', '3.9.0'
7
- gem 'minitest', '5.25.4'
8
- gem 'rake', '13.2.1'
9
- gem 'rake-compiler', '1.2.9'
6
+ gem 'listen', '3.10.0'
7
+ gem 'minitest', '6.0.1'
8
+ gem 'rake', '13.3.1'
9
+ gem 'rake-compiler', '1.3.1'
10
10
  gem 'simplecov', '0.22.0'
11
- gem 'yard', '0.9.37'
11
+ gem 'yard', '0.9.38'
12
12
  end
data/examples/simple.rb CHANGED
@@ -8,9 +8,19 @@ end
8
8
 
9
9
  require 'tp2'
10
10
 
11
+ BODY = 'foobar' * 1000
11
12
  app = ->(req) {
12
- req.respond('foobar', ':status' => Qeweney::Status::TEAPOT)
13
+ req.respond(BODY)
13
14
  }
14
15
 
15
- TP2.config(&app)
16
- TP2.run
16
+ PORT = 1234
17
+
18
+ machine = UM.new
19
+ opts = {
20
+ banner: TP2::BANNER,
21
+ }
22
+ # opts[:logger] = TP2::Logger.new(machine, **opts)
23
+ env = { bind: "127.0.0.1:#{PORT}" }.merge(opts)
24
+ puts "Listening on #{env[:bind]}"
25
+ server = TP2::Server.new(machine, env) { app&.call(it) }
26
+ server.run
@@ -153,25 +153,50 @@ module TP2
153
153
 
154
154
  SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
155
155
 
156
- # Sends response including headers and body. Waits for the request to complete
157
- # if not yet completed. The body is sent using chunked transfer encoding.
158
- # @param request [Qeweney::Request] HTTP request
159
- # @param body [String] response body
160
- # @param headers
161
- def respond(request, body, headers)
162
- headers = @response_headers.merge(headers) if @response_headers
156
+ EMPTY_CHUNK = "0\r\n\r\n"
157
+ EMPTY_CHUNK_LEN = EMPTY_CHUNK.bytesize
163
158
 
164
- formatted_headers = format_headers(headers, body)
165
- @response_headers = headers
166
- request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
167
- if body
168
- buf = "#{formatted_headers}#{body.bytesize.to_s(16)}\r\n#{body}\r\n#{EMPTY_CHUNK}"
169
- @machine.send(@fd, buf, buf.bytesize, SEND_FLAGS)
170
- else
171
- @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
159
+ CHUNKED_ENCODING_POSTLUDE = "\r\n#{EMPTY_CHUNK}"
160
+
161
+ if UM.instance_methods.include?(:sendv)
162
+ # Sends response including headers and body. Waits for the request to complete
163
+ # if not yet completed. The body is sent using chunked transfer encoding.
164
+ # @param request [Qeweney::Request] HTTP request
165
+ # @param body [String] response body
166
+ # @param headers
167
+ def respond(request, body, headers)
168
+ headers = @response_headers.merge(headers) if @response_headers
169
+
170
+ formatted_headers = format_headers(headers, body)
171
+ @response_headers = headers
172
+ request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
173
+ if body
174
+ chunk_prelude = "#{body.bytesize.to_s(16)}\r\n"
175
+ @machine.sendv(@fd, formatted_headers, chunk_prelude, body, CHUNKED_ENCODING_POSTLUDE)
176
+ else
177
+ @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
178
+ end
179
+ @logger&.info(request: request, response_headers: headers) if request
180
+ @done = true
181
+ end
182
+ else
183
+ def respond(request, body, headers)
184
+ headers = @response_headers.merge(headers) if @response_headers
185
+
186
+ formatted_headers = format_headers(headers, body)
187
+ @response_headers = headers
188
+ request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
189
+ if body
190
+ chunk_prelude = "#{body.bytesize.to_s(16)}\r\n"
191
+ buf = +''
192
+ buf << formatted_headers << chunk_prelude << body << CHUNKED_ENCODING_POSTLUDE
193
+ @machine.send(@fd, buf, SEND_FLAGS)
194
+ else
195
+ @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
196
+ end
197
+ @logger&.info(request: request, response_headers: headers) if request
198
+ @done = true
172
199
  end
173
- @logger&.info(request: request, response_headers: headers) if request
174
- @done = true
175
200
  end
176
201
 
177
202
  # Sends response headers. If empty_response is truthy, the response status
@@ -187,9 +212,6 @@ module TP2
187
212
  @response_headers = headers
188
213
  end
189
214
 
190
- EMPTY_CHUNK = "0\r\n\r\n"
191
- EMPTY_CHUNK_LEN = EMPTY_CHUNK.bytesize
192
-
193
215
  # Sends a response body chunk. If no headers were sent, default headers are
194
216
  # sent using #send_headers. if the done option is true(thy), an empty chunk
195
217
  # will be sent to signal response completion to the client.
data/lib/tp2/server.rb CHANGED
@@ -50,11 +50,15 @@ module TP2
50
50
 
51
51
  def run
52
52
  setup
53
- @machine.join(*@accept_fibers)
53
+ @machine.await_fibers(@accept_fibers)
54
54
  rescue UM::Terminate
55
55
  graceful_shutdown
56
56
  end
57
57
 
58
+ def stop!
59
+ graceful_shutdown
60
+ end
61
+
58
62
  private
59
63
 
60
64
  def setup
@@ -69,7 +73,7 @@ module TP2
69
73
  setup_server_extensions
70
74
 
71
75
  # map fibers
72
- @connection_fiber_map = {}
76
+ @connection_fibers = Set.new
73
77
  end
74
78
 
75
79
  def get_bind_entries
@@ -124,47 +128,52 @@ module TP2
124
128
  end
125
129
 
126
130
  def accept_incoming(listen_fd)
127
- @machine.accept_each(listen_fd) do |fd|
128
- conn = Connection.new(self, @machine, fd, @env, &@app)
129
- f = @machine.spin(conn) do
130
- it.run
131
- ensure
132
- @connection_fiber_map.delete(f)
133
- end
134
- @connection_fiber_map[f] = true
135
- end
131
+ @machine.accept_each(listen_fd) { start_client_connection(it) }
136
132
  rescue UM::Terminate
137
133
  # terminated
138
- rescue Exception => e
139
- p e
140
- p e.backtrace
141
- exit
134
+ end
135
+
136
+ def start_client_connection(fd)
137
+ conn = Connection.new(self, @machine, fd, @env, &@app)
138
+ f = @machine.spin(conn) do
139
+ it.run
140
+ ensure
141
+ @connection_fibers.delete(f)
142
+ end
143
+ @connection_fibers << f
142
144
  end
143
145
 
144
146
  def close_all_server_fds
145
147
  @server_fds.each { @machine.close_async(it) }
146
148
  end
147
149
 
150
+ STOP = UM::Terminate.new
151
+
152
+ def stop_accept_fibers
153
+ @accept_fibers.each { @machine.schedule(it, STOP) if !it.done? }
154
+ @machine.await_fibers(@accept_fibers)
155
+ end
156
+
148
157
  def graceful_shutdown
149
158
  @env[:logger]&.info(message: 'Shutting down gracefully...')
150
159
 
151
160
  # stop listening
152
161
  close_all_server_fds
162
+ stop_accept_fibers
153
163
  @machine.snooze
154
164
 
155
- return if @connection_fiber_map.empty?
165
+ return if @connection_fibers.empty?
156
166
 
157
167
  # sleep for a bit, let requests finish
158
168
  @machine.sleep(PENDING_REQUESTS_GRACE_PERIOD)
159
- return if @connection_fiber_map.empty?
169
+ return if @connection_fibers.empty?
160
170
 
161
171
  # terminate pending fibers
162
- pending = @connection_fiber_map.keys
163
- signal = UM::Terminate.new
164
- pending.each { @machine.schedule(it, signal) }
172
+ pending = @connection_fibers.to_a
173
+ pending.each { @machine.schedule(it, STOP) }
165
174
 
166
175
  @machine.timeout(PENDING_REQUESTS_TIMEOUT_PERIOD, UM::Terminate) do
167
- @machine.join(*@connection_fiber_map.keys)
176
+ @machine.await_fibers(@connection_fibers)
168
177
  rescue UM::Terminate
169
178
  # timeout on waiting for adapters to finish running, do nothing
170
179
  end
data/lib/tp2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TP2
2
- VERSION = '0.19.3'
2
+ VERSION = '0.20.0'
3
3
  end
@@ -491,9 +491,9 @@ class ConnectionTest < Minitest::Test
491
491
  @machine.snooze
492
492
  end
493
493
 
494
- content = IO.read(__FILE__)
494
+ content = IO.read(fn)
495
495
  file_size = content.bytesize
496
- expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
496
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n#{file_size.to_s(16)}\r\n#{content}\r\n0\r\n\r\n"
497
497
 
498
498
  assert_equal expected, response
499
499
  end
@@ -545,7 +545,7 @@ class ConnectionTest < Minitest::Test
545
545
  assert_equal expected, response
546
546
  end
547
547
 
548
- def test_set_response_headers
548
+ def test_set_response_headers_1
549
549
  @hook = ->(req) {
550
550
  req.set_response_headers("Set-Cookie" => 'foo=bar')
551
551
  req.respond('foo')
@@ -569,7 +569,7 @@ class ConnectionTest < Minitest::Test
569
569
  assert_equal expected, response
570
570
  end
571
571
 
572
- def test_set_response_headers
572
+ def test_set_response_headers_2
573
573
  @hook = ->(req) {
574
574
  req.set_response_headers("Set-Cookie" => 'foo=bar')
575
575
  req.set_response_headers("Foo" => 'bar')
data/test/test_server.rb CHANGED
@@ -38,15 +38,13 @@ class ServerTest < Minitest::Test
38
38
 
39
39
  def run_server
40
40
  @server.run
41
- rescue STOP
42
- # ignore
43
41
  ensure
44
42
  @server_done = true
45
43
  end
46
44
 
47
45
  def teardown
48
46
  @machine.close(@client_fd) rescue nil
49
- @machine.schedule(@f_server, STOP.new)
47
+ @server.stop!
50
48
  @machine.snooze until @server_done
51
49
  end
52
50
 
@@ -113,7 +111,7 @@ class ServerTest < Minitest::Test
113
111
  write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHMET /bar HTTP/1.1\r\n\r\n"
114
112
 
115
113
  @machine.sleep(0.01)
116
- @machine.schedule(@f_server, UM::Terminate.new)
114
+ @server.stop!
117
115
  @machine.snooze
118
116
 
119
117
  response = read_client_side
@@ -282,9 +280,9 @@ class ServerTest < Minitest::Test
282
280
  @env[:logger] = TestLogger.new
283
281
  @app = ->(req) { reqs << req; req.respond('Hello, world!', {}) }
284
282
 
285
- write_http_request "GET / HTTP/1.0\r\n\r\n"
283
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
286
284
  response = read_client_side
287
- expected = "HTTP/1.1 200\r\nContent-Length: 13\r\n\r\nHello, world!"
285
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nHello, world!\r\n0\r\n\r\n"
288
286
  assert_equal(expected, response)
289
287
 
290
288
  entries = @env[:logger].entries
data/tp2.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.required_ruby_version = '>= 3.4'
21
21
  s.executables = ['tp2']
22
22
 
23
- s.add_dependency 'uringmachine', '~> 0.22.0'
24
- s.add_dependency 'qeweney', '~> 0.23'
23
+ s.add_dependency 'uringmachine', '~> 0.23.1'
24
+ s.add_dependency 'qeweney', '~> 0.25'
25
25
  s.add_dependency 'rack', '~> 3.1.15'
26
26
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tp2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.3
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.22.0
18
+ version: 0.23.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.22.0
25
+ version: 0.23.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: qeweney
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.23'
32
+ version: '0.25'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.23'
39
+ version: '0.25'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: rack
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -111,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
111
  - !ruby/object:Gem::Version
112
112
  version: '0'
113
113
  requirements: []
114
- rubygems_version: 4.0.0
114
+ rubygems_version: 4.0.3
115
115
  specification_version: 4
116
116
  summary: Experimental HTTP/1 server for UringMachine
117
117
  test_files: []