rgossip2 0.1.0

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