raft4r 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ *.swp
4
+ *.log
data/AUTHORS ADDED
@@ -0,0 +1 @@
1
+ Yuheng Chen <chyh1990@gmail.com>
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ Raft4r
2
+ ==========
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
data/bin/fsm_tool.rb ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'raft4r'
3
+
4
+ dsl = Raft4r::FSMDrawer.new
5
+ dsl.instance_eval File.read(ARGV[0]), ARGV[0]
6
+ #dsl.dump
7
+ puts dsl.to_dot
8
+
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'raft4r'
4
+ require 'yaml'
5
+
6
+ config = YAML.load(File.open('config/raft_cluster.yml').read)
7
+ server = Raft4r::RaftServer.new config, ARGV[0]
8
+ server.start_loop
@@ -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:
@@ -0,0 +1,14 @@
1
+ require 'raft4r'
2
+ require 'socket'
3
+
4
+ EM.run {
5
+ c = Raft4r::RPC::EMRPCClient.new '127.0.0.1', 7711, "n1"
6
+
7
+ 10.times {
8
+ c.AppendEntries :help do |resp|
9
+ p resp
10
+ EM.stop
11
+ end
12
+ }
13
+ }
14
+
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(&current.onleave) if current.onleave
68
+ set_current s
69
+ self.instance_eval(&current.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(&current.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: []