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