protoplasm-blocking-client 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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in protoplasm.gemspec
4
+ gemspec :name => 'protoplasm-em-server'
5
+ gemspec :name => 'protoplasm-blocking-client'
6
+ gem 'bundler_push_host'
7
+ gem 'geminabox'
@@ -0,0 +1,101 @@
1
+ # Protoplasm
2
+
3
+ Protoplasm makes is easy to define an RPC server/client which is backed by protobuf through Beefcake.
4
+
5
+ ## Defining your service endpoints
6
+
7
+ The current service model is very simple. You can send only one type of protobuf object (the request object). This object must have an enum and a series of optional command fields to allow it to send commands. Here is an example of how to create a request object type:
8
+
9
+ ```ruby
10
+ class Command
11
+ include Beefcake::Message
12
+ module Type
13
+ PING = 1
14
+ UPCASE = 2
15
+ end
16
+ required :type, Type, 1
17
+ optional :ping_command, PingCommand, 2
18
+ optional :upcase_command, UpcaseCommand, 3
19
+ end
20
+ ```
21
+
22
+ In this case, your request object would be able to accept one, and only one subcommand object. Those types are `PingCommand`, `UpcaseCommand` and `EvenCommand`.
23
+
24
+ So, in order to mark this `Command` class as your request class, you'd do the following:
25
+
26
+ ```ruby
27
+ module Types
28
+ include Protoplasm::Types
29
+
30
+ # .. your actual classes would go here
31
+
32
+ request_class Command
33
+ request_type_field :type
34
+ end
35
+ ```
36
+
37
+ ## Defining your response objects
38
+
39
+ Every subcommand can choose to relay back no objects, one object, or stream any number of objects. Those objects must all be of the same type.
40
+
41
+ To define which objects you expect back, you must add the following.
42
+
43
+ ```ruby
44
+ module Types
45
+ rpc_map Command::Type::PING, :ping_command, nil
46
+ rpc_map Command::Type::UPCASE, :upcase_command, UpcaseResponse
47
+ rpc_map Command::Type::EVEN, :even_command, EvenResponse, :streaming => true
48
+ ```
49
+
50
+ In this case, this would define the ping command as returning no object, the upcase command returns a single object, of type `UpcaseResponse`, and the even command return any number of `EvenResponse` objects.
51
+
52
+ ## Server implementation
53
+
54
+ Currently there is a single server implementation `EMServer`, which defines a non-blocking EventMachine based server. To create an `EMServer`, you subclass `Protoplasm::EMServer` and setup handlers for each of your command types. For example, a worker server could look like this:
55
+
56
+ ```ruby
57
+ class Server < Protoplasm::EMServer
58
+ def process_ping_command(ping_command)
59
+ # do nothing
60
+ end
61
+
62
+ def process_upcase_command(upcase_command)
63
+ send_response(:response => cmd.word.upcase)
64
+ end
65
+
66
+ def process_even_command(even_command)
67
+ (1..even_command.top).each do |num|
68
+ send_response(:num => num) if num % 2 == 0
69
+ end
70
+ finish_streaming
71
+ end
72
+ end
73
+ ```
74
+
75
+ This server then could be started with `Server.start(3000)` which would start on port 3000 and process requests.
76
+
77
+ ## Client
78
+
79
+ Currently there is a single client implementation: `BlockingClient`. It defines a blocking `TCPSocket` based client. To create a client for this example, you would do the following.
80
+
81
+ ```ruby
82
+ class Client < Protoplasm::BlockingClient
83
+ def initialize(host, port)
84
+ super(Types, host, port)
85
+ end
86
+
87
+ def ping
88
+ send_request(:ping_command)
89
+ end
90
+
91
+ def upcase(word)
92
+ send_request(:upcase_command, :word => word).response
93
+ end
94
+
95
+ def evens(top)
96
+ send_request(:even_command, :top => top) { |resp| yield resp.num }
97
+ end
98
+ end
99
+ ```
100
+
101
+ Look at the full example under `test/test_helper.rb`.
@@ -0,0 +1,73 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ require './lib/protoplasm/version'
4
+
5
+ task :test do
6
+ Rake::TestTask.new do |t|
7
+ Dir['test/*_test.rb'].each{|f| require File.expand_path(f)}
8
+ end
9
+ end
10
+
11
+ def version
12
+ Protoplasm::VERSION
13
+ end
14
+
15
+ def version_tag
16
+ "v#{version}"
17
+ end
18
+
19
+ def tag_version
20
+ system("git tag -a -m \"Version #{version}\" #{version_tag}") or raise("Cannot tag version")
21
+ Bundler.ui.confirm "Tagged #{version_tag}"
22
+ yield
23
+ rescue
24
+ Bundler.ui.error "Untagged #{version_tag} due to error"
25
+ system("git tag -d #{version_tag}") or raise("Cannot untag version")
26
+ raise
27
+ end
28
+
29
+ desc "Release client & server (#{version})"
30
+ task :release do
31
+ tag_version do
32
+ Rake::Task["em_server:release_without_tagging"].invoke
33
+ Rake::Task["blocking_client:release_without_tagging"].invoke
34
+ end
35
+ end
36
+
37
+ desc "Install client & server (#{version})"
38
+ task :install do
39
+ Rake::Task["em_server:install"].invoke
40
+ Rake::Task["blocking_client:install"].invoke
41
+ end
42
+
43
+ desc "Build client & server (#{version})"
44
+ task :build do
45
+ Rake::Task["em_server:build"].invoke
46
+ Rake::Task["blocking_client:build"].invoke
47
+ end
48
+
49
+ namespace :em_server do
50
+ helper = Bundler::GemHelper.new(File.dirname(__FILE__), "protoplasm-em-server")
51
+ helper.install
52
+ helper.instance_eval do
53
+ task :release_without_tagging do
54
+ guard_clean
55
+ built_gem_path = build_gem
56
+ git_push
57
+ rubygem_push(built_gem_path)
58
+ end
59
+ end
60
+ end
61
+
62
+ namespace :blocking_client do
63
+ helper = Bundler::GemHelper.new(File.dirname(__FILE__), "protoplasm-blocking-client")
64
+ helper.install
65
+ helper.instance_eval do
66
+ task :release_without_tagging do
67
+ guard_clean
68
+ built_gem_path = build_gem
69
+ git_push
70
+ rubygem_push(built_gem_path)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ require "protoplasm/version"
2
+
3
+ # Protoplasm
4
+ # This defines BlockingClient and EMServer.
5
+ module Protoplasm
6
+ autoload :BlockingClient, "protoplasm/client/blocking_client"
7
+ autoload :EMServer, "protoplasm/server/em_server"
8
+ autoload :Types, "protoplasm/types/types"
9
+ end
@@ -0,0 +1,57 @@
1
+ require 'socket'
2
+
3
+ module Protoplasm
4
+ class BlockingClient
5
+ def self.for_types(types)
6
+ cls = Class.new(self)
7
+ cls.class_eval do
8
+ (class << self; self; end).send(:define_method, :_types) { types }
9
+ end
10
+ cls
11
+ end
12
+
13
+ private
14
+ def host_port
15
+ raise "Must be implemented by the client class"
16
+ end
17
+
18
+ def _socket
19
+ host, port = host_port
20
+ @_socket ||= TCPSocket.open(host, port)
21
+ end
22
+
23
+ def send_request(field, *args, &blk)
24
+ s = ''
25
+ type = self.class._types.request_type_for_field(field)
26
+ cmd_class = type.command_class.fields.values.find{|f| f.name == field}
27
+ cmd = self.class._types.request_class.new(self.class._types.request_type_field => type.type, type.field => cmd_class.type.new(*args))
28
+ cmd.encode(s)
29
+ socket = _socket
30
+ socket.write([0, s.size].pack("CQ"))
31
+ socket.write s
32
+ socket.flush
33
+ fetch_objects = true
34
+ obj = nil
35
+ while fetch_objects
36
+ response_code = socket.readpartial(1).unpack("C").first
37
+ case response_code
38
+ when Types::Response::NORMAL
39
+ fetch_objects = !type.void?
40
+ if fetch_objects
41
+ len_buf = ''
42
+ socket.readpartial(8 - len_buf.size, len_buf) while len_buf.size != 8
43
+ len = len_buf.unpack("Q").first
44
+ buf = ''
45
+ socket.readpartial(len - buf.size, buf) until buf.size == len
46
+ obj = type.response_class.decode(buf)
47
+ yield obj if block_given?
48
+ end
49
+ fetch_objects = false unless type.streaming?
50
+ when Types::Response::STOP_STREAMING
51
+ fetch_objects = false
52
+ end
53
+ end
54
+ obj
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,81 @@
1
+ require 'eventmachine'
2
+
3
+ module Protoplasm
4
+ class EMServer < EventMachine::Connection
5
+ def self.for_types(types)
6
+ cls = Class.new(self)
7
+ cls.class_eval do
8
+ (class << self; self; end).send(:define_method, :_types) { types }
9
+ end
10
+ cls
11
+ end
12
+
13
+
14
+ def self.start(port)
15
+ if EM.reactor_running?
16
+ EM::start_server("0.0.0.0", port, self) do |srv|
17
+ yield srv if block_given?
18
+ end
19
+ else
20
+ begin
21
+ EM.run do
22
+ start(port)
23
+ end
24
+ rescue Interrupt
25
+ end
26
+ end
27
+ end
28
+
29
+ def post_init
30
+ @_response_types = []
31
+ @data = ''
32
+ end
33
+
34
+ def receive_data(data)
35
+ @data << data
36
+ data_ready
37
+ end
38
+
39
+ def finish_streaming
40
+ @_response_types.shift
41
+ send_data [Types::Response::STOP_STREAMING].pack("C")
42
+ end
43
+
44
+ def send_void
45
+ @_response_types.shift
46
+ send_data [Types::Response::NORMAL].pack("C")
47
+ end
48
+
49
+ def data_ready
50
+ @control = @data.slice!(0, 1).unpack("C").first unless @control
51
+ case @control
52
+ when Types::Request::NORMAL
53
+ @size = @data.slice!(0, 8).unpack("Q").first unless @size
54
+ if @data.size >= @size
55
+ buf = @data.slice!(0, @size)
56
+ @size, @control = nil, nil
57
+ obj = self.class._types.request_class.decode(buf)
58
+ type = self.class._types.request_type_for_request(obj)
59
+ @_response_types << type
60
+ EM.next_tick do
61
+ send(:"process_#{type.field}", obj.send(type.field))
62
+ end
63
+ data_ready unless @data.empty?
64
+ end
65
+ else
66
+ # illegal char
67
+ close_connection
68
+ end
69
+ end
70
+
71
+ def send_response(*args)
72
+ type = @_response_types.first
73
+ @_response_types.shift unless type.streaming?
74
+ obj = type.response_class.new(*args)
75
+ s = ''
76
+ obj.encode(s)
77
+ send_data [Types::Response::NORMAL, s.size].pack("CQ")
78
+ send_data s
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,62 @@
1
+ require 'beefcake'
2
+
3
+ module Protoplasm
4
+ module Types
5
+ module Request
6
+ NORMAL = 0
7
+ end
8
+
9
+ module Response
10
+ NORMAL = 0
11
+ STOP_STREAMING = 10
12
+ end
13
+
14
+ def self.included(cls)
15
+ cls.extend(ClassMethods)
16
+ end
17
+
18
+ class RequestResponseType < Struct.new(:request_class, :response_class, :type, :field, :streaming)
19
+
20
+ alias_method :streaming?, :streaming
21
+
22
+ def command_class
23
+ request_class
24
+ end
25
+
26
+ def void?
27
+ response_class.nil?
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def request_class(request_class = nil)
33
+ request_class ? @request_class = request_class : @request_class
34
+ end
35
+
36
+ def request_type(request_obj)
37
+ request_obj.send(@request_type_field)
38
+ end
39
+
40
+ def request_type_field(field = nil)
41
+ field ? @request_type_field = field : @request_type_field
42
+ end
43
+
44
+ def rpc_map(type, field, response_class, opts = nil)
45
+ @response_map_by_field ||= {}
46
+ @response_map_by_type ||= {}
47
+ streaming = opts && opts.key?(:streaming) ? opts[:streaming] : false
48
+ rrt = RequestResponseType.new(@request_class, response_class, type, field, streaming)
49
+ @response_map_by_field[field] = rrt
50
+ @response_map_by_type[type] = rrt
51
+ end
52
+
53
+ def request_type_for_field(field)
54
+ @response_map_by_field[field]
55
+ end
56
+
57
+ def request_type_for_request(req)
58
+ @response_map_by_type[req.send(@request_type_field)]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ module Protoplasm
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "protoplasm/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "protoplasm-blocking-client"
7
+ s.version = Protoplasm::VERSION
8
+ s.authors = ["Josh Hull"]
9
+ s.email = ["joshbuddy@gmail.com"]
10
+ s.homepage = "https://github.com/bazaarlabs/protoplasm"
11
+ s.summary = %q{A blocking client for a Protoplasm server}
12
+ s.description = %q{A blocking client for a Protoplasm server.}
13
+
14
+ s.rubyforge_project = "protoplasm-blocking-client"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "beefcake", "~> 0.3.7"
22
+
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'minitest', "~> 2.6.1"
25
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "protoplasm/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "protoplasm-em-server"
7
+ s.version = Protoplasm::VERSION
8
+ s.authors = ["Josh Hull"]
9
+ s.email = ["joshbuddy@gmail.com"]
10
+ s.homepage = "https://github.com/bazaarlabs/protoplasm"
11
+ s.summary = %q{A protoplasm server backed by EventMachine}
12
+ s.description = %q{A protoplasm server backed by EventMachine.}
13
+
14
+ s.rubyforge_project = "protoplasm-em-server"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "eventmachine"
22
+ s.add_dependency "beefcake", "~> 0.3.7"
23
+
24
+ s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'minitest', "~> 2.6.1"
26
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Protoplasm test server" do
4
+ it "should ping" do
5
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
6
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
7
+ client.ping
8
+ pass
9
+ end
10
+ end
11
+
12
+ it "should upcase" do
13
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
14
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
15
+ assert_equal "LOWER", client.upcase('lower')
16
+ end
17
+ end
18
+
19
+ it "should give you even numbers" do
20
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
21
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
22
+ nums = []
23
+ client.evens(10) do |resp|
24
+ nums << resp
25
+ end
26
+ assert_equal [2, 4, 6, 8], nums
27
+ end
28
+ end
29
+
30
+ it "should allow multiple calls" do
31
+ with_proto_server(ProtoplasmTest::EMServer) do |port|
32
+ client = ProtoplasmTest::Client.new('127.0.0.1', port)
33
+ client.ping
34
+ assert_equal "LOWER", client.upcase('lower')
35
+ assert_equal "UPPER", client.upcase('upper')
36
+ nums = []
37
+ client.evens(10) do |resp|
38
+ nums << resp
39
+ end
40
+ assert_equal [2, 4, 6, 8], nums
41
+ client.ping
42
+ assert_equal "LOWER", client.upcase('lower')
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,122 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+ require 'protoplasm'
4
+ require 'timeout'
5
+
6
+ class ProtoplasmTest
7
+ module Types
8
+ include Protoplasm::Types
9
+
10
+ class PingCommand
11
+ include Beefcake::Message
12
+ end
13
+
14
+ class UpcaseCommand
15
+ include Beefcake::Message
16
+ required :word, :string, 1
17
+ end
18
+
19
+ class EvenCommand
20
+ include Beefcake::Message
21
+ required :top, :uint32, 1
22
+ end
23
+
24
+ class UpcaseResponse
25
+ include Beefcake::Message
26
+ required :response, :string, 1
27
+ end
28
+
29
+ class EvenResponse
30
+ include Beefcake::Message
31
+ required :num, :uint32, 1
32
+ end
33
+
34
+ class Command
35
+ include Beefcake::Message
36
+ module Type
37
+ PING = 1
38
+ UPCASE = 2
39
+ EVEN = 3
40
+ end
41
+ required :type, Type, 1
42
+ optional :ping_command, PingCommand, 2
43
+ optional :upcase_command, UpcaseCommand, 3
44
+ optional :even_command, EvenCommand, 4
45
+ end
46
+
47
+ request_class Command
48
+ request_type_field :type
49
+ rpc_map Command::Type::PING, :ping_command, nil
50
+ rpc_map Command::Type::UPCASE, :upcase_command, UpcaseResponse
51
+ rpc_map Command::Type::EVEN, :even_command, EvenResponse, :streaming => true
52
+ end
53
+
54
+ class EMServer < Protoplasm::EMServer.for_types(Types)
55
+ def process_upcase_command(cmd)
56
+ send_response(:response => cmd.word.upcase)
57
+ end
58
+
59
+ def process_even_command(cmd)
60
+ spit_out_even(1, cmd.top)
61
+ end
62
+
63
+ def process_ping_command(cmd)
64
+ send_void
65
+ end
66
+
67
+ def spit_out_even(cur, top)
68
+ EM.next_tick do
69
+ if cur == top
70
+ finish_streaming
71
+ else
72
+ if cur % 2 == 0
73
+ send_response(:num => cur)
74
+ end
75
+ spit_out_even(cur + 1, top)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ class Client < Protoplasm::BlockingClient.for_types(Types)
82
+ def initialize(host, port)
83
+ @host, @port = host, port
84
+ end
85
+
86
+ def ping
87
+ send_request(:ping_command)
88
+ end
89
+
90
+ def upcase(word)
91
+ send_request(:upcase_command, :word => word).response
92
+ end
93
+
94
+ def evens(top)
95
+ send_request(:even_command, :top => top) { |resp| yield resp.num }
96
+ end
97
+
98
+ def host_port
99
+ [@host, @port]
100
+ end
101
+ end
102
+ end
103
+
104
+ class MiniTest::Spec
105
+ def with_proto_server(cls)
106
+ port = 19866
107
+ pid = fork { cls.start(port) }
108
+ begin
109
+ Timeout.timeout(10.0) {
110
+ begin
111
+ TCPSocket.open("127.0.0.1", port).close
112
+ rescue
113
+ sleep(0.1)
114
+ retry
115
+ end
116
+ }
117
+ yield port
118
+ ensure
119
+ Process.kill("INT", pid) if pid
120
+ end
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protoplasm-blocking-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Josh Hull
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-29 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: beefcake
16
+ requirement: &70238713002280 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.3.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70238713002280
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70238713001380 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70238713001380
36
+ - !ruby/object:Gem::Dependency
37
+ name: minitest
38
+ requirement: &70238713000320 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.6.1
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70238713000320
47
+ description: A blocking client for a Protoplasm server.
48
+ email:
49
+ - joshbuddy@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - README.md
57
+ - Rakefile
58
+ - lib/protoplasm.rb
59
+ - lib/protoplasm/client/blocking_client.rb
60
+ - lib/protoplasm/server/em_server.rb
61
+ - lib/protoplasm/types/types.rb
62
+ - lib/protoplasm/version.rb
63
+ - protoplasm-blocking-client.gemspec
64
+ - protoplasm-em-server.gemspec
65
+ - test/protoplasm_test.rb
66
+ - test/test_helper.rb
67
+ homepage: https://github.com/bazaarlabs/protoplasm
68
+ licenses: []
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ segments:
80
+ - 0
81
+ hash: 380352482441046279
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ segments:
89
+ - 0
90
+ hash: 380352482441046279
91
+ requirements: []
92
+ rubyforge_project: protoplasm-blocking-client
93
+ rubygems_version: 1.8.10
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: A blocking client for a Protoplasm server
97
+ test_files:
98
+ - test/protoplasm_test.rb
99
+ - test/test_helper.rb