xchan.rb 0.19.0 → 0.20.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: a87a4e61f65ec4df37ecc2b09efd3d9abd4d39e57ac936db27e43ace4e99818d
4
- data.tar.gz: 1ac4be7c20abcdee66a441088e1a7bf67b92b96f63b16b3401227bb978afa6de
3
+ metadata.gz: 3faafc7100339e243744b476b137e7ee96591179027c862509006c60df6c7fe1
4
+ data.tar.gz: b7abc06e3a789f1f80a171e520c2cd3a64126c11e8bc01795bf90c4fcb5601bc
5
5
  SHA512:
6
- metadata.gz: fc46a04ae48123fbf24371bd3438013cfcb070e353993811d10b4ea9d8b63628b577bed465d441fab1d964a0dab4ec9a2040e293c59272faa6c2081819788383
7
- data.tar.gz: 267dd70488de1b49c345c49a87c5f28a7bdf0713c20fdeeba2efbef13e2d114960dc021e3b1ec77df90ad3c1189772e3075dff3ce0f2ebfac27b727acc4a56a8
6
+ metadata.gz: 1a14ea31ccd8ae96a56d4dfc667fe0141906a8d0123f3ba184e524ad07b4586b205501c870aec02f0af33d47d6ac8b788f023174afcddf8d6d1b90e853a3b40b
7
+ data.tar.gz: 98c9e23a2abd39852fdd315011c44385b6c57a1a9a000e0d9430580a36a3970091857482d7866c58c765015fce7cbb6523a9b3e411daa2c5520b79f911efaf05
data/README.md CHANGED
@@ -1,9 +1,29 @@
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
+
1
5
  ## About
2
6
 
3
- xchan.rb is an easy to use library for InterProcess
4
- Communication (IPC). The library provides a channel
7
+ xchan.rb is an easy to use, minimalist library for
8
+ InterProcess Communication (IPC). The library provides a channel
5
9
  that can help facilitate communication between Ruby
6
10
  processes who have a parent &lt;=&gt; child relationship.
