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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3faafc7100339e243744b476b137e7ee96591179027c862509006c60df6c7fe1
4
- data.tar.gz: b7abc06e3a789f1f80a171e520c2cd3a64126c11e8bc01795bf90c4fcb5601bc
3
+ metadata.gz: e8936112cb188471884a6fa66ee0db2b61d5229ac75acb8953c78e5b76587bc8
4
+ data.tar.gz: e76eb21f0f7f2a11e465ee3d4834b3abd48b54269949ee3623e2b2c2bdc1a17c
5
5
  SHA512:
6
- metadata.gz: 1a14ea31ccd8ae96a56d4dfc667fe0141906a8d0123f3ba184e524ad07b4586b205501c870aec02f0af33d47d6ac8b788f023174afcddf8d6d1b90e853a3b40b
7
- data.tar.gz: 98c9e23a2abd39852fdd315011c44385b6c57a1a9a000e0d9430580a36a3970091857482d7866c58c765015fce7cbb6523a9b3e411daa2c5520b79f911efaf05
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, 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,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, Chan::WaitLockable
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
- @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
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
- @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
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. Waits indefinitely with no arguments.
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. Waits indefinitely with no arguments.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chan
4
- VERSION = "0.20.0"
4
+ VERSION = "0.21.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.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