async-nats 0.1.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 +7 -0
- data/.rspec +3 -0
- data/BENCHMARK_RESULTS.md +155 -0
- data/BENCHMARK_SUMMARY.md +94 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE.txt +203 -0
- data/README.md +348 -0
- data/Rakefile +8 -0
- data/benchmark/pub_perf.rb +85 -0
- data/benchmark/pub_sub_perf.rb +117 -0
- data/benchmark/pub_sub_simple.rb +74 -0
- data/benchmark/sub_perf.rb +66 -0
- data/examples/basic-tls.rb +76 -0
- data/examples/basic-usage.rb +66 -0
- data/examples/basic.rb +46 -0
- data/examples/clustered.rb +79 -0
- data/examples/service_api/basic.rb +32 -0
- data/examples/service_api/discovery.rb +31 -0
- data/examples/service_api/errors.rb +33 -0
- data/examples/service_api/groups.rb +30 -0
- data/examples/service_api/stats.rb +40 -0
- data/lib/async/nats/client.rb +332 -0
- data/lib/async/nats/executor.rb +98 -0
- data/lib/async/nats/socket.rb +152 -0
- data/lib/async/nats/version.rb +7 -0
- data/lib/async/nats.rb +12 -0
- data/sig/async/nats.rbs +6 -0
- metadata +206 -0
@@ -0,0 +1,332 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2025 Tinco Andringa
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require "nats/io/client"
|
17
|
+
require "async"
|
18
|
+
require "async/queue"
|
19
|
+
require_relative "socket"
|
20
|
+
|
21
|
+
module Async
|
22
|
+
module NATS
|
23
|
+
# Async-based NATS client that inherits from NATS::IO::Client
|
24
|
+
# and overrides connection and threading methods to use async operations
|
25
|
+
class Client < ::NATS::IO::Client
|
26
|
+
# Override create_socket to use Async::NATS::Socket
|
27
|
+
def create_socket
|
28
|
+
socket_class = case @uri.scheme
|
29
|
+
when "nats", "tls"
|
30
|
+
Async::NATS::Socket
|
31
|
+
when "ws", "wss"
|
32
|
+
# TODO: Implement async websocket support
|
33
|
+
raise NotImplementedError, "WebSocket support not yet implemented for async-nats"
|
34
|
+
else
|
35
|
+
raise NotImplementedError, "#{@uri.scheme} protocol is not supported"
|
36
|
+
end
|
37
|
+
|
38
|
+
socket_class.new(
|
39
|
+
uri: @uri,
|
40
|
+
tls: {context: tls_context, hostname: @hostname},
|
41
|
+
connect_timeout: ::NATS::IO::DEFAULT_CONNECT_TIMEOUT
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Override establish_connection! to use async operations
|
46
|
+
private def establish_connection!
|
47
|
+
@ruby_pid = Process.pid # For fork detection
|
48
|
+
|
49
|
+
srv = nil
|
50
|
+
begin
|
51
|
+
srv = select_next_server
|
52
|
+
|
53
|
+
# Use the hostname from the server for TLS hostname verification.
|
54
|
+
if client_using_secure_connection? && single_url_connect_used?
|
55
|
+
# Always reuse the original hostname used to connect.
|
56
|
+
@hostname ||= srv[:hostname]
|
57
|
+
else
|
58
|
+
@hostname = srv[:hostname]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create TCP socket connection to NATS using async socket
|
62
|
+
@io = create_socket
|
63
|
+
@io.connect
|
64
|
+
|
65
|
+
# Capture state that we have had a TCP connection established against
|
66
|
+
# this server and could potentially be used for reconnecting.
|
67
|
+
srv[:was_connected] = true
|
68
|
+
|
69
|
+
# Connection established and now in process of sending CONNECT to NATS
|
70
|
+
@status = CONNECTING
|
71
|
+
|
72
|
+
# Established TCP connection successfully so can start connect
|
73
|
+
process_connect_init
|
74
|
+
|
75
|
+
# Reset reconnection attempts if connection is valid
|
76
|
+
srv[:reconnect_attempts] = 0
|
77
|
+
srv[:auth_required] ||= true if @server_info[:auth_required]
|
78
|
+
|
79
|
+
# Add back to rotation since successfully connected
|
80
|
+
server_pool << srv
|
81
|
+
rescue ::NATS::IO::NoServersError => e
|
82
|
+
@disconnect_cb.call(e) if @disconnect_cb
|
83
|
+
raise @last_err || e
|
84
|
+
rescue => e
|
85
|
+
# Capture sticky error
|
86
|
+
synchronize do
|
87
|
+
@last_err = e
|
88
|
+
srv[:auth_required] ||= true if @server_info[:auth_required]
|
89
|
+
server_pool << srv if can_reuse_server?(srv)
|
90
|
+
end
|
91
|
+
|
92
|
+
err_cb_call(self, e, nil) if @err_cb
|
93
|
+
|
94
|
+
if should_not_reconnect?
|
95
|
+
@disconnect_cb.call(e) if @disconnect_cb
|
96
|
+
raise e
|
97
|
+
end
|
98
|
+
|
99
|
+
# Clean up any connecting state and close connection without
|
100
|
+
# triggering the disconnection/closed callbacks.
|
101
|
+
close_connection(DISCONNECTED, false)
|
102
|
+
|
103
|
+
# Always sleep here to safe guard against errors before current[:was_connected]
|
104
|
+
# is set for the first time.
|
105
|
+
if @options[:reconnect_time_wait]
|
106
|
+
Async::Task.current.sleep(@options[:reconnect_time_wait])
|
107
|
+
end
|
108
|
+
|
109
|
+
# Continue retrying until there are no options left in the server pool
|
110
|
+
retry
|
111
|
+
end
|
112
|
+
|
113
|
+
# Initialize queues using async-compatible queues
|
114
|
+
@flush_queue = create_async_queue(::NATS::IO::MAX_FLUSH_KICK_SIZE)
|
115
|
+
@pending_queue = create_async_queue(::NATS::IO::MAX_PENDING_SIZE)
|
116
|
+
@pings_outstanding = 0
|
117
|
+
@pongs_received = 0
|
118
|
+
@pending_size = 0
|
119
|
+
|
120
|
+
# Server roundtrip went ok so consider to be connected at this point
|
121
|
+
@status = CONNECTED
|
122
|
+
|
123
|
+
# Connected to NATS so Ready to start parser loop, flusher and ping interval
|
124
|
+
start_async_tasks!
|
125
|
+
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
# Create an async-compatible queue
|
130
|
+
private def create_async_queue(size)
|
131
|
+
# Use a simple array with mutex for now, can be optimized with Async::Queue
|
132
|
+
queue = SizedQueue.new(size)
|
133
|
+
queue
|
134
|
+
end
|
135
|
+
|
136
|
+
# Override start_threads! to use async tasks instead of threads
|
137
|
+
private def start_async_tasks!
|
138
|
+
# Start the read loop as an async task
|
139
|
+
@read_loop_task = Async::Task.current.async do
|
140
|
+
read_loop
|
141
|
+
end
|
142
|
+
|
143
|
+
# Start the flusher as an async task
|
144
|
+
@flusher_task = Async::Task.current.async do
|
145
|
+
flusher_loop
|
146
|
+
end
|
147
|
+
|
148
|
+
# Start the ping interval as an async task
|
149
|
+
@ping_interval_task = Async::Task.current.async do
|
150
|
+
ping_interval_loop
|
151
|
+
end
|
152
|
+
|
153
|
+
# Use async-based executor instead of Concurrent::ThreadPoolExecutor
|
154
|
+
@subscription_executor = Async::NATS::Executor.new(
|
155
|
+
name: "nats:subscription",
|
156
|
+
max_threads: ::NATS::IO::DEFAULT_TOTAL_SUB_CONCURRENCY,
|
157
|
+
min_threads: defined?(JRUBY_VERSION) ? 2 : 0
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Override read_loop to use async operations
|
162
|
+
private def read_loop
|
163
|
+
loop do
|
164
|
+
should_bail = synchronize do
|
165
|
+
@status == CLOSED or @status == RECONNECTING
|
166
|
+
end
|
167
|
+
if !@io || @io.closed? || should_bail
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
# Read data from socket with async I/O
|
172
|
+
data = @io.read(::NATS::IO::MAX_SOCKET_READ_BYTES)
|
173
|
+
@parser.parse(data) if data
|
174
|
+
rescue Errno::ETIMEDOUT
|
175
|
+
# Retry on timeout
|
176
|
+
retry
|
177
|
+
rescue => e
|
178
|
+
# In case of reading/parser errors, trigger reconnection logic
|
179
|
+
process_op_error(e)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Override flusher_loop to use async operations
|
184
|
+
private def flusher_loop
|
185
|
+
loop do
|
186
|
+
# Blocks waiting for the flusher to be kicked...
|
187
|
+
@flush_queue.pop
|
188
|
+
|
189
|
+
should_bail = synchronize do
|
190
|
+
(@status != CONNECTED && !draining?) || @status == CONNECTING
|
191
|
+
end
|
192
|
+
return if should_bail
|
193
|
+
|
194
|
+
# Skip in case nothing remains pending already.
|
195
|
+
next if @pending_queue.empty?
|
196
|
+
|
197
|
+
force_flush!
|
198
|
+
|
199
|
+
synchronize do
|
200
|
+
@pending_size = 0
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Override ping_interval_loop to use async operations
|
206
|
+
private def ping_interval_loop
|
207
|
+
loop do
|
208
|
+
Async::Task.current.sleep(@options[:ping_interval])
|
209
|
+
|
210
|
+
# Skip ping interval until connected
|
211
|
+
next if !connected?
|
212
|
+
|
213
|
+
if @pings_outstanding >= @options[:max_outstanding_pings]
|
214
|
+
process_op_error(::NATS::IO::StaleConnectionError.new("nats: stale connection"))
|
215
|
+
return
|
216
|
+
end
|
217
|
+
|
218
|
+
@pings_outstanding += 1
|
219
|
+
send_command(PING_REQUEST)
|
220
|
+
@flush_queue << :ping
|
221
|
+
end
|
222
|
+
rescue => e
|
223
|
+
process_op_error(e)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Override drain to use async primitives when in async context
|
227
|
+
def drain
|
228
|
+
return if draining?
|
229
|
+
|
230
|
+
# Check if we're in an async context
|
231
|
+
if Async::Task.current?
|
232
|
+
# Async version - returns a task that can be awaited
|
233
|
+
synchronize do
|
234
|
+
@drain_task ||= Async::Task.current.async do
|
235
|
+
do_async_drain
|
236
|
+
end
|
237
|
+
end
|
238
|
+
else
|
239
|
+
# Fall back to parent thread-based implementation
|
240
|
+
super
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Override close to stop async tasks
|
245
|
+
def close
|
246
|
+
# Stop async tasks if they exist
|
247
|
+
# Use safe stop that works from both async and thread contexts
|
248
|
+
begin
|
249
|
+
@read_loop_task&.stop if @read_loop_task&.alive?
|
250
|
+
rescue => e
|
251
|
+
# Ignore errors during task stop
|
252
|
+
end
|
253
|
+
|
254
|
+
begin
|
255
|
+
@flusher_task&.stop if @flusher_task&.alive?
|
256
|
+
rescue => e
|
257
|
+
# Ignore errors during task stop
|
258
|
+
end
|
259
|
+
|
260
|
+
begin
|
261
|
+
@ping_interval_task&.stop if @ping_interval_task&.alive?
|
262
|
+
rescue => e
|
263
|
+
# Ignore errors during task stop
|
264
|
+
end
|
265
|
+
|
266
|
+
# Call parent close
|
267
|
+
super
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
272
|
+
# Async-aware drain implementation using Async::Barrier
|
273
|
+
def do_async_drain
|
274
|
+
synchronize { @status = DRAINING_SUBS }
|
275
|
+
|
276
|
+
# Create a barrier for all subscriptions
|
277
|
+
barrier = Async::Barrier.new
|
278
|
+
subs_to_drain = []
|
279
|
+
|
280
|
+
# Send UNSUB for all subscriptions and add barrier tasks
|
281
|
+
@subs.each do |_, sub|
|
282
|
+
next if sub == @resp_sub
|
283
|
+
|
284
|
+
drain_sub(sub)
|
285
|
+
subs_to_drain << sub
|
286
|
+
|
287
|
+
# Add a task to the barrier that waits for this sub to drain
|
288
|
+
barrier.async do
|
289
|
+
wait_for_sub_drained(sub)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
force_flush!
|
294
|
+
|
295
|
+
# Wait for all subscriptions to drain with timeout
|
296
|
+
drain_timeout = @options[:drain_timeout] || 30
|
297
|
+
begin
|
298
|
+
Async::Task.current.with_timeout(drain_timeout) do
|
299
|
+
barrier.wait # Waits for all tasks in the barrier
|
300
|
+
end
|
301
|
+
rescue Async::TimeoutError
|
302
|
+
barrier.stop # Stop all waiting tasks
|
303
|
+
e = ::NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
|
304
|
+
err_cb_call(self, e, nil) if @err_cb
|
305
|
+
ensure
|
306
|
+
barrier.stop rescue nil
|
307
|
+
end
|
308
|
+
|
309
|
+
# Shutdown subscription executor
|
310
|
+
@subscription_executor.shutdown
|
311
|
+
@subscription_executor.wait_for_termination(@options[:drain_timeout] || 30)
|
312
|
+
|
313
|
+
# Set status to DRAINING_PUBS and close
|
314
|
+
synchronize { @status = DRAINING_PUBS }
|
315
|
+
unsubscribe(@resp_sub) if @resp_sub
|
316
|
+
close
|
317
|
+
end
|
318
|
+
|
319
|
+
# Wait for a single subscription to drain (all messages processed)
|
320
|
+
def wait_for_sub_drained(sub)
|
321
|
+
loop do
|
322
|
+
# Check if subscription queue is empty
|
323
|
+
queue_size = sub.synchronize { sub.pending_queue.size }
|
324
|
+
break if queue_size == 0
|
325
|
+
|
326
|
+
# Yield to other tasks briefly
|
327
|
+
Async::Task.current.sleep(0.01)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
module NATS
|
5
|
+
# Async-based executor using a worker pool pattern with Async::Semaphore
|
6
|
+
# This provides bounded concurrency without creating unlimited nested tasks
|
7
|
+
class Executor
|
8
|
+
def initialize(name:, max_threads:, min_threads: 0)
|
9
|
+
@name = name
|
10
|
+
@max_concurrency = max_threads
|
11
|
+
@running = true
|
12
|
+
@work_queue = Thread::Queue.new # Thread-safe queue for work items
|
13
|
+
@workers = []
|
14
|
+
@mutex = Mutex.new
|
15
|
+
|
16
|
+
# Start worker tasks if we're in an async context
|
17
|
+
if Async::Task.current?
|
18
|
+
start_workers
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Post a block to be executed asynchronously
|
23
|
+
def post(&block)
|
24
|
+
return unless @running
|
25
|
+
|
26
|
+
# Add work to the queue
|
27
|
+
@work_queue << block
|
28
|
+
end
|
29
|
+
|
30
|
+
# Shutdown the executor
|
31
|
+
def shutdown
|
32
|
+
@running = false
|
33
|
+
|
34
|
+
# Signal workers to stop by adding nil items
|
35
|
+
@max_concurrency.times { @work_queue << nil }
|
36
|
+
|
37
|
+
# Stop all worker tasks
|
38
|
+
@mutex.synchronize do
|
39
|
+
@workers.each do |worker|
|
40
|
+
worker.stop rescue nil
|
41
|
+
end
|
42
|
+
@workers.clear
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Wait for termination (for compatibility with ThreadPoolExecutor)
|
47
|
+
def wait_for_termination(timeout = nil)
|
48
|
+
deadline = timeout ? Time.now + timeout : nil
|
49
|
+
|
50
|
+
loop do
|
51
|
+
@mutex.synchronize do
|
52
|
+
return true if @workers.empty? || @workers.all? { |w| !w.alive? }
|
53
|
+
end
|
54
|
+
|
55
|
+
if deadline && Time.now > deadline
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
sleep(0.01)
|
60
|
+
end
|
61
|
+
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def start_workers
|
68
|
+
# Create a pool of worker tasks
|
69
|
+
@max_concurrency.times do |i|
|
70
|
+
worker = Async::Task.current.async do
|
71
|
+
worker_loop
|
72
|
+
end
|
73
|
+
|
74
|
+
@mutex.synchronize do
|
75
|
+
@workers << worker
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def worker_loop
|
81
|
+
while @running
|
82
|
+
# Get work from the queue (blocks until work is available)
|
83
|
+
work = @work_queue.pop
|
84
|
+
|
85
|
+
# nil signals shutdown
|
86
|
+
break if work.nil?
|
87
|
+
|
88
|
+
begin
|
89
|
+
work.call
|
90
|
+
rescue => e
|
91
|
+
# Log error but don't crash the worker
|
92
|
+
warn "Async::NATS::Executor worker error: #{e.class}: #{e.message}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2025 Tinco Andringa
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require "async"
|
17
|
+
require "io/endpoint"
|
18
|
+
require "io/endpoint/host_endpoint"
|
19
|
+
require "io/stream"
|
20
|
+
require "openssl"
|
21
|
+
|
22
|
+
module Async
|
23
|
+
module NATS
|
24
|
+
# Async-based socket implementation for NATS connections
|
25
|
+
# This replaces the blocking I/O operations in NATS::IO::Socket with async operations
|
26
|
+
class Socket
|
27
|
+
attr_accessor :socket
|
28
|
+
|
29
|
+
def initialize(options = {})
|
30
|
+
@uri = options[:uri]
|
31
|
+
@connect_timeout = options[:connect_timeout]
|
32
|
+
@write_timeout = options[:write_timeout]
|
33
|
+
@read_timeout = options[:read_timeout]
|
34
|
+
@socket = nil
|
35
|
+
@stream = nil
|
36
|
+
@tls = options[:tls]
|
37
|
+
|
38
|
+
# Cache async task reference to avoid repeated context checks
|
39
|
+
# This is checked once at initialization to avoid overhead on every I/O operation
|
40
|
+
@async_task = Async::Task.current? rescue nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def connect
|
44
|
+
# Create endpoint for the connection
|
45
|
+
endpoint = IO::Endpoint.tcp(@uri.hostname, @uri.port)
|
46
|
+
|
47
|
+
# Connect with timeout
|
48
|
+
task = Async::Task.current
|
49
|
+
task.with_timeout(@connect_timeout) do
|
50
|
+
@socket = endpoint.connect
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set TCP no delay by default
|
54
|
+
@socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) if @socket.respond_to?(:setsockopt)
|
55
|
+
|
56
|
+
# Wrap in IO::Stream for buffered I/O
|
57
|
+
@stream = IO::Stream(@socket)
|
58
|
+
rescue Async::TimeoutError
|
59
|
+
raise NATS::IO::SocketTimeoutError
|
60
|
+
end
|
61
|
+
|
62
|
+
# (Re-)connect using secure connection if server and client agreed on using it.
|
63
|
+
def setup_tls!
|
64
|
+
# Setup TLS connection by rewrapping the socket
|
65
|
+
tls_context = @tls.fetch(:context)
|
66
|
+
|
67
|
+
# Create SSL socket
|
68
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(@socket, tls_context)
|
69
|
+
ssl_socket.sync_close = true
|
70
|
+
ssl_socket.hostname = @tls[:hostname]
|
71
|
+
|
72
|
+
# Connect with async
|
73
|
+
task = Async::Task.current
|
74
|
+
task.async do
|
75
|
+
ssl_socket.connect
|
76
|
+
end.wait
|
77
|
+
|
78
|
+
@socket = ssl_socket
|
79
|
+
@stream = IO::Stream(@socket)
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_line(deadline = nil)
|
83
|
+
# deadline is a timeout duration in seconds, not an absolute time
|
84
|
+
# NATS protocol uses \r\n line endings
|
85
|
+
|
86
|
+
# Use cached task reference (checked once at initialization)
|
87
|
+
if @async_task && deadline
|
88
|
+
@async_task.with_timeout(deadline) do
|
89
|
+
@stream.read_until("\r\n") + "\r\n"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
@stream.read_until("\r\n") + "\r\n"
|
93
|
+
end
|
94
|
+
rescue Async::TimeoutError
|
95
|
+
raise NATS::IO::SocketTimeoutError
|
96
|
+
rescue EOFError
|
97
|
+
raise Errno::ECONNRESET
|
98
|
+
end
|
99
|
+
|
100
|
+
def read(max_bytes, deadline = nil)
|
101
|
+
# deadline is a timeout duration in seconds, not an absolute time
|
102
|
+
|
103
|
+
# Use cached task reference (checked once at initialization)
|
104
|
+
if @async_task && deadline
|
105
|
+
@async_task.with_timeout(deadline) do
|
106
|
+
@stream.read_partial(max_bytes)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
@stream.read_partial(max_bytes)
|
110
|
+
end
|
111
|
+
rescue Async::TimeoutError
|
112
|
+
raise NATS::IO::SocketTimeoutError
|
113
|
+
rescue EOFError => e
|
114
|
+
if (RUBY_ENGINE == "jruby") && (e.message == "No message available")
|
115
|
+
# FIXME: <EOFError: No message available> can happen in jruby
|
116
|
+
return nil
|
117
|
+
end
|
118
|
+
raise Errno::ECONNRESET
|
119
|
+
end
|
120
|
+
|
121
|
+
def write(data, deadline = nil)
|
122
|
+
# deadline is a timeout duration in seconds, not an absolute time
|
123
|
+
|
124
|
+
# Use cached task reference (checked once at initialization)
|
125
|
+
if @async_task && deadline
|
126
|
+
@async_task.with_timeout(deadline) do
|
127
|
+
@stream.write(data)
|
128
|
+
@stream.flush
|
129
|
+
end
|
130
|
+
else
|
131
|
+
@stream.write(data)
|
132
|
+
@stream.flush
|
133
|
+
end
|
134
|
+
|
135
|
+
data.bytesize
|
136
|
+
rescue Async::TimeoutError
|
137
|
+
raise NATS::IO::SocketTimeoutError
|
138
|
+
rescue EOFError
|
139
|
+
raise Errno::ECONNRESET
|
140
|
+
end
|
141
|
+
|
142
|
+
def close
|
143
|
+
@stream&.close
|
144
|
+
@socket&.close
|
145
|
+
end
|
146
|
+
|
147
|
+
def closed?
|
148
|
+
@socket.nil? || @socket.closed?
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/lib/async/nats.rb
ADDED