raft4r 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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/AUTHORS +1 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/Rakefile +8 -0
- data/bin/fsm_tool.rb +8 -0
- data/bin/raft_server.rb +8 -0
- data/config/raft_cluster.yml +21 -0
- data/examples/example.fsm +32 -0
- data/examples/rpc_client.rb +14 -0
- data/lib/raft4r.rb +275 -0
- data/lib/raft4r/fsm.rb +134 -0
- data/lib/raft4r/rpc_base.rb +108 -0
- data/raft4r.gemspec +17 -0
- data/test/test_fsm.rb +56 -0
- data/test/test_raft_server.rb +13 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: deb0a7edb9cbae2e5d2031e35bc7bd7ffc4e5c89
|
4
|
+
data.tar.gz: e14075c2586165f5b047faca7c0428ac8ac564cc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 47417328d1a49738d41ed689de1feebf65bdab7ccd070cef7739e6faf8999cc452467d3d8d7fe4c0e261205e70ab739d8e478856ecd982fab05f98d137ef60cf
|
7
|
+
data.tar.gz: 69c4f7b8ce64706b9a4c5f8c8494009c81a53906ccafe4aa79823e2fa9f959d3440dbbe9a5429e002166db691eab794c240c52321666c3cc6dc32188680724b4
|
data/.gitignore
ADDED
data/AUTHORS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Yuheng Chen <chyh1990@gmail.com>
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2014-2015 Yuheng Chen
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/fsm_tool.rb
ADDED
data/bin/raft_server.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
n1:
|
2
|
+
bind: 127.0.0.1
|
3
|
+
port: 7701
|
4
|
+
n2:
|
5
|
+
bind: 127.0.0.1
|
6
|
+
port: 7702
|
7
|
+
n3:
|
8
|
+
bind: 127.0.0.1
|
9
|
+
port: 7703
|
10
|
+
n4:
|
11
|
+
bind: 127.0.0.1
|
12
|
+
port: 7704
|
13
|
+
n5:
|
14
|
+
bind: 127.0.0.1
|
15
|
+
port: 7705
|
16
|
+
n6:
|
17
|
+
bind: 127.0.0.1
|
18
|
+
port: 7706
|
19
|
+
n7:
|
20
|
+
bind: 127.0.0.1
|
21
|
+
port: 7707
|
@@ -0,0 +1,32 @@
|
|
1
|
+
def helper
|
2
|
+
puts "helper"
|
3
|
+
end
|
4
|
+
|
5
|
+
init :init
|
6
|
+
state :init do
|
7
|
+
enter do
|
8
|
+
@t = 0
|
9
|
+
end
|
10
|
+
trigger :t1 do
|
11
|
+
helper
|
12
|
+
goto :done
|
13
|
+
end
|
14
|
+
trigger :t2 do |arg|
|
15
|
+
@t = arg
|
16
|
+
goto :s1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
state :done do
|
21
|
+
enter do
|
22
|
+
@t = 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
state :s1 do
|
27
|
+
trigger :t3 do
|
28
|
+
goto :done
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# vim: set ft=ruby:
|
data/lib/raft4r.rb
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'logger'
|
3
|
+
require 'raft4r/rpc_base.rb'
|
4
|
+
require 'raft4r/fsm.rb'
|
5
|
+
|
6
|
+
module Raft4r
|
7
|
+
VERSION = '0.1.0'
|
8
|
+
LOGGER = Logger.new STDERR
|
9
|
+
|
10
|
+
RaftCluster = Struct.new :config, :conn
|
11
|
+
LogEntry = Struct.new :term, :log
|
12
|
+
class RaftHandler < FSM
|
13
|
+
#HEARTBEAT_TIMEOUT = 5
|
14
|
+
#HEARTBEAT_TIMEOUT = 3
|
15
|
+
#REELECT_TIMEOUT_MAX = 0.4
|
16
|
+
ELECTION_TIMEOUT_MIN_MS = 2000
|
17
|
+
HEARTBEAT_PER_TIMEOUT = 3
|
18
|
+
attr_reader :node_id, :config
|
19
|
+
|
20
|
+
include RPC::RPCMachine
|
21
|
+
def initialize config, node_id
|
22
|
+
@config = config
|
23
|
+
@node_id = node_id
|
24
|
+
@node_config = @config[node_id]
|
25
|
+
|
26
|
+
@last_heartbeat = 0
|
27
|
+
@cluster = {}
|
28
|
+
|
29
|
+
# XXX debug only
|
30
|
+
@election_timeout = (ELECTION_TIMEOUT_MIN_MS + rand(ELECTION_TIMEOUT_MIN_MS) ) / 1000.0
|
31
|
+
# persistent state
|
32
|
+
@current_term = 0
|
33
|
+
@vote_for = nil
|
34
|
+
@log = []
|
35
|
+
|
36
|
+
@log << LogEntry.new(@current_term, nil)
|
37
|
+
|
38
|
+
# volatile states
|
39
|
+
@commit_index = 0
|
40
|
+
@last_applied = 0
|
41
|
+
|
42
|
+
# leader volatile
|
43
|
+
@next_index = []
|
44
|
+
@match_index = []
|
45
|
+
|
46
|
+
# vote state
|
47
|
+
@get_votes = {}
|
48
|
+
@current_leader = nil
|
49
|
+
|
50
|
+
create_fsm
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_init
|
54
|
+
@config.each {|k,v|
|
55
|
+
next if k == @node_id
|
56
|
+
c = RaftCluster.new v, RPC::EMRPCClient.new(v['bind'], v['port'], @node_id)
|
57
|
+
@cluster[k] = c
|
58
|
+
}
|
59
|
+
info "init: election_timeout #{@election_timeout}s"
|
60
|
+
#p @cluster
|
61
|
+
EM::PeriodicTimer.new(5) { print_state }
|
62
|
+
|
63
|
+
# reset FSM
|
64
|
+
reset
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def create_fsm
|
69
|
+
init :follower
|
70
|
+
state :follower do
|
71
|
+
enter do
|
72
|
+
info "become follower"
|
73
|
+
reset_election_timer
|
74
|
+
end
|
75
|
+
|
76
|
+
trigger :election_timeout do
|
77
|
+
info "election timout by follower"
|
78
|
+
goto :candidate
|
79
|
+
end
|
80
|
+
|
81
|
+
trigger [:discover_higher_term, :discover_current_leader] do
|
82
|
+
# do nothing
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
state :candidate do
|
87
|
+
enter do
|
88
|
+
info "become candidate"
|
89
|
+
reset_election_timer
|
90
|
+
start_new_election
|
91
|
+
@current_leader = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
trigger :election_timeout do
|
95
|
+
info "election timout by candidate"
|
96
|
+
start_new_election
|
97
|
+
end
|
98
|
+
|
99
|
+
trigger [:discover_higher_term, :discover_current_leader] do
|
100
|
+
goto :follower
|
101
|
+
end
|
102
|
+
|
103
|
+
trigger :get_majority do
|
104
|
+
goto :leader
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
state :leader do
|
109
|
+
enter do
|
110
|
+
info 'become leader'
|
111
|
+
@current_leader = @node_id
|
112
|
+
on_timer_heartbeat
|
113
|
+
@heartbeat_timer = EM::PeriodicTimer.new(ELECTION_TIMEOUT_MIN_MS / 1000.0 / HEARTBEAT_PER_TIMEOUT) { on :heartbeat_timer }
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
trigger :election_timeout do
|
118
|
+
# do nothing
|
119
|
+
end
|
120
|
+
|
121
|
+
trigger :heartbeat_timer do
|
122
|
+
on_timer_heartbeat
|
123
|
+
end
|
124
|
+
|
125
|
+
trigger :discover_higher_term do
|
126
|
+
goto :follower
|
127
|
+
end
|
128
|
+
|
129
|
+
leave do
|
130
|
+
@heartbeat_timer.cancel
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def info str
|
136
|
+
LOGGER.info "#{@node_id}: #{str}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def print_state
|
140
|
+
info "State: #{current_state}, leader: #{@current_leader}, term: #{@current_term}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def set_term term
|
144
|
+
@current_term = term
|
145
|
+
@vote_for = nil
|
146
|
+
@get_votes = {}
|
147
|
+
#reset_election_timer
|
148
|
+
info "set term to #{@current_term}"
|
149
|
+
end
|
150
|
+
|
151
|
+
def reset_election_timer
|
152
|
+
@election_timer.cancel if @election_timer
|
153
|
+
@election_timer = EM::PeriodicTimer.new(@election_timeout) { on :election_timeout }
|
154
|
+
end
|
155
|
+
|
156
|
+
def on_vote node_id
|
157
|
+
@get_votes[node_id] = true
|
158
|
+
if @get_votes.size > @cluster.size / 2
|
159
|
+
info "get majority"
|
160
|
+
on :get_majority
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def start_new_election
|
165
|
+
fail 'ILLEGAL STATE' unless current_state == :candidate
|
166
|
+
info "start new election"
|
167
|
+
# no leader...
|
168
|
+
@current_leader = nil
|
169
|
+
@get_votes = 0
|
170
|
+
# reset election timer
|
171
|
+
|
172
|
+
set_term @current_term + 1
|
173
|
+
# random step back
|
174
|
+
#timeout = (100 + rand(200)) / 1000.0
|
175
|
+
# vote for self
|
176
|
+
@vote_for = @node_id
|
177
|
+
on_vote @node_id
|
178
|
+
|
179
|
+
@cluster.each {|k,v|
|
180
|
+
# XXX what if get reply in the future?
|
181
|
+
v.conn.RequestVote @current_term, @node_id, @log.size, @log.last.term do |req, resp|
|
182
|
+
next unless current_state == :candidate
|
183
|
+
info "Get vote from #{k}: #{resp.response[1]}"
|
184
|
+
on_vote resp.node_id if resp.response[1]
|
185
|
+
end
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
def on_timer_heartbeat
|
190
|
+
return unless current_state == :leader
|
191
|
+
|
192
|
+
#print_state
|
193
|
+
@cluster.each { |k,v|
|
194
|
+
# TODO
|
195
|
+
v.conn.AppendEntries @current_term, @node_id, 0, 0, nil, 0
|
196
|
+
}
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
def on_rpc_common req
|
201
|
+
if req.arguments[0] > @current_term
|
202
|
+
set_term req.arguments[0]
|
203
|
+
on :discover_higher_term
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
public
|
208
|
+
# on RPC request
|
209
|
+
def AppendEntries req
|
210
|
+
#info "AppendEntries from #{req.node_id}"
|
211
|
+
# check req
|
212
|
+
return [@current_term, false] if req.arguments[0] < @current_term
|
213
|
+
on_rpc_common req
|
214
|
+
# heartbeat from new leader
|
215
|
+
if req.arguments[0] == @current_term
|
216
|
+
# if state is candidate
|
217
|
+
on :discover_current_leader
|
218
|
+
end
|
219
|
+
|
220
|
+
reset_election_timer
|
221
|
+
@current_leader = req.node_id
|
222
|
+
return [@current_term, true]
|
223
|
+
end
|
224
|
+
|
225
|
+
def RequestVote req
|
226
|
+
info "RequestVote from #{req.node_id}"
|
227
|
+
return [@current_term, false] if req.arguments[0] < @current_term
|
228
|
+
on_rpc_common req
|
229
|
+
|
230
|
+
# or @vote_for == candidateId??
|
231
|
+
candidateId = req.arguments[1]
|
232
|
+
if @vote_for.nil? || @vote_for == candidateId
|
233
|
+
# if candidate is 'up-to-date'
|
234
|
+
vote = false
|
235
|
+
if @log.last.term < req.arguments[3]
|
236
|
+
vote = true
|
237
|
+
elsif @log.last.term == req.arguments[3]
|
238
|
+
# longer log wins
|
239
|
+
vote = req.arguments[2] >= @log.size
|
240
|
+
end
|
241
|
+
if vote
|
242
|
+
info "Vote for #{candidateId}"
|
243
|
+
@vote_for = candidateId
|
244
|
+
reset_election_timer
|
245
|
+
return [@current_term, true]
|
246
|
+
else
|
247
|
+
return [@current_term, false]
|
248
|
+
end
|
249
|
+
else
|
250
|
+
# this node already voted
|
251
|
+
return [@current_term, false]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
class RaftServer
|
258
|
+
# XXX handle cluster reconfig
|
259
|
+
def initialize config, node_id
|
260
|
+
s = config[node_id]
|
261
|
+
raise 'Node not found' unless s
|
262
|
+
|
263
|
+
@config = config
|
264
|
+
@node_id = node_id
|
265
|
+
@addr = s['bind']
|
266
|
+
@port = s['port']
|
267
|
+
LOGGER.info "Node: #{@node_id}, #{@addr}:#{@port}"
|
268
|
+
end
|
269
|
+
def start_loop
|
270
|
+
LOGGER.info "Start RaftServer #{@addr}:#{@port}..."
|
271
|
+
RPC::EMRPCServer.start_server @addr, @port, RaftHandler.new(@config, @node_id)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
data/lib/raft4r/fsm.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
module Raft4r
|
2
|
+
class StateScope
|
3
|
+
attr_reader :name, :triggers, :onenter, :onleave
|
4
|
+
def initialize name
|
5
|
+
@name = name
|
6
|
+
@triggers = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def trigger t, &block
|
10
|
+
t = [t] if !(Array === t)
|
11
|
+
t.each {|e|
|
12
|
+
raise 'trigger already exists' if @triggers[e]
|
13
|
+
@triggers[e] = block
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def enter &block
|
18
|
+
@onenter = block
|
19
|
+
end
|
20
|
+
|
21
|
+
def leave &block
|
22
|
+
@onleave = block
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class FSM
|
27
|
+
def initialize &block
|
28
|
+
self.instance_eval(&block) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
def current
|
32
|
+
curr = @fsm_states[@fsm_current]
|
33
|
+
raise "Invalid state #{s}" unless curr
|
34
|
+
curr
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_current s
|
38
|
+
raise "Invalid new state #{s}" unless @fsm_states[s]
|
39
|
+
@fsm_current = s
|
40
|
+
end
|
41
|
+
private :current, :set_current
|
42
|
+
|
43
|
+
def init st
|
44
|
+
@fsm_states = {}
|
45
|
+
@fsm_current = nil
|
46
|
+
@fsm_init = st
|
47
|
+
end
|
48
|
+
|
49
|
+
def state s, &block
|
50
|
+
raise 'State already exists' if @fsm_states[s]
|
51
|
+
ss = StateScope.new s
|
52
|
+
ss.instance_eval(&block)
|
53
|
+
@fsm_states[s] = ss
|
54
|
+
end
|
55
|
+
|
56
|
+
def on s, *arguments
|
57
|
+
curr = @fsm_states[@fsm_current]
|
58
|
+
if curr.triggers[s]
|
59
|
+
self.instance_exec(*arguments, &curr.triggers[s])
|
60
|
+
else
|
61
|
+
STDERR.puts "State: #{@fsm_current}, trigger #{s} not exists"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def goto s
|
66
|
+
return if s == @current
|
67
|
+
self.instance_eval(¤t.onleave) if current.onleave
|
68
|
+
set_current s
|
69
|
+
self.instance_eval(¤t.onenter) if current.onenter
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset
|
73
|
+
raise 'No init state' unless @fsm_init
|
74
|
+
set_current @fsm_init
|
75
|
+
self.instance_eval(¤t.onenter) if current.onenter
|
76
|
+
end
|
77
|
+
|
78
|
+
def dump
|
79
|
+
p self
|
80
|
+
end
|
81
|
+
|
82
|
+
def current_state
|
83
|
+
@fsm_current
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class FSMDrawer < BasicObject
|
88
|
+
def initialize
|
89
|
+
@init = nil
|
90
|
+
@states = {}
|
91
|
+
end
|
92
|
+
|
93
|
+
def init s
|
94
|
+
@init = s
|
95
|
+
end
|
96
|
+
|
97
|
+
def goto s
|
98
|
+
@triggers << s
|
99
|
+
end
|
100
|
+
|
101
|
+
def trigger t, &block
|
102
|
+
@triggers = @curr[t] || []
|
103
|
+
self.instance_eval &block
|
104
|
+
@curr[t] = @triggers
|
105
|
+
end
|
106
|
+
|
107
|
+
def state s, &block
|
108
|
+
@curr = {}
|
109
|
+
self.instance_eval &block
|
110
|
+
@states[s] = @curr
|
111
|
+
end
|
112
|
+
|
113
|
+
def dump
|
114
|
+
::Kernel::p @states
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_dot
|
118
|
+
s = []
|
119
|
+
s << "digraph graphname{"
|
120
|
+
@states.each {|k,v|
|
121
|
+
v.each {|tn, ns|
|
122
|
+
s << "\t#{k} -> #{ns.first} [label=\"#{tn}\"]"
|
123
|
+
}
|
124
|
+
}
|
125
|
+
s << "\t__init__ -> #{@init}"
|
126
|
+
s << "}"
|
127
|
+
s.join("\n")
|
128
|
+
end
|
129
|
+
|
130
|
+
def method_missing(name, *args, &block)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Raft4r
|
4
|
+
module RPC
|
5
|
+
# sender node_id
|
6
|
+
Request = Struct.new :node_id, :req_id, :method, :arguments
|
7
|
+
Response = Struct.new :node_id, :req_id, :code, :response
|
8
|
+
|
9
|
+
class Request
|
10
|
+
def to_id
|
11
|
+
"#{node_id}_#{req_id}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module RPCMachine
|
16
|
+
def call_method conn, r
|
17
|
+
@req_pool ||= Hash.new
|
18
|
+
# if the machine supports async call,
|
19
|
+
# try it first
|
20
|
+
if self.respond_to? :"Async#{r.method}"
|
21
|
+
@req_pool[r.to_id] = [conn, r]
|
22
|
+
self.__send__ r.method.to_sym, r
|
23
|
+
else
|
24
|
+
v = self.__send__ r.method.to_sym, r
|
25
|
+
p = Marshal.dump Response.new(@node_id, r.req_id, 0, v)
|
26
|
+
conn.send_data p
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def response_method req, resp
|
31
|
+
r = @req_pool[req.to_id]
|
32
|
+
return unless r
|
33
|
+
p = Marshal.dump Response.new(@node_id, req.req_id, 0, resp)
|
34
|
+
r[0].send_data p
|
35
|
+
#r[0].close_connection_after_writing
|
36
|
+
@req_pool.delete req.to_id
|
37
|
+
#LOGGER.info req.inspect
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class RPCConn < EventMachine::Connection
|
42
|
+
def initialize mach
|
43
|
+
super
|
44
|
+
@mach = mach
|
45
|
+
end
|
46
|
+
|
47
|
+
def post_init
|
48
|
+
#EM.add_periodic_timer(1) {puts "sec"}
|
49
|
+
end
|
50
|
+
|
51
|
+
def receive_data data
|
52
|
+
r = Marshal.load(data)
|
53
|
+
@mach.call_method self, r
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class EMRPCServer
|
58
|
+
def self.start_server addr, port, handler
|
59
|
+
EM.run {
|
60
|
+
handler.on_init if handler.respond_to? :on_init
|
61
|
+
us = EM.open_datagram_socket addr, port, RPCConn, handler
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class RPCClientConn < EventMachine::Connection
|
67
|
+
def initialize h
|
68
|
+
@h = h
|
69
|
+
end
|
70
|
+
def receive_data data
|
71
|
+
resp = Marshal.load(data)
|
72
|
+
req = @h.pending[resp.req_id]
|
73
|
+
return unless req
|
74
|
+
@h.pending.delete resp.req_id
|
75
|
+
req[1].call req[0], resp if req[1]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class EMRPCClient
|
80
|
+
attr_reader :pending
|
81
|
+
RPC_TIMEOUT = 1
|
82
|
+
def initialize addr, port, sender_node_id
|
83
|
+
@addr = addr
|
84
|
+
@port = port
|
85
|
+
@current_reqid = Time.now.to_i + rand(1000)
|
86
|
+
@pending = {}
|
87
|
+
@node_id = sender_node_id
|
88
|
+
@us = EM.open_datagram_socket '127.0.0.1', 0, RPCClientConn, self
|
89
|
+
EM.add_periodic_timer(RPC_TIMEOUT / 2) do
|
90
|
+
now = Time.now
|
91
|
+
s1 = @pending.size
|
92
|
+
@pending.delete_if {|k,v| now - v[2] > RPC_TIMEOUT }
|
93
|
+
timeout_rpcs = s1 - @pending.size
|
94
|
+
LOGGER.warn "timeout rpcs: #{timeout_rpcs}" if timeout_rpcs > 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def method_missing method_sym, *arguments, &block
|
99
|
+
@current_reqid += 1
|
100
|
+
req = Request.new(@node_id, @current_reqid, method_sym, arguments)
|
101
|
+
pack = Marshal.dump(req)
|
102
|
+
@pending[req.req_id] = [req, block, Time.now]
|
103
|
+
@us.send_datagram pack, @addr, @port
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
data/raft4r.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'raft4r'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.date = '2015-01-24'
|
5
|
+
s.summary = "Ruby Raft implementation"
|
6
|
+
s.description = "Ruby Raft implementation and services based on it."
|
7
|
+
s.authors = ["Yuheng Chen"]
|
8
|
+
s.email = 'chyh1990@gmail.com'
|
9
|
+
s.homepage = 'http://rubygems.org/gems/raft4r'
|
10
|
+
s.license = 'MIT'
|
11
|
+
|
12
|
+
s.files = `git ls-files`.split("\n")
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
|
15
|
+
s.add_dependency 'eventmachine', '~> 1.0'
|
16
|
+
s.add_development_dependency 'test-unit', '~> 3.0'
|
17
|
+
end
|
data/test/test_fsm.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'raft4r'
|
3
|
+
include Raft4r
|
4
|
+
class MyFSM < FSM
|
5
|
+
attr_reader :t
|
6
|
+
def do_helper
|
7
|
+
end
|
8
|
+
def initialize
|
9
|
+
init :init
|
10
|
+
state :init do
|
11
|
+
enter do
|
12
|
+
@t = 0
|
13
|
+
end
|
14
|
+
trigger :t1 do
|
15
|
+
goto :done
|
16
|
+
end
|
17
|
+
trigger :t2 do |arg|
|
18
|
+
do_helper
|
19
|
+
@t = arg
|
20
|
+
goto :s1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
state :done do
|
25
|
+
enter do
|
26
|
+
@t = 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
state :s1 do
|
31
|
+
trigger :t3 do
|
32
|
+
goto :done
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class FSMTest < Test::Unit::TestCase
|
39
|
+
def setup
|
40
|
+
@fsm = MyFSM.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_reset
|
44
|
+
@fsm.reset
|
45
|
+
assert_equal @fsm.t, 0
|
46
|
+
@fsm.on :t1
|
47
|
+
assert_equal @fsm.t, 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_on_arguments
|
51
|
+
@fsm.reset
|
52
|
+
assert_equal @fsm.t, 0
|
53
|
+
@fsm.on :t2, 3
|
54
|
+
assert_equal @fsm.t, 3
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'yaml'
|
3
|
+
require 'raft4r'
|
4
|
+
|
5
|
+
class RaftServerTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@config = YAML.load(File.open('config/raft_cluster.yml').read)
|
8
|
+
end
|
9
|
+
def test_start_server
|
10
|
+
server = Raft4r::RaftServer.new @config, 'n1'
|
11
|
+
server.start_loop
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: raft4r
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yuheng Chen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: eventmachine
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: test-unit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
description: Ruby Raft implementation and services based on it.
|
42
|
+
email: chyh1990@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- ".gitignore"
|
48
|
+
- AUTHORS
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- bin/fsm_tool.rb
|
54
|
+
- bin/raft_server.rb
|
55
|
+
- config/raft_cluster.yml
|
56
|
+
- examples/example.fsm
|
57
|
+
- examples/rpc_client.rb
|
58
|
+
- lib/raft4r.rb
|
59
|
+
- lib/raft4r/fsm.rb
|
60
|
+
- lib/raft4r/rpc_base.rb
|
61
|
+
- raft4r.gemspec
|
62
|
+
- test/test_fsm.rb
|
63
|
+
- test/test_raft_server.rb
|
64
|
+
homepage: http://rubygems.org/gems/raft4r
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.4.5
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: Ruby Raft implementation
|
88
|
+
test_files: []
|