oskie_rpc 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 83c95d50dce23208e4a87cd0fdc01b8e7e02cb3c
4
- data.tar.gz: e0b414711d5ab9b51479c149c6bd60e20ce5f5ea
3
+ metadata.gz: c78399fe3ca90c945e034f61daf677e6a722d376
4
+ data.tar.gz: d1584ec71a777b3dd6078e50acff5073dbf12ec1
5
5
  SHA512:
6
- metadata.gz: fca7464ec46d3c3fe859684b1e8deb0e78cb6db969b155cb346d545fff2eb707f2e1385a921bf9efc08b6049013bf389249efa491bf1bd156008f28f27ba86d9
7
- data.tar.gz: eb23f34873330d8f7fb78f5e3d327cab75fb59242d3bfd96bd1c0d4a0c0fb5abf636b0b42864041985c6e6c26d23c64e89def2df977391ee16b0e2a72ff8399e
6
+ metadata.gz: f357a816d8347f51871abf2d0e2e8e217afcbf81726f97f6a402e2bf4f4254e8ca4466b11c64e430e0af32f7c782ee44466cda06cbf558c226dbe9c7ad3d797e
7
+ data.tar.gz: 68dd5517d6d4df394e1800d133cb4b9a7b8ac45177f450bb71215ac17c7f4e9ff0bd0524c14da28ea78e1a6f0a42a6a931e8806fb8d028d6764094b0b1bfd82b
data/README.md CHANGED
@@ -3,12 +3,13 @@
3
3
  Oskie RPC is an extremely simple and modular RPC library for Ruby.
4
4
  Design goals include:
5
5
 
