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 +4 -4
- data/README.md +66 -5
- data/lib/oskie_rpc/exceptions.rb +3 -0
- data/lib/oskie_rpc/message.rb +33 -13
- data/lib/oskie_rpc/package.rb +5 -0
- data/lib/oskie_rpc/processor.rb +74 -9
- data/lib/oskie_rpc/request.rb +96 -0
- data/lib/oskie_rpc/response.rb +37 -0
- data/lib/oskie_rpc/version.rb +1 -1
- data/lib/oskie_rpc.rb +3 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c78399fe3ca90c945e034f61daf677e6a722d376
|
4
|
+
data.tar.gz: d1584ec71a777b3dd6078e50acff5073dbf12ec1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
7
|
-
- Modular design using the [Filter Chain](https://github.com/chadrem/filter_chain)
|
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
|
-
##
|
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
|
-
|
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
|
|
data/lib/oskie_rpc/exceptions.rb
CHANGED
@@ -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
|
data/lib/oskie_rpc/message.rb
CHANGED
@@ -1,23 +1,43 @@
|
|
1
1
|
module OskieRpc
|
2
|
-
class Message
|
3
|
-
|
4
|
-
|
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,
|
6
|
+
def initialize(command = '', params = {})
|
9
7
|
@command = command
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@parser = opts[:processor]
|
8
|
+
@params = params
|
9
|
+
@message_id = SecureRandom.uuid
|
13
10
|
end
|
14
11
|
|
15
|
-
def
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/oskie_rpc/processor.rb
CHANGED
@@ -1,33 +1,62 @@
|
|
1
1
|
module OskieRpc
|
2
2
|
class Processor
|
3
|
-
def initialize(
|
4
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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(
|
29
|
-
|
30
|
-
|
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::
|
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
|
data/lib/oskie_rpc/version.rb
CHANGED
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.
|
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-
|
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:
|