11
+ A channel lock is provided by
12
+ [lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3) and a temporary, unlinked file to protect against race conditions
13
+ that can happen when multiple processes access the same channel
14
+ at the same time.
15
+
16
+ ## Features
17
+
18
+ * Minimalist Inter-Process Communication (IPC) for parent &lt;=&gt; child processes.
19
+ * Channel-based communication.
20
+ * Support for multiple serializers (`:marshal`, `:json`, `:yaml`) and raw string communication (`:pure`).
21
+ * Blocking (`#send`, `#recv`) and non-blocking (`#send_nonblock`, `#recv_nonblock`) operations.
22
+ * Built-in file-based locking ([lockf(3)](https://man.freebsd.org/cgi/man.cgi?query=lockf&sektion=3)) to prevent race conditions.
23
+ * Option to use a null lock for scenarios where locking is not needed.
24
+ * Access to underlying UNIX sockets for fine-grained control over socket options.
25
+ * Mac, BSD, and Linux support.
26
+ * Good docs.
7
27
 
8
28
  ## Examples
9
29
 
@@ -26,7 +46,7 @@ require "xchan"
26
46
  # Marshal as the serializer
27
47
  ch = xchan(:marshal)
28
48
  Process.wait fork { ch.send(5) }
29
- print "#{ch.recv} + 7 = 12", "\n"
49
+ puts "#{ch.recv} + 7 = 12"
30
50
  ch.close
31
51
 
32
52
  ##
@@ -40,7 +60,7 @@ ch.close
40
60
  The `ch.recv` method performs a blocking read. A read
41
61
  can block when a lock is held by another process, or
42
62
  when a read from
43
- [Chan::UNIXSocket#r](https://0x1eef.github,io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
63
+ [Chan::UNIXSocket#r](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
44
64
  blocks. The example performs a read that blocks until
45
65
  the parent process writes to the channel:
46
66
 
@@ -53,7 +73,7 @@ fork do
53
73
  print "Received a random number (child process): ", ch.recv, "\n"
54
74
  end
55
75
  sleep(1)
56
- print "Send a random number (from parent process)", "\n"
76
+ puts "Send a random number (from parent process)"
57
77
  ch.send(rand(21))
58
78
  ch.close
59
79
  Process.wait
@@ -69,7 +89,7 @@ The non-blocking counterpart to `#recv` is `#recv_nonblock`.
69
89
  The `#recv_nonblock` method raises `Chan::WaitLockable` when
70
90
  a read blocks because of a lock held by another process, and
71
91
  the method raises `Chan::WaitReadable` when a read from
72
- [Chan::UNIXSocket#r](https://0x1eef.github,io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
92
+ [Chan::UNIXSocket#r](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
73
93
  blocks:
74
94
 
75
95
  ```ruby
@@ -79,11 +99,12 @@ require "xchan"
79
99
  def read(ch)
80
100
  ch.recv_nonblock
81
101
  rescue Chan::WaitReadable
82
- print "Wait 1 second for channel to be readable", "\n"
102
+ puts "Wait 1 second for channel to be readable"
83
103
  ch.wait_readable(1)
84
104
  retry
85
105
  rescue Chan::WaitLockable
86
- sleep 0.01
106
+ puts "Wait 1 second for channel to be lockable"
107
+ ch.wait_lockable(1)
87
108
  retry
88
109
  end
89
110
  trap("SIGINT") { exit(1) }
@@ -102,7 +123,7 @@ read(xchan(:marshal))
102
123
  The `ch.send` method performs a blocking write.
103
124
  A write can block when a lock is held by another
104
125
  process, or when a write to
105
- [Chan::UNIXSocket#w](https://0x1eef.github,io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
126
+ [Chan::UNIXSocket#w](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
106
127
  blocks. The example fills the send buffer:
107
128
 
108
129
  ```ruby
@@ -123,7 +144,7 @@ The non-blocking counterpart to `#send` is
123
144
  `Chan::WaitLockable` when a write blocks because of
124
145
  a lock held by another process, and the method raises
125
146
  `Chan::WaitWritable` when a write to
126
- [Chan::UNIXSocket#w](https://0x1eef.github,io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
147
+ [Chan::UNIXSocket#w](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
127
148
  blocks. The example frees space on the send buffer:
128
149
 
129
150
  ```ruby
@@ -133,11 +154,11 @@ require "xchan"
133
154
  def send_nonblock(ch, buf)
134
155
  ch.send_nonblock(buf)
135
156
  rescue Chan::WaitWritable
136
- print "Blocked - free send buffer", "\n"
157
+ puts "Blocked - free send buffer"
137
158
  ch.recv
138
159
  retry
139
160
  rescue Chan::WaitLockable
140
- sleep 0.01
161
+ ch.wait_lockable
141
162
  retry
142
163
  end
143
164
 
@@ -166,6 +187,9 @@ processes:
166
187
  #!/usr/bin/env ruby
167
188
  require "xchan"
168
189
 
190
+ ##
191
+ # 'lock: :file' is added just for the example
192
+ # It is the default behavior, and not necessary
169
193
  ch = xchan(:marshal, lock: :file)
170
194
  5.times.map do
171
195
  fork do
@@ -179,7 +203,7 @@ end.each { Process.wait(_1) }
179
203
  The null lock is the same as using no lock at all. The null lock is
180
204
  implemented as a collection of no-op operations. The null lock is
181
205
  implemented in the
182
- [Chan::NullLock](https://0x1eef.github,io/x/xchan.rb/Chan/NullLock.html)
206
+ [Chan::NullLock](https://0x1eef.github.io/x/xchan.rb/Chan/NullLock.html)
183
207
  class, and in certain situations, it can be useful and preferable
184
208
  to using a file lock:
185
209
 
@@ -200,9 +224,9 @@ Process.wait
200
224
 
201
225
  A channel has one socket for read operations and another
202
226
  socket for write operations.
203
- [Chan::UNIXSocket#r](https://0x1eef.github,io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
227
+ [Chan::UNIXSocket#r](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#r-instance_method)
204
228
  returns the socket used for read operations, and
205
- [Chan::UNIXSocket#w](https://0x1eef.github,io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
229
+ [Chan::UNIXSocket#w](https://0x1eef.github.io/x/xchan.rb/Chan/UNIXSocket.html#w-instance_method)
206
230
  returns the socket used for write operations:
207
231
 
208
232
  ```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 .json], tmpdir:)
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)).tap { io.rewind }
84
+ io.write(serialize(bytes))
85
+ io.rewind
84
86
  end
85
87
 
86
88
  def serialize(bytes)
87
- JSON.dump(bytes)
89
+ bytes.pack("Q>*")
88
90
  end
89
91
 
90
92
  def deserialize(bytes)
91
- JSON.parse(bytes)
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 .json], tmpdir:)
16
- write(@io, {"bytes_read" => 0, "bytes_written" => 0})
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).fetch("bytes_written")
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).fetch("bytes_read")
31
+ bytes, _ = read(@io)
32
+ bytes
31
33
  end
32
34
 
33
35
  ##
34
- # @param [Hash] new_stat
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!(new_stat)
38
- stat = read(@io)
39
- new_stat.each { stat[_1.to_s] += _2 }
40
- write(@io, stat)
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, o)
58
+ def write(io, bytes_read, bytes_written)
59
+ io.rewind
46
60
  io.truncate(0)
