nl 0.2.4 → 0.3.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 +8 -0
- data/lib/nl/async/dispatcher.rb +199 -0
- data/lib/nl/async/driver.rb +39 -0
- data/lib/nl/async/mailbox.rb +83 -0
- data/lib/nl/async/operation.rb +279 -0
- data/lib/nl/async.rb +15 -0
- data/lib/nl/blocking_transport.rb +131 -0
- data/lib/nl/connection.rb +63 -0
- data/lib/nl/core.rb +1 -1
- data/lib/nl/datagram.rb +24 -0
- data/lib/nl/decoder.rb +9 -1
- data/lib/nl/error.rb +10 -0
- data/lib/nl/exchange.rb +102 -0
- data/lib/nl/family.rb +134 -17
- data/lib/nl/genl/connection.rb +89 -0
- data/lib/nl/genl.rb +18 -31
- data/lib/nl/notification.rb +115 -0
- data/lib/nl/notification_router.rb +85 -0
- data/lib/nl/protocols/genl.rb +47 -28
- data/lib/nl/protocols/raw.rb +164 -99
- data/lib/nl/sequence_allocator.rb +36 -0
- data/lib/nl/socket.rb +15 -12
- data/lib/nl/version.rb +1 -1
- data/lib/nl.rb +5 -6
- metadata +20 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06d7f10231a9e4b6ce47149f17099f6c72eaad8c72101ac768d3413ee1a8b12b
|
|
4
|
+
data.tar.gz: 9ecf3d32020db7cebc41de402a6e47b524aa3146db8d935cdba467d31faeda6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ec97708c16968c722830441a0755419ebb38e5740c11a996d4ea7e6f9f07dec1803877159eb248096b069c4c1808398777fd13aff5c6aa92a8dafc47d901bf7
|
|
7
|
+
data.tar.gz: 112cfdc2829d1e6f135995315cf1fa3ef3b3aa81e1ba5ac47da3011e029113bb04a6a6c2e4a369346a38cb08a8735bb021d52b1eba0a0d540d31559f6337e71c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v0.3.0 (2026-09-01)
|
|
6
|
+
|
|
7
|
+
- Fix encoding of nested attributes and mark them with `NLA_F_NESTED`.
|
|
8
|
+
- Support encoding and decoding of nested type-value attributes.
|
|
9
|
+
- Support multiplexing requests via `Family.async`
|
|
10
|
+
- Support receiving notifications (unsolicited messages).
|
|
11
|
+
- Rename `Genl::Connection#open` to `Genl::Connection#family`.
|
|
12
|
+
|
|
5
13
|
## v0.2.4 (2026-08-22)
|
|
6
14
|
|
|
7
15
|
## v0.2.3 (2026-03-24)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative '../datagram'
|
|
4
|
+
require_relative '../error'
|
|
5
|
+
require_relative '../exchange'
|
|
6
|
+
require_relative '../notification_router'
|
|
7
|
+
require_relative '../sequence_allocator'
|
|
8
|
+
|
|
9
|
+
module Nl
|
|
10
|
+
module Async
|
|
11
|
+
# Owns the single receive loop for a socket and routes replies by sequence.
|
|
12
|
+
class Dispatcher
|
|
13
|
+
Pending = Data.define(:exchange, :protocol, :reply_class, :sink)
|
|
14
|
+
private_constant :Pending
|
|
15
|
+
|
|
16
|
+
def initialize(socket, executor: :thread, notifications:)
|
|
17
|
+
@socket = socket
|
|
18
|
+
@sequences = SequenceAllocator.new
|
|
19
|
+
@notifications = notifications
|
|
20
|
+
@driver = executor.respond_to?(:start) ? executor : Async.driver(executor)
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@send_mutex = Mutex.new
|
|
23
|
+
@pending = {}
|
|
24
|
+
@closed = false
|
|
25
|
+
@task = @driver.start { receive_loop }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exchange(protocol, kind, request_class, reply_class, args, &block)
|
|
29
|
+
operation = exchange_async(protocol, kind, request_class, reply_class, args)
|
|
30
|
+
if kind == :dump
|
|
31
|
+
if block
|
|
32
|
+
operation.each(&block)
|
|
33
|
+
nil
|
|
34
|
+
else
|
|
35
|
+
operation.to_a
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
operation.await
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exchange_async(protocol, kind, request_class, reply_class, args, stream_capacity: nil)
|
|
43
|
+
request = protocol.build_request(kind, request_class, args)
|
|
44
|
+
|
|
45
|
+
key = nil
|
|
46
|
+
seq = pid = nil
|
|
47
|
+
operation = sink = nil
|
|
48
|
+
@send_mutex.synchronize do
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
raise ClosedError, 'dispatcher is closed' if @closed
|
|
51
|
+
|
|
52
|
+
pid = @socket.local_port_id
|
|
53
|
+
seq = @sequences.next { @pending.key?([it, pid]) }
|
|
54
|
+
key = [seq, pid]
|
|
55
|
+
mailbox = Mailbox.new(capacity: stream_capacity)
|
|
56
|
+
operation, sink = if kind == :dump
|
|
57
|
+
Stream.build(mailbox:, on_close: -> { discard(key) })
|
|
58
|
+
else
|
|
59
|
+
Future.build(mailbox:, on_close: -> { discard(key) })
|
|
60
|
+
end
|
|
61
|
+
exchange = Exchange.new(kind:, expects_reply: !reply_class.nil?)
|
|
62
|
+
@pending[key] = Pending.new(exchange:, protocol:, reply_class:, sink:)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
protocol.send_message(@socket, request, seq:, pid:)
|
|
67
|
+
rescue Exception => error
|
|
68
|
+
fail_pending(key, error)
|
|
69
|
+
raise
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
operation
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def async_capable? #: true
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def receive_notification(protocol, timeout: nil)
|
|
80
|
+
@notifications.channel(protocol).pop(timeout:)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def close
|
|
84
|
+
task = @mutex.synchronize do
|
|
85
|
+
next if @closed
|
|
86
|
+
|
|
87
|
+
@closed = true
|
|
88
|
+
@socket.close unless @socket.closed?
|
|
89
|
+
@task
|
|
90
|
+
end
|
|
91
|
+
fail_all(ClosedError.new('dispatcher is closed'))
|
|
92
|
+
@driver.stop(task) if task && !task.equal?(::Thread.current)
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private def receive_loop
|
|
97
|
+
loop do
|
|
98
|
+
break if @mutex.synchronize { @closed }
|
|
99
|
+
|
|
100
|
+
@socket.wait_readable
|
|
101
|
+
data = @socket.recvmsg_nonblock(exception: false)
|
|
102
|
+
next if data == :wait_readable
|
|
103
|
+
|
|
104
|
+
dispatch_datagram(IO::Buffer.for(data.first))
|
|
105
|
+
rescue Errno::ENOBUFS => e
|
|
106
|
+
@notifications.lose_all(NotificationLossError.new('kernel receive buffer overflowed'))
|
|
107
|
+
fail_all(e)
|
|
108
|
+
end
|
|
109
|
+
rescue Exception => e
|
|
110
|
+
fail_receive_loop(e)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private def fail_receive_loop(error)
|
|
114
|
+
failed = @mutex.synchronize do
|
|
115
|
+
next false if @closed
|
|
116
|
+
|
|
117
|
+
@closed = true
|
|
118
|
+
@socket.close unless @socket.closed?
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
return unless failed
|
|
122
|
+
|
|
123
|
+
fail_all(error)
|
|
124
|
+
@notifications.close
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private def dispatch_datagram(buffer)
|
|
128
|
+
Datagram.each_frame(buffer) do |header, payload|
|
|
129
|
+
key = [header.seq, header.pid]
|
|
130
|
+
pending = @mutex.synchronize { @pending[key] }
|
|
131
|
+
unless pending
|
|
132
|
+
@notifications.route(header, payload) if header.seq.zero?
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
message = pending.protocol.decode_frame(header, payload, pending.reply_class)
|
|
138
|
+
dispatch_outcome(key, pending, pending.exchange.accept(message))
|
|
139
|
+
rescue Exception => error
|
|
140
|
+
fail_pending(key, error) if pending
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private def dispatch_outcome(key, pending, outcome)
|
|
146
|
+
return unless outcome
|
|
147
|
+
|
|
148
|
+
case outcome
|
|
149
|
+
when Exchange::Item
|
|
150
|
+
begin
|
|
151
|
+
pending.sink.push(outcome.value)
|
|
152
|
+
rescue Mailbox::FullError
|
|
153
|
+
fail_pending(key, StreamOverflowError.new("reply buffer exceeded for sequence #{key.first}"))
|
|
154
|
+
end
|
|
155
|
+
when Exchange::Complete
|
|
156
|
+
complete_pending(key, pending, pending.exchange.result)
|
|
157
|
+
when Exchange::Failure
|
|
158
|
+
fail_pending(key, outcome.exception)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private def complete_pending(key, pending, value)
|
|
163
|
+
removed = @mutex.synchronize { @pending.delete(key) }
|
|
164
|
+
return unless removed
|
|
165
|
+
return if pending.exchange.cancelled?
|
|
166
|
+
|
|
167
|
+
if pending.exchange.kind == :dump
|
|
168
|
+
pending.sink.finish
|
|
169
|
+
elsif pending.exchange.expects_reply?
|
|
170
|
+
pending.sink.succeed(value)
|
|
171
|
+
else
|
|
172
|
+
pending.sink.finish
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private def discard(key)
|
|
177
|
+
@mutex.synchronize do
|
|
178
|
+
if pending = @pending[key]
|
|
179
|
+
pending.exchange.cancel
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private def fail_pending(key, error)
|
|
185
|
+
pending = @mutex.synchronize { @pending.delete(key) }
|
|
186
|
+
pending&.sink&.fail(error) unless pending&.exchange&.cancelled?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private def fail_all(error)
|
|
190
|
+
pending = @mutex.synchronize do
|
|
191
|
+
old = @pending.values
|
|
192
|
+
@pending.clear
|
|
193
|
+
old
|
|
194
|
+
end
|
|
195
|
+
pending.each { it.sink.fail(error) unless it.exchange.cancelled? }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module Nl
|
|
4
|
+
module Async
|
|
5
|
+
module Drivers
|
|
6
|
+
class Thread
|
|
7
|
+
def start(&block)
|
|
8
|
+
::Thread.new(&block)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def stop(task)
|
|
12
|
+
task.join
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Fiber
|
|
17
|
+
def start(&block)
|
|
18
|
+
raise ArgumentError, 'Fiber.scheduler is not installed' unless ::Fiber.scheduler
|
|
19
|
+
|
|
20
|
+
::Fiber.schedule(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stop(_task)
|
|
24
|
+
# Scheduled fibers have no general join operation. Closing the socket
|
|
25
|
+
# makes the receive loop terminate at its next scheduling point.
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.driver(name)
|
|
31
|
+
case name
|
|
32
|
+
when :thread then Drivers::Thread.new
|
|
33
|
+
when :fiber then Drivers::Fiber.new
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, "unknown async executor: #{name.inspect}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative '../error'
|
|
4
|
+
|
|
5
|
+
module Nl
|
|
6
|
+
module Async
|
|
7
|
+
# A thread-safe, scheduler-aware, single-consumer mailbox.
|
|
8
|
+
class Mailbox
|
|
9
|
+
class FullError < StandardError; end
|
|
10
|
+
|
|
11
|
+
def initialize(capacity: nil)
|
|
12
|
+
raise ArgumentError, 'capacity must be positive' if capacity && capacity <= 0
|
|
13
|
+
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@condition = ConditionVariable.new
|
|
16
|
+
@queue = []
|
|
17
|
+
@closed = false
|
|
18
|
+
@capacity = capacity
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Adds a value without executing consumer code.
|
|
22
|
+
# @rbs (untyped) -> bool
|
|
23
|
+
def push(value)
|
|
24
|
+
push_value(value, enforce_capacity: true)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Adds a completion/error event even when the value capacity is full.
|
|
28
|
+
def push_terminal(value)
|
|
29
|
+
push_value(value, enforce_capacity: false)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def push_value(value, enforce_capacity:)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
return false if @closed
|
|
35
|
+
if enforce_capacity && @capacity && @queue.length >= @capacity
|
|
36
|
+
raise FullError, 'mailbox capacity exceeded'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
wake = @queue.empty?
|
|
40
|
+
@queue << value
|
|
41
|
+
@condition.signal if wake
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Removes the next value, suspending through the current thread or Fiber scheduler if empty.
|
|
47
|
+
# @rbs (?timeout: Numeric?) -> untyped
|
|
48
|
+
def pop(timeout: nil)
|
|
49
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
50
|
+
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
loop do
|
|
53
|
+
return @queue.shift unless @queue.empty?
|
|
54
|
+
raise ClosedError if @closed
|
|
55
|
+
|
|
56
|
+
remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
57
|
+
raise TimeoutError if remaining && remaining <= 0
|
|
58
|
+
|
|
59
|
+
@condition.wait(@mutex, remaining)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def close
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
return nil if @closed
|
|
67
|
+
|
|
68
|
+
@closed = true
|
|
69
|
+
@condition.broadcast
|
|
70
|
+
end
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def closed?
|
|
75
|
+
@mutex.synchronize { @closed }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def empty?
|
|
79
|
+
@mutex.synchronize { @queue.empty? }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative '../error'
|
|
4
|
+
|
|
5
|
+
module Nl
|
|
6
|
+
module Async
|
|
7
|
+
module Events
|
|
8
|
+
Item = Data.define(:value)
|
|
9
|
+
Done = Data.define
|
|
10
|
+
Error = Data.define(:exception)
|
|
11
|
+
end
|
|
12
|
+
private_constant :Events
|
|
13
|
+
|
|
14
|
+
class ConcurrentConsumptionError < StandardError; end
|
|
15
|
+
class StreamOverflowError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Future for the result of an already-started, single-reply operation.
|
|
18
|
+
# @rbs generic out Result
|
|
19
|
+
class Future
|
|
20
|
+
def self.build(mailbox: Mailbox.new, on_close: nil)
|
|
21
|
+
operation = new(mailbox:, on_close:)
|
|
22
|
+
[operation, Sink.new(mailbox)]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(mailbox:, on_close:)
|
|
26
|
+
@mailbox = mailbox
|
|
27
|
+
@on_close = on_close
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@state = :open
|
|
30
|
+
@value = nil
|
|
31
|
+
@error = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs (?timeout: Numeric?) -> Result
|
|
35
|
+
def await(timeout: nil)
|
|
36
|
+
return terminal_result unless @mutex.synchronize { @state == :open }
|
|
37
|
+
|
|
38
|
+
event = @mailbox.pop(timeout:)
|
|
39
|
+
case event
|
|
40
|
+
when Events::Item
|
|
41
|
+
settle_done(event.value)
|
|
42
|
+
when Events::Done
|
|
43
|
+
settle_done(nil)
|
|
44
|
+
when Events::Error
|
|
45
|
+
settle_failed(event.exception)
|
|
46
|
+
end
|
|
47
|
+
@mailbox.close
|
|
48
|
+
terminal_result
|
|
49
|
+
rescue ClosedError
|
|
50
|
+
terminal_result
|
|
51
|
+
end
|
|
52
|
+
alias value await
|
|
53
|
+
|
|
54
|
+
def ready? #: bool
|
|
55
|
+
@mutex.synchronize { @state != :open } || !@mailbox.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Stops local delivery. The dispatcher still drains the Netlink exchange.
|
|
59
|
+
def close #: nil
|
|
60
|
+
closed, callback = @mutex.synchronize do
|
|
61
|
+
next [false, nil] if @state != :open
|
|
62
|
+
|
|
63
|
+
@state = :closed
|
|
64
|
+
[true, @on_close]
|
|
65
|
+
end
|
|
66
|
+
if closed
|
|
67
|
+
callback&.call
|
|
68
|
+
@mailbox.close
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private def settle_done(value)
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
if @state == :open
|
|
76
|
+
@state = :done
|
|
77
|
+
@value = value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def settle_failed(error)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
if @state == :open
|
|
85
|
+
@state = :failed
|
|
86
|
+
@error = error
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private def terminal_result
|
|
92
|
+
state, value, error = @mutex.synchronize { [@state, @value, @error] }
|
|
93
|
+
case state
|
|
94
|
+
when :done then value
|
|
95
|
+
when :failed then raise error
|
|
96
|
+
else raise ClosedError, 'future is closed'
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class Sink
|
|
101
|
+
def initialize(mailbox)
|
|
102
|
+
@mailbox = mailbox
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def succeed(value) = @mailbox.push(Events::Item.new(value))
|
|
106
|
+
def finish = @mailbox.push_terminal(Events::Done.new)
|
|
107
|
+
def fail(exception) = @mailbox.push_terminal(Events::Error.new(exception))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Single-pass consumer handle for a multipart Netlink operation.
|
|
112
|
+
# @rbs generic out Item
|
|
113
|
+
class Stream
|
|
114
|
+
include Enumerable #[Item]
|
|
115
|
+
|
|
116
|
+
def self.build(mailbox: Mailbox.new, on_close: nil)
|
|
117
|
+
operation = new(mailbox:, on_close:)
|
|
118
|
+
[operation, Sink.new(mailbox)]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def initialize(mailbox:, on_close:)
|
|
122
|
+
@mailbox = mailbox
|
|
123
|
+
@on_close = on_close
|
|
124
|
+
@mutex = Mutex.new
|
|
125
|
+
@owner = nil
|
|
126
|
+
@state = :open
|
|
127
|
+
@error = nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @rbs () { (Item) -> void } -> self
|
|
131
|
+
# | () -> Enumerator[Item, self]
|
|
132
|
+
def each
|
|
133
|
+
return enum_for(__method__) unless block_given?
|
|
134
|
+
|
|
135
|
+
state, error = current_state
|
|
136
|
+
case state
|
|
137
|
+
when :completed then return self
|
|
138
|
+
when :closed then raise ClosedError, 'stream is closed'
|
|
139
|
+
when :failed then raise error
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
claim_consumer!
|
|
143
|
+
begin
|
|
144
|
+
loop do
|
|
145
|
+
case event = @mailbox.pop
|
|
146
|
+
when Events::Item
|
|
147
|
+
yield event.value
|
|
148
|
+
when Events::Done
|
|
149
|
+
completed = mark_completed
|
|
150
|
+
@mailbox.close
|
|
151
|
+
terminal_result unless completed
|
|
152
|
+
break
|
|
153
|
+
when Events::Error
|
|
154
|
+
failed = mark_failed(event.exception)
|
|
155
|
+
@mailbox.close
|
|
156
|
+
raise event.exception if failed
|
|
157
|
+
|
|
158
|
+
terminal_result
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
self
|
|
162
|
+
rescue ClosedError
|
|
163
|
+
terminal_result
|
|
164
|
+
ensure
|
|
165
|
+
close if open?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @rbs (?timeout: Numeric?) -> Item
|
|
170
|
+
def next(timeout: nil)
|
|
171
|
+
state, error = current_state
|
|
172
|
+
case state
|
|
173
|
+
when :completed then raise StopIteration
|
|
174
|
+
when :closed then raise ClosedError, 'stream is closed'
|
|
175
|
+
when :failed then raise error
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
claim_consumer!
|
|
179
|
+
case event = @mailbox.pop(timeout:)
|
|
180
|
+
when Events::Item
|
|
181
|
+
event.value
|
|
182
|
+
when Events::Done
|
|
183
|
+
completed = mark_completed
|
|
184
|
+
@mailbox.close
|
|
185
|
+
raise StopIteration if completed
|
|
186
|
+
|
|
187
|
+
terminal_result(next_item: true)
|
|
188
|
+
when Events::Error
|
|
189
|
+
failed = mark_failed(event.exception)
|
|
190
|
+
@mailbox.close
|
|
191
|
+
raise event.exception if failed
|
|
192
|
+
|
|
193
|
+
terminal_result(next_item: true)
|
|
194
|
+
end
|
|
195
|
+
rescue ClosedError
|
|
196
|
+
terminal_result(next_item: true)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Stops local delivery. The dispatcher still drains the Netlink exchange.
|
|
200
|
+
def close #: nil
|
|
201
|
+
closed, callback = @mutex.synchronize do
|
|
202
|
+
next [false, nil] if @state != :open
|
|
203
|
+
|
|
204
|
+
@state = :closed
|
|
205
|
+
[true, @on_close]
|
|
206
|
+
end
|
|
207
|
+
if closed
|
|
208
|
+
callback&.call
|
|
209
|
+
@mailbox.close
|
|
210
|
+
end
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def closed? #: bool
|
|
215
|
+
@mutex.synchronize { @state != :open }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private def current_state
|
|
219
|
+
@mutex.synchronize { [@state, @error] }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private def open?
|
|
223
|
+
@mutex.synchronize { @state == :open }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private def claim_consumer!
|
|
227
|
+
owner = Fiber.current
|
|
228
|
+
@mutex.synchronize do
|
|
229
|
+
@owner ||= owner
|
|
230
|
+
unless @owner.equal?(owner)
|
|
231
|
+
raise ConcurrentConsumptionError, 'a stream can only be consumed by one fiber'
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private def mark_completed
|
|
237
|
+
@mutex.synchronize do
|
|
238
|
+
next false unless @state == :open
|
|
239
|
+
|
|
240
|
+
@state = :completed
|
|
241
|
+
true
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private def mark_failed(error)
|
|
246
|
+
@mutex.synchronize do
|
|
247
|
+
next false unless @state == :open
|
|
248
|
+
|
|
249
|
+
@state = :failed
|
|
250
|
+
@error = error
|
|
251
|
+
true
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private def terminal_result(next_item: false)
|
|
256
|
+
state, error = current_state
|
|
257
|
+
case state
|
|
258
|
+
when :completed
|
|
259
|
+
raise StopIteration if next_item
|
|
260
|
+
self
|
|
261
|
+
when :failed
|
|
262
|
+
raise error
|
|
263
|
+
else
|
|
264
|
+
raise ClosedError, 'stream is closed'
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
class Sink
|
|
269
|
+
def initialize(mailbox)
|
|
270
|
+
@mailbox = mailbox
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def push(value) = @mailbox.push(Events::Item.new(value))
|
|
274
|
+
def finish = @mailbox.push_terminal(Events::Done.new)
|
|
275
|
+
def fail(exception) = @mailbox.push_terminal(Events::Error.new(exception))
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
data/lib/nl/async.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
|
|
5
|
+
module Nl
|
|
6
|
+
# Asynchronous request handling support.
|
|
7
|
+
module Async
|
|
8
|
+
class UnavailableError < StandardError; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative 'async/mailbox'
|
|
13
|
+
require_relative 'async/operation'
|
|
14
|
+
require_relative 'async/driver'
|
|
15
|
+
require_relative 'async/dispatcher'
|