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.
@@ -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