tiq 0.1

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: 8e436fceef44012df5924defa9947216cac33a6703f222a738a0abc56f48c21b
4
+ data.tar.gz: b802bf48b8133fce415abc875069ee33eb2e3d362cc8e5043288c534d8caf856
5
+ SHA512:
6
+ metadata.gz: 6bf88679ecc201e7a814724d8efa5888d8944515325c8d3ec087d4da95d23d8eae66745b941731ea7a195ac94a010f7785687773749de13ba2691baac6757f91
7
+ data.tar.gz: 80d673141cf5ea1d7cb213dc6ae0a4fa2c843f10fe4ec373ffa8ccf8c1e897c6d209520ee2ce23b7ad2f2ffad20edfcd8e20ab2455f88de434f4e316bb5eb38d
data/CHANGELOG.md ADDED
File without changes
data/LICENSE.md ADDED
@@ -0,0 +1,29 @@
1
+ # License
2
+
3
+ Copyright (C) 2022, Ecsypno <https://ecsypno.com/>
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
24
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # Tiq
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Version</th>
6
+ <td>0.0.1</td>
7
+ </tr>
8
+ <tr>
9
+ <th>Github page</th>
10
+ <td><a href="http://github.com/qadron/tiq">http://github.com/qadron/tiq</a></td>
11
+ </tr>
12
+ <tr>
13
+ <th>Code Documentation</th>
14
+ <td><a href="http://rubydoc.info/github/qadron/tiq/">http://rubydoc.info/github/qadron/tiq/</a></td>
15
+ </tr>
16
+ <tr>
17
+ <th>Author</th>
18
+ <td><a href="mailto:tasos.laskos@gmail.com">Tasos Laskos</a></td>
19
+ </tr>
20
+ <tr>
21
+ <th>Copyright</th>
22
+ <td>2025 <a href="https://ecsypno.com">Ecsypno</a></td>
23
+ </tr>
24
+ <tr>
25
+ <th>License</th>
26
+ <td><a href="file.LICENSE.html">3-clause BSD</a></td>
27
+ </tr>
28
+ </table>
29
+
30
+ ## Synopsis
31
+
32
+ Tiq is a simple and lightweight clustering solution protocol.
33
+
34
+ This implementation is based on [Toq](https://github.com/qadron/toq) for Remote Procedure Call needs.
35
+
36
+ ## Concepts
37
+
38
+ There are a few key concepts in `Tik`:
39
+
40
+ ### Node
41
+
42
+ `Tik::Node` offers _Node_ representations, _server-side_ presences if you must.
43
+
44
+ To start a _Node_, you need to create a class that inherits from `Tiq::Node`
45
+ and instantiate it with a URL to bind to.
46
+
47
+ ```ruby
48
+ cass MyNode < Tiq::Node
49
+ end
50
+
51
+ node = MyNode.new( url: "localhost:9999" )
52
+ ```
53
+
54
+ ### Cluster
55
+
56
+ To create a _Cluster_, you need to create another _Node_ and specify any already
57
+ existing one, as a _Cluster_ member, to be its _peer_.
58
+
59
+ Any existing _Node_ would do.
60
+
61
+ ```ruby
62
+ class MySecondNode < Tiq::Node
63
+ end
64
+
65
+ node_2 = MySecondNode.new( url: "localhost:9998", peer: 'localhost:9999' )
66
+ ```
67
+
68
+ ### Client
69
+
70
+ `Tik::Client` offers a _Client_ to enable _Node_/User communications.
71
+
72
+ ```ruby
73
+ client = MyClient.new( "localhost:9999" )
74
+
75
+ # Issue calls on the server side and get us the responses.
76
+ # Client will return `true`.
77
+ p client.alive?
78
+ # Client will return information about the Grid members.
79
+ p client.peers
80
+ # Client will return information about the Grid members.
81
+ p client.info
82
+ ```
83
+
84
+ ### Channel
85
+
86
+ Chanel _Nodes_ offer client-side access to the _Shared Data_; a Shared HashMap.
87
+
88
+ This enables the establishment of channels via callbacks upon _Data_
89
+ operations.
90
+
91
+ ```ruby
92
+ class MyNode < Tiq::Node
93
+ end
94
+
95
+ class MySecondNode < Tiq::Node
96
+ end
97
+
98
+ class MyChannelNode < Tiq::Node
99
+ end
100
+
101
+ # Set up initial Node, the start of the cluster.
102
+ node_1 = MyNode.new( url: "localhost:9999" )
103
+ node_2 = MySecondNode.new( url: "localhost:9998", peer: 'localhost:9999' )
104
+ channel = MyChannelNode.new( url: "localhost:9997", peer: 'localhost:9999' ).data
105
+ sleep 1
106
+
107
+ channel.on_set :my_signal do |value|
108
+ p "#{:on_set} - #{value}"
109
+ end
110
+
111
+ node_1.data.set :my_signal, 'tada!'
112
+ sleep 1
113
+
114
+ ```
115
+
116
+ ### Add-ons
117
+
118
+ _Node_ Add-ons are most commonly _Services_, and are are immensly easy to deploy
119
+ and have up and running for
120
+ every _Node_.
121
+
122
+ ```ruby
123
+ class MyNode < Tiq::Node
124
+ end
125
+
126
+ class MyClient < Tiq::Client
127
+ end
128
+
129
+ # Set up initial Node.
130
+ node_1 = MyNode.new( url: "localhost:9999" )
131
+
132
+ # Add a service to the node, called :poll.
133
+ Tiq::Addon::Attach node_1, :poll do |arguments = nil|
134
+ p "SERVICE: #{arguments}"
135
+ end
136
+
137
+ # Interact with the service via a Client.
138
+ client_1 = MyClient.new( "localhost:9999" )
139
+ Tiq::Addon client_1, :poll, 'ping' do
140
+ puts "CLIENT: #{r}"
141
+ end
142
+ ```
143
+
144
+ ### Data
145
+
146
+ Data can be shared across _Nodes_ by means of broadcasting upon
147
+ change - optional.
148
+
149
+ ```ruby
150
+ class MyNode < Tiq::Node
151
+ end
152
+
153
+ class MySecondNode < Tiq::Node
154
+ end
155
+
156
+ # Set up initial Node, the start of the cluster.
157
+ node_1 = MyNode.new( url: "localhost:9999" )
158
+ node_2 = MySecondNode.new( url: "localhost:9998", peer: 'localhost:9999' )
159
+
160
+ sleep 1
161
+
162
+ node_2.data.on_set :my_signal do |value|
163
+ p "#{:on_set} - #{value}"
164
+ end
165
+
166
+ node_1.data.set :my_signal, 'tada!'
167
+ sleep 1
168
+
169
+ ```
170
+
171
+ ## Installation
172
+
173
+ gem install tiq
174
+
175
+ ## License
176
+
177
+ Tiq is provided under the 3-clause BSD license.
178
+ See the `LICENSE` file for more information.
@@ -0,0 +1,18 @@
1
+ require 'msgpack'
2
+ require 'toq'
3
+
4
+ module Tiq
5
+ class Channel
6
+
7
+ def initialize( url, options = {} )
8
+ host, port = url.split( ':' )
9
+ @client = Toq::Client.new( options.merge( host: host, port: port.to_i ) )
10
+ end
11
+
12
+ def method_missing( method, *args, &block )
13
+ @client.call( "data.#{method}", *args, &block )
14
+ end
15
+
16
+ end
17
+
18
+ end
data/lib/tiq/client.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'msgpack'
2
+
3
+ module Tiq
4
+ class Client < Toq::Client
5
+
6
+ def initialize( url, options = {} )
7
+ host, port = url.split( ':' )
8
+ super( options.merge( host: host, port: port.to_i ) )
9
+ end
10
+
11
+ def method_missing( method, *args, &block )
12
+ call( "node.#{method}", *args, &block )
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,90 @@
1
+ module Tiq
2
+
3
+ def self.Addon( node, shortname, *args, &block )
4
+ node.call_addon( shortname, *args, &block )
5
+ end
6
+
7
+ module Addon
8
+ def self.Attach( node, shortname = nil, &block )
9
+ node.attach_addon shortname, proc { |*arguments| block.call( *arguments ) }
10
+ end
11
+
12
+ def self.Dettach( node, shortname )
13
+ node.dettach_addon shortname
14
+ end
15
+
16
+ extend self
17
+ end
18
+
19
+ class Node
20
+
21
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
22
+ class Addon
23
+
24
+ attr_reader :node
25
+ attr_reader :data
26
+ attr_reader :options
27
+
28
+ def initialize( node, payload, options = {} )
29
+ @node = node
30
+ @options = options
31
+ @data = @node.data
32
+ @payload = payload
33
+ end
34
+
35
+ def call( *aguments, &block )
36
+ @payload.call( *aguments, &block )
37
+ end
38
+
39
+ # @return [Server::node::Node]
40
+ # Local node.
41
+ def node
42
+ node.instance_eval { @node }
43
+ end
44
+
45
+ # Defers a blocking operation in order to avoid blocking the main Reactor loop.
46
+ #
47
+ # The operation will be run in its own Thread - DO NOT block forever.
48
+ #
49
+ # Accepts either 2 parameters (an `operation` and a `callback` or an operation
50
+ # as a block.
51
+ #
52
+ # @param [Proc] operation
53
+ # Operation to defer.
54
+ # @param [Proc] callback
55
+ # Block to call with the results of the operation.
56
+ #
57
+ # @param [Block] block
58
+ # Operation to defer.
59
+ def defer( operation = nil, callback = nil, &block )
60
+ Thread.new( *[operation, callback].compact, &block )
61
+ end
62
+
63
+ # Runs a block as soon as possible in the Reactor loop.
64
+ #
65
+ # @param [Block] block
66
+ def run_asap( &block )
67
+ Raktr.global.next_tick( &block )
68
+ end
69
+
70
+ # @param [Array] list
71
+ #
72
+ # @return [Raktr::Iterator]
73
+ # Iterator for the provided array.
74
+ def iterator_for( list, max_concurrency = 10 )
75
+ Raktr.global.create_iterator( list, max_concurrency )
76
+ end
77
+
78
+ # Connects to a node by `url`
79
+ #
80
+ # @param [String] url
81
+ #
82
+ # @return [Client::node]
83
+ def connect_to_node( url )
84
+ @node_connections ||= {}
85
+ @node_connections[url] ||= Tiq::Client.new( url )
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,132 @@
1
+ module Tiq
2
+ class Node
3
+ class Data
4
+
5
+ CONCURRENCY = 20
6
+
7
+ def initialize( node )
8
+ @hash = {}
9
+
10
+ @on_set_cb = {}
11
+ @on_delete_cb = {}
12
+
13
+ @node = node
14
+ end
15
+
16
+ def update( data )
17
+ @hash.merge! data
18
+ nil
19
+ end
20
+
21
+ def get( k )
22
+ @hash[sanitize_key( k )]
23
+ end
24
+
25
+ def set( k, v, broadcast = true, &block )
26
+ k = sanitize_key( k )
27
+
28
+ if @hash[k] == v
29
+ block.call if block_given?
30
+ return
31
+ end
32
+
33
+ # p "#{@node} set #{k} #{v} 1"
34
+
35
+ @hash[k] = v
36
+ call_on_set( k, v )
37
+
38
+ if broadcast
39
+ each_peer do |peer, iterator|
40
+ peer.set( k, v, false ) { iterator.next }
41
+ end
42
+ end
43
+
44
+ block.call if block_given?
45
+ nil
46
+ end
47
+
48
+ def delete( k, broadcast = true, &block )
49
+ k = sanitize_key( k )
50
+
51
+ if !@hash.include? k
52
+ block.call if block_given?
53
+ return
54
+ end
55
+
56
+ @hash.delete( k )
57
+ call_on_delete( k )
58
+
59
+ if broadcast
60
+ each_peer do |peer, iterator|
61
+ peer.delete( k, false ) { iterator.next }
62
+ end
63
+ end
64
+
65
+ block.call if block_given?
66
+ nil
67
+ end
68
+
69
+ def on_set( k, &block )
70
+ # p "#{@node} on_set #{k} 0 #{block}"
71
+ (@on_set_cb[sanitize_key( k )] ||= []) << block
72
+ nil
73
+ end
74
+
75
+ def on_delete( k, &block )
76
+ (@on_delete_cb[sanitize_key( k )] ||= []) << block
77
+ nil
78
+ end
79
+
80
+ def to_h
81
+ @hash.dup
82
+ end
83
+
84
+ private
85
+
86
+ def call_on_set( k, v )
87
+ k = sanitize_key( k )
88
+
89
+ # p "#{@node} call_on_set #{k} 2"
90
+ # p @on_set_cb
91
+ # p "--- PEERS #{@node.peers}"
92
+
93
+ if @on_set_cb[k]
94
+ @on_set_cb[k].each do |cb|
95
+ cb.call k, v
96
+ end
97
+ end
98
+
99
+ nil
100
+ end
101
+
102
+ def call_on_delete( k )
103
+ k = sanitize_key( k )
104
+ return if !@on_delete_cb[k]
105
+
106
+ @on_delete_cb[k].each do |cb|
107
+ cb.call k
108
+ end
109
+
110
+ nil
111
+ end
112
+
113
+ def each_peer( &block )
114
+ each = proc do |url, iterator|
115
+ block.call connect_to_peer( url ), iterator
116
+ end
117
+ @node.reactor.create_iterator( @node.peers, CONCURRENCY ).each( each )
118
+ end
119
+
120
+ def connect_to_peer( url, options = {} )
121
+ @rpc_clients ||= {}
122
+ @rpc_clients[url] ||= Tiq::Channel.new( url, options )
123
+ end
124
+
125
+ def sanitize_key( k )
126
+ k.to_s
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+ end
data/lib/tiq/node.rb ADDED
@@ -0,0 +1,300 @@
1
+ require 'set'
2
+ require_relative 'node/data'
3
+ require_relative 'channel'
4
+ require_relative 'client'
5
+
6
+ module Tiq
7
+
8
+ class Node
9
+
10
+ INTERVAL_PING = 5
11
+
12
+ attr_reader :data
13
+ attr_reader :services
14
+ attr_reader :reactor
15
+
16
+ # Initializes the node by:
17
+ #
18
+ # * Adding the peer (if the user has supplied one) to the peer list.
19
+ # * Getting the peer's peer list and appending them to its own.
20
+ # * Announces itself to the peer and instructs it to propagate our URL
21
+ # to the others.
22
+ #
23
+ # @param [Cuboid::Options] options
24
+ def initialize( options )
25
+ @options = options
26
+ @url = @options[:url]
27
+
28
+ $stdout.puts 'Initializing node...'
29
+
30
+ @dead_nodes = Set.new
31
+ @peers = Set.new
32
+ @addons = {}
33
+ @nodes_info_cache = []
34
+
35
+ host, port = @url.split( ':' )
36
+ options[:host] ||= host || 'localhost'
37
+ options[:port] ||= port || 9999
38
+
39
+ @server = Toq::Server.new( host: options[:host], port: options[:port] )
40
+ @reactor = @server.reactor
41
+ @server.add_async_check do |method|
42
+ # methods that expect a block are async
43
+ method.parameters.flatten.include? :block
44
+ end
45
+ @server.add_handler( 'node', self )
46
+
47
+ @reactor.run_in_thread if !@reactor.running?
48
+
49
+ @data = Data.new( self )
50
+ @server.add_handler( 'data', @data )
51
+
52
+ @reactor.on_error do |_, e|
53
+ $stderr.puts "Reactor: #{e}"
54
+
55
+ e.backtrace.each do |l|
56
+ $stderr.puts "Reactor: #{l}"
57
+ end
58
+ end
59
+
60
+ @reactor.at_interval( @options[:ping_interval] || INTERVAL_PING ) do
61
+ ping
62
+ check_for_comebacks
63
+ end
64
+
65
+ if( peer = @options[:peer] )
66
+ # Grab the peer's peers.
67
+ connect_to_peer( peer ).peers do |grid_peers|
68
+ if grid_peers.rpc_exception?
69
+ $stdout.puts "Peer seems dead: #{peer}"
70
+ $stderr.puts "Reactor: #{grid_peers}"
71
+
72
+ if grid_peers.backtrace
73
+ grid_peers.backtrace.each do |l|
74
+ $stderr.puts "Reactor: #{l}"
75
+ end
76
+ end
77
+
78
+ add_dead_peer( peer )
79
+ next
80
+ end
81
+
82
+ begin
83
+ grid_peers << peer
84
+ grid_peers.each { |url| add_peer url }
85
+
86
+ announce @url
87
+ rescue => e
88
+ p e
89
+ end
90
+ end
91
+ end
92
+
93
+ $stdout.puts 'Node ready.'
94
+
95
+ log_updated_peers
96
+
97
+ run
98
+ end
99
+
100
+ def attach_addon( name, service, options = {} )
101
+ @addons[name.to_s] = Addon.new( self, service, options )
102
+ nil
103
+ end
104
+
105
+ def call_addon( name, *arguments )
106
+ @addons[name.to_s].call( *arguments )
107
+ end
108
+
109
+ def addons
110
+ @addons.keys
111
+ end
112
+
113
+ def dettach_addon( name )
114
+ @addons.delete name
115
+ end
116
+
117
+ # @return [Boolean]
118
+ # `true` if grid member, `false` otherwise.
119
+ def grid_member?
120
+ @peers.any?
121
+ end
122
+
123
+ def unplug
124
+ @server.create_iterator( @peers, 20 ).each do |peer, iterator|
125
+ connect_to_peer( peer ).remove_peer( @url ) { iterator.next }
126
+ end
127
+
128
+ @peers.clear
129
+ @dead_nodes.clear
130
+
131
+ nil
132
+ end
133
+
134
+ # Adds a peer to the peer list.
135
+ #
136
+ # @param [String] node_url
137
+ # URL of a peering node.
138
+ def add_peer( node_url )
139
+ $stdout.puts "Adding peer: #{node_url}"
140
+ @peers << node_url
141
+
142
+ connect_to_peer( node_url ){ update_data( @data.to_h ) }
143
+
144
+ log_updated_peers
145
+ true
146
+ end
147
+
148
+ def update_data( data )
149
+ @data.update( data )
150
+ nil
151
+ end
152
+
153
+ def remove_peer( url )
154
+ @peers.delete url
155
+ @dead_nodes.delete url
156
+ nil
157
+ end
158
+
159
+ # @return [Array]
160
+ # Peer/node/peer URLs.
161
+ def peers
162
+ @peers.to_a
163
+ end
164
+
165
+ def peers_with_info( &block )
166
+ fail 'This method requires a block!' if !block_given?
167
+
168
+ @peers_cmp = ''
169
+
170
+ if @nodes_info_cache.empty? || @peers_cmp != peers.to_s
171
+ @peers_cmp = peers.to_s
172
+
173
+ each = proc do |peer, iter|
174
+ connect_to_peer( peer ).info do |info|
175
+ if info.rpc_exception?
176
+ $stdout.puts "Peer seems dead: #{peer}"
177
+ add_dead_peer( peer )
178
+ log_updated_peers
179
+
180
+ iter.return( nil )
181
+ else
182
+ iter.return( info )
183
+ end
184
+ end
185
+ end
186
+
187
+ after = proc do |nodes|
188
+ @nodes_info_cache = nodes.compact
189
+ block.call( @nodes_info_cache )
190
+ end
191
+
192
+ @reactor.create_iterator( peers ).map( each, after )
193
+ else
194
+ block.call( @nodes_info_cache )
195
+ end
196
+ end
197
+
198
+ # @return [Hash]
199
+ #
200
+ # * `url` -- This node's URL.
201
+ # * `name` -- Nickname
202
+ # * `peers` -- Array of peers.
203
+ def info
204
+ {
205
+ 'url' => @url,
206
+ 'name' => @options[:name],
207
+ 'peers' => self.peers,
208
+ 'unreachable_peers' => @dead_nodes.to_a
209
+ }
210
+ end
211
+
212
+ def alive?
213
+ true
214
+ end
215
+
216
+ def run
217
+ $stdout.puts 'Running'
218
+ @server.start
219
+ rescue => e
220
+ $stderr.puts e
221
+ $stderr.puts "Could not start server"
222
+ e.backtrace.each do |l|
223
+ $stderr.puts l
224
+ end
225
+
226
+ exit 1
227
+ end
228
+
229
+ def shutdown
230
+ Thread.new do
231
+ $stdout.puts 'Shutting down...'
232
+ @reactor.stop
233
+ end
234
+ end
235
+
236
+ private
237
+
238
+ def add_dead_peer( url )
239
+ remove_peer( url )
240
+ @dead_nodes << url
241
+ end
242
+
243
+ def log_updated_peers
244
+ $stdout.puts 'Updated peers:'
245
+
246
+ if !peers.empty?
247
+ peers.each { |node| $stdout.puts( '---- ' + node ) }
248
+ else
249
+ $stdout.puts '<empty>'
250
+ end
251
+ end
252
+
253
+ def ping
254
+ peers.each do |peer|
255
+ connect_to_peer( peer ).alive? do |res|
256
+ next if !res.rpc_exception?
257
+ add_dead_peer( peer )
258
+ $stdout.puts "Found dead peer: #{peer} "
259
+ end
260
+ end
261
+ end
262
+
263
+ def check_for_comebacks
264
+ @dead_nodes.dup.each do |url|
265
+ peer = connect_to_peer( url )
266
+ peer.alive? do |res|
267
+ next if res.rpc_exception?
268
+
269
+ $stdout.puts "Peer came back to life: #{url}"
270
+ ([@url] | peers).each do |node|
271
+ peer.add_peer( node ){}
272
+ end
273
+
274
+ add_peer( url )
275
+ @dead_nodes.delete url
276
+ end
277
+ end
278
+ end
279
+
280
+ # Announces the node to the ones in the peer list
281
+ #
282
+ # @param [String] node
283
+ # URL
284
+ def announce( node )
285
+ $stdout.puts "Announcing: #{node}"
286
+
287
+ peers.each do |peer|
288
+ $stdout.puts "---- to: #{peer}"
289
+ connect_to_peer( peer ).add_peer( node ) do |res|
290
+ add_dead_peer( peer ) if res.rpc_exception?
291
+ end
292
+ end
293
+ end
294
+
295
+ def connect_to_peer( url, options = {} )
296
+ @rpc_clients ||= {}
297
+ @rpc_clients[url] ||= Tiq::Client.new( url, options )
298
+ end
299
+ end
300
+ end
data/lib/tiq.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'msgpack'
2
+ require_relative 'tiq/node'
3
+ require_relative 'tiq/client'
4
+ require_relative 'tiq/channel'
5
+
6
+ module Tiq
7
+ VERSION = '0.1'
8
+ end
@@ -0,0 +1,8 @@
1
+ require_relative '../lib/tiq'
2
+
3
+ RSpec.configure do |config|
4
+ config.color = true
5
+ config.add_formatter :documentation
6
+
7
+ # config.filter_run_including focus: true
8
+ end
@@ -0,0 +1 @@
1
+ require 'spec_helper'
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tiq::Client do
4
+
5
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Tiq::Node::Addon' do
4
+ let( :node ) { @node ||= Tiq::Node.new( url: 'localhost:9999' ) }
5
+ let( :peer ) { @peer ||= Tiq::Node.new( url: 'localhost:9998', peer: 'localhost:9999' ) }
6
+
7
+ before( :each ) do
8
+ node
9
+ peer
10
+ sleep 0.1
11
+ end
12
+
13
+ after( :each ) do
14
+ @peer.shutdown
15
+ @peer = nil
16
+ @node.shutdown
17
+ @node = nil
18
+ sleep 1
19
+ end
20
+
21
+ it 'attaches and calls an addon' do
22
+ node.attach_addon( 'echo', 'Tiq::Service::Echo' )
23
+ result = node.call_addon( 'echo', 'hello' )
24
+ expect( result ).to eq 'hello'
25
+ end
26
+
27
+ it 'lists addons' do
28
+ node.attach_addon( 'echo', 'Tiq::Service::Echo' )
29
+ expect( node.addons ).to include 'echo'
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tiq::Node::Data do
4
+ let( :node ) { @node ||= Tiq::Node.new( url: 'localhost:9999' ) }
5
+ let( :peer ) { @peer ||= Tiq::Node.new( url: 'localhost:9998', peer: 'localhost:9999' ) }
6
+
7
+ before( :each ) do
8
+ node
9
+ peer
10
+ sleep 0.1
11
+ end
12
+
13
+ after( :each ) do
14
+ @peer.shutdown
15
+ @peer = nil
16
+ @node.shutdown
17
+ @node = nil
18
+ sleep 1
19
+ end
20
+
21
+ it 'sets and gets data locally' do
22
+ node.data.set( 'key1', 'value1' )
23
+ expect( node.data.get( 'key1' ) ).to eq 'value1'
24
+ end
25
+
26
+ it 'propagates data to peers' do
27
+ node.data.set( 'key2', 'value2' )
28
+ sleep 1
29
+ expect( peer.data.get( 'key2' ) ).to eq 'value2'
30
+ end
31
+
32
+ it 'does not propagate data when broadcast is false' do
33
+ node.data.set( 'key3', 'value3', false )
34
+ sleep 0.1
35
+ expect( peer.data.get( 'key3' ) ).to be_nil
36
+ end
37
+
38
+ it 'calls on_set callbacks' do
39
+ called = false
40
+ peer.data.on_set( 'key4' ) { |k, v| called = (k == 'key4' && v == 'value4') }
41
+ node.data.set( 'key4', 'value4' )
42
+ sleep 1
43
+ expect( called ).to be true
44
+ end
45
+
46
+ it 'calls on_delete callbacks' do
47
+ called = false
48
+ peer.data.set( 'key5', 'value5' )
49
+ sleep 1
50
+ peer.data.on_delete( 'key5' ) { |k| called = (k == 'key5') }
51
+ node.data.delete( 'key5' )
52
+ sleep 1
53
+ expect( called ).to be true
54
+ end
55
+
56
+ it 'does not call on_set callback when value is unchanged' do
57
+ count = 0
58
+ peer.data.on_set( 'key6' ) { count += 1 }
59
+ node.data.set( 'key6', 'value6' )
60
+ node.data
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tiq::Node do
4
+ let( :node ) { @node ||= Tiq::Node.new( url: 'localhost:9999' ) }
5
+ let( :peer ) { @peer ||= Tiq::Node.new( url: 'localhost:9998', peer: 'localhost:9999' ) }
6
+
7
+ before( :each ) do
8
+ node
9
+ peer
10
+ sleep 0.1
11
+ end
12
+
13
+ after( :each ) do
14
+ @peer.shutdown
15
+ @peer = nil
16
+ @node.shutdown
17
+ @node = nil
18
+ sleep 1
19
+ end
20
+
21
+ it 'sets and gets data locally' do
22
+ node.data.set( 'key1', 'value1' )
23
+ expect( node.data.get( 'key1' ) ).to eq 'value1'
24
+ end
25
+
26
+ it 'propagates data to peers' do
27
+ node.data.set( 'key2', 'value2' )
28
+ sleep 1
29
+ expect( peer.data.get( 'key2' ) ).to eq 'value2'
30
+ end
31
+
32
+ it 'does not propagate data when broadcast is false' do
33
+ node.data.set( 'key3', 'value3', false )
34
+ sleep 0.1
35
+ expect( peer.data.get( 'key3' ) ).to be_nil
36
+ end
37
+
38
+ it 'calls on_set callbacks' do
39
+ called = false
40
+ peer.data.on_set( 'key4' ) { |k, v| called = (k == 'key4' && v == 'value4') }
41
+ node.data.set( 'key4', 'value4' )
42
+ sleep 1
43
+ expect( called ).to be true
44
+ end
45
+
46
+ it 'calls on_delete callbacks' do
47
+ called = false
48
+ peer.data.set( 'key5', 'value5' )
49
+ sleep 1
50
+ peer.data.on_delete( 'key5' ) { |k| called = (k == 'key5') }
51
+ node.data.delete( 'key5' )
52
+ sleep 1
53
+ expect( called ).to be true
54
+ end
55
+
56
+ it 'does not call on_set callback when value is unchanged' do
57
+ count = 0
58
+ peer.data.on_set( 'key6' ) { count += 1 }
59
+ node.data.set( 'key6', 'value6' )
60
+ node.data.set( 'key6', 'value6' )
61
+ end
62
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tiq
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Tasos Laskos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: msgpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: toq
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: ''
42
+ email: tasos.laskos@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files:
46
+ - README.md
47
+ - LICENSE.md
48
+ - CHANGELOG.md
49
+ files:
50
+ - CHANGELOG.md
51
+ - LICENSE.md
52
+ - README.md
53
+ - lib/tiq.rb
54
+ - lib/tiq/channel.rb
55
+ - lib/tiq/client.rb
56
+ - lib/tiq/node.rb
57
+ - lib/tiq/node/addon.rb
58
+ - lib/tiq/node/data.rb
59
+ - spec/spec_helper.rb
60
+ - spec/tiq/channel_spec.rb
61
+ - spec/tiq/client_spec.rb
62
+ - spec/tiq/node/addon_spec.rb
63
+ - spec/tiq/node/data_spec.rb
64
+ - spec/tiq/node_spec.rb
65
+ homepage: https://github.com/qadron/tiq
66
+ licenses:
67
+ - BSD 3-Clause
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options:
71
+ - "--charset=UTF-8"
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.4.22
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Simple RPC protocol.
89
+ test_files:
90
+ - spec/tiq/client_spec.rb
91
+ - spec/tiq/node_spec.rb
92
+ - spec/tiq/channel_spec.rb
93
+ - spec/tiq/node/data_spec.rb
94
+ - spec/tiq/node/addon_spec.rb
95
+ - spec/spec_helper.rb