rloss 0.0.1
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 +19 -0
- data/.rspec +1 -0
- data/.travis.yml +3 -0
- data/.yardopts +3 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +102 -0
- data/Rakefile +8 -0
- data/examples/distributed_hash.rb +36 -0
- data/lib/floss.rb +7 -0
- data/lib/floss/count_down_latch.rb +23 -0
- data/lib/floss/latch.rb +53 -0
- data/lib/floss/log.rb +69 -0
- data/lib/floss/log/simple.rb +55 -0
- data/lib/floss/log_replicator.rb +148 -0
- data/lib/floss/node.rb +366 -0
- data/lib/floss/one_off_latch.rb +23 -0
- data/lib/floss/peer.rb +32 -0
- data/lib/floss/proxy.rb +25 -0
- data/lib/floss/rpc.rb +22 -0
- data/lib/floss/rpc/in_memory.rb +39 -0
- data/lib/floss/rpc/zmq.rb +120 -0
- data/lib/floss/test_helper.rb +15 -0
- data/lib/floss/version.rb +3 -0
- data/rloss.gemspec +23 -0
- data/spec/functional/log_spec.rb +59 -0
- data/spec/functional/node_spec.rb +10 -0
- data/spec/functional/rpc_spec.rb +76 -0
- data/spec/spec_helper.rb +9 -0
- data/test.rb +51 -0
- metadata +136 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'floss/rpc'
|
2
|
+
|
3
|
+
class Floss::RPC::InMemory
|
4
|
+
class Client < Floss::RPC::Client
|
5
|
+
include Celluloid
|
6
|
+
|
7
|
+
attr_accessor :address
|
8
|
+
|
9
|
+
def initialize(address)
|
10
|
+
self.address = address
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(command, payload)
|
14
|
+
timeout(Floss::RPC::TIMEOUT) { actor.execute(command, payload) }
|
15
|
+
rescue Celluloid::DeadActorError, Celluloid::Task::TimeoutError
|
16
|
+
raise Floss::TimeoutError
|
17
|
+
end
|
18
|
+
|
19
|
+
def actor
|
20
|
+
Celluloid::Actor[address]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Server < Floss::RPC::Server
|
25
|
+
include Celluloid
|
26
|
+
|
27
|
+
execute_block_on_receiver :initialize
|
28
|
+
|
29
|
+
def initialize(address, &handler)
|
30
|
+
super
|
31
|
+
|
32
|
+
Actor[address] = Actor.current
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute(command, payload)
|
36
|
+
handler.call(command, payload)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'floss/rpc'
|
2
|
+
require 'celluloid/io/stream'
|
3
|
+
require 'celluloid/zmq'
|
4
|
+
|
5
|
+
class Floss::RPC::ZMQ
|
6
|
+
class Client < Floss::RPC::Client
|
7
|
+
include Celluloid::ZMQ
|
8
|
+
|
9
|
+
# @return [Celluloid::IO::Stream::Latch]
|
10
|
+
attr_accessor :latch
|
11
|
+
|
12
|
+
attr_accessor :address
|
13
|
+
|
14
|
+
# @return [Celluloid::ZMQ::ReqSocket]
|
15
|
+
attr_accessor :socket
|
16
|
+
|
17
|
+
# Disconnect when shutting the client down.
|
18
|
+
finalizer :disconnect
|
19
|
+
|
20
|
+
def initialize(address)
|
21
|
+
self.latch = Celluloid::IO::Stream::Latch.new
|
22
|
+
self.address = address
|
23
|
+
connect
|
24
|
+
end
|
25
|
+
|
26
|
+
def connect
|
27
|
+
self.socket = Celluloid::ZMQ::ReqSocket.new
|
28
|
+
socket.connect(address)
|
29
|
+
end
|
30
|
+
|
31
|
+
def disconnect
|
32
|
+
socket.close if socket
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(command, payload)
|
36
|
+
message = encode_request(command, payload)
|
37
|
+
response = latch.synchronize { request(message) }
|
38
|
+
decode_response(response)
|
39
|
+
end
|
40
|
+
|
41
|
+
def request(message)
|
42
|
+
timeout(Floss::RPC::TIMEOUT) do
|
43
|
+
socket.send(message)
|
44
|
+
socket.read
|
45
|
+
end
|
46
|
+
rescue Celluloid::Task::TimeoutError
|
47
|
+
disconnect
|
48
|
+
connect
|
49
|
+
abort Floss::TimeoutError.new("RPC timed out (#{address}).")
|
50
|
+
end
|
51
|
+
|
52
|
+
def encode_request(command, payload)
|
53
|
+
"#{command}:#{Marshal.dump(payload)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def decode_response(response)
|
57
|
+
Marshal.load(response)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Server < Floss::RPC::Server
|
62
|
+
include Celluloid::ZMQ
|
63
|
+
include Celluloid::Logger
|
64
|
+
|
65
|
+
attr_accessor :socket
|
66
|
+
|
67
|
+
execute_block_on_receiver :initialize
|
68
|
+
finalizer :finalize
|
69
|
+
|
70
|
+
def initialize(address, &handler)
|
71
|
+
super
|
72
|
+
async.run
|
73
|
+
end
|
74
|
+
|
75
|
+
def run
|
76
|
+
self.socket = RepSocket.new
|
77
|
+
|
78
|
+
begin
|
79
|
+
info("Binding to #{address}")
|
80
|
+
socket.bind(address)
|
81
|
+
rescue IOError
|
82
|
+
socket.close
|
83
|
+
raise
|
84
|
+
end
|
85
|
+
|
86
|
+
async.loop!
|
87
|
+
end
|
88
|
+
|
89
|
+
def loop!
|
90
|
+
loop { handle(socket.read) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param [String] request A request string containing command and payload separated by a colon.
|
94
|
+
def handle(request)
|
95
|
+
command, payload = decode_request(request)
|
96
|
+
response = handler.call(command, payload)
|
97
|
+
socket.send(encode_response(response))
|
98
|
+
end
|
99
|
+
|
100
|
+
def encode_response(response)
|
101
|
+
Marshal.dump(response)
|
102
|
+
end
|
103
|
+
|
104
|
+
def decode_request(request)
|
105
|
+
command, payload = request.split(':', 2)
|
106
|
+
payload = Marshal.load(payload)
|
107
|
+
|
108
|
+
[command.to_sym, payload]
|
109
|
+
end
|
110
|
+
|
111
|
+
def finalize
|
112
|
+
socket.close if socket
|
113
|
+
end
|
114
|
+
|
115
|
+
def terminate
|
116
|
+
super
|
117
|
+
socket.close if socket
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'floss'
|
2
|
+
|
3
|
+
module Floss::TestHelper
|
4
|
+
extend self
|
5
|
+
|
6
|
+
# Takes a list of node ids and yields a list of peers for each id.
|
7
|
+
def cluster(ids, &block)
|
8
|
+
cluster_size = ids.size
|
9
|
+
|
10
|
+
cluster_size.times.map do |i|
|
11
|
+
combination = ids.rotate(i)
|
12
|
+
block.call(combination.first, combination[1..-1])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/rloss.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/floss/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Robby Ranshous"]
|
6
|
+
gem.email = ["rranshous@gmail.com"]
|
7
|
+
gem.description = "Floss distributed consensus module for Celluloid"
|
8
|
+
gem.summary = "Floss is an implementation of the Raft distributed consensus protocol for Celluloid"
|
9
|
+
gem.homepage = "https://github.com/rranshous/rloss"
|
10
|
+
gem.license = 'MIT'
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "rloss"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = Floss::VERSION
|
18
|
+
|
19
|
+
gem.add_development_dependency 'rspec'
|
20
|
+
gem.add_development_dependency 'rake'
|
21
|
+
gem.add_runtime_dependency 'celluloid-zmq'
|
22
|
+
gem.add_runtime_dependency 'celluloid-io'
|
23
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'floss/log'
|
2
|
+
require 'floss/log/simple'
|
3
|
+
|
4
|
+
shared_examples 'a Log implementation' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@log = described_class.new {}
|
8
|
+
@log.remove_starting_with(0)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns empty when there are no entries' do
|
12
|
+
@log.should be_empty
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'appends entries' do
|
16
|
+
entries = [Floss::Log::Entry.new('command1',1),
|
17
|
+
Floss::Log::Entry.new('command2',1),
|
18
|
+
Floss::Log::Entry.new('command3',1)
|
19
|
+
]
|
20
|
+
@log.append(entries)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can return an entry by index' do
|
24
|
+
entries = [Floss::Log::Entry.new('command1',1)]
|
25
|
+
@log.append(entries)
|
26
|
+
entry = @log[0]
|
27
|
+
entry.command.should eql('command1')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'can return a range of entries' do
|
31
|
+
entries = [Floss::Log::Entry.new('command1',1),
|
32
|
+
Floss::Log::Entry.new('command2',1),
|
33
|
+
Floss::Log::Entry.new('command3',1)
|
34
|
+
]
|
35
|
+
@log.append(entries)
|
36
|
+
range = @log.starting_with(1)
|
37
|
+
range.size.should eql(2)
|
38
|
+
range[0].command.should eql('command2')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'can return index of the last entry' do
|
42
|
+
@log.append([Floss::Log::Entry.new('command1',1),
|
43
|
+
Floss::Log::Entry.new('command2',1)])
|
44
|
+
idx = @log.last_index
|
45
|
+
idx.should eql(1)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'returns the term of the last entry' do
|
49
|
+
@log.append([Floss::Log::Entry.new('command1',1),
|
50
|
+
Floss::Log::Entry.new('command2',1)])
|
51
|
+
term = @log.last_term
|
52
|
+
term.should eql(1)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
describe Floss::Log::Simple do
|
58
|
+
it_should_behave_like 'a Log implementation'
|
59
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'floss/node'
|
2
|
+
|
3
|
+
describe Floss::Node do
|
4
|
+
it "doesn't crash when all of its peers are down" do
|
5
|
+
opts = {id: 'tcp://127.0.0.1:7001', peers: ['tcp://127.0.0.1:7002', 'tcp://127.0.0.1:7003']}
|
6
|
+
node = described_class.new(opts)
|
7
|
+
sleep(node.broadcast_time)
|
8
|
+
expect(node).to be_alive
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'floss/rpc'
|
2
|
+
require 'floss/rpc/zmq'
|
3
|
+
require 'floss/rpc/in_memory'
|
4
|
+
|
5
|
+
class TestActor
|
6
|
+
include Celluloid
|
7
|
+
|
8
|
+
execute_block_on_receiver :exec
|
9
|
+
|
10
|
+
def exec
|
11
|
+
yield
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
shared_examples 'an RPC implementation' do
|
16
|
+
def actor_run(&block)
|
17
|
+
actor = TestActor.new
|
18
|
+
result = actor.exec(&block)
|
19
|
+
actor.terminate
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:server_class) { described_class::Server }
|
24
|
+
let(:client_class) { described_class::Client }
|
25
|
+
|
26
|
+
let(:command) { :command }
|
27
|
+
let(:payload) { Hash[key: 'value'] }
|
28
|
+
|
29
|
+
it 'executes calls' do
|
30
|
+
server = server_class.new(address) { |command, payload| [command, payload] }
|
31
|
+
client = client_class.new(address)
|
32
|
+
actor_run { client.call(command, payload) }.should eq([command, payload])
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'executes multiple calls sequentially' do
|
36
|
+
calls = 3.times.map { |i| [:command, i] }
|
37
|
+
server = server_class.new(address) { |command, payload| [command, payload] }
|
38
|
+
client = client_class.new(address)
|
39
|
+
actor_run { calls.map { |args| client.call(*args) } }.should eq(calls)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'raises an error if server is not available' do
|
43
|
+
client = client_class.new(address)
|
44
|
+
expect { actor_run { client.call(:command, :payload) } }.to raise_error(Floss::TimeoutError)
|
45
|
+
expect { client.to be_alive }
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises an error if server does not respond in time' do
|
49
|
+
server = server_class.new(address) { |command, payload| sleep 1.5; [command, payload] }
|
50
|
+
client = client_class.new(address)
|
51
|
+
expect { actor_run { client.call(:command, :payload) } }.to raise_error(Floss::TimeoutError)
|
52
|
+
expect { client.to be_alive }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe Floss::RPC::InMemory do
|
57
|
+
let(:address) { :node1 }
|
58
|
+
|
59
|
+
before(:each) do
|
60
|
+
Celluloid.shutdown
|
61
|
+
Celluloid.boot
|
62
|
+
end
|
63
|
+
|
64
|
+
it_should_behave_like 'an RPC implementation'
|
65
|
+
end
|
66
|
+
|
67
|
+
describe Floss::RPC::ZMQ do
|
68
|
+
let(:address) { 'tcp://127.0.0.1:12345' }
|
69
|
+
|
70
|
+
before(:each) do
|
71
|
+
Celluloid.shutdown
|
72
|
+
Celluloid.boot
|
73
|
+
end
|
74
|
+
|
75
|
+
it_should_behave_like 'an RPC implementation'
|
76
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/test.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
$: << File.expand_path('../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'floss/node'
|
4
|
+
|
5
|
+
CLUSTER_SIZE = 5
|
6
|
+
|
7
|
+
nodes = CLUSTER_SIZE.times.map do |i|
|
8
|
+
port = 50000 + i
|
9
|
+
"tcp://127.0.0.1:#{port}"
|
10
|
+
end
|
11
|
+
|
12
|
+
supervisor = Celluloid::SupervisionGroup.run!
|
13
|
+
|
14
|
+
CLUSTER_SIZE.times.map do |i|
|
15
|
+
combination = nodes.rotate(i)
|
16
|
+
options = {id: combination.first, peers: combination[1..-1]}
|
17
|
+
supervisor.supervise(Floss::Node, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
sleep 1
|
21
|
+
|
22
|
+
begin
|
23
|
+
leader = supervisor.actors.find(&:leader?)
|
24
|
+
puts "The leader is #{leader.id}"
|
25
|
+
|
26
|
+
leader.execute("Hello World!")
|
27
|
+
rescue => e
|
28
|
+
puts "Couldn't execute my command!"
|
29
|
+
p e
|
30
|
+
end
|
31
|
+
|
32
|
+
sleep 1
|
33
|
+
|
34
|
+
begin
|
35
|
+
leader = supervisor.actors.find(&:leader?)
|
36
|
+
puts "The leader is #{leader.id}"
|
37
|
+
|
38
|
+
leader.execute("Hello Again!")
|
39
|
+
rescue => e
|
40
|
+
puts "Couldn't execute my command!"
|
41
|
+
p e
|
42
|
+
end
|
43
|
+
|
44
|
+
sleep 0.5
|
45
|
+
|
46
|
+
supervisor.actors.each do |actor|
|
47
|
+
Celluloid.logger.info("Log of #{actor.id}: #{actor.log.entries}")
|
48
|
+
end
|
49
|
+
|
50
|
+
sleep 1
|
51
|
+
exit
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rloss
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robby Ranshous
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-12-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: celluloid-zmq
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: celluloid-io
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Floss distributed consensus module for Celluloid
|
70
|
+
email:
|
71
|
+
- rranshous@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".travis.yml"
|
79
|
+
- ".yardopts"
|
80
|
+
- Gemfile
|
81
|
+
- LICENSE
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- examples/distributed_hash.rb
|
85
|
+
- lib/floss.rb
|
86
|
+
- lib/floss/count_down_latch.rb
|
87
|
+
- lib/floss/latch.rb
|
88
|
+
- lib/floss/log.rb
|
89
|
+
- lib/floss/log/simple.rb
|
90
|
+
- lib/floss/log_replicator.rb
|
91
|
+
- lib/floss/node.rb
|
92
|
+
- lib/floss/one_off_latch.rb
|
93
|
+
- lib/floss/peer.rb
|
94
|
+
- lib/floss/proxy.rb
|
95
|
+
- lib/floss/rpc.rb
|
96
|
+
- lib/floss/rpc/in_memory.rb
|
97
|
+
- lib/floss/rpc/zmq.rb
|
98
|
+
- lib/floss/test_helper.rb
|
99
|
+
- lib/floss/version.rb
|
100
|
+
- log/.gitkeep
|
101
|
+
- rloss.gemspec
|
102
|
+
- spec/functional/log_spec.rb
|
103
|
+
- spec/functional/node_spec.rb
|
104
|
+
- spec/functional/rpc_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
- test.rb
|
107
|
+
homepage: https://github.com/rranshous/rloss
|
108
|
+
licenses:
|
109
|
+
- MIT
|
110
|
+
metadata: {}
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 2.5.2
|
128
|
+
signing_key:
|
129
|
+
specification_version: 4
|
130
|
+
summary: Floss is an implementation of the Raft distributed consensus protocol for
|
131
|
+
Celluloid
|
132
|
+
test_files:
|
133
|
+
- spec/functional/log_spec.rb
|
134
|
+
- spec/functional/node_spec.rb
|
135
|
+
- spec/functional/rpc_spec.rb
|
136
|
+
- spec/spec_helper.rb
|