anytls 0.1.3 → 0.1.4

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: 97d6d911a444c22bb45735dd22aede45733d1af86990df618339690fe9069d2c
4
- data.tar.gz: 0acb6a2669394ad05207c19dafffcb3419ac1eabe1c43024a876da302c20bdd6
3
+ metadata.gz: 614c54295c75168e63b13a6c1cb9703321cc70bbaa672a5a3d064ed630b014ad
4
+ data.tar.gz: 51446c2245624537d53fb9fcd8b49f51b3a57f3241bcfe5785eaa5206404c02e
5
5
  SHA512:
6
- metadata.gz: d3d97bc849f71208a4c81ee69867704d0bd6ca744f7b130d341a7ca0271dbd91fa41928d73d4a231236e48b41c76d9390cb41bbcb092a466c5d3477c58a59ded
7
- data.tar.gz: 8b95f3c45e6664619ab5e9615aa62a62f555e2f99747a646cbb2f6c05085584fe240c58b16437cbffdd77b2160e3d4e7a261d4b292d0f16ab29d96316faa57ea
6
+ metadata.gz: 1da8ddbc879a82a96263c991f27cbdaf8f9ae2b4ff90a112a811ad42ab63b102e09078af8fff6b8893c121c8b5683c68a65b942907181ffe464a0c8674e012f7
7
+ data.tar.gz: b3cece03e15e6593db595b7ca78e3291fecaf8eae3a1e4bc71cd73ba1b08ff22aab8a3cec5891e3eb8851e4af56b8cdfde760fc1c52197e88fa95cb79e58ddee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (2026-10-05)
4
+
5
+ - 临时接入网络错误后继续接受连接,保留已有代理会话。
6
+ - 修复 HTTP 上传完成后的响应超时,保留慢上传、提前拒绝、流式响应和 keep-alive;临时响应不重置超时预算。
7
+ - 接受 HTTP 字段值两端的合法空白,继续拒绝重复 Host、重复消息长度和冲突定界。
8
+ - 减少 HTTP 请求头写入、多流接收统计和频繁连接复用的处理开销。
9
+
3
10
  ## 0.1.3 (2026-10-05)
4
11
 
5
12
  - 忽略 CI 的 Bundler 安装缓存,避免发布前工作区检查误判为未提交源文件。
data/README.md CHANGED
@@ -4,7 +4,7 @@ Ruby 3.4+ 的 AnyTLS gem 与命令行客户端、服务端。参考版本固定
4
4
  [`anytls-go` 的 `fd6167a`](https://github.com/anytls/anytls-go/tree/fd6167acd6d73b9fa3e607659951847fbc9e6c50)。
5
5
  核心代码不依赖 Rails。
6
6
 
7
- 当前版本为 `0.1.1`,Ruby 实现使用 [MIT 许可证](LICENSE)。项目及参考代码的许可边界见
7
+ 当前版本为 `0.1.4`,Ruby 实现使用 [MIT 许可证](LICENSE)。项目及参考代码的许可边界见
8
8
  [SOURCES](docs/SOURCES.md)。
9
9
 
10
10
  ## 使用
@@ -99,7 +99,8 @@ server.run # 阻塞;由其他正常线程调用 server.stop(grace: 5)
99
99
 
100
100
  构造 Client 不拨号;首次连接时才启动会话池。`min_idle_sessions` 是回收时保留已有空闲
101
101
  session 的数量,不会主动创建连接填满池。默认最多 128 个 session,空闲超时与检查间隔
102
- 各 30 秒。客户端租用空闲 session;并发请求需要时建立新的 session。服务端默认最多
102
+ 各 30 秒;归还 session 不提前触发空闲扫描。客户端租用空闲 session;并发请求需要时
103
+ 建立新的 session。服务端默认最多
103
104
  256 个活动 stream。`Client#stats`、`Server#stats` 返回冻结的计数快照。
104
105
 
105
106
  ## 功能与边界
@@ -110,10 +111,15 @@ session 的数量,不会主动创建连接填满池。默认最多 128 个 ses
110
111
  SOCKS UDP 分片和 UoT v1 不在当前接口范围。
111
112
 
112
113
  HTTP 转发使用 `protocol-http1` 解析消息边界,拒绝冲突 Content-Length/Transfer-Encoding
113
- 及重复 Host。带请求体的 HTTP Upgrade 返回 400,避免丢弃消息体;无体 Upgrade 继续支持。
114
+ 及重复 Host,接受字段值两端合法的空格和制表符。带请求体的 HTTP Upgrade 返回 400,
115
+ 避免丢弃消息体;无体 Upgrade 继续支持。
116
+ 普通 HTTP 上传完成后,最终响应头须在 `handshake_timeout` 内到达,否则返回 504;
117
+ 上传过程及响应体传输不计入该响应头预算,临时响应不重置预算。目标提前拒绝上传时仍立即返回响应。
114
118
  HTTPS 目标使用 CONNECT。代理客户端监听仅默认绑定回环地址;扩大监听
115
119
  范围时由部署环境限制访问者。
116
120
 
121
+ 服务端与 CLI 监听器会对待接入连接的暂时网络错误退避重试,保留已有连接;监听 fd 故障仍会报错退出。
122
+
117
123
  每条 TLS 连接只有一个 owner Thread。协议接收按 stream/session 字节预算限流;慢消费者
118
124
  超过预算会被关闭,避免无限增长或阻塞其他 stream。TLS/TCP 本身的队首阻塞仍然存在。
119
125
  设计和状态边界见 [ARCHITECTURE](docs/ARCHITECTURE.md)。
data/docs/ARCHITECTURE.md CHANGED
@@ -38,6 +38,7 @@ FIN 结束整个虚拟 stream,不提供 read/write 两侧独立关闭语义。
38
38
  Client pool 同一时刻将一个 session 租给一条业务 stream,完成后将其归还并复用。
39
39
  服务端 Session 解码器支持多条 stream 的帧,入站数量另受 session 和 server 限制。
40
40
  旧空闲连接检测、心跳和 peer stream-ID 限制都在 session 生命周期中处理,不由应用代理层重试。
41
+ 池容量等待与定时回收使用独立通知;租用、归还不触发全池回收扫描,关闭会立即唤醒两类等待者。
41
42
 
42
43
  ## 线程与 TLS 所有权
43
44
 
@@ -89,10 +90,19 @@ Session 状态锁只保护短操作;锁内不等待网络、不回调调用者
89
90
  `ByteBuffer` 将输入合并为最多 16 KiB 的块,读取用头块偏移推进,避免小读后下一次写入
90
91
  复制整个接收缓冲。容量按未读字节计算,消费完的块立即移除;小输入合并到尾块,避免
91
92
  逐字节输入产生与字节数相同的队列对象。远端结束仍允许排空,显式关闭总会释放未读块。
93
+ 活跃流共享 `ByteBuffer::Budget`:入队先预留,读取释放,结束时一次性退出共享计费,
94
+ 因此每帧不再扫描所有 stream。锁顺序为 buffer → budget,不反向取 session/stream 锁。
95
+ 与原先扫描活跃流的范围一致,远端结束后由调用者保留的尾部不计入 session 总量,
96
+ 仍受单流容量限制;调用者应及时排空或关闭这些 Stream。
92
97
 
93
98
  HTTP 提前拒绝上传时,先停止客户端上传读取并转发完整响应,再关闭上游和等待上传线程。
94
99
  不能在转发响应前等待上传线程,因为该线程可能正在等待上游写入背压;也不能提前关闭
95
100
  AnyTLS Stream,否则已接收的响应体会被丢弃。
101
+ 上传线程完成写入后,以同一线程和条件变量守候最终响应头,避免新增定时线程或轮询。
102
+ 预算从上传完成开始,到最终响应头结束;临时响应不延长预算,响应体不受此预算限制。
103
+ 超时先记录原因再关闭上游,唤醒已经阻塞的读取者;提前响应与清理均解除等待并回收上传线程。
104
+ HTTP 字段值仅修剪 SP/HTAB,保持重复字段及定界冲突检查。头字段写入以最多 16 KiB
105
+ 缓冲合并,保留依赖的校验和字段顺序,返回前发送;消息体仍直接流式转发。
96
106
 
97
107
  同一 TCP 连接天然存在队首阻塞;有限缓冲只能限制 Ruby 自身的资源占用和慢消费者影响。
98
108
  当前没有扩展协议增加 per-stream credit,不承诺无限慢消费者仍可无损持续接收。
@@ -108,6 +118,8 @@ Client 默认验证证书和目标名称;`insecure: true` 必须显式选择
108
118
  `close` 可重复调用。`ensure` 负责 socket、session、stream 与线程回收,保留原始业务错误。
109
119
  INT/TERM trap 只向 pipe 写字节;实际清理在正常线程执行。没有 gem publish、在线下载设备配置
110
120
  或全局代理设置等隐藏副作用。
121
+ 监听器仅对 accept 返回的待接入网络错误及资源不足退避重试,日志有界;已取得 socket
122
+ 后的设置错误、失效监听 fd 等错误仍向外传播。退避可被停止请求唤醒,不影响已建立流。
111
123
 
112
124
  服务端 `stop(grace:)` 先停止监听,允许已经建立的 stream 在宽限期继续传输;到期再关闭
113
125
  剩余 session/目标 socket 并等待 worker 退出。并发 `stop` 服从第一次停止请求确定的边界,
data/docs/VALIDATION.md CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  CI 的 Ubuntu / Ruby 4.0 任务在验证通过后上传 `anytls-gem` artifact,包含 gem 和 `SHA256SUMS`。
27
27
  发布前须确认同一提交的完整 Ruby 矩阵和 Go 互通任务均通过,下载该 artifact 并核对校验和。
28
- 运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.3.gem` 可验证已有包与当前源码
28
+ 运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.4.gem` 可验证已有包与当前源码
29
29
  的文件内容、元数据和依赖一致,并隔离安装该包;提供路径时不会重新构建 gem。
30
30
  GitHub Release 和 RubyGems 必须使用这一份验证过的包,发布后下载两处产物并核对 SHA256。
31
31
 
@@ -4,11 +4,34 @@ module AnyTLS
4
4
  # 有界二进制接收队列;生产者不阻塞,读取者可等待数据。
5
5
  # 远端结束后先读完缓冲再报告 EOF 或异常;本地关闭则丢弃缓冲。
6
6
  class ByteBuffer
7
+ # 多个活跃接收队列共用的字节预算。仅从 buffer 锁进入预算锁,
8
+ # 不反向获取队列、stream 或 session 锁,也不调用应用回调。
9
+ class Budget
10
+ def initialize(limit)
11
+ @limit = limit
12
+ @size = 0
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def reserve(size)
17
+ @mutex.synchronize do
18
+ raise BufferLimitExceeded, "session receive buffer limit exceeded" if @size + size > @limit
19
+
20
+ @size += size
21
+ end
22
+ end
23
+
24
+ def release(size)
25
+ @mutex.synchronize { @size -= size }
26
+ end
27
+ end
28
+
7
29
  CHUNK_SIZE = 16_384
8
30
  private_constant :CHUNK_SIZE
9
31
 
10
- def initialize(limit: 262_144)
32
+ def initialize(limit: 262_144, budget: nil)
11
33
  @limit = limit
34
+ @budget = budget
12
35
  @chunks = []
13
36
  @offset = 0
14
37
  @size = 0
@@ -30,6 +53,7 @@ module AnyTLS
30
53
  return if @finished
31
54
  raise BufferLimitExceeded, "stream receive buffer limit exceeded" if @size + bytes.bytesize > @limit
32
55
 
56
+ @budget&.reserve(bytes.bytesize)
33
57
  # 合并小片段,避免逐字节输入生成大量队列对象;只复制有界尾块。
34
58
  # 已消费的前缀用偏移记录,不在每次小读后切出整个剩余缓冲。
35
59
  cursor = 0
@@ -75,6 +99,7 @@ module AnyTLS
75
99
  end
76
100
  end
77
101
  @size -= count
102
+ @budget&.release(count)
78
103
  result
79
104
  end
80
105
  end
@@ -84,6 +109,10 @@ module AnyTLS
84
109
  unless @finished
85
110
  @error = error
86
111
  @finished = true
112
+ # 流移出会话后退出共享计费;远端 FIN 保留的尾部仍受单流上限约束,
113
+ # 并可读到 EOF。之后的读、重复 finish/close 不得再次释放预算。
114
+ @budget&.release(@size)
115
+ @budget = nil
87
116
  end
88
117
  if discard
89
118
  @chunks.clear
@@ -60,8 +60,16 @@ module AnyTLS
60
60
  end
61
61
  start_worker(socket)
62
62
  @resource_limited = false
63
+ rescue *IOHelpers::ACCEPT_RETRY_ERRORS => error
64
+ raise if socket.is_a?(IO)
65
+
66
+ pause_accept(shutdown, error)
63
67
  rescue Errno::EMFILE, Errno::ENFILE, ThreadError => error
64
68
  socket.close if socket.is_a?(IO) && !socket.closed?
69
+ pause_accept(shutdown, error)
70
+ end
71
+
72
+ def pause_accept(shutdown, error)
65
73
  Log.emit(@logger, :warn, "proxy admission paused (#{error.class})") unless @resource_limited
66
74
  @resource_limited = true
67
75
  IO.select([shutdown.reader], nil, nil, 0.05)
@@ -17,8 +17,8 @@ module AnyTLS
17
17
  attr_reader :declared_content_length
18
18
 
19
19
  def read_headers
20
- headers = super
21
- fields = headers.to_a
20
+ # OWS 仅包含 SP/HTAB;保留重复字段,避免规范化掩盖消息定界冲突。
21
+ fields = super.to_a.map { |name, value| [name, value.gsub(/\A[ \t]+|[ \t]+\z/, "")] }
22
22
  lengths = fields.select { |name, _| name.downcase == "content-length" }.map(&:last)
23
23
  transfers = fields.select { |name, _| name.downcase == "transfer-encoding" }.map(&:last)
24
24
  hosts = fields.count { |name, _| name.downcase == "host" }
@@ -36,7 +36,7 @@ module AnyTLS
36
36
  fields.map(&:last).each do |value|
37
37
  raise ProtocolError, "invalid HTTP header value" if value.match?(/[\x00-\x08\x0a-\x1f\x7f]/)
38
38
  end
39
- headers
39
+ ::Protocol::HTTP::Headers.new(fields)
40
40
  end
41
41
 
42
42
  def read_request_body(method, headers)
@@ -49,8 +49,85 @@ module AnyTLS
49
49
 
50
50
  super
51
51
  end
52
+
53
+ # 仅在同步写出头字段期间合并小写入;不缓存消息体,结束时即发送。
54
+ # 继续使用依赖的字段校验、顺序和重复字段语义。
55
+ def write_headers(headers)
56
+ @header_buffer = +"".b
57
+ super
58
+ bytes = @header_buffer
59
+ @header_buffer = nil
60
+ write_stream(bytes) unless bytes.empty?
61
+ ensure
62
+ @header_buffer = nil
63
+ end
64
+
65
+ private
66
+
67
+ def write_stream(bytes)
68
+ return super unless @header_buffer
69
+
70
+ if @header_buffer.bytesize + bytes.bytesize > 16_384
71
+ super(@header_buffer) unless @header_buffer.empty?
72
+ @header_buffer.clear
73
+ end
74
+ if bytes.bytesize > 16_384
75
+ super
76
+ else
77
+ @header_buffer << bytes
78
+ end
79
+ end
52
80
  end
53
81
 
82
+ # 复用上传线程守候最终响应头,不增加每请求的定时器线程。
83
+ # 上传结束才启动预算;提前响应和异常清理均唤醒此等待者。
84
+ class ResponseWait
85
+ def initialize(timeout)
86
+ @timeout = timeout
87
+ @mutex = Mutex.new
88
+ @changed = ConditionVariable.new
89
+ @uploading = true
90
+ @complete = false
91
+ end
92
+
93
+ def uploading?
94
+ @mutex.synchronize { @uploading }
95
+ end
96
+
97
+ def error
98
+ @mutex.synchronize { @error }
99
+ end
100
+
101
+ def fail(error)
102
+ @mutex.synchronize do
103
+ return if @error
104
+
105
+ @error = error
106
+ end
107
+ end
108
+
109
+ def uploaded
110
+ @mutex.synchronize do
111
+ @uploading = false
112
+ deadline = Deadline.new(@timeout)
113
+ deadline.wait(@changed, @mutex) until @complete
114
+ rescue TimeoutError => error
115
+ # 必须先发布超时再关闭上游,让被唤醒的读取者得到原始错误。
116
+ @error = error
117
+ raise
118
+ end
119
+ end
120
+
121
+ def complete
122
+ @mutex.synchronize do
123
+ @complete = true
124
+ @changed.broadcast
125
+ end
126
+ end
127
+ end
128
+
129
+ private_constant :ResponseWait
130
+
54
131
  def initialize(client:, logger: nil, handshake_timeout: 5)
55
132
  @client = client
56
133
  @logger = logger
@@ -136,27 +213,33 @@ module AnyTLS
136
213
  transport.write("\r\n")
137
214
  elsif body
138
215
  cancel_upload = false
216
+ response_wait = ResponseWait.new(@handshake_timeout)
139
217
  upload = Thread.new do
140
218
  upstream.write_body("HTTP/1.1", body)
219
+ response_wait.uploaded
141
220
  nil
142
221
  rescue StandardError => error
222
+ response_wait.fail(error)
143
223
  stream.close unless cancel_upload || stream.closed?
144
224
  error
145
225
  end
146
226
  else
147
227
  upstream.write_body("HTTP/1.1", body)
148
228
  end
229
+ transport.timeout = upload ? nil : @handshake_timeout
149
230
  loop do
150
- transport.timeout = upload&.alive? ? nil : @handshake_timeout
151
231
  transport.begin_headers!
152
232
  _response_version, status, reason, response_headers, response_body = upstream.read_response(method)
153
- transport.timeout = nil
154
233
  if status.between?(100, 199) && status != 101
155
234
  downstream.write_interim_response(version, status, filtered_headers(response_headers), reason)
156
235
  next
157
236
  end
158
237
 
159
- if upload&.alive?
238
+ response_wait&.complete
239
+ raise response_wait.error if response_wait&.error
240
+
241
+ transport.timeout = nil
242
+ if response_wait&.uploading?
160
243
  # 目标服务可能在读取完整请求体之前拒绝上传。
161
244
  # 先停止本地上传读取并交付完整响应。上传也可能停在上游写入,
162
245
  # 必须在 ensure 关闭上游后再 join,否则背压会阻止响应交付。
@@ -193,15 +276,17 @@ module AnyTLS
193
276
  break
194
277
  end
195
278
  rescue StandardError => error
196
- upload_error = upload.value if upload && !upload.alive?
197
- raise(upload_error || error)
279
+ raise(response_wait&.error || error)
198
280
  ensure
281
+ response_wait&.complete
199
282
  stream&.close
200
- if upload&.alive?
283
+ # 上传结束后线程可能仍在等待响应或退出;不能用 alive? 判断
284
+ # 客户端是否还有未读请求体,否则会误关 keep-alive 的读方向。
285
+ if response_wait&.uploading?
201
286
  cancel_upload = true
202
287
  io.close_read
203
- upload.join
204
288
  end
289
+ upload&.join
205
290
  end
206
291
 
207
292
  def filtered_headers(headers, upgrade: false)
@@ -4,6 +4,13 @@ module AnyTLS
4
4
  # 为原生 socket、StringIO 和支持超时的流提供完整二进制读写。
5
5
  # 部分进度消耗同一预算;辅助方法不重放失败写入。
6
6
  module IOHelpers
7
+ # accept 可返回待接入连接的网络错误;它们不代表监听 fd 已失效。
8
+ # 常量按平台提供的 errno 构建,EBADF/EINVAL 等监听器故障仍向外传播。
9
+ ACCEPT_RETRY_ERRORS = %i[ECONNABORTED EPROTO ENETDOWN ENOPROTOOPT EHOSTDOWN ENONET
10
+ EHOSTUNREACH EOPNOTSUPP ENETUNREACH].filter_map do |name|
11
+ Errno.const_get(name) if Errno.const_defined?(name)
12
+ end.freeze
13
+
7
14
  module_function
8
15
 
9
16
  # Linux 上仅关闭描述符不能唤醒另一线程的 select;先终止 socket 通信。
data/lib/anytls/server.rb CHANGED
@@ -59,14 +59,15 @@ module AnyTLS
59
59
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
60
60
  accept_connection(socket)
61
61
  socket = nil # 所有权已交给会话,或连接已被拒绝并关闭。
62
+ rescue *IOHelpers::ACCEPT_RETRY_ERRORS => error
63
+ # 仅重试 accept 本身;已接入 socket 的设置或会话构造错误不可吞掉。
64
+ raise if socket
65
+
66
+ pause_accept(error)
67
+ break if @mutex.synchronize { @stopping }
62
68
  rescue Errno::EMFILE, Errno::ENFILE, ThreadError => error
63
69
  socket&.close unless socket&.closed?
64
- now = Deadline.now
65
- if !@resource_warning_at || now - @resource_warning_at >= 5
66
- Log.emit(@logger, :warn, "accept capacity exhausted (#{error.class})")
67
- @resource_warning_at = now
68
- end
69
- @mutex.synchronize { @changed.wait(@mutex, 0.05) unless @stopping }
70
+ pause_accept(error)
70
71
  break if @mutex.synchronize { @stopping }
71
72
  ensure
72
73
  socket&.close unless socket&.closed?
@@ -130,6 +131,15 @@ module AnyTLS
130
131
 
131
132
  private
132
133
 
134
+ def pause_accept(error)
135
+ now = Deadline.now
136
+ if !@resource_warning_at || now - @resource_warning_at >= 5
137
+ Log.emit(@logger, :warn, "accept paused (#{error.class})")
138
+ @resource_warning_at = now
139
+ end
140
+ @mutex.synchronize { @changed.wait(@mutex, 0.05) unless @stopping }
141
+ end
142
+
133
143
  def accept_connection(socket)
134
144
  @mutex.synchronize do
135
145
  if @stopping || @sessions.size >= @max_sessions
@@ -53,7 +53,7 @@ module AnyTLS
53
53
  @open_timeout = open_timeout
54
54
  @max_streams = max_streams
55
55
  @receive_limit = receive_limit
56
- @session_receive_limit = session_receive_limit
56
+ @receive_budget = ByteBuffer::Budget.new(session_receive_limit)
57
57
  @send_limit = send_limit
58
58
  @logger = logger
59
59
  @on_stream = on_stream
@@ -507,10 +507,6 @@ module AnyTLS
507
507
  stream = @streams[frame.stream_id]
508
508
  return unless stream
509
509
 
510
- if @streams.values.sum(&:buffered_bytes) + frame.payload.bytesize > @session_receive_limit
511
- raise BufferLimitExceeded, "session receive buffer limit exceeded"
512
- end
513
-
514
510
  stream.receive(frame.payload)
515
511
  rescue BufferLimitExceeded => error
516
512
  remove_stream(frame.stream_id)&.finish(error)
@@ -546,7 +542,7 @@ module AnyTLS
546
542
  end
547
543
 
548
544
  def register_stream(id)
549
- stream = Stream.new(self, id, receive_limit: @receive_limit)
545
+ stream = Stream.new(self, id, receive_limit: @receive_limit, receive_budget: @receive_budget)
550
546
  @mutex.synchronize do
551
547
  @streams[id] = stream
552
548
  @active_count = @streams.size
@@ -29,6 +29,7 @@ module AnyTLS
29
29
  @factory = factory
30
30
  @mutex = Mutex.new
31
31
  @changed = ConditionVariable.new
32
+ @maintenance_changed = ConditionVariable.new
32
33
  @sessions = {}
33
34
  @idle = {}
34
35
  @creating = 0
@@ -123,6 +124,7 @@ module AnyTLS
123
124
  sessions = @mutex.synchronize do
124
125
  @closed = true
125
126
  @changed.broadcast
127
+ @maintenance_changed.broadcast
126
128
  result = @sessions.keys
127
129
  @sessions.clear
128
130
  @idle.clear
@@ -143,7 +145,12 @@ module AnyTLS
143
145
  @maintenance = Thread.new do
144
146
  loop do
145
147
  expired = @mutex.synchronize do
146
- @changed.wait(@mutex, @interval) unless @closed
148
+ # 租用者的容量通知不触发全池扫描;关闭仍须立即唤醒维护线程。
149
+ # 绝对时间防止虚假唤醒提前扫描或重新消耗完整间隔。
150
+ check_at = Deadline.now + @interval
151
+ while !@closed && (remaining = check_at - Deadline.now).positive?
152
+ @maintenance_changed.wait(@mutex, remaining)
153
+ end
147
154
  break if @closed
148
155
 
149
156
  retained = @idle.keys.sort_by { |session| -session.sequence }.first(@min_idle_sessions)
data/lib/anytls/stream.rb CHANGED
@@ -7,10 +7,10 @@ module AnyTLS
7
7
  class Stream
8
8
  attr_reader :id
9
9
 
10
- def initialize(session, id, receive_limit: 262_144)
10
+ def initialize(session, id, receive_limit: 262_144, receive_budget: nil)
11
11
  @session = session
12
12
  @id = id
13
- @buffer = ByteBuffer.new(limit: receive_limit)
13
+ @buffer = ByteBuffer.new(limit: receive_limit, budget: receive_budget)
14
14
  @mutex = Mutex.new
15
15
  @changed = ConditionVariable.new
16
16
  @closed = false
@@ -2,5 +2,5 @@
2
2
 
3
3
  module AnyTLS
4
4
  # gemspec、命令行和对端标识共用的唯一版本来源。
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.4"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anytls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - AnyTLS Ruby contributors