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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3faafc7100339e243744b476b137e7ee96591179027c862509006c60df6c7fe1
4
- data.tar.gz: b7abc06e3a789f1f80a171e520c2cd3a64126c11e8bc01795bf90c4fcb5601bc
3
+ metadata.gz: 2d385aa7803c7861610c50348dec4108760bf2e0a21d36bd6a9402788ec56f62
4
+ data.tar.gz: 5292469bfb8a3cc8fb581d12ecde445cef46551000604b6e8d1c3b9ca96b2d93
5
5
  SHA512:
6
- metadata.gz: 1a14ea31ccd8ae96a56d4dfc667fe0141906a8d0123f3ba184e524ad07b4586b205501c870aec02f0af33d47d6ac8b788f023174afcddf8d6d1b90e853a3b40b
7
- data.tar.gz: 98c9e23a2abd39852fdd315011c44385b6c57a1a9a000e0d9430580a36a3970091857482d7866c58c765015fce7cbb6523a9b3e411daa2c5520b79f911efaf05
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, minimalist library for
8
- InterProcess Communication (IPC). The library provides a channel
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 &lt;=&gt; 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
@@ -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, Chan::WaitLockable
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
- @lock.lock_nonblock
103
- raise IOError, "channel closed" if closed?
104
- len = @w.write_nonblock(serialize(object))
105
- @bytes.push(len)
106
- @counter.increment!(bytes_written: len)
107
- len.tap { @lock.release }
108
- rescue IOError, IO::WaitWritable, Errno::ENOBUFS => ex
109
- @lock.release
110
- raise Chan::WaitWritable, ex.message
111
- rescue Errno::EWOULDBLOCK => ex
112
- raise Chan::WaitLockable, ex.message
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
- @lock.lock_nonblock
150
- raise IOError, "closed channel" if closed?
151
- len = @bytes.shift
152
- obj = deserialize(@r.read_nonblock(len.zero? ? 1 : len))
153
- @counter.increment!(bytes_read: len)
154
- obj.tap { @lock.release }
155
- rescue IOError => ex
156
- @lock.release
157
- raise(ex)
158
- rescue IO::WaitReadable => ex
159
- @bytes.unshift(len)
160
- @lock.release
161
- raise Chan::WaitReadable, ex.message
162
- rescue Errno::EAGAIN => ex
163
- raise Chan::WaitLockable, ex.message
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
- # @return [Integer]
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
- # @endgroup
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. Waits indefinitely with no arguments.
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. Waits indefinitely with no arguments.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chan
4
- VERSION = "0.20.0"
4
+ VERSION = "0.22.0"
5
5
  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
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.20.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: 3.6.9
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: []