oskie_rpc 0.1.0 → 0.2.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.
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: