xchan.rb 0.19.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 +36 -16
- data/lib/xchan/bytes.rb +7 -5
- data/lib/xchan/counter.rb +32 -17
- data/lib/xchan/null_lock.rb +4 -4
- data/lib/xchan/unix_socket.rb +69 -37
- data/lib/xchan/version.rb +1 -1
- data/lib/xchan.rb +1 -1
- data/test/thread_safety_test.rb +57 -0
- data/test/xchan_test.rb +39 -0
- data/xchan.rb.gemspec +1 -1
- metadata +6 -8
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,9 +1,25 @@
|
|
|
1
1
|
## About
|
|
2
2
|
|
|
3
|
-
xchan.rb is an easy to use library for InterProcess
|
|
4
|
-
|
|
5
|
-
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
|
|
6
6
|
processes who have a parent <=> child relationship.
|
|
7
|
+
A channel lock is provided by
|
|
8
|
+
[lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3) and a temporary, unlinked file to protect against race conditions
|
|
9
|
+
that can happen when multiple processes access the same channel
|
|
10
|
+
at the same time.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
* Minimalist Inter-Process Communication (IPC) for parent <=> child processes.
|
|
15
|
+
* Channel-based communication.
|
|
16
|
+
* Support for multiple serializers (`:marshal`, `:json`, `:yaml`) and raw string communication (`:pure`).
|
|
17
|
+
* Blocking (`#send`, `#recv`) and non-blocking (`#send_nonblock`, `#recv_nonblock`) operations.
|
|
18
|
+
* Built-in file-based locking ([lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)) to prevent race conditions.
|
|
19
|
+
* Option to use a null lock for scenarios where locking is not needed.
|
|
20
|
+
* Access to underlying UNIX sockets for fine-grained control over socket options.
|
|
21
|
+
* Mac, BSD, and Linux support.
|
|
22
|
+
* Good docs.
|
|
7
23
|
|
|
8
24
|
## Examples
|
|
9
25
|
|
|
@@ -26,7 +42,7 @@ require "xchan"
|
|
|
26
42
|
# Marshal as the serializer
|
|
27
43
|
ch = xchan(:marshal)
|
|
28
44
|
Process.wait fork { ch.send(5) }
|
|
29
|
-
|
|
45
|
+
puts "#{ch.recv} + 7 = 12"
|
|
30
46
|
ch.close
|
|
31
47
|
|
|
32
48
|
##
|
|
@@ -40,7 +56,7 @@ ch.close
|
|
|
40
56
|
The `ch.recv` method performs a blocking read. A read
|
|
41
57
|
can block when a lock is held by another process, or
|
|
42
58
|
when a read from
|
|
43
|
-
[Chan::UNIXSocket#r](https://0x1eef.github
|
|
59
|
+
[Chan::UNIXSocket#r](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
|
|
44
60
|
blocks. The example performs a read that blocks until
|
|
45
61
|
the parent process writes to the channel:
|
|
46
62
|
|
|
@@ -53,7 +69,7 @@ fork do
|
|
|
53
69
|
print "Received a random number (child process): ", ch.recv, "\n"
|
|
54
70
|
end
|
|
55
71
|
sleep(1)
|
|
56
|
-
|
|
72
|
+
puts "Send a random number (from parent process)"
|
|
57
73
|
ch.send(rand(21))
|
|
58
74
|
ch.close
|
|
59
75
|
Process.wait
|
|
@@ -69,7 +85,7 @@ The non-blocking counterpart to `#recv` is `#recv_nonblock`.
|
|
|
69
85
|
The `#recv_nonblock` method raises `Chan::WaitLockable` when
|
|
70
86
|
a read blocks because of a lock held by another process, and
|
|
71
87
|
the method raises `Chan::WaitReadable` when a read from
|
|
72
|
-
[Chan::UNIXSocket#r](https://0x1eef.github
|
|
88
|
+
[Chan::UNIXSocket#r](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
|
|
73
89
|
blocks:
|
|
74
90
|
|
|
75
91
|
```ruby
|
|
@@ -79,11 +95,12 @@ require "xchan"
|
|
|
79
95
|
def read(ch)
|
|
80
96
|
ch.recv_nonblock
|
|
81
97
|
rescue Chan::WaitReadable
|
|
82
|
-
|
|
98
|
+
puts "Wait 1 second for channel to be readable"
|
|
83
99
|
ch.wait_readable(1)
|
|
84
100
|
retry
|
|
85
101
|
rescue Chan::WaitLockable
|
|
86
|
-
|
|
102
|
+
puts "Wait 1 second for channel to be lockable"
|
|
103
|
+
ch.wait_lockable(1)
|
|
87
104
|
retry
|
|
88
105
|
end
|
|
89
106
|
trap("SIGINT") { exit(1) }
|
|
@@ -102,7 +119,7 @@ read(xchan(:marshal))
|
|
|
102
119
|
The `ch.send` method performs a blocking write.
|
|
103
120
|
A write can block when a lock is held by another
|
|
104
121
|
process, or when a write to
|
|
105
|
-
[Chan::UNIXSocket#w](https://0x1eef.github
|
|
122
|
+
[Chan::UNIXSocket#w](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
|
|
106
123
|
blocks. The example fills the send buffer:
|
|
107
124
|
|
|
108
125
|
```ruby
|
|
@@ -123,7 +140,7 @@ The non-blocking counterpart to `#send` is
|
|
|
123
140
|
`Chan::WaitLockable` when a write blocks because of
|
|
124
141
|
a lock held by another process, and the method raises
|
|
125
142
|
`Chan::WaitWritable` when a write to
|
|
126
|
-
[Chan::UNIXSocket#w](https://0x1eef.github
|
|
143
|
+
[Chan::UNIXSocket#w](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
|
|
127
144
|
blocks. The example frees space on the send buffer:
|
|
128
145
|
|
|
129
146
|
```ruby
|
|
@@ -133,11 +150,11 @@ require "xchan"
|
|
|
133
150
|
def send_nonblock(ch, buf)
|
|
134
151
|
ch.send_nonblock(buf)
|
|
135
152
|
rescue Chan::WaitWritable
|
|
136
|
-
|
|
153
|
+
puts "Blocked - free send buffer"
|
|
137
154
|
ch.recv
|
|
138
155
|
retry
|
|
139
156
|
rescue Chan::WaitLockable
|
|
140
|
-
|
|
157
|
+
ch.wait_lockable
|
|
141
158
|
retry
|
|
142
159
|
end
|
|
143
160
|
|
|
@@ -166,6 +183,9 @@ processes:
|
|
|
166
183
|
#!/usr/bin/env ruby
|
|
167
184
|
require "xchan"
|
|
168
185
|
|
|
186
|
+
##
|
|
187
|
+
# 'lock: :file' is added just for the example
|
|
188
|
+
# It is the default behavior, and not necessary
|
|
169
189
|
ch = xchan(:marshal, lock: :file)
|
|
170
190
|
5.times.map do
|
|
171
191
|
fork do
|
|
@@ -179,7 +199,7 @@ end.each { Process.wait(_1) }
|
|
|
179
199
|
The null lock is the same as using no lock at all. The null lock is
|
|
180
200
|
implemented as a collection of no-op operations. The null lock is
|
|
181
201
|
implemented in the
|
|
182
|
-
[Chan::NullLock](https://0x1eef.github
|
|
202
|
+
[Chan::NullLock](https://0x1eef.github.io/x/xchan.rb/Chan/NullLock.html)
|
|
183
203
|
class, and in certain situations, it can be useful and preferable
|
|
184
204
|
to using a file lock:
|
|
185
205
|
|
|
@@ -200,9 +220,9 @@ Process.wait
|
|
|
200
220
|
|
|
201
221
|
A channel has one socket for read operations and another
|
|
202
222
|
socket for write operations.
|
|
203
|
-
[Chan::UNIXSocket#r](https://0x1eef.github
|
|
223
|
+
[Chan::UNIXSocket#r](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
|
|
204
224
|
returns the socket used for read operations, and
|
|
205
|
-
[Chan::UNIXSocket#w](https://0x1eef.github
|
|
225
|
+
[Chan::UNIXSocket#w](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
|
|
206
226
|
returns the socket used for write operations:
|
|
207
227
|
|
|
208
228
|
```ruby
|
data/lib/xchan/bytes.rb
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
# increases in size, and when an object is read from
|
|
8
8
|
# a channel, the collection decreases in size.
|
|
9
9
|
class Chan::Bytes
|
|
10
|
-
require "json"
|
|
11
10
|
require_relative "counter"
|
|
12
11
|
|
|
13
12
|
##
|
|
@@ -15,7 +14,8 @@ class Chan::Bytes
|
|
|
15
14
|
# Directory where temporary files are stored
|
|
16
15
|
# @return [Chan::Bytes]
|
|
17
16
|
def initialize(tmpdir)
|
|
18
|
-
@io = Chan.temporary_file(%w[bytes .
|
|
17
|
+
@io = Chan.temporary_file(%w[bytes .bin], tmpdir:)
|
|
18
|
+
@io.binmode
|
|
19
19
|
@io.sync = true
|
|
20
20
|
write(@io, [])
|
|
21
21
|
end
|
|
@@ -79,15 +79,17 @@ class Chan::Bytes
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def write(io, bytes)
|
|
82
|
+
io.rewind
|
|
82
83
|
io.truncate(0)
|
|
83
|
-
io.write(serialize(bytes))
|
|
84
|
+
io.write(serialize(bytes))
|
|
85
|
+
io.rewind
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def serialize(bytes)
|
|
87
|
-
|
|
89
|
+
bytes.pack("Q>*")
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
def deserialize(bytes)
|
|
91
|
-
|
|
93
|
+
bytes.unpack("Q>*")
|
|
92
94
|
end
|
|
93
95
|
end
|
data/lib/xchan/counter.rb
CHANGED
|
@@ -5,57 +5,72 @@
|
|
|
5
5
|
# for the number of written and received bytes on a
|
|
6
6
|
# given channel.
|
|
7
7
|
class Chan::Counter
|
|
8
|
-
require "json"
|
|
9
|
-
|
|
10
8
|
##
|
|
11
9
|
# @param [String] tmpdir
|
|
12
10
|
# Directory where temporary files are stored
|
|
13
11
|
# @return [Chan::Counter]
|
|
14
12
|
def initialize(tmpdir)
|
|
15
|
-
@io = Chan.temporary_file(%w[counter .
|
|
16
|
-
|
|
13
|
+
@io = Chan.temporary_file(%w[counter .bin], tmpdir:)
|
|
14
|
+
@io.binmode
|
|
15
|
+
@io.sync = true
|
|
16
|
+
write(@io, 0, 0)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
##
|
|
20
20
|
# @return [Integer]
|
|
21
21
|
# Returns the number of bytes written to a channel
|
|
22
22
|
def bytes_written
|
|
23
|
-
read(@io)
|
|
23
|
+
_, bytes = read(@io)
|
|
24
|
+
bytes
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
##
|
|
27
28
|
# @return [Integer]
|
|
28
29
|
# Returns the number of bytes read from a channel
|
|
29
30
|
def bytes_read
|
|
30
|
-
read(@io)
|
|
31
|
+
bytes, _ = read(@io)
|
|
32
|
+
bytes
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
##
|
|
34
|
-
# @param [
|
|
36
|
+
# @param [Integer] bytes_read
|
|
37
|
+
# Number of bytes read to increment the counter by
|
|
38
|
+
# @param [Integer] bytes_written
|
|
39
|
+
# Number of bytes written to increment the counter by
|
|
35
40
|
# @return [void]
|
|
36
41
|
# @private
|
|
37
|
-
def increment!(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
def increment!(bytes_read: 0, bytes_written: 0)
|
|
43
|
+
bytes_in, bytes_out = read(@io)
|
|
44
|
+
bytes_in += bytes_read
|
|
45
|
+
bytes_out += bytes_written
|
|
46
|
+
write(@io, bytes_in, bytes_out)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Close the counter
|
|
51
|
+
# @return [void]
|
|
52
|
+
def close
|
|
53
|
+
@io.close
|
|
41
54
|
end
|
|
42
55
|
|
|
43
56
|
private
|
|
44
57
|
|
|
45
|
-
def write(io,
|
|
58
|
+
def write(io, bytes_read, bytes_written)
|
|
59
|
+
io.rewind
|
|
46
60
|
io.truncate(0)
|
|
47
|
-
io.write(serialize(
|
|
61
|
+
io.write(serialize(bytes_read, bytes_written))
|
|
62
|
+
io.rewind
|
|
48
63
|
end
|
|
49
64
|
|
|
50
65
|
def read(io)
|
|
51
66
|
deserialize(io.read).tap { io.rewind }
|
|
52
67
|
end
|
|
53
68
|
|
|
54
|
-
def serialize(
|
|
55
|
-
|
|
69
|
+
def serialize(bytes_read, bytes_written)
|
|
70
|
+
[bytes_read, bytes_written].pack("Q>Q>")
|
|
56
71
|
end
|
|
57
72
|
|
|
58
|
-
def deserialize(
|
|
59
|
-
|
|
73
|
+
def deserialize(payload)
|
|
74
|
+
payload.unpack("Q>Q>")
|
|
60
75
|
end
|
|
61
76
|
end
|
data/lib/xchan/null_lock.rb
CHANGED
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
|
##
|
|
@@ -61,8 +62,8 @@ class Chan::UNIXSocket
|
|
|
61
62
|
# @return [void]
|
|
62
63
|
def close
|
|
63
64
|
@lock.lock
|
|
64
|
-
raise IOError, "channel
|
|
65
|
-
[@r, @w, @bytes, @lock].each(&:close)
|
|
65
|
+
raise IOError, "closed channel" if closed?
|
|
66
|
+
[@r, @w, @bytes, @counter, @lock].each(&:close)
|
|
66
67
|
rescue IOError => ex
|
|
67
68
|
@lock.release
|
|
68
69
|
raise(ex)
|
|
@@ -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
|
|
|
@@ -222,22 +232,40 @@ class Chan::UNIXSocket
|
|
|
222
232
|
|
|
223
233
|
##
|
|
224
234
|
# Waits for the channel to become readable
|
|
225
|
-
# @param [Float, Integer, nil]
|
|
226
|
-
# The number of seconds to wait
|
|
235
|
+
# @param [Float, Integer, nil] timeout
|
|
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
|
-
def wait_readable(
|
|
230
|
-
@r.wait_readable(
|
|
240
|
+
def wait_readable(timeout = nil)
|
|
241
|
+
@r.wait_readable(timeout) and self
|
|
231
242
|
end
|
|
232
243
|
|
|
233
244
|
##
|
|
234
245
|
# Waits for the channel to become writable
|
|
235
|
-
# @param [Float, Integer, nil]
|
|
236
|
-
# The number of seconds to wait
|
|
246
|
+
# @param [Float, Integer, nil] timeout
|
|
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
|
-
def wait_writable(
|
|
240
|
-
@w.wait_writable(
|
|
251
|
+
def wait_writable(timeout = nil)
|
|
252
|
+
@w.wait_writable(timeout) and self
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
# Waits for the channel to become lockable
|
|
257
|
+
# @param [Float, Integer, nil] timeout
|
|
258
|
+
# The number of seconds to wait before timeout.
|
|
259
|
+
# Waits indefinitely with no arguments
|
|
260
|
+
# @return [Chan::UNIXSocket, nil]
|
|
261
|
+
# Returns self when the channel is lockable, otherwise returns nil
|
|
262
|
+
def wait_lockable(timeout = nil)
|
|
263
|
+
start = (timeout ? gettime : nil)
|
|
264
|
+
loop do
|
|
265
|
+
break(nil) if start && (gettime - start) >= timeout
|
|
266
|
+
break(self) if @lock.lockable?
|
|
267
|
+
sleep 0.01
|
|
268
|
+
end
|
|
241
269
|
end
|
|
242
270
|
|
|
243
271
|
##
|
|
@@ -259,4 +287,8 @@ class Chan::UNIXSocket
|
|
|
259
287
|
def deserialize(str)
|
|
260
288
|
@s.load(str)
|
|
261
289
|
end
|
|
290
|
+
|
|
291
|
+
def gettime
|
|
292
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
293
|
+
end
|
|
262
294
|
end
|
data/lib/xchan/version.rb
CHANGED
data/lib/xchan.rb
CHANGED
|
@@ -65,7 +65,7 @@ module Chan
|
|
|
65
65
|
def self.locks
|
|
66
66
|
{
|
|
67
67
|
null: lambda { |_tmpdir| Chan::NullLock },
|
|
68
|
-
file: lambda { |tmpdir|
|
|
68
|
+
file: lambda { |tmpdir| Lockf.new Chan.temporary_file(%w[xchan lock], tmpdir:) }
|
|
69
69
|
}
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -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
|
data/test/xchan_test.rb
CHANGED
|
@@ -223,3 +223,42 @@ class Chan::TemporaryFileTest < Chan::Test
|
|
|
223
223
|
@file ||= Chan.temporary_file %w[foobar .txt]
|
|
224
224
|
end
|
|
225
225
|
end
|
|
226
|
+
|
|
227
|
+
##
|
|
228
|
+
# Chan::UNIXSocket#wait_lockable
|
|
229
|
+
class Chan::WaitLockableTest < Chan::Test
|
|
230
|
+
def test_wait_lockable_on_lockable_channel
|
|
231
|
+
assert_instance_of Chan::UNIXSocket, ch.wait_lockable
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def test_wait_lockable_on_locked_channel
|
|
235
|
+
aux = xchan(:pure)
|
|
236
|
+
lock! do
|
|
237
|
+
Process.wait fork { aux.send ch.wait_lockable(0.1).class.to_s }
|
|
238
|
+
end
|
|
239
|
+
assert_equal "NilClass", aux.recv
|
|
240
|
+
ensure
|
|
241
|
+
aux.close
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_wait_lockable_on_null_lock
|
|
245
|
+
ch = xchan(:pure, lock: :null)
|
|
246
|
+
assert_instance_of Chan::UNIXSocket, ch.wait_lockable
|
|
247
|
+
ensure
|
|
248
|
+
ch.close
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def lock!
|
|
254
|
+
ch.instance_variable_get(:@lock).lock
|
|
255
|
+
yield
|
|
256
|
+
ensure
|
|
257
|
+
release!
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def release!
|
|
261
|
+
ch.instance_variable_get(:@lock).release
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
data/xchan.rb.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
|
|
|
18
18
|
gem.require_paths = ["lib"]
|
|
19
19
|
gem.summary = "An easy to use InterProcess Communication (IPC) library"
|
|
20
20
|
gem.description = gem.summary
|
|
21
|
-
gem.add_runtime_dependency "lockf.rb", "~>
|
|
21
|
+
gem.add_runtime_dependency "lockf.rb", "~> 3.0"
|
|
22
22
|
gem.add_development_dependency "test-unit", "~> 3.5.7"
|
|
23
23
|
gem.add_development_dependency "yard", "~> 0.9"
|
|
24
24
|
gem.add_development_dependency "kramdown", "~> 2.5"
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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'
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: lockf.rb
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '3.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '3.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: test-unit
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -147,13 +146,13 @@ files:
|
|
|
147
146
|
- share/xchan.rb/examples/write_operations/2_nonblocking_write.rb
|
|
148
147
|
- test/readme_test.rb
|
|
149
148
|
- test/setup.rb
|
|
149
|
+
- test/thread_safety_test.rb
|
|
150
150
|
- test/xchan_test.rb
|
|
151
151
|
- xchan.rb.gemspec
|
|
152
152
|
homepage: https://github.com/0x1eef/xchan.rb#readme
|
|
153
153
|
licenses:
|
|
154
154
|
- 0BSD
|
|
155
155
|
metadata: {}
|
|
156
|
-
post_install_message:
|
|
157
156
|
rdoc_options: []
|
|
158
157
|
require_paths:
|
|
159
158
|
- lib
|
|
@@ -168,8 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
168
167
|
- !ruby/object:Gem::Version
|
|
169
168
|
version: '0'
|
|
170
169
|
requirements: []
|
|
171
|
-
rubygems_version: 3.
|
|
172
|
-
signing_key:
|
|
170
|
+
rubygems_version: 3.6.9
|
|
173
171
|
specification_version: 4
|
|
174
172
|
summary: An easy to use InterProcess Communication (IPC) library
|
|
175
173
|
test_files: []
|