xchan.rb 0.18.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: 49c3ec314ad19f47d91dd30c854e94ad184fd70a0cb5ed7fd0139b506b295c6e
4
- data.tar.gz: 1ba90eb765464330c3c6c35385c6ce6732f0bd95cd21e85be236576b7a0ba7c5
3
+ metadata.gz: 3faafc7100339e243744b476b137e7ee96591179027c862509006c60df6c7fe1
4
+ data.tar.gz: b7abc06e3a789f1f80a171e520c2cd3a64126c11e8bc01795bf90c4fcb5601bc
5
5
  SHA512:
6
- metadata.gz: d29f66b50713d1277b25ad2900bbcd5a99ac1589a921e0b0a8ccd2028e534204ffc8a382b68f3ba48947aa2be8fda90843adda2fbdf133db3cf9fb230f2a5873
7
- data.tar.gz: 218b603663b0cac0af9c6780e942c74decb096045e9a7eef14daf5c6644286bd2ee8e1969445260952425bfad08b4bd2fb588a6a1d1f3be0b45d0913337575b5
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
 
@@ -19,13 +39,14 @@ serializers are available by default: `xchan(:marshal)`,
19
39
  `xchan(:json)`, and `xchan(:yaml)`.
20
40
 
21
41
  ```ruby
42
+ #!/usr/bin/env ruby
22
43
  require "xchan"
23
44
 
24
45
  ##
25
46
  # Marshal as the serializer
26
47
  ch = xchan(:marshal)
27
48
  Process.wait fork { ch.send(5) }
28
- print "#{ch.recv} + 7 = 12", "\n"
49
+ puts "#{ch.recv} + 7 = 12"
29
50
  ch.close
30
51
 
31
52
  ##
@@ -39,11 +60,12 @@ ch.close
39
60
  The `ch.recv` method performs a blocking read. A read
40
61
  can block when a lock is held by another process, or
41
62
  when a read from
42
- [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)
43
64
  blocks. The example performs a read that blocks until
44
65
  the parent process writes to the channel:
45
66
 
46
67
  ```ruby
68
+ #!/usr/bin/env ruby
47
69
  require "xchan"
48
70
 
49
71
  ch = xchan(:marshal)
@@ -51,7 +73,7 @@ fork do
51
73
  print "Received a random number (child process): ", ch.recv, "\n"
52
74
  end
53
75
  sleep(1)
54
- print "Send a random number (from parent process)", "\n"
76
+ puts "Send a random number (from parent process)"
55
77
  ch.send(rand(21))
56
78
  ch.close
57
79
  Process.wait
@@ -67,20 +89,22 @@ The non-blocking counterpart to `#recv` is `#recv_nonblock`.
67
89
  The `#recv_nonblock` method raises `Chan::WaitLockable` when
68
90
  a read blocks because of a lock held by another process, and
69
91
  the method raises `Chan::WaitReadable` when a read from
70
- [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)
71
93
  blocks:
72
94
 
73
95
  ```ruby
96
+ #!/usr/bin/env ruby
74
97
  require "xchan"
75
98
 
76
99
  def read(ch)
77
100
  ch.recv_nonblock
78
101
  rescue Chan::WaitReadable
79
- print "Wait 1 second for channel to be readable", "\n"
102
+ puts "Wait 1 second for channel to be readable"
80
103
  ch.wait_readable(1)
81
104
  retry
82
105
  rescue Chan::WaitLockable
83
- sleep 0.01
106
+ puts "Wait 1 second for channel to be lockable"
107
+ ch.wait_lockable(1)
84
108
  retry
85
109
  end
86
110
  trap("SIGINT") { exit(1) }
@@ -99,13 +123,14 @@ read(xchan(:marshal))
99
123
  The `ch.send` method performs a blocking write.
100
124
  A write can block when a lock is held by another
101
125
  process, or when a write to
102
- [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)
103
127
  blocks. The example fills the send buffer:
104
128
 
105
129
  ```ruby
130
+ #!/usr/bin/env ruby
106
131
  require "xchan"
107
132
 
108
- ch = xchan(:marshal, sock_type: Socket::SOCK_STREAM)
133
+ ch = xchan(:marshal, sock: Socket::SOCK_STREAM)
109
134
  sndbuf = ch.w.getsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF)
110
135
  while ch.bytes_sent <= sndbuf.int
111
136
  ch.send(1)
@@ -119,24 +144,25 @@ The non-blocking counterpart to `#send` is
119
144
  `Chan::WaitLockable` when a write blocks because of
