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 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
+