xchan.rb 0.20.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/README.md +3 -7
- data/lib/xchan/unix_socket.rb +83 -36
- data/lib/xchan/version.rb +1 -1
- data/test/thread_safety_test.rb +57 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d385aa7803c7861610c50348dec4108760bf2e0a21d36bd6a9402788ec56f62
|
|
4
|
+
data.tar.gz: 5292469bfb8a3cc8fb581d12ecde445cef46551000604b6e8d1c3b9ca96b2d93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10b83c2ad3e4861715b4348f0e4aa54e9f84377fa08acf3fb59caaccbb164909326ae1d527afaac5bed6a7401a4981942f75b767a370cb276553784ab0bce2b0
|
|
7
|
+
data.tar.gz: 990ed1ae55dba125d7f95a4e2d2477353b7bddeaa275149713fc2769fd36cea564ebe179567fe93924b1a37bc18f9e4c51ea7da1310587c34c94190975154fb8
|
data/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
> **Designed for minimalism** <br>
|
|
2
|
-
> One direct runtime dependency ([lockf.rb](https://github.com/0x1eef/lockf.rb#readme)) <br>
|
|
3
|
-
> Zero indirect dependencies outside Ruby's standard library
|
|
4
|
-
|
|
5
1
|
## About
|
|
6
2
|
|
|
7
|
-
xchan.rb is an easy to use
|
|
8
|
-
|
|
9
|
-
that can help facilitate communication between Ruby
|
|
3
|
+
xchan.rb is an easy to use library for InterProcess Communication (IPC).
|
|
4
|
+
|
|
5
|
+
The library provides a channel that can help facilitate communication between Ruby
|
|
10
6
|
processes who have a parent <=> child relationship.
|
|
11
7
|
A channel lock is provided by
|
|
12
8
|
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3) and a temporary, unlinked file to protect against race conditions
|
data/lib/xchan/unix_socket.rb
CHANGED
|
@@ -45,6 +45,9 @@ class Chan::UNIXSocket
|
|
|
45
45
|
@bytes = Chan::Bytes.new(tmpdir)
|
|
46
46
|
@counter = Chan::Counter.new(tmpdir)
|
|
47
47
|
@lock = Chan.locks[lock]&.call(tmpdir) || lock
|
|
48
|
+
@mutex = Mutex.new
|
|
49
|
+
@partial = nil
|
|
50
|
+
@partial_len = 0
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
##
|
|
@@ -81,7 +84,11 @@ class Chan::UNIXSocket
|
|
|
81
84
|
# Returns the number of bytes written to the channel
|
|
82
85
|
def send(object)
|
|
83
86
|
send_nonblock(object)
|
|
84
|
-
rescue Chan::WaitWritable
|
|
87
|
+
rescue Chan::WaitWritable
|
|
88
|
+
wait_writable
|
|
89
|
+
retry
|
|
90
|
+
rescue Chan::WaitLockable
|
|
91
|
+
wait_lockable
|
|
85
92
|
retry
|
|
86
93
|
end
|
|
87
94
|
alias_method :write, :send
|
|
@@ -99,17 +106,21 @@ class Chan::UNIXSocket
|
|
|
99
106
|
# @return [Integer, nil]
|
|
100
107
|
# Returns the number of bytes written to the channel
|
|
101
108
|
def send_nonblock(object)
|
|
102
|
-
@
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
@lock.lock_nonblock
|
|
111
|
+
raise IOError, "channel closed" if closed?
|
|
112
|
+
data = serialize(object)
|
|
113
|
+
head = [data.bytesize].pack("Q>")
|
|
114
|
+
n = @w.write_nonblock(head << data)
|
|
115
|
+
@bytes.push(data.bytesize)
|
|
116
|
+
@counter.increment!(bytes_written: data.bytesize)
|
|
117
|
+
data.bytesize.tap { @lock.release }
|
|
118
|
+
rescue IOError, IO::WaitWritable, Errno::ENOBUFS => ex
|
|
119
|
+
@lock.release
|
|
120
|
+
raise Chan::WaitWritable, ex.message
|
|
121
|
+
rescue Errno::EWOULDBLOCK => ex
|
|
122
|
+
raise Chan::WaitLockable, ex.message
|
|
123
|
+
end
|
|
113
124
|
end
|
|
114
125
|
alias_method :write_nonblock, :send_nonblock
|
|
115
126
|
|
|
@@ -131,6 +142,7 @@ class Chan::UNIXSocket
|
|
|
131
142
|
wait_readable
|
|
132
143
|
retry
|
|
133
144
|
rescue Chan::WaitLockable
|
|
145
|
+
wait_lockable
|
|
134
146
|
retry
|
|
135
147
|
end
|
|
136
148
|
alias_method :read, :recv
|
|
@@ -146,21 +158,43 @@ class Chan::UNIXSocket
|
|
|
146
158
|
# @return [Object]
|
|
147
159
|
# Returns an object from the channel
|
|
148
160
|
def recv_nonblock
|
|
149
|
-
@
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
@mutex.synchronize do
|
|
162
|
+
@lock.lock_nonblock
|
|
163
|
+
raise IOError, "closed channel" if closed?
|
|
164
|
+
stream = @r.local_address.socktype == Socket::SOCK_STREAM
|
|
165
|
+
unless @partial
|
|
166
|
+
if stream
|
|
167
|
+
head = String.new
|
|
168
|
+
while head.bytesize < 8
|
|
169
|
+
head << @r.read_nonblock(8 - head.bytesize)
|
|
170
|
+
end
|
|
171
|
+
@partial_len = head.unpack1("Q>")
|
|
172
|
+
@partial = String.new(capacity: @partial_len)
|
|
173
|
+
else
|
|
174
|
+
buf = @r.read_nonblock(65536)
|
|
175
|
+
@partial_len = buf[0, 8].unpack1("Q>")
|
|
176
|
+
@partial = buf[8, @partial_len]
|
|
177
|
+
end
|
|
178
|
+
@bytes.shift
|
|
179
|
+
end
|
|
180
|
+
if stream
|
|
181
|
+
while @partial.bytesize < @partial_len
|
|
182
|
+
@partial << @r.read_nonblock(@partial_len - @partial.bytesize)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
obj = deserialize(@partial)
|
|
186
|
+
@counter.increment!(bytes_read: @partial.bytesize)
|
|
187
|
+
@partial = nil
|
|
188
|
+
obj.tap { @lock.release }
|
|
189
|
+
rescue IOError => ex
|
|
190
|
+
@lock.release
|
|
191
|
+
raise(ex)
|
|
192
|
+
rescue IO::WaitReadable => ex
|
|
193
|
+
@lock.release
|
|
194
|
+
raise Chan::WaitReadable, ex.message
|
|
195
|
+
rescue Errno::EAGAIN => ex
|
|
196
|
+
raise Chan::WaitLockable, ex.message
|
|
197
|
+
end
|
|
164
198
|
end
|
|
165
199
|
alias_method :read_nonblock, :recv_nonblock
|
|
166
200
|
|
|
@@ -188,6 +222,13 @@ class Chan::UNIXSocket
|
|
|
188
222
|
lock { size.zero? }
|
|
189
223
|
end
|
|
190
224
|
|
|
225
|
+
##
|
|
226
|
+
# @return [Integer]
|
|
227
|
+
# Returns the number of elements waiting to be read
|
|
228
|
+
def size
|
|
229
|
+
@bytes.size
|
|
230
|
+
end
|
|
231
|
+
|
|
191
232
|
##
|
|
192
233
|
# @group Stat methods
|
|
193
234
|
|
|
@@ -208,14 +249,16 @@ class Chan::UNIXSocket
|
|
|
208
249
|
alias_method :bytes_read, :bytes_received
|
|
209
250
|
|
|
210
251
|
##
|
|
211
|
-
# @
|
|
212
|
-
# Returns the number of objects waiting to be read
|
|
213
|
-
def size
|
|
214
|
-
lock { @bytes.size }
|
|
215
|
-
end
|
|
252
|
+
# @endgroup
|
|
216
253
|
|
|
217
254
|
##
|
|
218
|
-
# @
|
|
255
|
+
# @return [void]
|
|
256
|
+
def flush
|
|
257
|
+
@lock.lock
|
|
258
|
+
to_a
|
|
259
|
+
ensure
|
|
260
|
+
@lock.release
|
|
261
|
+
end
|
|
219
262
|
|
|
220
263
|
##
|
|
221
264
|
# @group Wait methods
|
|
@@ -223,7 +266,8 @@ class Chan::UNIXSocket
|
|
|
223
266
|
##
|
|
224
267
|
# Waits for the channel to become readable
|
|
225
268
|
# @param [Float, Integer, nil] timeout
|
|
226
|
-
# The number of seconds to wait
|
|
269
|
+
# The number of seconds to wait before timeout.
|
|
270
|
+
# Waits indefinitely with no arguments
|
|
227
271
|
# @return [Chan::UNIXSocket, nil]
|
|
228
272
|
# Returns self when the channel is readable, otherwise returns nil
|
|
229
273
|
def wait_readable(timeout = nil)
|
|
@@ -233,7 +277,8 @@ class Chan::UNIXSocket
|
|
|
233
277
|
##
|
|
234
278
|
# Waits for the channel to become writable
|
|
235
279
|
# @param [Float, Integer, nil] timeout
|
|
236
|
-
# The number of seconds to wait
|
|
280
|
+
# The number of seconds to wait before timeout.
|
|
281
|
+
# Waits indefinitely with no arguments
|
|
237
282
|
# @return [Chan::UNIXSocket, nil]
|
|
238
283
|
# Returns self when the channel is writable, otherwise returns nil
|
|
239
284
|
def wait_writable(timeout = nil)
|
|
@@ -243,8 +288,10 @@ class Chan::UNIXSocket
|
|
|
243
288
|
##
|
|
244
289
|
# Waits for the channel to become lockable
|
|
245
290
|
# @param [Float, Integer, nil] timeout
|
|
246
|
-
# The number of seconds to wait before timeout
|
|
291
|
+
# The number of seconds to wait before timeout.
|
|
292
|
+
# Waits indefinitely with no arguments
|
|
247
293
|
# @return [Chan::UNIXSocket, nil]
|
|
294
|
+
# Returns self when the channel is lockable, otherwise returns nil
|
|
248
295
|
def wait_lockable(timeout = nil)
|
|
249
296
|
start = (timeout ? gettime : nil)
|
|
250
297
|
loop do
|
data/lib/xchan/version.rb
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "xchan"
|
|
4
|
+
require "test/unit"
|
|
5
|
+
|
|
6
|
+
class ThreadSafetyTest < Test::Unit::TestCase
|
|
7
|
+
##
|
|
8
|
+
# Without a Mutex wrapping the critical sections in send_nonblock and
|
|
9
|
+
# recv_nonblock, lockf(3) record locks do not provide thread safety
|
|
10
|
+
# because they are per-process (PID-based). Two threads sharing the
|
|
11
|
+
# same channel can both acquire the lock simultaneously, causing the
|
|
12
|
+
# @bytes and @counter tempfiles to be read and written concurrently.
|
|
13
|
+
#
|
|
14
|
+
# This test reproduces the race by having one thread write many
|
|
15
|
+
# messages while another reads them. A mismatch between the byte
|
|
16
|
+
# length recorded by @bytes.push and the actual data read from the
|
|
17
|
+
# socket triggers Marshal.load errors.
|
|
18
|
+
def test_concurrent_send_and_recv
|
|
19
|
+
ch = xchan(:marshal)
|
|
20
|
+
n = 1000
|
|
21
|
+
writer = Thread.new do
|
|
22
|
+
n.times { ch.send("hello") }
|
|
23
|
+
end
|
|
24
|
+
reader = Thread.new do
|
|
25
|
+
count = 0
|
|
26
|
+
n.times do
|
|
27
|
+
ch.recv
|
|
28
|
+
count += 1
|
|
29
|
+
end
|
|
30
|
+
count
|
|
31
|
+
end
|
|
32
|
+
count = [writer, reader].map(&:value).last
|
|
33
|
+
assert_equal n, count
|
|
34
|
+
ensure
|
|
35
|
+
ch.close unless ch.closed?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_concurrent_send_nonblock_and_recv_nonblock
|
|
39
|
+
ch = xchan(:marshal)
|
|
40
|
+
n = 1000
|
|
41
|
+
writer = Thread.new do
|
|
42
|
+
n.times { ch.send("world") }
|
|
43
|
+
end
|
|
44
|
+
reader = Thread.new do
|
|
45
|
+
count = 0
|
|
46
|
+
n.times do
|
|
47
|
+
ch.recv
|
|
48
|
+
count += 1
|
|
49
|
+
end
|
|
50
|
+
count
|
|
51
|
+
end
|
|
52
|
+
count = [writer, reader].map(&:value).last
|
|
53
|
+
assert_equal n, count
|
|
54
|
+
ensure
|
|
55
|
+
ch.close unless ch.closed?
|
|
56
|
+
end
|
|
57
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: xchan.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- '0x1eef'
|
|
@@ -146,6 +146,7 @@ files:
|
|
|
146
146
|
- share/xchan.rb/examples/write_operations/2_nonblocking_write.rb
|
|
147
147
|
- test/readme_test.rb
|
|
148
148
|
- test/setup.rb
|
|
149
|
+
- test/thread_safety_test.rb
|
|
149
150
|
- test/xchan_test.rb
|
|
150
151
|
- xchan.rb.gemspec
|
|
151
152
|
homepage: https://github.com/0x1eef/xchan.rb#readme
|
|
@@ -166,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
167
|
- !ruby/object:Gem::Version
|
|
167
168
|
version: '0'
|
|
168
169
|
requirements: []
|
|
169
|
-
rubygems_version:
|
|
170
|
+
rubygems_version: 4.0.6
|
|
170
171
|
specification_version: 4
|
|
171
172
|
summary: An easy to use InterProcess Communication (IPC) library
|
|
172
173
|
test_files: []
|