xchan.rb 0.16.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f17c01d5c34d8de2176fa5241ca08bc985798d6a3d7d6b4fa355cd0f2cc6e739
4
+ data.tar.gz: 0f1908263e8a272e32d2ec6e8026c21e836c1da92ee2ed12a0a9949e69ed1d46
5
+ SHA512:
6
+ metadata.gz: ac3779d634423270951d4c2d4818c021a856f066837f3b2015365d50af390cb5ccc2453475773f4ee0ed13ce1c19a30787e873ee43d84dc7d1ebd7f744e7a3f9
7
+ data.tar.gz: d7fccca83af9d0d73302b17b50d7b03843ce1c7d599f711e1a7f5b9252d1a8e9f40f7919134ffb25fb88007ee2578b0a9e7049a51b29c3cb0440df66a5ea0d09
@@ -0,0 +1,26 @@
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.1, 3.2]
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 bundle exec rake
24
+ - run: SERIALIZER=json bundle exec rake
25
+ - run: SERIALIZER=yaml bundle exec rake
26
+ - run: SERIALIZER=plain bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .localgems/
3
+ .bundle
4
+ Gemfile.lock
5
+ .yardoc/
6
+ .dev/
7
+ doc/
8
+ pkg/
9
+ .gems/
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,12 @@
1
+ stages:
2
+ - test
3
+
4
+ test-ruby32:
5
+ stage: test
6
+ image: ruby:3.2.0
7
+ script:
8
+ - bundle install
9
+ - SERIALIZER=marshal bundle exec rake
10
+ - SERIALIZER=json bundle exec rake
11
+ - SERIALIZER=yaml bundle exec rake
12
+ - SERIALIZER=plain bundle exec rake
data/.projectile ADDED
@@ -0,0 +1 @@
1
+ +.
data/.rubocop.yml ADDED
@@ -0,0 +1,34 @@
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 ADDED
@@ -0,0 +1,4 @@
1
+ -m markdown -M redcarpet --no-private
2
+ -
3
+ README.md
4
+ LICENSE
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gem "lockf.rb", github: "0x1eef/lockf.rb", tag: "v0.10.6"
5
+ gem "test-cmd.rb", github: "0x1eef/test-cmd.rb", tag: "v0.2.0"
6
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,15 @@
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/README.md ADDED
@@ -0,0 +1,242 @@
1
+ ## About
2
+
3
+ xchan.rb is an easy to use library that provides a channel for
4
+ InterProcess Communication (IPC) between a parent Ruby process,
5
+ and its child processes. The channel is implemented through both
6
+ serialization, and an unbound unix socket.
7
+
8
+ ## Examples
9
+
10
+ ### Serialization
11
+
12
+ #### Options
13
+
14
+ When a channel is written to or read from, a Ruby object is serialized
15
+ (on write) or deserialized (on read). The default serializers are available as
16
+ `xchan(:marshal)`, `xchan(:json)`, and `xchan(:yaml)`. For scenarios where it
17
+ is preferred to send and receive plain strings, the "plain" serializer is
18
+ available as `xchan(:plain)`. This example uses
19
+ [`Marshal`](https://www.rubydoc.info/stdlib/core/Marshal):
20
+
21
+ ```ruby
22
+ require "xchan"
23
+
24
+ ##
25
+ # This channel uses Marshal to serialize objects.
26
+ ch = xchan
27
+ pid = fork { print "Received message: ", ch.recv[:msg], "\n" }
28
+ ch.send(msg: "serialized by Marshal")
29
+ ch.close
30
+ Process.wait(pid)
31
+
32
+ ##
33
+ # This channel also uses Marshal to serialize objects.
34
+ ch = xchan(:marshal)
35
+ pid = fork { print "Received message: ", ch.recv[:msg], "\n"
36
+ ch.send(msg: "serialized by Marshal")
37
+ ch.close
38
+ Process.wait(pid)
39
+
40
+ ##
41
+ # Received message: serialized by Marshal
42
+ # Received message: serialized by Marshal
43
+ ```
44
+
45
+ ### Read operations
46
+
47
+ #### `#recv`
48
+
49
+ The `ch.recv` method performs a blocking read. A read can block when
50
+ a lock is held by another process, or when a read from the underlying
51
+ socket blocks. This example performs a read that blocks in a child
52
+ process until the parent process writes to the channel:
53
+
54
+ ```ruby
55
+ require "xchan"
56
+
57
+ ch = xchan
58
+ pid = fork do
59
+ print "Received a random number (child process): ", ch.recv, "\n"
60
+ end
61
+ # Delay for a second to let a process fork, and call "ch.recv"
62
+ sleep(1)
63
+ print "Send a random number (from parent process)", "\n"
64
+ ch.send(rand(21))
65
+ Process.wait(pid)
66
+ ch.close
67
+
68
+ ##
69
+ # Send a random number (from parent process)
70
+ # Received random number (child process): XX
71
+ ```
72
+
73
+ #### `#recv_nonblock`
74
+
75
+ The non-blocking counterpart to `#recv` is `#recv_nonblock`. The `#recv_nonblock`
76
+ method raises `Chan::WaitLockable` when a read blocks because of a lock held by
77
+ another process, and the method raises `Chan::WaitReadable` when a read on the
78
+ underlying socket blocks. This example performs a read that will
79
+ raise `Chan::WaitReadable`:
80
+
81
+ ```ruby
82
+ require "xchan"
83
+
84
+ def read(ch)
85
+ ch.recv_nonblock
86
+ rescue Chan::WaitReadable
87
+ print "Wait 1 second for channel to be readable", "\n"
88
+ ch.wait_readable(1)
89
+ retry
90
+ rescue Chan::WaitLockable
91
+ sleep 0.01
92
+ retry
93
+ end
94
+ trap("SIGINT") { exit(1) }
95
+ read(xchan)
96
+
97
+ ##
98
+ # Wait 1 second for channel to be readable
99
+ # Wait 1 second for channel to be readable
100
+ # ^C
101
+ ```
102
+
103
+ ### Write operations
104
+
105
+ #### `#send`
106
+
107
+ The `ch.send` method performs a blocking write. A write can block when a lock
108
+ is held by another process, or when a write to the underlying socket blocks.
109
+ This example performs a write that will block when the send buffer becomes full:
110
+
111
+ ```ruby
112
+ require "xchan"
113
+
114
+ ch = xchan(:marshal, socket: Socket::SOCK_STREAM)
115
+ sndbuf = ch.getsockopt(:reader, Socket::SOL_SOCKET, Socket::SO_SNDBUF)
116
+ while ch.bytes_sent <= sndbuf.int
117
+ ch.send(1)
118
+ end
119
+ ```
120
+
121
+ #### `#send_nonblock`
122
+
123
+ The non-blocking counterpart to `#send` is `#send_nonblock`. The `#send_nonblock`
124
+ method raises `Chan::WaitLockable` when a write blocks because of a lock held
125
+ by another process, and the method raises `Chan::WaitReadable` when a write to
126
+ the underlying socket blocks. This example builds on the previous example by
127
+ freeing space on the send buffer when a write is found to block:
128
+
129
+ ```ruby
130
+ require "xchan"
131
+
132
+ def send_nonblock(ch, buf)
133
+ ch.send_nonblock(buf)
134
+ rescue Chan::WaitWritable
135
+ print "Blocked - free send buffer", "\n"
136
+ ch.recv
137
+ retry
138
+ rescue Chan::WaitLockable
139
+ sleep 0.01
140
+ retry
141
+ end
142
+
143
+ ch = xchan(:marshal, socket: Socket::SOCK_STREAM)
144
+ sndbuf = ch.getsockopt(:writer, Socket::SOL_SOCKET, Socket::SO_SNDBUF)
145
+ while ch.bytes_sent <= sndbuf.int
146
+ send_nonblock(ch, 1)
147
+ end
148
+
149
+ ##
150
+ # Blocked - free send buffer
151
+ ```
152
+
153
+ ### Socket
154
+
155
+ #### Types
156
+
157
+ A channel can be created with one of three sockets types:
158
+
159
+ * `Socket::SOCK_DGRAM`
160
+ * `Socket::SOCK_STREAM`
161
+ * `Socket::SOCK_SEQPACKET`
162
+
163
+ The default is `Socket::SOCK_DGRAM` because its default settings
164
+ provide the most buffer space. The socket type can be specified as
165
+ a keyword argument:
166
+
167
+ ```ruby
168
+ require "xchan"
169
+ ch = xchan(:marshal, socket: Socket::SOCK_STREAM)
170
+ ```
171
+
172
+ #### Options
173
+
174
+ A channel is composed of two sockets, one for reading and the other for writing.
175
+ Socket options can be read and set on either of the two sockets with the
176
+ `Chan::UNIXSocket#getsockopt`, and `Chan::UNIXSocket#setsockopt` methods.
177
+ Besides the first argument (`:reader`, or `:writer`), the rest of the arguments
178
+ are identical to `Socket#{getsockopt,setsockopt}`. This example's results can
179
+ vary depending on the operating system it is run on:
180
+
181
+ ```ruby
182
+ require "xchan"
183
+ ch = xchan(:marshal)
184
+
185
+ ##
186
+ # Print the value of SO_RCVBUF
187
+ rcvbuf = ch.getsockopt(:reader, Socket::SOL_SOCKET, Socket::SO_RCVBUF)
188
+ print "The read buffer can contain a maximum of: ", rcvbuf.int, " bytes.\n"
189
+
190
+ ##
191
+ # Print the value of SO_SNDBUF
192
+ sndbuf = ch.getsockopt(:writer, Socket::SOL_SOCKET, Socket::SO_SNDBUF)
193
+ print "The maximum size of a single message is: ", sndbuf.int, " bytes.\n"
194
+
195
+ ##
196
+ # The read buffer can contain a maximum of: 16384 bytes.
197
+ # The maximum size of a single message is: 2048 bytes.
198
+ ```
199
+
200
+ ### Temporary files
201
+
202
+ #### tmpdir
203
+
204
+ A single channel creates three temporary files that are removed
205
+ from the filesystem as soon as they are created. By default the
206
+ files are stored - for a short time - in `Dir.tmpdir`. Read and
207
+ write permissions are reserved for the process that created
208
+ them, inclusive of its child processes.
209
+
210
+ The parent directory of the temporary files can be changed with the
211
+ `tmpdir` option:
212
+
213
+ ```ruby
214
+ require "xchan"
215
+ ch = xchan(:marshal, tmpdir: Dir.home)
216
+ ```
217
+
218
+ ## Sources
219
+
220
+ * [Source code (GitHub)](https://github.com/0x1eef/xchan.rb#readme)
221
+ * [Source code (GitLab)](https://gitlab.com/0x1eef/xchan.rb#about)
222
+
223
+ ## Install
224
+
225
+ xchan.rb is distributed as a RubyGem through its git repositories. <br>
226
+ [GitHub](https://github.com/0x1eef/xchan.rb),
227
+ and
228
+ [GitLab](https://gitlab.com/0x1eef/xchan.rb)
229
+ are available as sources.
230
+
231
+ **Gemfile**
232
+
233
+ ```ruby
234
+ gem "xchan.rb", github: "0x1eef/xchan.rb", tag: "v0.16.3"
235
+ gem "lockf.rb", github: "0x1eef/lockf.rb", tag: "v0.10.6"
236
+ ```
237
+
238
+ ## <a id="license"> License </a>
239
+
240
+ [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/).
241
+ <br>
242
+ See [LICENSE](./LICENSE).
data/Rakefile.rb ADDED
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {Chan::Bytes Chan::Bytes} class is similar
5
+ # to an array, where each element represents the
6
+ # number of bytes used to store an object on a
7
+ # channel. When an object is written to a channel,
8
+ # the array increases in size, and when an object
9
+ # is read from a channel, the array decreases in
10
+ # size.
11
+ class Chan::Bytes
12
+ require "json"
13
+ require_relative "stat"
14
+
15
+ ##
16
+ # @return [Chan::Stat]
17
+ attr_reader :stat
18
+
19
+ ##
20
+ # @param [String] tmpdir
21
+ # Path to a directory where temporary files will be stored.
22
+ #
23
+ # @return [Chan::Bytes]
24
+ def initialize(tmpdir)
25
+ @serializer = JSON
26
+ @io = Chan.temporary_file("xchan.bytes", tmpdir:)
27
+ @io.sync = true
28
+ @stat = Chan::Stat.new(tmpdir)
29
+ write(@io, [])
30
+ end
31
+
32
+ ##
33
+ # Insert a byte count at the head of the array
34
+ #
35
+ # @param [Integer] len
36
+ # Number of bytes
37
+ #
38
+ # @return [void]
39
+ def unshift(len)
40
+ return 0 if len.nil? || len.zero?
41
+ bytes = read(@io)
42
+ bytes.unshift(len)
43
+ write(@io, bytes)
44
+ @stat.store(bytes_written: len)
45
+ len
46
+ end
47
+
48
+ ##
49
+ # Insert a byte count at the tail of the array
50
+ #
51
+ # @param [Integer] len
52
+ # Number of bytes
53
+ #
54
+ # @return [void]
55
+ def push(len)
56
+ return 0 if len.nil? || len.zero?
57
+ bytes = read(@io)
58
+ bytes.push(len)
59
+ write(@io, bytes)
60
+ @stat.store(bytes_written: len)
61
+ len
62
+ end
63
+
64
+ ##
65
+ # @return [Integer]
66
+ # Returns (and removes) a byte count from the head of the array
67
+ def shift
68
+ bytes = read(@io)
69
+ return 0 if bytes.size.zero?
70
+ len = bytes.shift
71
+ write(@io, bytes)
72
+ @stat.store(bytes_read: len)
73
+ len
74
+ end
75
+
76
+ ##
77
+ # @return [Integer]
78
+ # Returns the size of the array
79
+ def size
80
+ read(@io).size
81
+ end
82
+
83
+ ##
84
+ # Close the underlying IO
85
+ #
86
+ # @return [void]
87
+ def close
88
+ @io.close
89
+ end
90
+
91
+ private
92
+
93
+ def read(io)
94
+ deserialize(io.read).tap { io.rewind }
95
+ end
96
+
97
+ def write(io, bytes)
98
+ io.truncate(0)
99
+ io.write(serialize(bytes)).tap { io.rewind }
100
+ end
101
+
102
+ def serialize(bytes)
103
+ @serializer.dump(bytes)
104
+ end
105
+
106
+ def deserialize(bytes)
107
+ @serializer.load(bytes)
108
+ end
109
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # A module that is included into Ruby's {Object} class.
5
+ module Chan::Mixin
6
+ ##
7
+ # @example
8
+ # ch = xchan
9
+ # ch.send([1,2,3])
10
+ # ch.recv.pop # => 3
11
+ # ch.close
12
+ #
13
+ # @param serializer (see Chan::UNIXSocket#initialize)
14
+ # @param socket (see Chan::UNIXSocket#initialize)
15
+ # @param tmpdir (see Chan::UNIXSocket#initialize)
16
+ # @return (see Chan::UNIXSocket#initialize)
17
+ def xchan(serializer = :marshal, **kw_args)
18
+ Chan::UNIXSocket.new(serializer, **kw_args)
19
+ end
20
+ end
data/lib/xchan/stat.rb ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {Chan::Stat Chan::Stat} class provides statistics
5
+ # (eg number of bytes read, number of bytes written) for
6
+ # a given channel.
7
+ class Chan::Stat
8
+ require "json"
9
+
10
+ ##
11
+ # @param [String] tmpdir
12
+ # Path to a directory where temporary files will be stored.
13
+ #
14
+ # @return [Chan::Stat]
15
+ def initialize(tmpdir)
16
+ @serializer = JSON
17
+ @io = Chan.temporary_file("xchan.stat", tmpdir:)
18
+ write(@io, {"bytes_read" => 0, "bytes_written" => 0})
19
+ end
20
+
21
+ ##
22
+ # @return [Integer]
23
+ # Returns the number of bytes written to a channel.
24
+ def bytes_written
25
+ read(@io).fetch("bytes_written")
26
+ end
27
+
28
+ ##
29
+ # @return [Integer]
30
+ # Returns the number of bytes read from a channel.
31
+ def bytes_read
32
+ read(@io).fetch("bytes_read")
33
+ end
34
+
35
+ ##
36
+ # @param [Hash] new_stat
37
+ # @return [void]
38
+ # @private
39
+ def store(new_stat)
40
+ stat = read(@io)
41
+ new_stat.each { stat[_1.to_s] += _2 }
42
+ write(@io, stat)
43
+ end
44
+
45
+ private
46
+
47
+ def write(io, o)
48
+ io.truncate(0)
49
+ io.write(serialize(o)).tap { io.rewind }
50
+ end
51
+
52
+ def read(io)
53
+ deserialize(io.read).tap { io.rewind }
54
+ end
55
+
56
+ def serialize(bytes)
57
+ @serializer.dump(bytes)
58
+ end
59
+
60
+ def deserialize(bytes)
61
+ @serializer.load(bytes)
62
+ end
63
+ end