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 +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE.md +29 -0
- data/README.md +178 -0
- data/lib/tiq/channel.rb +18 -0
- data/lib/tiq/client.rb +17 -0
- data/lib/tiq/node/addon.rb +90 -0
- data/lib/tiq/node/data.rb +132 -0
- data/lib/tiq/node.rb +300 -0
- data/lib/tiq.rb +8 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/tiq/channel_spec.rb +1 -0
- data/spec/tiq/client_spec.rb +5 -0
- data/spec/tiq/node/addon_spec.rb +31 -0
- data/spec/tiq/node/data_spec.rb +62 -0
- data/spec/tiq/node_spec.rb +62 -0
- metadata +95 -0
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.
|
data/lib/tiq/channel.rb
ADDED
|
@@ -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
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
@@ -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
|