xchan.rb 0.16.4

Sign up to get free protection for your applications and to get access to all the features.
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