xchan.rb 0.20.0 → 0.21.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 +44 -30
- data/lib/xchan/version.rb +1 -1
- data/test/thread_safety_test.rb +57 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8936112cb188471884a6fa66ee0db2b61d5229ac75acb8953c78e5b76587bc8
|
|
4
|
+
data.tar.gz: e76eb21f0f7f2a11e465ee3d4834b3abd48b54269949ee3623e2b2c2bdc1a17c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6ff8db4154e29a41a41b3878a93355ab3ce8092112f76f7fa4a61042a9747138f9a270217decf0e39cc5a9890f7ce5f34ebaa8a93e74aeb68d1cf5b0d470a09
|
|
7
|
+
data.tar.gz: bbe432d09de33e607b7940005a1d29ee0c1de6c14d3a2a56105c5fa26e32ea81a6327a87a6e520163f34836ffe820296674bf29a46c72cc45c603872500f090f
|
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,7 @@ 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
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
##
|
|
@@ -81,7 +82,11 @@ class Chan::UNIXSocket
|
|
|
81
82
|
# Returns the number of bytes written to the channel
|
|
82
83
|
def send(object)
|
|
83
84
|
send_nonblock(object)
|
|
84
|
-
rescue Chan::WaitWritable
|
|
85
|
+
rescue Chan::WaitWritable
|
|
86
|
+
wait_writable
|
|
87
|
+
retry
|
|
88
|
+
rescue Chan::WaitLockable
|
|
89
|
+
wait_lockable
|
|
85
90
|
retry
|
|
86
91
|
end
|
|
87
92
|
alias_method :write, :send
|
|
@@ -99,17 +104,19 @@ class Chan::UNIXSocket
|
|
|
99
104
|
# @return [Integer, nil]
|
|
100
105
|
# Returns the number of bytes written to the channel
|
|
101
106
|
def send_nonblock(object)
|
|
102
|
-
@
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
@mutex.synchronize do
|
|
108
|
+
@lock.lock_nonblock
|
|
109
|
+
raise IOError, "channel closed" if closed?
|
|
110
|
+
len = @w.write_nonblock(serialize(object))
|
|
111
|
+
@bytes.push(len)
|
|
112
|
+
@counter.increment!(bytes_written: len)
|
|
113
|
+
len.tap { @lock.release }
|
|
114
|
+
rescue IOError, IO::WaitWritable, Errno::ENOBUFS => ex
|
|
115
|
+
@lock.release
|
|
116
|
+
raise Chan::WaitWritable, ex.message
|
|
117
|
+
rescue Errno::EWOULDBLOCK => ex
|
|
118
|
+
raise Chan::WaitLockable, ex.message
|
|
119
|
+
end
|
|
113
120
|
end
|
|
114
121
|
alias_method :write_nonblock, :send_nonblock
|
|
115
122
|
|
|
@@ -131,6 +138,7 @@ class Chan::UNIXSocket
|
|
|
131
138
|
wait_readable
|
|
132
139
|
retry
|
|
133
140
|
rescue Chan::WaitLockable
|
|
141
|
+
wait_lockable
|
|
134
142
|
retry
|
|
135
143
|
end
|
|
136
144
|
alias_method :read, :recv
|
|
@@ -146,21 +154,23 @@ class Chan::UNIXSocket
|
|
|
146
154
|
# @return [Object]
|
|
147
155
|
# Returns an object from the channel
|
|
148
156
|
def recv_nonblock
|
|
149
|
-
@
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
@mutex.synchronize do
|
|
158
|
+
@lock.lock_nonblock
|
|
159
|
+
raise IOError, "closed channel" if closed?
|
|
160
|
+
len = @bytes.shift
|
|
161
|
+
obj = deserialize(@r.read_nonblock(len.zero? ? 1 : len))
|
|
162
|
+
@counter.increment!(bytes_read: len)
|
|
163
|
+
obj.tap { @lock.release }
|
|
164
|
+
rescue IOError => ex
|
|
165
|
+
@lock.release
|
|
166
|
+
raise(ex)
|
|
167
|
+
rescue IO::WaitReadable => ex
|
|
168
|
+
@bytes.unshift(len)
|
|
169
|
+
@lock.release
|
|
170
|
+
raise Chan::WaitReadable, ex.message
|
|
171
|
+
rescue Errno::EAGAIN => ex
|
|
172
|
+
raise Chan::WaitLockable, ex.message
|
|
173
|
+
end
|
|
164
174
|
end
|
|
165
175
|
alias_method :read_nonblock, :recv_nonblock
|
|
166
176
|
|
|
@@ -223,7 +233,8 @@ class Chan::UNIXSocket
|
|
|
223
233
|
##
|
|
224
234
|
# Waits for the channel to become readable
|
|
225
235
|
# @param [Float, Integer, nil] timeout
|
|
226
|
-
# The number of seconds to wait
|
|
236
|
+
# The number of seconds to wait before timeout.
|
|
237
|
+
# Waits indefinitely with no arguments
|
|
227
238
|
# @return [Chan::UNIXSocket, nil]
|
|
228
239
|
# Returns self when the channel is readable, otherwise returns nil
|
|
229
240
|
def wait_readable(timeout = nil)
|
|
@@ -233,7 +244,8 @@ class Chan::UNIXSocket
|
|
|
233
244
|
##
|
|
234
245
|
# Waits for the channel to become writable
|
|
235
246
|
# @param [Float, Integer, nil] timeout
|
|
236
|
-
# The number of seconds to wait
|
|
247
|
+
# The number of seconds to wait before timeout.
|
|
248
|
+
# Waits indefinitely with no arguments
|
|
237
249
|
# @return [Chan::UNIXSocket, nil]
|
|
238
250
|
# Returns self when the channel is writable, otherwise returns nil
|
|
239
251
|
def wait_writable(timeout = nil)
|
|
@@ -243,8 +255,10 @@ class Chan::UNIXSocket
|
|
|
243
255
|
##
|
|
244
256
|
# Waits for the channel to become lockable
|
|
245
257
|
# @param [Float, Integer, nil] timeout
|
|
246
|
-
# The number of seconds to wait before timeout
|
|
258
|
+
# The number of seconds to wait before timeout.
|
|
259
|
+
# Waits indefinitely with no arguments
|
|
247
260
|
# @return [Chan::UNIXSocket, nil]
|
|
261
|
+
# Returns self when the channel is lockable, otherwise returns nil
|
|
248
262
|
def wait_lockable(timeout = nil)
|
|
249
263
|
start = (timeout ? gettime : nil)
|
|
250
264
|
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.21.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
|