rgossip2 0.1.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.
- data/README +69 -0
- data/bin/gossip +71 -0
- data/lib/rgossip2/client.rb +197 -0
- data/lib/rgossip2/context.rb +82 -0
- data/lib/rgossip2/context_helper.rb +55 -0
- data/lib/rgossip2/gossipper.rb +80 -0
- data/lib/rgossip2/node.rb +94 -0
- data/lib/rgossip2/node_list.rb +94 -0
- data/lib/rgossip2/receiver.rb +145 -0
- data/lib/rgossip2/timer.rb +77 -0
- data/lib/rgossip2.rb +24 -0
- metadata +88 -0
data/README
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
= rgossip
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Basic implementation of a gossip protocol.
|
6
|
+
|
7
|
+
This is a porting of Java implementation.
|
8
|
+
|
9
|
+
see http://code.google.com/p/gossip-protocol-java/
|
10
|
+
|
11
|
+
== Install
|
12
|
+
|
13
|
+
gem install rgossip2
|
14
|
+
|
15
|
+
== Example
|
16
|
+
|
17
|
+
require 'rubygems'
|
18
|
+
require 'rgossip2'
|
19
|
+
|
20
|
+
gossip = RGossip2.client(
|
21
|
+
:initial_nodes => ['10.150.174.161', '10.150.185.250', '10.150.174.30'],
|
22
|
+
:auth_key => 'onion'
|
23
|
+
)
|
24
|
+
|
25
|
+
gossip.data = 'Node 01: data'
|
26
|
+
gossip.start
|
27
|
+
#gossip.join
|
28
|
+
|
29
|
+
loop do
|
30
|
+
case gets
|
31
|
+
when /list/i
|
32
|
+
gossip.each do |address, timestamp, data|
|
33
|
+
puts "#{address}: #{data}"
|
34
|
+
# (example output)
|
35
|
+
# 10.150.174.161: node-1
|
36
|
+
# 10.150.174.30: node-3
|
37
|
+
# 10.150.185.250: node-2
|
38
|
+
end
|
39
|
+
when /^add\s+(.+)$/
|
40
|
+
gossip.add_node $1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
== Command-line interface
|
45
|
+
|
46
|
+
shell> gossip
|
47
|
+
I, [2011-10-30T22:41:05.975084 #2282] INFO -- : Client is initialized: initial_nodes=[], address=10.142.43.53, data=nil
|
48
|
+
gossip> add ip-10-142-30-230
|
49
|
+
gossip> list
|
50
|
+
IP Address Timestamp Data
|
51
|
+
--------------- -------------------------- ---------------------------
|
52
|
+
10.142.43.53 2011/10/30 22:41:05.000000
|
53
|
+
10.142.24.230 1970/01/01 09:00:00.000000
|
54
|
+
gossip> start
|
55
|
+
I, [2011-10-30T22:41:22.083898 #2282] INFO -- : Client is started: address=10.142.43.53
|
56
|
+
I, [2011-10-30T22:41:22.084197 #2282] INFO -- : Transmission was started: interval=0.1, port=10870
|
57
|
+
I, [2011-10-30T22:41:22.084580 #2282] INFO -- : Reception is started: port=10870
|
58
|
+
gossip> list
|
59
|
+
IP Address Timestamp Data
|
60
|
+
--------------- -------------------------- ---------------------------
|
61
|
+
10.142.43.53 2011/10/30 22:41:24.000000
|
62
|
+
10.142.24.230 2011/10/30 22:41:23.000000
|
63
|
+
gossip> data my data
|
64
|
+
gossip> list
|
65
|
+
IP Address Timestamp Data
|
66
|
+
--------------- -------------------------- ---------------------------
|
67
|
+
10.142.43.53 2011/10/30 22:41:30.000000 my data
|
68
|
+
10.142.24.230 2011/10/30 22:41:30.000000
|
69
|
+
|
data/bin/gossip
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'logger'
|
6
|
+
require 'optparse'
|
7
|
+
require 'readline'
|
8
|
+
require 'rgossip2'
|
9
|
+
|
10
|
+
options = {:auth_key => 'onion'}
|
11
|
+
|
12
|
+
ARGV.options do |opt|
|
13
|
+
opt.on('-i', '--initial-nodes=VAL') {|v| options[:initial_nodes] = (v || '').split(/\s*,\s*/) }
|
14
|
+
opt.on('-a', '--address=VAL') {|v| options[:address] = v }
|
15
|
+
opt.on('-d', '--data=VAL') {|v| options[:data] = v }
|
16
|
+
opt.on('-k', '--auth-key=VAL') {|v| options[:auth_key] = v }
|
17
|
+
opt.parse!
|
18
|
+
end
|
19
|
+
|
20
|
+
$stdout.sync = true
|
21
|
+
$stderr.sync = true
|
22
|
+
|
23
|
+
gossip = RGossip2.client(options)
|
24
|
+
|
25
|
+
commands = %w(start stop status list data add del clear logger exit)
|
26
|
+
Readline.completion_proc = proc {|word| commands.grep(/\A#{Regexp.quote word}/) }
|
27
|
+
|
28
|
+
def list_nodes(gossip)
|
29
|
+
puts <<-EOS
|
30
|
+
IP Address Timestamp Data
|
31
|
+
--------------- -------------------------- ---------------------------
|
32
|
+
EOS
|
33
|
+
|
34
|
+
gossip.each do |address, timestamp, data|
|
35
|
+
t = Time.at(timestamp.slice(0, 10).to_i, timestamp.slice(10..-1).to_i)
|
36
|
+
puts '%-15s %19s.%06d %s' % [address, t.strftime('%Y/%m/%d %H:%M:%S'), t.usec , data]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
while cmd = Readline.readline('gossip> ', true)
|
41
|
+
begin
|
42
|
+
case cmd
|
43
|
+
when /\A\s*start\s*\Z/i
|
44
|
+
gossip.start
|
45
|
+
when /\A\s*stop\s*\Z/i
|
46
|
+
gossip.stop
|
47
|
+
when /\A\s*status\s*\Z/i
|
48
|
+
puts "running=#{gossip.running?}"
|
49
|
+
when /\A\s*list\s*\Z/i
|
50
|
+
list_nodes(gossip)
|
51
|
+
when /\A\s*data\s+(.+)\s*\Z/
|
52
|
+
gossip.data = $1
|
53
|
+
when /\A\s*add\s+(.+)\s*\Z/
|
54
|
+
gossip.add_node $1
|
55
|
+
when /\A\s*del\s+(.+)\s*\Z/
|
56
|
+
gossip.delete_node $1
|
57
|
+
when /\A\s*clear\s*\Z/
|
58
|
+
gossip.clear_dead_list
|
59
|
+
when /\A\s*log\s*\Z/
|
60
|
+
puts [:debug, :info, :warn, :error, :fatal][gossip.logger.level]
|
61
|
+
when /\A\s*log\s+(.+)\s*\Z/
|
62
|
+
gossip.logger.level = Logger.const_get($1.upcase)
|
63
|
+
when /\A\s*exit\s*\Z/
|
64
|
+
exit
|
65
|
+
else
|
66
|
+
puts "commands: #{commands.join '|'}"
|
67
|
+
end
|
68
|
+
rescue => e
|
69
|
+
$stderr.puts e
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module RGossip2
|
4
|
+
|
5
|
+
#
|
6
|
+
# class Client
|
7
|
+
# ゴシッププロトコルのクライアント兼サーバ
|
8
|
+
#
|
9
|
+
# +----------+ +--------+
|
10
|
+
# | Client |<>---+---+| Node |
|
11
|
+
# +----------+ | +--------+
|
12
|
+
# | +-----------------------+
|
13
|
+
# +---+| @node_list:NodeList |
|
14
|
+
# | +-----------------------+
|
15
|
+
# | +-----------------------+
|
16
|
+
# +---+| @dead_list:NodeList |
|
17
|
+
# +-----------------------+
|
18
|
+
#
|
19
|
+
class Client
|
20
|
+
include Enumerable
|
21
|
+
include ContextHelper
|
22
|
+
|
23
|
+
attr_reader :node_list
|
24
|
+
attr_reader :dead_list
|
25
|
+
attr_reader :self_node
|
26
|
+
|
27
|
+
attr_reader :context
|
28
|
+
|
29
|
+
def initialize(context, initial_nodes = [], address = nil, data = nil)
|
30
|
+
@context = context
|
31
|
+
|
32
|
+
# データがバッファサイズを超える場合はエラー
|
33
|
+
if data and data.length > @context.buffer_size
|
34
|
+
raise 'Data is too large'
|
35
|
+
end
|
36
|
+
|
37
|
+
# IPアドレスを取得。デフォルトはローカルホストアドレス
|
38
|
+
@address = name2addr(address || IPSocket.getaddress(Socket.gethostname))
|
39
|
+
info("Client is initialized: initial_nodes=#{initial_nodes.inspect}, address=#{@address}, data=#{data.inspect}")
|
40
|
+
|
41
|
+
# NodeListを生成
|
42
|
+
@node_list = create(NodeList)
|
43
|
+
@dead_list = create(NodeList)
|
44
|
+
|
45
|
+
# Nodeを生成
|
46
|
+
@self_node = create(Node, @node_list, @dead_list, @address, data, nil)
|
47
|
+
@self_node.update_timestamp
|
48
|
+
@node_list[@address] = @self_node
|
49
|
+
|
50
|
+
# 初期ノードを追加
|
51
|
+
initial_nodes.uniq.each do |i|
|
52
|
+
addr = name2addr(i)
|
53
|
+
@node_list[addr] = create(Node, @node_list, @dead_list, addr, nil, nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Gossiper、Receiverを生成
|
57
|
+
@gossiper = create(Gossiper, @self_node, @node_list)
|
58
|
+
@receiver = create(Receiver, @self_node, @node_list, @dead_list)
|
59
|
+
end
|
60
|
+
|
61
|
+
def start
|
62
|
+
# 開始している場合はスキップ
|
63
|
+
return if @running
|
64
|
+
|
65
|
+
info("Client is started: address=#{@address}")
|
66
|
+
|
67
|
+
# NodoのTimerをスタート
|
68
|
+
@node_list.each do |node|
|
69
|
+
if node.address != @self_node.address
|
70
|
+
node.start_timer
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
@gossiper.start
|
75
|
+
@receiver.start
|
76
|
+
ensure
|
77
|
+
@running = true
|
78
|
+
end
|
79
|
+
|
80
|
+
def stop
|
81
|
+
# 停止している場合はスキップ
|
82
|
+
return unless @running
|
83
|
+
|
84
|
+
info("Client is stopped")
|
85
|
+
|
86
|
+
@gossiper.stop
|
87
|
+
@receiver.stop
|
88
|
+
ensure
|
89
|
+
@running = false
|
90
|
+
end
|
91
|
+
|
92
|
+
def join
|
93
|
+
@gossiper.join
|
94
|
+
@receiver.join
|
95
|
+
end
|
96
|
+
|
97
|
+
def running?
|
98
|
+
!!@running
|
99
|
+
end
|
100
|
+
|
101
|
+
def address
|
102
|
+
@self_node.address
|
103
|
+
end
|
104
|
+
|
105
|
+
def data
|
106
|
+
@node_list.synchronize {
|
107
|
+
@self_node.data
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def data=(v)
|
112
|
+
@node_list.synchronize {
|
113
|
+
@self_node.data = v
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
# ノードの追加
|
118
|
+
def add_node(address)
|
119
|
+
address = name2addr(address)
|
120
|
+
|
121
|
+
@node_list.synchronize {
|
122
|
+
@dead_list.synchronize {
|
123
|
+
# すでに存在する場合はエラー
|
124
|
+
raise 'The node already exists' if @node_list[address]
|
125
|
+
|
126
|
+
node = create(Node, @node_list, @dead_list, address, nil, nil)
|
127
|
+
@node_list[address] = node
|
128
|
+
|
129
|
+
# デッドリストからは追加したノードを削除
|
130
|
+
@dead_list.delete(address)
|
131
|
+
|
132
|
+
node.start_timer if @running
|
133
|
+
|
134
|
+
callback(:add, address, nil, nil)
|
135
|
+
}
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
# ノードの削除
|
140
|
+
def delete_node(address)
|
141
|
+
address = name2addr(address)
|
142
|
+
|
143
|
+
# 自分自身は削除できない
|
144
|
+
raise 'Own node cannot be deleted' if @self_node.address == address
|
145
|
+
|
146
|
+
@node_list.synchronize {
|
147
|
+
@dead_list.synchronize {
|
148
|
+
# ノードリストから削除してTimerを止める
|
149
|
+
node = @node_list.delete(address)
|
150
|
+
node.stop_timer if node
|
151
|
+
|
152
|
+
# デッドリストからも削除
|
153
|
+
node = @dead_list.delete(address)
|
154
|
+
node.stop_timer if node
|
155
|
+
|
156
|
+
callback(:delete, address, nil, nil)
|
157
|
+
}
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
# デッドリストのクリーニング
|
162
|
+
def clear_dead_list
|
163
|
+
@dead_list.synchronize {
|
164
|
+
@dead_list.clear
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
# ノードを舐める
|
169
|
+
def each
|
170
|
+
@node_list.each do |node|
|
171
|
+
address = node.address.dup
|
172
|
+
timestamp = node.timestamp.dup
|
173
|
+
|
174
|
+
if data = node.data
|
175
|
+
data = data.dup
|
176
|
+
end
|
177
|
+
|
178
|
+
yield([address, timestamp, data])
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def logger
|
183
|
+
@context.logger
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
def name2addr(name)
|
188
|
+
if /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z/ =~ name
|
189
|
+
name
|
190
|
+
else
|
191
|
+
IPSocket.getaddress(name)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
end # Client
|
196
|
+
|
197
|
+
end # RGossip2
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'msgpack'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module RGossip2
|
6
|
+
|
7
|
+
#
|
8
|
+
# class Context
|
9
|
+
# ゴシッププロトコルのための各種変数格納クラス
|
10
|
+
# ほとんどのクラスから参照される
|
11
|
+
#
|
12
|
+
class Context
|
13
|
+
# ポート番号
|
14
|
+
attr_accessor :port
|
15
|
+
|
16
|
+
# バッファサイズと遊び
|
17
|
+
# 「buffer_size * allowance + digest_length」が 65515bytes 以下になるようにする
|
18
|
+
attr_accessor :buffer_size
|
19
|
+
attr_accessor :allowance
|
20
|
+
|
21
|
+
# ハッシュ関数のアルゴリズムと長さ
|
22
|
+
attr_accessor :digest_algorithm
|
23
|
+
attr_accessor :digest_length
|
24
|
+
|
25
|
+
# HMACの秘密鍵
|
26
|
+
attr_accessor :auth_key
|
27
|
+
|
28
|
+
# Nodeの寿命
|
29
|
+
attr_accessor :node_lifetime
|
30
|
+
|
31
|
+
# 送信インターバル
|
32
|
+
attr_accessor :gossip_interval
|
33
|
+
|
34
|
+
# 受信タイムアウト
|
35
|
+
attr_accessor :receive_timeout
|
36
|
+
|
37
|
+
# ロガー
|
38
|
+
attr_accessor :logger
|
39
|
+
|
40
|
+
# 各種ハンドラ
|
41
|
+
attr_accessor :callback_handler
|
42
|
+
attr_accessor :error_handler
|
43
|
+
|
44
|
+
def initialize(options = {})
|
45
|
+
unless @auth_key = options[:auth_key]
|
46
|
+
raise ':auth_key is required'
|
47
|
+
end
|
48
|
+
|
49
|
+
default_logger = Logger.new($stderr)
|
50
|
+
default_logger.level = Logger::INFO
|
51
|
+
|
52
|
+
defaults = {
|
53
|
+
:port => 10870,
|
54
|
+
:buffer_size => 512,
|
55
|
+
:allowance => 3,
|
56
|
+
:node_lifetime => 10,
|
57
|
+
:gossip_interval => 0.1,
|
58
|
+
:receive_timeout => 3,
|
59
|
+
:digest_algorithm => OpenSSL::Digest::SHA256,
|
60
|
+
:digest_length => 32, # 256 / 8
|
61
|
+
:logger => default_logger,
|
62
|
+
:callback_handler => nil,
|
63
|
+
}
|
64
|
+
|
65
|
+
defaults[:error_handler] = lambda do |e|
|
66
|
+
message = (["#{e.class}: #{e.message}"] + (e.backtrace || [])).join("\n\tfrom ")
|
67
|
+
|
68
|
+
if self.logger
|
69
|
+
self.logger.error(message)
|
70
|
+
else
|
71
|
+
$stderr.puts(message)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
defaults.each do |k, v|
|
76
|
+
self.instance_variable_set("@#{k}", options.fetch(k, v))
|
77
|
+
end
|
78
|
+
end # initialize
|
79
|
+
|
80
|
+
end # Context
|
81
|
+
|
82
|
+
end # RGossip2
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module RGossip2
|
2
|
+
|
3
|
+
#
|
4
|
+
# module ContextHelper
|
5
|
+
# レシーバなしでコンテキストを操作するためのモジュール
|
6
|
+
#
|
7
|
+
module ContextHelper
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def create(*args)
|
12
|
+
@context.create(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
# 他のクラスのインスタンスを生成して自分自身をセットする
|
16
|
+
def create(klass, *args)
|
17
|
+
klass.new(@context, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
# 各種ハンドラプロキシメソッド
|
21
|
+
def callback(action, address, timestamp, data)
|
22
|
+
if @context.callback_handler
|
23
|
+
@context.callback_handler.call([action, address, timestamp, data])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def handle_error(e)
|
28
|
+
if @context.error_handler
|
29
|
+
@context.error_handler.call(e)
|
30
|
+
else
|
31
|
+
raise e
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# ノード情報群からハッシュ値とメッセージを生成する
|
36
|
+
def digest_and_message(nodes)
|
37
|
+
message = nodes.map {|i| i.to_a }.to_msgpack
|
38
|
+
hash = OpenSSL::HMAC::digest(@context.digest_algorithm.new, @context.auth_key, message)
|
39
|
+
[hash, message]
|
40
|
+
end
|
41
|
+
|
42
|
+
# ロギングプロキシメソッド
|
43
|
+
[:fatal, :error, :worn, :info, :debug].each do |name|
|
44
|
+
define_method(name) do |message|
|
45
|
+
if @context.logger
|
46
|
+
@context.logger.send(name, message)
|
47
|
+
else
|
48
|
+
$stderr.puts("#{name}: #{message}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end # ContextHelper
|
54
|
+
|
55
|
+
end # RGossip2
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module RGossip2
|
2
|
+
|
3
|
+
#
|
4
|
+
# class Gossiper
|
5
|
+
# ゴシッププロトコルの送信クラス
|
6
|
+
#
|
7
|
+
# +------------+ +--------+
|
8
|
+
# | Gossiper |<>---+---+| Node |
|
9
|
+
# +------------+ | +--------+
|
10
|
+
# | +-----------------------+
|
11
|
+
# +---+| @node_list:NodeList |
|
12
|
+
# +-----------------------+
|
13
|
+
#
|
14
|
+
class Gossiper
|
15
|
+
include ContextHelper
|
16
|
+
|
17
|
+
def initialize(context, self_node, node_list)
|
18
|
+
@context = context
|
19
|
+
@self_node = self_node
|
20
|
+
@node_list = node_list
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
info("Transmission was started: interval=#{@context.gossip_interval} port=#{@context.port}")
|
25
|
+
|
26
|
+
@running = true
|
27
|
+
|
28
|
+
# パケット送信スレッドを開始
|
29
|
+
@thread = Thread.start {
|
30
|
+
begin
|
31
|
+
sock = UDPSocket.open
|
32
|
+
|
33
|
+
while @running
|
34
|
+
begin
|
35
|
+
@node_list.synchronize { gossip(sock) }
|
36
|
+
rescue Exception => e
|
37
|
+
handle_error(e)
|
38
|
+
end
|
39
|
+
|
40
|
+
sleep(@context.gossip_interval)
|
41
|
+
end
|
42
|
+
ensure
|
43
|
+
sock.close
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end # start
|
47
|
+
|
48
|
+
def stop
|
49
|
+
info("Transmission was stopped")
|
50
|
+
|
51
|
+
# フラグをfalseにしてスレッドを終了させる
|
52
|
+
@running = false
|
53
|
+
end
|
54
|
+
|
55
|
+
def join
|
56
|
+
@thread.join
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# 送信処理の本体
|
62
|
+
def gossip(sock)
|
63
|
+
# 送信前にタイムスタンプを更新する
|
64
|
+
@self_node.update_timestamp
|
65
|
+
|
66
|
+
# ランダムで送信先を決定
|
67
|
+
dest = @node_list.choose_except(@self_node)
|
68
|
+
return unless dest # ないとは思うけど…
|
69
|
+
|
70
|
+
debug("Data is transmitted: address=#{dest.address}")
|
71
|
+
|
72
|
+
# チャンクに分けてデータを送信
|
73
|
+
@node_list.serialize.each do |chunk|
|
74
|
+
sock.send(chunk, 0, dest.address, @context.port)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end # Gossiper
|
79
|
+
|
80
|
+
end # RGossip2
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
module RGossip2
|
4
|
+
|
5
|
+
#
|
6
|
+
# class Node
|
7
|
+
# ノード情報を格納するクラス
|
8
|
+
# タイムアウトすると破棄される(=デッドリストに追加される)
|
9
|
+
#
|
10
|
+
# +------------+ +--------+ +-----------------------+
|
11
|
+
# | NodeList |<>---+---+| Node |<>---+---+| @node_list:NodeList |
|
12
|
+
# +------------+ | +--------+ | +-----------------------+
|
13
|
+
# +------------+ | | +-----------------------+
|
14
|
+
# | Receiver |<>---+ +---+| @dead_list:NodeList |
|
15
|
+
# +------------+ | | +-----------------------+
|
16
|
+
# +------------+ | | +---------+
|
17
|
+
# | Gossiper |<>---+ +---+| Timer |
|
18
|
+
# +------------+ +---------+
|
19
|
+
#
|
20
|
+
class Node
|
21
|
+
include ContextHelper
|
22
|
+
|
23
|
+
attr_reader :address
|
24
|
+
attr_accessor :timestamp
|
25
|
+
attr_accessor :data
|
26
|
+
|
27
|
+
# クラスの生成・初期化はContextクラスからのみ行う
|
28
|
+
# addressはユニークであること
|
29
|
+
def initialize(context, node_list, dead_list, address, data, timestamp)
|
30
|
+
@context = context
|
31
|
+
|
32
|
+
@node_list = node_list
|
33
|
+
@dead_list = dead_list
|
34
|
+
@address = address
|
35
|
+
@data = data
|
36
|
+
@timestamp = timestamp || ''
|
37
|
+
|
38
|
+
# node_lifetimeの時間内に更新されない場合
|
39
|
+
# TimerがNodeを破棄する
|
40
|
+
@timer = Timer.new(@context.node_lifetime) do
|
41
|
+
debug("Node timed out: address=#{@address}")
|
42
|
+
|
43
|
+
# ノードリストからNodeを削除
|
44
|
+
@node_list.synchronize {
|
45
|
+
@node_list.delete(@address)
|
46
|
+
}
|
47
|
+
|
48
|
+
# デッドリストにNodeを追加
|
49
|
+
@dead_list.synchronize {
|
50
|
+
@dead_list[@address] = self
|
51
|
+
}
|
52
|
+
|
53
|
+
# 破棄時の処理をコールバック
|
54
|
+
callback(:delete, @address, @timestamp, @data)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Nodeのタイムスタンプを更新
|
59
|
+
def update_timestamp
|
60
|
+
now = Time.now
|
61
|
+
@timestamp = "#{now.tv_sec}#{now.tv_usec}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Arrayへの変換
|
65
|
+
def to_a
|
66
|
+
[@address, @timestamp, @data]
|
67
|
+
end
|
68
|
+
alias to_ary to_a
|
69
|
+
|
70
|
+
def start_timer
|
71
|
+
debug("Node timer is started: address=#{@address}")
|
72
|
+
@timer.start
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset_timer
|
76
|
+
debug("Node timer is reset: address=#{@address}")
|
77
|
+
@timer.reset
|
78
|
+
end
|
79
|
+
|
80
|
+
def stop_timer
|
81
|
+
debug("Node timer is suspended: address=#{@address}")
|
82
|
+
@timer.stop
|
83
|
+
end
|
84
|
+
|
85
|
+
# ノード情報のシリアライズ
|
86
|
+
# ただし、シリアライズ後の長さを調べるだけで
|
87
|
+
# 実際のデータ送信には使われない
|
88
|
+
def serialize
|
89
|
+
self.to_a.to_msgpack
|
90
|
+
end
|
91
|
+
|
92
|
+
end # Node
|
93
|
+
|
94
|
+
end # RGossip2
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'openssl'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module RGossip2
|
6
|
+
|
7
|
+
#
|
8
|
+
# class NodeList
|
9
|
+
# Nodeのコンテナ
|
10
|
+
#
|
11
|
+
# +------------+ +------------+ +--------+
|
12
|
+
# | Gossiper |<>---+---+ | NodeList |<>-----+| Node |
|
13
|
+
# +------------+ | +------------+ +--------+
|
14
|
+
# +------------+ |
|
15
|
+
# | Receiver |<>---+
|
16
|
+
# +------------+
|
17
|
+
#
|
18
|
+
class NodeList
|
19
|
+
include ContextHelper
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
def initialize(context)
|
23
|
+
@context = context
|
24
|
+
@nodes = {}
|
25
|
+
@mutex = Mutex.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Hashに委譲
|
29
|
+
def_delegators :@nodes, :[], :[]=, :delete
|
30
|
+
|
31
|
+
# Nodeの配列でイテレートする
|
32
|
+
def each
|
33
|
+
@nodes.values.each do |i|
|
34
|
+
yield(i)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Mutex_mだとエラーになるので自前で定義
|
39
|
+
def synchronize
|
40
|
+
@mutex.synchronize do
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# 指定したNode以外のNodeをリストからランダムに選択する
|
46
|
+
def choose_except(node)
|
47
|
+
node_list = []
|
48
|
+
|
49
|
+
@nodes.each do |k, v|
|
50
|
+
node_list << v if k != node.address
|
51
|
+
end
|
52
|
+
|
53
|
+
node_list.empty? ? nil : node_list[rand(node_list.size)]
|
54
|
+
end
|
55
|
+
|
56
|
+
# ノード情報をいくつかの塊にごとにシリアライズする
|
57
|
+
def serialize
|
58
|
+
chunks = []
|
59
|
+
nodes = []
|
60
|
+
datasum = ''
|
61
|
+
|
62
|
+
# バッファサイズ
|
63
|
+
bufsiz = @context.buffer_size - @context.digest_length
|
64
|
+
|
65
|
+
# Nodeはランダムな順序に変換
|
66
|
+
@nodes.sort_by { rand }.each do |addr, node|
|
67
|
+
# 長さを知るためにシリアライズ
|
68
|
+
packed = node.serialize
|
69
|
+
|
70
|
+
# シリアライズしてバッファサイズ以下ならチャンクに追加
|
71
|
+
if (datasum + packed).length <= bufsiz
|
72
|
+
nodes << node
|
73
|
+
datasum << packed
|
74
|
+
else
|
75
|
+
chunks << digest_and_message(nodes).join
|
76
|
+
nodes.clear
|
77
|
+
datasum.replace('')
|
78
|
+
|
79
|
+
# バッファサイズを超える場合は次のチャンクに追加
|
80
|
+
redo
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# 残りのNodeをチャンクに追加
|
85
|
+
unless nodes.empty?
|
86
|
+
chunks << digest_and_message(nodes).join
|
87
|
+
end
|
88
|
+
|
89
|
+
return chunks
|
90
|
+
end # serialize
|
91
|
+
|
92
|
+
end # Nodes
|
93
|
+
|
94
|
+
end # RGossip2
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
module RGossip2
|
5
|
+
|
6
|
+
#
|
7
|
+
# class Receiver
|
8
|
+
# ゴシッププロトコルの受信クラス
|
9
|
+
#
|
10
|
+
# +------------+ +--------+
|
11
|
+
# | Receiver |<>---+---+| Node |
|
12
|
+
# +------------+ | +--------+
|
13
|
+
# | +-----------------------+
|
14
|
+
# +---+| @node_list:NodeList |
|
15
|
+
# | +-----------------------+
|
16
|
+
# | +-----------------------+
|
17
|
+
# +---+| @dead_list:NodeList |
|
18
|
+
# +-----------------------+
|
19
|
+
#
|
20
|
+
class Receiver
|
21
|
+
include ContextHelper
|
22
|
+
|
23
|
+
def initialize(context, self_node, node_list, dead_list)
|
24
|
+
@context = context
|
25
|
+
@self_node = self_node
|
26
|
+
@node_list = node_list
|
27
|
+
@dead_list = dead_list
|
28
|
+
end
|
29
|
+
|
30
|
+
def start
|
31
|
+
info("Reception is started: port=#{@context.port}")
|
32
|
+
|
33
|
+
@running = true
|
34
|
+
|
35
|
+
# パケット受信スレッドを開始
|
36
|
+
@thread = Thread.start {
|
37
|
+
begin
|
38
|
+
sock = UDPSocket.open
|
39
|
+
sock.bind(@self_node.address, @context.port)
|
40
|
+
|
41
|
+
while @running
|
42
|
+
receive(sock)
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
sock.close
|
46
|
+
end
|
47
|
+
}
|
48
|
+
end # start
|
49
|
+
|
50
|
+
def stop
|
51
|
+
info("Reception is stopped")
|
52
|
+
|
53
|
+
# フラグをfalseにしてスレッドを終了させる
|
54
|
+
@running = false
|
55
|
+
end
|
56
|
+
|
57
|
+
def join
|
58
|
+
@thread.join
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# 受信処理の本体
|
64
|
+
def receive(sock)
|
65
|
+
return unless select([sock], [], [], @context.receive_timeout)
|
66
|
+
message, (afam, port, host, ip) = sock.recvfrom(@context.buffer_size * @context.allowance)
|
67
|
+
|
68
|
+
debug("Data was received: from=#{ip}")
|
69
|
+
|
70
|
+
recv_nodes = unpack_message(message)
|
71
|
+
|
72
|
+
if recv_nodes
|
73
|
+
@node_list.synchronize {
|
74
|
+
merge_lists(recv_nodes)
|
75
|
+
}
|
76
|
+
else
|
77
|
+
# データが取得できなかった場合は無効なデータとして処理
|
78
|
+
debug("Invalid data was received: from=#{ip}")
|
79
|
+
end
|
80
|
+
rescue Exception => e
|
81
|
+
handle_error(e)
|
82
|
+
end
|
83
|
+
|
84
|
+
# ハッシュ値をチェックしてメッセージをデシリアライズ
|
85
|
+
def unpack_message(message)
|
86
|
+
recv_hash = message.slice!(0, @context.digest_length)
|
87
|
+
recv_nodes = MessagePack.unpack(message)
|
88
|
+
hash, xxx = digest_and_message(recv_nodes)
|
89
|
+
(recv_hash == hash) ? recv_nodes : nil
|
90
|
+
rescue MessagePack::UnpackError => e
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# ノードのマージ
|
95
|
+
def merge_lists(recv_nodes)
|
96
|
+
recv_nodes.each do |address, timestamp, data|
|
97
|
+
# 自分自身のアドレスならスキップ
|
98
|
+
next if address == @self_node.address
|
99
|
+
|
100
|
+
# ノードリストからアドレスの一致するNodeを探す
|
101
|
+
if (node = @node_list[address])
|
102
|
+
# ノードリストに見つかった場合
|
103
|
+
|
104
|
+
# 受信したNodeのタイムスタンプが新しければ
|
105
|
+
# 持っているNodeを更新
|
106
|
+
if timestamp > node.timestamp
|
107
|
+
debug("The node was updated: address=#{address} timestamp=#{timestamp}")
|
108
|
+
|
109
|
+
node.timestamp = timestamp
|
110
|
+
node.data = data
|
111
|
+
node.reset_timer
|
112
|
+
|
113
|
+
callback(:update, address, timestamp, data)
|
114
|
+
end
|
115
|
+
elsif (node = @dead_list.synchronize { @dead_list[address] })
|
116
|
+
# デッドリストに見つかった場合
|
117
|
+
@dead_list.synchronize {
|
118
|
+
# 受信したNodeのタイムスタンプが新しければ
|
119
|
+
# デッドリストのノードを復活させる
|
120
|
+
if timestamp > node.timestamp
|
121
|
+
debug("Node revived: address=#{address} timestamp=#{timestamp}")
|
122
|
+
|
123
|
+
@dead_list.delete(address)
|
124
|
+
@node_list[address] = node
|
125
|
+
node.start_timer
|
126
|
+
|
127
|
+
callback(:comeback, address, timestamp, data)
|
128
|
+
end
|
129
|
+
}
|
130
|
+
else
|
131
|
+
# リストにない場合はNodeを追加
|
132
|
+
debug("Node was added: address=#{address} timestamp=#{timestamp}")
|
133
|
+
|
134
|
+
node = create(Node, @node_list, @dead_list, address, data, timestamp)
|
135
|
+
@node_list[address] = node
|
136
|
+
node.start_timer
|
137
|
+
|
138
|
+
callback(:add, address, timestamp, data)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end # merge_lists
|
142
|
+
|
143
|
+
end # Receiver
|
144
|
+
|
145
|
+
end # RGossip2
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module RGossip2
|
2
|
+
|
3
|
+
#
|
4
|
+
# class Timer
|
5
|
+
# 一定時間でNodeを削除するためのクラス
|
6
|
+
# 唯一、Contextを参照しない
|
7
|
+
#
|
8
|
+
# +--------+ +---------+
|
9
|
+
# | Node |<>-----+| Timer |
|
10
|
+
# +--------+ +---------+
|
11
|
+
#
|
12
|
+
class Timer
|
13
|
+
|
14
|
+
def initialize(timeout, &block)
|
15
|
+
@timeout = timeout
|
16
|
+
@block = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def start
|
20
|
+
# 既存のスレッドは破棄
|
21
|
+
@thread.kill if alive?
|
22
|
+
@start_time = Time.now
|
23
|
+
|
24
|
+
# スタート時点の「開始時刻」を引数に渡す
|
25
|
+
@thread = Thread.start(@start_time) {|start_time|
|
26
|
+
loop do
|
27
|
+
# タイムアウトするまでスリープ
|
28
|
+
sleep @timeout
|
29
|
+
|
30
|
+
if @start_time == start_time
|
31
|
+
# 開始時刻が変わっていない=リセットされない場合
|
32
|
+
# 破棄の処理を呼び出してループを抜ける(=スレッドの終了)
|
33
|
+
@block.call
|
34
|
+
break
|
35
|
+
elsif @start_time.nil?
|
36
|
+
# Timerがストップされていた場合
|
37
|
+
# 何もしないでループを抜ける(=スレッドの終了)
|
38
|
+
break
|
39
|
+
else
|
40
|
+
# 開始時刻が更新された場合=リセットされた場合
|
41
|
+
# start_timeを更新してループを継続(=スレッドの継続)
|
42
|
+
start_time = @start_time
|
43
|
+
end
|
44
|
+
end # loop
|
45
|
+
} # Thread.start
|
46
|
+
end
|
47
|
+
|
48
|
+
# カウントダウンをリセットする
|
49
|
+
def reset
|
50
|
+
if alive?
|
51
|
+
@start_time = Time.now
|
52
|
+
@thread.run # 停止中のスレッドを強制起動してスリープ時間を更新
|
53
|
+
end
|
54
|
+
rescue ThreadError
|
55
|
+
# @thread.runで発生する可能性があるが無視
|
56
|
+
end
|
57
|
+
|
58
|
+
# カウントダウンを停止する
|
59
|
+
def stop
|
60
|
+
if alive?
|
61
|
+
@start_time = nil
|
62
|
+
@thread.run # 停止中のスレッドを強制起動して終了させる
|
63
|
+
end
|
64
|
+
rescue ThreadError
|
65
|
+
# @thread.runで発生する可能性があるが無視
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
# @thread: nil => Timerが開始されていない
|
70
|
+
# @thread: dead => Timerはすでに終了
|
71
|
+
def alive?
|
72
|
+
@thread and @thread.alive?
|
73
|
+
end
|
74
|
+
|
75
|
+
end # Timer
|
76
|
+
|
77
|
+
end # RGossip2
|
data/lib/rgossip2.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rgossip2/context'
|
2
|
+
require 'rgossip2/context_helper'
|
3
|
+
require 'rgossip2/client'
|
4
|
+
require 'rgossip2/node'
|
5
|
+
require 'rgossip2/node_list'
|
6
|
+
require 'rgossip2/gossipper'
|
7
|
+
require 'rgossip2/receiver'
|
8
|
+
require 'rgossip2/timer'
|
9
|
+
|
10
|
+
module RGossip2
|
11
|
+
|
12
|
+
# Clientの生成
|
13
|
+
# 直接、Client#newは実行しない
|
14
|
+
def client(options = {})
|
15
|
+
initial_nodes = options.delete(:initial_nodes) || []
|
16
|
+
address = options.delete(:address)
|
17
|
+
data = options.delete(:data)
|
18
|
+
|
19
|
+
context = Context.new(options)
|
20
|
+
Client.new(context, initial_nodes, address, data)
|
21
|
+
end
|
22
|
+
module_function :client
|
23
|
+
|
24
|
+
end # RGossip2
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rgossip2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- winebarrel
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-03 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: msgpack
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description:
|
35
|
+
email: sgwr_dts@yahoo.co.jp
|
36
|
+
executables:
|
37
|
+
- gossip
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- README
|
44
|
+
- bin/gossip
|
45
|
+
- lib/rgossip2.rb
|
46
|
+
- lib/rgossip2/timer.rb
|
47
|
+
- lib/rgossip2/gossipper.rb
|
48
|
+
- lib/rgossip2/client.rb
|
49
|
+
- lib/rgossip2/receiver.rb
|
50
|
+
- lib/rgossip2/context.rb
|
51
|
+
- lib/rgossip2/node.rb
|
52
|
+
- lib/rgossip2/context_helper.rb
|
53
|
+
- lib/rgossip2/node_list.rb
|
54
|
+
homepage: https://bitbucket.org/winebarrel/rgossip2
|
55
|
+
licenses: []
|
56
|
+
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
hash: 3
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.1
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Basic implementation of a gossip protocol. This is a porting of Java implementation. see http://code.google.com/p/gossip-protocol-java/
|
87
|
+
test_files: []
|
88
|
+
|