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 +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:
|