47
- io.write(serialize(o)).tap { io.rewind }
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(bytes)
55
- JSON.dump(bytes)
69
+ def serialize(bytes_read, bytes_written)
70
+ [bytes_read, bytes_written].pack("Q>Q>")
56
71
  end
57
72
 
58
- def deserialize(bytes)
59
- JSON.parse(bytes)
73
+ def deserialize(payload)
74
+ payload.unpack("Q>Q>")
60
75
  end
61
76
  end
@@ -36,9 +36,9 @@ class Chan::NullLock
36
36
  end
37
37
 
38
38
  ##
39
- # @return [void]
40
- # This method always returns false
41
- def self.locked?
42
- false
39
+ # @return [true]
40
+ # Always returns true
41
+ def self.lockable?
42
+ true
43
43
  end
44
44
  end
@@ -61,8 +61,8 @@ class Chan::UNIXSocket
61
61
  # @return [void]
62
62
  def close
63
63
  @lock.lock
64
- raise IOError, "channel is closed" if closed?
65
- [@r, @w, @bytes, @lock].each(&:close)
64
+ raise IOError, "closed channel" if closed?
65
+ [@r, @w, @bytes, @counter, @lock].each(&:close)
66
66
  rescue IOError => ex
67
67
  @lock.release
68
68
  raise(ex)
@@ -222,22 +222,36 @@ class Chan::UNIXSocket
222
222
 
223
223
  ##
224
224
  # Waits for the channel to become readable
225
- # @param [Float, Integer, nil] s
225
+ # @param [Float, Integer, nil] timeout
226
226
  # The number of seconds to wait. Waits indefinitely with no arguments.
227
227
  # @return [Chan::UNIXSocket, nil]
228
228
  # Returns self when the channel is readable, otherwise returns nil
229
- def wait_readable(s = nil)
230
- @r.wait_readable(s) and self
229
+ def wait_readable(timeout = nil)
230
+ @r.wait_readable(timeout) and self
231
231
  end
232
232
 
233
233
  ##
234
234
  # Waits for the channel to become writable
235
- # @param [Float, Integer, nil] s
235
+ # @param [Float, Integer, nil] timeout
236
236
  # The number of seconds to wait. Waits indefinitely with no arguments.
237
237
  # @return [Chan::UNIXSocket, nil]
238
238
  # Returns self when the channel is writable, otherwise returns nil
239
- def wait_writable(s = nil)
240
- @w.wait_writable(s) and self
239
+ def wait_writable(timeout = nil)
240
+ @w.wait_writable(timeout) and self
241
+ end
242
+
243
+ ##
244
+ # Waits for the channel to become lockable
245
+ # @param [Float, Integer, nil] timeout
246
+ # The number of seconds to wait before timeout
247
+ # @return [Chan::UNIXSocket, nil]
248
+ def wait_lockable(timeout = nil)
249
+ start = (timeout ? gettime : nil)
250
+ loop do
251
+ break(nil) if start && (gettime - start) >= timeout
252
+ break(self) if @lock.lockable?
253
+ sleep 0.01
254
+ end
241
255
  end
242
256
 
243
257
  ##
@@ -259,4 +273,8 @@ class Chan::UNIXSocket
259
273
  def deserialize(str)
260
274
  @s.load(str)
261
275
  end
276
+
277
+ def gettime
278
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
279
+ end
262
280
  end
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.19.0"
4
+ VERSION = "0.20.0"
5
5
  end
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| Lock::File.new Chan.temporary_file(%w[xchan lock], tmpdir:) }
68
+ file: lambda { |tmpdir| Lockf.new Chan.temporary_file(%w[xchan lock], tmpdir:) }
69
69
  }
70
70
  end
71
71
  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", "~> 2.1"
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.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - '0x1eef'
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-04-09 00:00:00.000000000 Z
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: '2.1'
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: '2.1'
25
+ version: '3.0'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: test-unit
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -153,7 +152,6 @@ homepage: https://github.com/0x1eef/xchan.rb#readme
153
152
  licenses:
154
153
  - 0BSD
155
154
  metadata: {}
156
- post_install_message:
157
155
  rdoc_options: []
158
156
  require_paths:
159
157
  - lib
@@ -168,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
166
  - !ruby/object:Gem::Version
169
167
  version: '0'
170
168
  requirements: []
171
- rubygems_version: 3.5.23
172
- signing_key:
169
+ rubygems_version: 3.6.9
173
170
  specification_version: 4
174
171
  summary: An easy to use InterProcess Communication (IPC) library
175
172
  test_files: []