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 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: []