6
- - Transport independent so no dependencies on TCP/IP, IO, or any other classes (it simply takes input and gives back output through callbacks).
7
- - Modular design using the [Filter Chain](https://github.com/chadrem/filter_chain) gem so that the protocol can be modified if you so desire.
6
+ - Transport independent so no dependencies on TCP/IP, IO, or any other classes.
7
+ - Modular protocol design using the [Filter Chain](https://github.com/chadrem/filter_chain).
8
8
  - Supports both messages and requests (requests can be replied to, messages can't).
9
9
  - Very simple binary protocol with data encoded in JSON.
10
- - Thread safe.
11
10
  - Easy to port to other languages.
11
+ - Bi-directional.
12
+ - Thread safe.
12
13
 
13
14
  ## Installation
14
15
 
@@ -24,9 +25,69 @@ Or install it yourself as:
24
25
 
25
26
  $ gem install oskie_rpc
26
27
 
27
- ## Usage
28
+ ## Messages
29
+
30
+ The ````Message```` class is the basic unit of work for Oskie RPC.
31
+ Messages contain a few simple pieces of data:
32
+
33
+ - ````command````: This is how you distinguish different types of messages.
34
+ - ````params````: Optional data you pass in addition to a command.
35
+
36
+ Messages are fire-and-forget.
37
+
38
+ ## Requests
39
+
40
+ The ````Request```` class is a specialized type of message that expects a response.
41
+ You use them if you need a return value value from the other end.
42
+ Requests support timeouts (see below section on heartbeats).
43
+ The default timeout is 60 seconds.
44
+
45
+ ## Responses
46
+
47
+ The ````Response```` class is used to respond to a request.
48
+ In general you don't work directly with this class.
49
+ Responses are sent using the ````respond```` method on a request object.
50
+
51
+ ## Processors
52
+
53
+ The ````Processor```` class is the engine for Oskie RPC.
54
+ It is network agnostic and simply takes input, generates output, and executes callbacks.
55
+
56
+ # Create a processor and define its callbacks.
57
+ processor = OskieRpc::Processor.new do |p|
58
+ p.on(:message) do |message|
59
+ puts "Received message: #{message.inspect}"
60
+ end
61
+
62
+ p.on(:request) do |request|
63
+ puts "Received request: #{request.inspect}"
64
+ case request.command
65
+ when 'echo'
66
+ request.respond do
67
+ request.params # Last expression of the block is the return value.
68
+ end
69
+ end
70
+ end
71
+
72
+ p.on(:output) do |output|
73
+ puts "Generated output: #{output.inspect}"
74
+ end
75
+ end
76
+
77
+ # Send a message.
78
+ message = OskieRpc::Message.new('chat', :message => 'hello world')
79
+ processor.deliver(message)
80
+
81
+ # Simulate receiving a request (it will be echo'ed back).
82
+ processor << "\x00\x00\x00|{\"type\":\"rpcRequest\",\"request\":{\"command\":\"echo\",\"params\":{\"foo\":\"bar\"},\"messageId\":\"6c10b3ed-9f07-44c2-8f12-3e04cb7792e2\"}}"
83
+
84
+ #### Heartbeats
28
85
 
29
- Coming soon.
86
+ Processors require an external clock signal to properly timeout requests.
87
+ This is done on purpose so that you can integrate with the timers provided by your networking framework.
88
+ An alternative is to create your own clock using a dedicated thread that periodically wakes up.
89
+ Your clock should call the processor's ````heartbeat```` method to signal that time has changed.
90
+ A good rule of thumb is to call this method once per second.
30
91
 
31
92
  ## Contributing
32
93
 
@@ -2,4 +2,7 @@ module OskieRpc
2
2
  class OskieRpcError < RuntimeError; end
3
3
  class MissingCallbackError < OskieRpcError; end
4
4
  class InvalidStateError < OskieRpcError; end
5
+ class UnknownPayloadTypeError < OskieRpcError; end
6
+ class UnknownDeliveryClassError < OskieRpcError; end
7
+ class ValidationError < OskieRpcError; end
5
8
  end
@@ -1,23 +1,43 @@
1
1
  module OskieRpc
2
- class Message
3
- attr_reader :command
4
- attr_reader :uuid
5
- attr_reader :data
6
- attr_reader :processor
2
+ class Message < Package
3
+ attr_accessor :command
4
+ attr_accessor :params
7
5
 
8
- def initialize(command, opts = {})
6
+ def initialize(command = '', params = {})
9
7
  @command = command
10
- @uuid = opts[:uuid] || SecureRandom.uuid
11
- @data = opts[:data]
12
- @parser = opts[:processor]
8
+ @params = params
9
+ @message_id = SecureRandom.uuid
13
10
  end
14
11
 
15
- def to_hash
12
+ def load(payload)
13
+ @command = payload['message']['command']
14
+ @params = payload['message']['params']
15
+ @message_id = payload['message']['messageId']
16
+
17
+ validate!
18
+
19
+ self
20
+ end
21
+
22
+ def dump
23
+ validate!
24
+
16
25
  {
17
- :command => @command,
18
- :uuid => @uuid,
19
- :data => @data
26
+ 'type' => 'rpcMessage',
27
+ 'message' => {
28
+ 'command' => @command,
29
+ 'params' => @params,
30
+ 'messageId' => @message_id
31
+ }
20
32
  }
21
33
  end
34
+
35
+ def validate!
36
+ @command.is_a?(String) || raise(ValidationError, "Command is not a string.")
37
+ @params.is_a?(Hash) || raise(ValidationError, "Params is not a hash.")
38
+ @message_id.is_a?(String) || raise(ValidationError, "Message ID is not a string.")
39
+
40
+ nil
41
+ end
22
42
  end
23
43
  end
@@ -0,0 +1,5 @@
1
+ module OskieRpc
2
+ class Package
3
+ attr_accessor :message_id
4
+ end
5
+ end
@@ -1,33 +1,62 @@
1
1
  module OskieRpc
2
2
  class Processor
3
- def initialize(opts = {}, &block)
4
- @opts = opts
5
- @lock = Mutex.new
3
+ def initialize(&block)
4
+ @lock = Monitor.new
6
5
  @input_chain = create_input_chain
7
6
  @output_chain = create_output_chain
8
7
  @callbacks = {}
8
+ @requests = SortedSet.new
9
9
  @state = :initializing
10
10
  block.call(self) if block
11
11
  raise MissingCallbackError, :message unless @callbacks[:message]
12
+ raise MissingCallbackError, :request unless @callbacks[:request]
12
13
  raise MissingCallbackError, :output unless @callbacks[:output]
13
14
  @state = :initialized
14
15
  end
15
16
 
16
17
  def <<(bytes)
17
- @input_chain << bytes
18
+ @lock.synchronize do
19
+ @input_chain << bytes
20
+ end
18
21
 
19
22
  nil
20
23
  end
21
24
 
22
25
  def on(name, &block)
23
- @callbacks[name.to_sym] = block
26
+ @lock.synchronize do
27
+ raise InvalidStateError unless @state == :initializing
28
+ @callbacks[name.to_sym] = block
29
+ end
24
30
 
25
31
  nil
26
32
  end
27
33
 
28
- def deliver(message)
29
- raise InvalidStateError unless @state == :initialized
30
- @output_chain << message.to_hash
34
+ def deliver(delivery)
35
+ @lock.synchronize do
36
+ raise InvalidStateError unless @state == :initialized
37
+
38
+ if delivery.is_a?(Request)
39
+ delivery.__request_sent
40
+ @requests << delivery
41
+ end
42
+
43
+ @output_chain << delivery.dump
44
+ end
45
+
46
+ nil
47
+ end
48
+
49
+ def heartbeat
50
+ @lock.synchronize do
51
+ @requests.each do |request|
52
+ if request.timed_out?
53
+ @requests.delete(request)
54
+ request.__response_failed
55
+ else
56
+ return
57
+ end
58
+ end
59
+ end
31
60
 
32
61
  nil
33
62
  end
@@ -38,7 +67,8 @@ module OskieRpc
38
67
  FilterChain::Chain.new do |chain|
39
68
  chain.add(FilterChain::DemultiplexFilter.new)
40
69
  chain.add(FilterChain::DeserializeFilter.new(:format => :json))
41
- chain.add(FilterChain::Terminator.new { |message| message_handler(message) })
70
+ chain.add(FilterChain::ProcFilter.new { |payload| payload_handler(payload) })
71
+ chain.add(FilterChain::Terminator.new { |delivery| delivery_handler(delivery) })
42
72
  end
43
73
  end
44
74
 
@@ -50,10 +80,45 @@ module OskieRpc
50
80
  end
51
81
  end
52
82
 
83
+ def payload_handler(payload)
84
+ message = case payload['type']
85
+ when 'rpcMessage' then Message.new
86
+ when 'rpcRequest' then Request.new
87
+ when 'rpcResponse' then Response.new
88
+ else
89
+ raise UnknownPayloadTypeError, payload['type']
90
+ end
91
+
92
+ message.load(payload)
93
+ end
94
+
95
+ def delivery_handler(delivery)
96
+ case delivery
97
+ when Message then message_handler(delivery)
98
+ when Request then request_handler(delivery)
99
+ when Response then response_handler(delivery)
100
+ else
101
+ raise UnknownDeliveryClassError, message.class.name
102
+ end
103
+ end
104
+
53
105
  def message_handler(message)
54
106
  @callbacks[:message].call(message)
55
107
  end
56
108
 
109
+ def request_handler(request)
110
+ request.__request_received(self)
111
+ @callbacks[:request].call(request)
112
+ end
113
+
114
+ def response_handler(response)
115
+ request = @requests.find { |request| request.message_id == response.message_id }
116
+ return unless request
117
+ @requests.delete(request)
118
+ request.__response_received(response)
119
+ end
120
+
121
+
57
122
  def output_handler(bytes)
58
123
  @callbacks[:output].call(bytes)
59
124
  end
@@ -0,0 +1,96 @@
1
+ module OskieRpc
2
+ class Request < Package
3
+ attr_accessor :command
4
+ attr_accessor :params
5
+ attr_accessor :timeout # Seconds.
6
+
7
+ def initialize(command = '', params = {})
8
+ @command = command
9
+ @params = params
10
+ @timeout = 60
11
+ @message_id = SecureRandom.uuid
12
+ @callbacks = {}
13
+ @state = :initialized
14
+ end
15
+
16
+ def <=>(request)
17
+ timeout_at <=> request.timeout_at
18
+ end
19
+
20
+ def load(payload)
21
+ @command = payload['request']['command']
22
+ @params = payload['request']['params']
23
+ @message_id = payload['request']['messageId']
24
+
25
+ validate!
26
+
27
+ self
28
+ end
29
+
30
+ def dump
31
+ validate!
32
+
33
+ {
34
+ 'type' => 'rpcRequest',
35
+ 'request' => {
36
+ 'command' => @command,
37
+ 'params' => @params,
38
+ 'messageId' => @message_id
39
+ }
40
+ }
41
+ end
42
+
43
+ def on(name, &block)
44
+ @callbacks[name.to_sym] = block
45
+ end
46
+
47
+ def timed_out?
48
+ Time.now.utc > timeout_at
49
+ end
50
+
51
+ def timeout_at
52
+ @sent_at + timeout
53
+ end
54
+
55
+ def respond
56
+ raise InvalidStateError unless @state == :received
57
+ response = Response.new(message_id)
58
+ response.result = yield
59
+ @processor.deliver(response)
60
+
61
+ nil
62
+ end
63
+
64
+ def validate!
65
+ @command.is_a?(String) || raise(ValidationError, "Command is not a string.")
66
+ @params.is_a?(Hash) || raise(ValidationError, "Params is not a hash.")
67
+ @message_id.is_a?(String) || raise(ValidationError, "Message ID is not a string.")
68
+
69
+ nil
70
+ end
71
+
72
+ #
73
+ # Private API for use by Processor only.
74
+ #
75
+
76
+ def __request_sent
77
+ @state = :sent
78
+ @sent_at = Time.now.utc
79
+ end
80
+
81
+ def __request_received(processor)
82
+ @state = :received
83
+ @processor = processor
84
+ end
85
+
86
+ def __response_received(response)
87
+ @state = :responded
88
+ @callbacks[:response].call(response) if @callbacks[:response]
89
+ end
90
+
91
+ def __response_failed
92
+ @state = :failed
93
+ @callbacks[:failure].call if @callbacks[:failure]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,37 @@
1
+ module OskieRpc
2
+ class Response < Package
3
+ attr_accessor :result
4
+
5
+ def initialize(message_id = nil, result = nil)
6
+ @message_id = message_id
7
+ @result = result
8
+ end
9
+
10
+ def load(payload)
11
+ @result = payload['response']['result']
12
+ @message_id = payload['response']['messageId']
13
+
14
+ validate!
15
+
16
+ self
17
+ end
18
+
19
+ def dump
20
+ validate!
21
+
22
+ {
23
+ 'type' => 'rpcResponse',
24
+ 'response' => {
25
+ 'result' => @result,
26
+ 'messageId' => @message_id
27
+ }
28
+ }
29
+ end
30
+
31
+ def validate!
32
+ @message_id.is_a?(String) || raise(ValidationError, "Message ID is not a string.")
33
+
34
+ nil
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module OskieRpc
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/oskie_rpc.rb CHANGED
@@ -5,7 +5,10 @@ require "filter_chain"
5
5
 
6
6
  require "oskie_rpc/version"
7
7
  require "oskie_rpc/exceptions"
8
+ require "oskie_rpc/package"
8
9
  require "oskie_rpc/message"
10
+ require "oskie_rpc/request"
11
+ require "oskie_rpc/response"
9
12
  require "oskie_rpc/processor"
10
13
 
11
14
  module OskieRpc
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oskie_rpc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chad Remesch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-07-08 00:00:00.000000000 Z
11
+ date: 2015-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: filter_chain
@@ -84,7 +84,10 @@ files:
84
84
  - lib/oskie_rpc.rb
85
85
  - lib/oskie_rpc/exceptions.rb
86
86
  - lib/oskie_rpc/message.rb
87
+ - lib/oskie_rpc/package.rb
87
88
  - lib/oskie_rpc/processor.rb
89
+ - lib/oskie_rpc/request.rb
90
+ - lib/oskie_rpc/response.rb
88
91
  - lib/oskie_rpc/version.rb
89
92
  - oskie_rpc.gemspec
90
93
  homepage: