rloss 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Floss
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'logger'
4
+ require 'celluloid'
5
+
6
+ logfile = File.open(File.expand_path("../../log/test.log", __FILE__), 'w')
7
+ logfile.sync = true
8
+ Celluloid.logger = Logger.new(logfile)
9
+ Celluloid.shutdown_timeout = 1
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