120
145
  a lock held by another process, and the method raises
121
146
  `Chan::WaitWritable` when a write to
122
- [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)
123
148
  blocks. The example frees space on the send buffer:
124
149
 
125
150
  ```ruby
151
+ #!/usr/bin/env ruby
126
152
  require "xchan"
127
153
 
128
154
  def send_nonblock(ch, buf)
129
155
  ch.send_nonblock(buf)
130
156
  rescue Chan::WaitWritable
131
- print "Blocked - free send buffer", "\n"
157
+ puts "Blocked - free send buffer"
132
158
  ch.recv
133
159
  retry
134
160
  rescue Chan::WaitLockable
135
- sleep 0.01
161
+ ch.wait_lockable
136
162
  retry
137
163
  end
138
164
 
139
- ch = xchan(:marshal, sock_type: Socket::SOCK_STREAM)
165
+ ch = xchan(:marshal, sock: Socket::SOCK_STREAM)
140
166
  sndbuf = ch.w.getsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF)
141
167
  while ch.bytes_sent <= sndbuf.int
142
168
  send_nonblock(ch, 1)
@@ -146,18 +172,65 @@ end
146
172
  # Blocked - free send buffer
147
173
  ```
148
174
 
175
+ ### Lock
176
+
177
+ #### File
178
+
179
+ The default lock for a channel is a file lock. The locking mechanism is
180
+ implemented with the
181
+ [lockf](https://man.freebsd.org/cgi/man.cgi?query=lockf&apropos=0&sektion=3&manpath=FreeBSD+14.2-RELEASE+and+Ports&arch=default&format=html)
182
+ function from the C standard library. Nothing special has to be done to
183
+ use it, and it allows a channel to be safely accessed across multiple
184
+ processes:
185
+
186
+ ```ruby
187
+ #!/usr/bin/env ruby
188
+ require "xchan"
189
+
190
+ ##
191
+ # 'lock: :file' is added just for the example
192
+ # It is the default behavior, and not necessary
193
+ ch = xchan(:marshal, lock: :file)
194
+ 5.times.map do
195
+ fork do
196
+ ch.send(5)
197
+ end
198
+ end.each { Process.wait(_1) }
199
+ ```
200
+
201
+ #### Null
202
+
203
+ The null lock is the same as using no lock at all. The null lock is
204
+ implemented as a collection of no-op operations. The null lock is
205
+ implemented in the
206
+ [Chan::NullLock](https://0x1eef.github.io/x/xchan.rb/Chan/NullLock.html)
207
+ class, and in certain situations, it can be useful and preferable
208
+ to using a file lock:
209
+
210
+ ```ruby
211
+ #!/usr/bin/env ruby
212
+ require "xchan"
213
+
214
+ ch = xchan(:marshal, lock: :null)
215
+ fork do
216
+ ch.send(5)
217
+ end
218
+ Process.wait
219
+ ```
220
+
149
221
  ### Socket
150
222
 
151
223
  #### Options
152
224
 
153
225
  A channel has one socket for read operations and another
154
226
  socket for write operations.
155
- [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)
156
228
  returns the socket used for read operations, and
157
- [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)
158
230
  returns the socket used for write operations:
159
231
 
160
232
  ```ruby
233
+ #!/usr/bin/env ruby
161
234
  require "xchan"
162
235
  ch = xchan(:marshal)
163
236
 
@@ -189,11 +262,11 @@ xchan.rb can be installed via rubygems.org:
189
262
 
190
263
  ## Sources
191
264
 
192
- * [GitHub](https://github.com/0x1eef/xchan.rb#readme)
193
- * [GitLab](https://gitlab.com/0x1eef/xchan.rb#about)
265
+ * [github.com/@0x1eef](https://github.com/0x1eef/xchan.rb#readme)
266
+ * [gitlab.com/@0x1eef](https://gitlab.com/0x1eef/xchan.rb#about)
194
267
 
195
268
  ## License
196
269
 
197
270
  [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
198
271
  <br>
199
- See [LICENSE](./LICENSE)
272
+ See [share/xchan.rb/LICENSE](./share/xchan.rb/LICENSE)
data/lib/xchan/bytes.rb CHANGED
@@ -7,26 +7,23 @@
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
  ##
14
13
  # @param [String] tmpdir
15
14
  # Directory where temporary files are stored
16
- #
17
15
  # @return [Chan::Bytes]
18
16
  def initialize(tmpdir)
19
- @io = Chan.temporary_file(%w[bytes .json], tmpdir:)
17
+ @io = Chan.temporary_file(%w[bytes .bin], tmpdir:)
18
+ @io.binmode
20
19
  @io.sync = true
21
20
  write(@io, [])
22
21
  end
23
22
 
24
23
  ##
25
24
  # Adds a count to the start of the collection
26
- #
27
25
  # @param [Integer] len
28
26
  # The bytesize of an object
29
- #
30
27
  # @return [void]
31
28
  def unshift(len)
32
29
  return 0 if len.nil? || len.zero?
@@ -38,10 +35,8 @@ class Chan::Bytes
38
35
 
39
36
  ##
40
37
  # Adds a count to the end of the collection
41
- #
42
38
  # @param [Integer] len
43
39
  # The bytesize of an object
44
- #
45
40
  # @return [void]
46
41
  def push(len)
47
42
  return 0 if len.nil? || len.zero?
@@ -53,7 +48,6 @@ class Chan::Bytes
53
48
 
54
49
  ##
55
50
  # Removes a count from the start of the collection
56
- #
57
51
  # @return [Integer]
58
52
  # Returns the removed byte count
59
53
  def shift
@@ -73,7 +67,6 @@ class Chan::Bytes
73
67
 
74
68
  ##
75
69
  # Close the underlying IO
76
- #
77
70
  # @return [void]
78
71
  def close
79
72
  @io.close
@@ -86,15 +79,17 @@ class Chan::Bytes
86
79
  end
87
80
 
88
81
  def write(io, bytes)
82
+ io.rewind
89
83
  io.truncate(0)
90
- io.write(serialize(bytes)).tap { io.rewind }
84
+ io.write(serialize(bytes))
85
+ io.rewind
91
86
  end
92
87
 
93
88
  def serialize(bytes)
94
- JSON.dump(bytes)
89
+ bytes.pack("Q>*")
95
90
  end
96
91
 
97
92
  def deserialize(bytes)
98
- JSON.parse(bytes)
93
+ bytes.unpack("Q>*")
99
94
  end
100
95
  end
data/lib/xchan/counter.rb CHANGED
@@ -5,58 +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
- #
14
11
  # @return [Chan::Counter]
15
12
  def initialize(tmpdir)
16
- @io = Chan.temporary_file(%w[counter .json], tmpdir:)
17
- 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)
18
17
  end
19
18
 
20
19
  ##
21
20
  # @return [Integer]
22
21
  # Returns the number of bytes written to a channel
23
22
  def bytes_written
24
- read(@io).fetch("bytes_written")
23
+ _, bytes = read(@io)
24
+ bytes
25
25
  end
26
26
 
27
27
  ##
28
28
  # @return [Integer]
29
29
  # Returns the number of bytes read from a channel
30
30
  def bytes_read
31
- read(@io).fetch("bytes_read")
31
+ bytes, _ = read(@io)
32
+ bytes
32
33
  end
33
34
 
34
35
  ##
35
- # @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
36
40
  # @return [void]
37
41
  # @private
38
- def increment!(new_stat)
39
- stat = read(@io)
40
- new_stat.each { stat[_1.to_s] += _2 }
41
- 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
42
54
  end
43
55
 
44
56
  private
45
57
 
46
- def write(io, o)
58
+ def write(io, bytes_read, bytes_written)
59
+ io.rewind
47
60
  io.truncate(0)
48
- io.write(serialize(o)).tap { io.rewind }
61
+ io.write(serialize(bytes_read, bytes_written))
62
+ io.rewind
49
63
  end
50
64
 
51
65
  def read(io)
52
66
  deserialize(io.read).tap { io.rewind }
53
67
  end
54
68
 
55
- def serialize(bytes)
56
- JSON.dump(bytes)
69
+ def serialize(bytes_read, bytes_written)
70
+ [bytes_read, bytes_written].pack("Q>Q>")
57
71
  end
58
72
 
59
- def deserialize(bytes)
60
- JSON.parse(bytes)
73
+ def deserialize(payload)
74
+ payload.unpack("Q>Q>")
61
75
  end
62
76
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # {Chan::NullLock Chan::NullLock} is a no-op lock that can be used
5
+ # instead of the standard file lock when a lock is not needed
6
+ #
7
+ # @example
8
+ # ch = xchan(:marshal, lock: Chan::NullLock)
9
+ # ch.send([1,2,3])
10
+ # # ditto
11
+ # ch = xchan(:marshal, lock: :null)
12
+ # ch.send([1,2,3])
13
+ class Chan::NullLock
14
+ ##
15
+ # @return [void]
16
+ # This method is a no-op
17
+ def self.lock
18
+ end
19
+
20
+ ##
21
+ # @return [void]
22
+ # This method is a no-op
23
+ def self.lock_nonblock
24
+ end
25
+
26
+ ##
27
+ # @return [void]
28
+ # This method is a no-op
29
+ def self.release
30
+ end
31
+
32
+ ##
33
+ # @return [void]
34
+ # This method is a no-op
35
+ def self.close
36
+ end
37
+
38
+ ##
39
+ # @return [true]
40
+ # Always returns true
41
+ def self.lockable?
42
+ true
43
+ end
44
+ end
@@ -242,8 +242,8 @@ class Chan::Tempfile < DelegateClass(File)
242
242
  # ensure
243
243
  # f.close
244
244
  # end
245
- def self.open(*args, **kwargs)
246
- tempfile = new(*args, **kwargs)
245
+ def self.open(*, **)
246
+ tempfile = new(*, **)
247
247
 
248
248
  if block_given?
249
249
  begin
@@ -29,24 +29,22 @@ class Chan::UNIXSocket
29
29
  # ch.send([1,2,3])
30
30
  # ch.recv.pop # => 3
31
31
  # ch.close
32
- #
33
- # @param [Symbol, <#dump, #load>] s
32
+ # @param [Symbol, <#dump, #load>] serializer
34
33
  # The name of a serializer
35
- #
36
- # @param [Integer] sock_type
34
+ # @param [Integer] sock
37
35
  # Type of socket (eg `Socket::SOCK_STREAM`)
38
- #
39
36
  # @param [String] tmpdir
40
37
  # Directory where temporary files can be stored
41
- #
38
+ # @param [Symbol, <Lock::File, Chan::NullLock>] lock
39
+ # The name of a lock, or an instance of `Lock::File`, or {Chan::NullLock Chan::NullLock}
42
40
  # @return [Chan::UNIXSocket]
43
41
  # Returns an instance of {Chan::UNIXSocket Chan::UNIXSocket}
44
- def initialize(s, sock_type: Socket::SOCK_DGRAM, tmpdir: Dir.tmpdir)
45
- @s = Chan.serializers[s]&.call || s
46
- @r, @w = ::UNIXSocket.pair(sock_type)
42
+ def initialize(serializer, sock: Socket::SOCK_DGRAM, tmpdir: Dir.tmpdir, lock: :file)
43
+ @s = Chan.serializers[serializer]&.call || serializer
44
+ @r, @w = ::UNIXSocket.pair(sock)
47
45
  @bytes = Chan::Bytes.new(tmpdir)
48
46
  @counter = Chan::Counter.new(tmpdir)
49
- @lockf = Lock::File.new Chan.temporary_file(%w[xchan .lock], tmpdir:)
47
+ @lock = Chan.locks[lock]&.call(tmpdir) || lock
50
48
  end
51
49
 
52
50
  ##
@@ -58,17 +56,15 @@ class Chan::UNIXSocket
58
56
 
59
57
  ##
60
58
  # Closes the channel
61
- #
62
59
  # @raise [IOError]
63
60
  # When the channel is closed
64
- #
65
61
  # @return [void]
66
62
  def close
67
- @lockf.lock
68
- raise IOError, "channel is closed" if closed?
69
- [@r, @w, @bytes, @lockf].each(&:close)
63
+ @lock.lock
64
+ raise IOError, "closed channel" if closed?
65
+ [@r, @w, @bytes, @counter, @lock].each(&:close)
70
66
  rescue IOError => ex
71
- @lockf.release
67
+ @lock.release
72
68
  raise(ex)
73
69
  end
74
70
 
@@ -77,13 +73,10 @@ class Chan::UNIXSocket
77
73
 
78
74
  ##
79
75
  # Performs a blocking write
80
- #
81
76
  # @param [Object] object
82
77
  # An object
83
- #
84
78
  # @raise [IOError]
85
79
  # When the channel is closed
86
- #
87
80
  # @return [Object]
88
81
  # Returns the number of bytes written to the channel
89
82
  def send(object)
@@ -95,30 +88,25 @@ class Chan::UNIXSocket
95
88
 
96
89
  ##
97
90
  # Performs a non-blocking write
98
- #
99
91
  # @param [Object] object
100
92
  # An object
101
- #
102
93
  # @raise [IOError]
103
94
  # When the channel is closed
104
- #
105
95
  # @raise [Chan::WaitWritable]
106
96
  # When a write to {#w} blocks
107
- #
108
97
  # @raise [Chan::WaitLockable]
109
98
  # When a write blocks because of a lock held by another process
110
- #
111
99
  # @return [Integer, nil]
112
100
  # Returns the number of bytes written to the channel
113
101
  def send_nonblock(object)
114
- @lockf.lock_nonblock
102
+ @lock.lock_nonblock
115
103
  raise IOError, "channel closed" if closed?
116
104
  len = @w.write_nonblock(serialize(object))
117
105
  @bytes.push(len)
118
106
  @counter.increment!(bytes_written: len)
119
- len.tap { @lockf.release }
107
+ len.tap { @lock.release }
120
108
  rescue IOError, IO::WaitWritable, Errno::ENOBUFS => ex
121
- @lockf.release
109
+ @lock.release
122
110
  raise Chan::WaitWritable, ex.message
123
111
  rescue Errno::EWOULDBLOCK => ex
124
112
  raise Chan::WaitLockable, ex.message
@@ -133,10 +121,8 @@ class Chan::UNIXSocket
133
121
 
134
122
  ##
135
123
  # Performs a blocking read
136
- #
137
124
  # @raise [IOError]
138
125
  # When the channel is closed
139
- #
140
126
  # @return [Object]
141
127
  # Returns an object from the channel
142
128
  def recv
@@ -151,31 +137,27 @@ class Chan::UNIXSocket
151
137
 
152
138
  ##
153
139
  # Performs a non-blocking read
154
- #
155
140
  # @raise [IOError]
156
141
  # When the channel is closed
157
- #
158
142
  # @raise [Chan::WaitReadable]
159
143
  # When a read from {#r} blocks
160
- #
161
144
  # @raise [Chan::WaitLockable]
162
145
  # When a read blocks because of a lock held by another process
163
- #
164
146
  # @return [Object]
165
147
  # Returns an object from the channel
166
148
  def recv_nonblock
167
- @lockf.lock_nonblock
149
+ @lock.lock_nonblock
168
150
  raise IOError, "closed channel" if closed?
169
151
  len = @bytes.shift
170
152
  obj = deserialize(@r.read_nonblock(len.zero? ? 1 : len))
171
153
  @counter.increment!(bytes_read: len)
172
- obj.tap { @lockf.release }
154
+ obj.tap { @lock.release }
173
155
  rescue IOError => ex
174
- @lockf.release
156
+ @lock.release
175
157
  raise(ex)
176
158
  rescue IO::WaitReadable => ex
177
159
  @bytes.unshift(len)
178
- @lockf.release
160
+ @lock.release
179
161
  raise Chan::WaitReadable, ex.message
180
162
  rescue Errno::EAGAIN => ex
181
163
  raise Chan::WaitLockable, ex.message
@@ -190,7 +172,6 @@ class Chan::UNIXSocket
190
172
  # ch = xchan(:pure)
191
173
  # 1.upto(4) { ch.send(_1) }
192
174
  # ch.to_a.last # => "4"
193
- #
194
175
  # @return [Array<Object>]
195
176
  # Returns the contents of the channel
196
177
  def to_a
@@ -241,26 +222,36 @@ class Chan::UNIXSocket
241
222
 
242
223
  ##
243
224
  # Waits for the channel to become readable
244
- #
245
- # @param [Float, Integer, nil] s
225
+ # @param [Float, Integer, nil] timeout
246
226
  # The number of seconds to wait. Waits indefinitely with no arguments.
247
- #
248
227
  # @return [Chan::UNIXSocket, nil]
249
228
  # Returns self when the channel is readable, otherwise returns nil
250
- def wait_readable(s = nil)
251
- @r.wait_readable(s) and self
229
+ def wait_readable(timeout = nil)
230
+ @r.wait_readable(timeout) and self
252
231
  end
253
232
 
254
233
  ##
255
234
  # Waits for the channel to become writable
256
- #
257
- # @param [Float, Integer, nil] s
235
+ # @param [Float, Integer, nil] timeout
258
236
  # The number of seconds to wait. Waits indefinitely with no arguments.
259
- #
260
237
  # @return [Chan::UNIXSocket, nil]
261
238
  # Returns self when the channel is writable, otherwise returns nil
262
- def wait_writable(s = nil)
263
- @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
264
255
  end
265
256
 
266
257
  ##
@@ -269,10 +260,10 @@ class Chan::UNIXSocket
269
260
  private
270
261
 
271
262
  def lock
272
- @lockf.lock
263
+ @lock.lock
273
264
  yield
274
265
  ensure
275
- @lockf.release
266
+ @lock.release
276
267
  end
277
268
 
278
269
  def serialize(obj)
@@ -282,4 +273,8 @@ class Chan::UNIXSocket
282
273
  def deserialize(str)
283
274
  @s.load(str)
284
275
  end
276
+
277
+ def gettime
278
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
279
+ end
285
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.18.0"
4
+ VERSION = "0.20.0"
5
5
  end
data/lib/xchan.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Chan
4
4
  require_relative "xchan/version"
5
5
  require_relative "xchan/unix_socket"
6
+ require_relative "xchan/null_lock"
6
7
  require_relative "xchan/tempfile"
7
8
 
8
9
  WaitReadable = Class.new(IO::EAGAINWaitReadable)
@@ -32,10 +33,8 @@ module Chan
32
33
  #
33
34
  # @param [String] basename
34
35
  # Basename of the temporary file
35
- #
36
36
  # @param [String] tmpdir
37
37
  # Parent directory of the temporary file
38
- #
39
38
  # @return [Chan::Tempfile]
40
39
  # Returns an instance of {Chan::Tempfile Chan::Tempfile}
41
40
  def self.temporary_file(basename, tmpdir: Dir.tmpdir)
@@ -59,6 +58,16 @@ module Chan
59
58
  }
60
59
  }
61
60
  end
61
+
62
+ ##
63
+ # @return [Hash<Symbol, Proc>]
64
+ # Returns the default locks
65
+ def self.locks
66
+ {
67
+ null: lambda { |_tmpdir| Chan::NullLock },
68
+ file: lambda { |tmpdir| Lockf.new Chan.temporary_file(%w[xchan lock], tmpdir:) }
69
+ }
70
+ end
62
71
  end
63
72
 
64
73
  module Kernel
@@ -68,10 +77,10 @@ module Kernel
68
77
  # ch.send([1,2,3])
69
78
  # ch.recv.pop # => 3
70
79
  # ch.close
71
- #
72
- # @param s (see Chan::UNIXSocket#initialize)
73
- # @param sock_type (see Chan::UNIXSocket#initialize)
80
+ # @param serializer (see Chan::UNIXSocket#initialize)
81
+ # @param sock (see Chan::UNIXSocket#initialize)
74
82
  # @param tmpdir (see Chan::UNIXSocket#initialize)
83
+ # @param lock (see Chan::UNIXSocket#initialize)
75
84
  # @return (see Chan::UNIXSocket#initialize)
76
85
  def xchan(s, ...)
77
86
  Chan::UNIXSocket.new(s, ...)
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env ruby
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative "../setup"
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env ruby
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative "../setup"
@@ -1,8 +1,8 @@
1
+ #!/usr/bin/env ruby
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative "../setup"
4
5
  require "xchan"
5
- require "xchan"
6
6
 
7
7
  ##
8
8
  # Marshal as the serializer
@@ -1,3 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
1
4
  require "xchan"
2
5
  ch = xchan(:marshal)
3
6
 
@@ -1,9 +1,10 @@
1
+ #!/usr/bin/env ruby
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative "../setup"
4
5
  require "xchan"
5
6
 
6
- ch = xchan(:marshal, sock_type: Socket::SOCK_STREAM)
7
+ ch = xchan(:marshal, sock: Socket::SOCK_STREAM)
7
8
  sndbuf = ch.w.getsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF)
8
9
  while ch.bytes_sent <= sndbuf.int
9
10
  ch.send(1)
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env ruby
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative "../setup"
@@ -14,7 +15,7 @@ rescue Chan::WaitLockable
14
15
  retry
15
16
  end
16
17
 
17
- ch = xchan(:marshal, sock_type: Socket::SOCK_STREAM)
18
+ ch = xchan(:marshal, sock: Socket::SOCK_STREAM)
18
19
  sndbuf = ch.w.getsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF)
19
20
  while ch.bytes_sent <= sndbuf.int
20
21
  send_nonblock(ch, 1)
data/test/readme_test.rb CHANGED
@@ -27,10 +27,10 @@ class Chan::ReadmeTest < Test::Unit::TestCase
27
27
  end
28
28
 
29
29
  def test_socket_2_options
30
- r = 'The read buffer can contain a maximum of: \d{1,6} bytes.\s*' \
31
- 'The maximum size of a single message is: \d{1,6} bytes.\s*'
30
+ r = 'The read buffer can contain a maximum of: \d{1,7} bytes.\s*' \
31
+ 'The maximum size of a single message is: \d{1,7} bytes.\s*'
32
32
  assert_match Regexp.new(r),
33
- cmd("ruby", readme_example("socket/2_options.rb"))
33
+ cmd("ruby", readme_example("socket/1_options.rb"))
34
34
  .stdout
35
35
  .tr("\n", " ")
36
36
  end
data/test/xchan_test.rb CHANGED
@@ -84,7 +84,7 @@ class Chan::RecvNonBlockTest < Chan::Test
84
84
  end
85
85
 
86
86
  def test_recv_nonblock_with_a_lock
87
- ch.instance_variable_get(:@lockf).lock
87
+ ch.instance_variable_get(:@lock).lock
88
88
  pid = fork do
89
89
  ch.recv_nonblock
90
90
  exit(1)
@@ -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
@@ -8,15 +8,22 @@ Gem::Specification.new do |gem|
8
8
  gem.homepage = "https://github.com/0x1eef/xchan.rb#readme"
9
9
  gem.version = Chan::VERSION
10
10
  gem.licenses = ["0BSD"]
11
- gem.files = `git ls-files`.split($/)
11
+ gem.files = Dir[
12
+ "README.md", "LICENSE",
13
+ "share/xchan.rb/**/*.rb",
14
+ "lib/*.rb", "lib/**/*.rb",
15
+ "test/*.rb", "test/**/*.rb",
16
+ "xchan.rb.gemspec"
17
+ ]
12
18
  gem.require_paths = ["lib"]
13
19
  gem.summary = "An easy to use InterProcess Communication (IPC) library"
14
20
  gem.description = gem.summary
15
- gem.add_runtime_dependency "lockf.rb", "~> 2.1"
21
+ gem.add_runtime_dependency "lockf.rb", "~> 3.0"
16
22
  gem.add_development_dependency "test-unit", "~> 3.5.7"
17
23
  gem.add_development_dependency "yard", "~> 0.9"
18
- gem.add_development_dependency "redcarpet", "~> 3.5"
24
+ gem.add_development_dependency "kramdown", "~> 2.5"
19
25
  gem.add_development_dependency "standard", "~> 1.13"
20
26
  gem.add_development_dependency "test-cmd.rb", "~> 0.12.4"
21
27
  gem.add_development_dependency "rake", "~> 13.1"
28
+ gem.add_development_dependency "irb", "~> 1.14"
22
29
  end
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.18.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: 2024-06-30 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
@@ -53,19 +52,19 @@ dependencies:
53
52
  - !ruby/object:Gem::Version
54
53
  version: '0.9'
55
54
  - !ruby/object:Gem::Dependency
56
- name: redcarpet
55
+ name: kramdown
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
58
  - - "~>"
60
59
  - !ruby/object:Gem::Version
61
- version: '3.5'
60
+ version: '2.5'
62
61
  type: :development
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
65
  - - "~>"
67
66
  - !ruby/object:Gem::Version
68
- version: '3.5'
67
+ version: '2.5'
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: standard
71
70
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +107,20 @@ dependencies:
108
107
  - - "~>"
109
108
  - !ruby/object:Gem::Version
110
109
  version: '13.1'
110
+ - !ruby/object:Gem::Dependency
111
+ name: irb
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.14'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.14'
111
124
  description: An easy to use InterProcess Communication (IPC) library
112
125
  email:
113
126
  - 0x1eef@protonmail.com
@@ -115,18 +128,11 @@ executables: []
115
128
  extensions: []
116
129
  extra_rdoc_files: []
117
130
  files:
118
- - ".github/workflows/tests.yml"
119
- - ".gitignore"
120
- - ".projectile"
121
- - ".rubocop.yml"
122
- - ".yardopts"
123
- - Gemfile
124
- - LICENSE
125
131
  - README.md
126
- - Rakefile.rb
127
132
  - lib/xchan.rb
128
133
  - lib/xchan/bytes.rb
129
134
  - lib/xchan/counter.rb
135
+ - lib/xchan/null_lock.rb
130
136
  - lib/xchan/tempfile.rb
131
137
  - lib/xchan/unix_socket.rb
132
138
  - lib/xchan/version.rb
@@ -134,7 +140,7 @@ files:
134
140
  - share/xchan.rb/examples/read_operations/2_nonblocking_read.rb
135
141
  - share/xchan.rb/examples/serialization/1_serializers.rb
136
142
  - share/xchan.rb/examples/setup.rb
137
- - share/xchan.rb/examples/socket/2_options.rb
143
+ - share/xchan.rb/examples/socket/1_options.rb
138
144
  - share/xchan.rb/examples/stress_tests/1_parallel_access_stress_test.rb
139
145
  - share/xchan.rb/examples/write_operations/1_blocking_write.rb
140
146
  - share/xchan.rb/examples/write_operations/2_nonblocking_write.rb
@@ -146,7 +152,6 @@ homepage: https://github.com/0x1eef/xchan.rb#readme
146
152
  licenses:
147
153
  - 0BSD
148
154
  metadata: {}
149
- post_install_message:
150
155
  rdoc_options: []
151
156
  require_paths:
152
157
  - lib
@@ -161,8 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
166
  - !ruby/object:Gem::Version
162
167
  version: '0'
163
168
  requirements: []
164
- rubygems_version: 3.5.11
165
- signing_key:
169
+ rubygems_version: 3.6.9
166
170
  specification_version: 4
167
171
  summary: An easy to use InterProcess Communication (IPC) library
168
172
  test_files: []
@@ -1,26 +0,0 @@
1
- name: xchan.rb
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- specs:
11
- strategy:
12
- fail-fast: false
13
- matrix:
14
- os: [ubuntu-latest, macos-latest]
15
- ruby: [3.2, 3.3]
16
- runs-on: ${{ matrix.os }}
17
- steps:
18
- - uses: actions/checkout@v2
19
- - uses: ruby/setup-ruby@v1
20
- with:
21
- ruby-version: ${{ matrix.ruby }}
22
- - run: bundle install
23
- - run: SERIALIZER=marshal; for t in *_test.rb; do ruby test/${t}; done
24
- - run: SERIALIZER=json; for t in *_test.rb; do ruby test/${t}; done
25
- - run: SERIALIZER=yaml; for t in *_test.rb; do ruby test/${t}; done
26
- - run: SERIALIZER=pure; for t in *_test.rb; do ruby test/${t}; done
data/.gitignore DELETED
@@ -1,8 +0,0 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- .yardoc/
5
- .dev/
6
- doc/
7
- pkg/
8
- .gems/
data/.projectile DELETED
@@ -1,5 +0,0 @@
1
- +/
2
- +/.github/
3
- -/doc/
4
- -/.yardoc/
5
- -/.gems/
data/.rubocop.yml DELETED
@@ -1,34 +0,0 @@
1
- ##
2
- # Plugins
3
- require:
4
- - standard
5
-
6
- ##
7
- # Defaults: standard-rb
8
- inherit_gem:
9
- standard: config/base.yml
10
-
11
- ##
12
- # All cops
13
- AllCops:
14
- TargetRubyVersion: 3.2
15
- Include:
16
- - lib/*.rb
17
- - lib/**/*.rb
18
- - test/*_test.rb
19
-
20
- ##
21
- # Enabled
22
- Style/FrozenStringLiteralComment:
23
- Enabled: true
24
-
25
- ##
26
- # Disabled
27
- Layout/ArgumentAlignment:
28
- Enabled: false
29
- Layout/MultilineMethodCallIndentation:
30
- Enabled: false
31
- Layout/EmptyLineBetweenDefs:
32
- Enabled: false
33
- Style/TrivialAccessors:
34
- Enabled: false
data/.yardopts DELETED
@@ -1,4 +0,0 @@
1
- -m markdown -M redcarpet --no-private
2
- -
3
- README.md
4
- LICENSE
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
- gemspec
data/LICENSE DELETED
@@ -1,15 +0,0 @@
1
- Copyright (C) 2023 by 0x1eef <0x1eef@protonmail.com>
2
-
3
- Permission to use, copy, modify, and/or distribute this
4
- software for any purpose with or without fee is hereby
5
- granted.
6
-
7
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS
8
- ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
9
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
- EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
12
- RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
14
- ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
15
- OF THIS SOFTWARE.
data/Rakefile.rb DELETED
@@ -1,9 +0,0 @@
1
- require "rake/testtask"
2
- require "bundler/setup"
3
-
4
- Rake::TestTask.new do |t|
5
- t.test_files = FileList['test/*_test.rb']
6
- t.verbose = true
7
- t.warning = false
8
- end
9
- task default: :test