fluffle 0.1.1 → 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/.travis.yml +2 -3
- data/README.md +2 -0
- data/fluffle.gemspec +1 -1
- data/lib/fluffle/client.rb +4 -3
- data/lib/fluffle/connectable.rb +1 -1
- data/lib/fluffle/railtie.rb +8 -0
- data/lib/fluffle/server.rb +118 -34
- data/lib/fluffle/testing.rb +15 -13
- data/lib/fluffle/version.rb +1 -1
- data/lib/fluffle.rb +15 -9
- data/spec/server_spec.rb +33 -4
- data/spec/spec_helper.rb +3 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2de3913524a4ba8c3270b96d5272f806c1b307e1
|
4
|
+
data.tar.gz: f2d2d81a77fed5dd7ed95a79e70e986c53c5bca4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b33a3af2b4958527059766387e030d491d56ee05dbfc6e3141f57255f0c664680e575319fd2aa4cd38bbe7ae1466e6e00da17d82c111c81fc29bfa7c33b9a263
|
7
|
+
data.tar.gz: a92a7984a455c82071dd492b50fa87ad9d81aa86bb7a6059fdc51a707a056e4fc9b505aa62678d2c1689cfad228cbb1a1953459f01143a93fa55ae5fdae32032
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# fluffle
|
2
2
|
|
3
|
+
[](https://travis-ci.org/Everlane/fluffle)
|
4
|
+
|
3
5
|
An implementation of [JSON-RPC][] over RabbitMQ through the [Bunny][] library. Provides both a client and server.
|
4
6
|
|
5
7
|

|
data/fluffle.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.homepage = 'https://github.com/Everlane/fluffle'
|
13
13
|
s.license = 'MIT'
|
14
14
|
|
15
|
-
s.required_ruby_version = '>= 2.
|
15
|
+
s.required_ruby_version = '>= 2.1'
|
16
16
|
|
17
17
|
s.add_dependency 'bunny', '~> 2.5.0'
|
18
18
|
s.add_dependency 'concurrent-ruby', '~> 1.0.2'
|
data/lib/fluffle/client.rb
CHANGED
@@ -90,8 +90,9 @@ module Fluffle
|
|
90
90
|
# After publishing it waits for the `IVar` to be set with the response.
|
91
91
|
# It also clears that `IVar` if it times out to avoid leaking.
|
92
92
|
#
|
93
|
-
# Returns a Hash from the JSON response from the server
|
94
|
-
# Raises TimeoutError if server failed to respond
|
93
|
+
# Returns a `Hash` from the JSON response from the server
|
94
|
+
# Raises `Fluffle::Errors::TimeoutError` if the server failed to respond
|
95
|
+
# within the given time in `timeout:`
|
95
96
|
def publish_and_wait(payload, queue:, timeout:)
|
96
97
|
id = payload['id']
|
97
98
|
|
@@ -104,7 +105,7 @@ module Fluffle
|
|
104
105
|
|
105
106
|
if ivar.incomplete?
|
106
107
|
method = payload['method']
|
107
|
-
arity = payload['params']
|
108
|
+
arity = (payload['params'] && payload['params'].length) || 0
|
108
109
|
raise Errors::TimeoutError.new("Timed out waiting for response to `#{method}/#{arity}'")
|
109
110
|
end
|
110
111
|
|
data/lib/fluffle/connectable.rb
CHANGED
data/lib/fluffle/server.rb
CHANGED
@@ -2,13 +2,15 @@ module Fluffle
|
|
2
2
|
class Server
|
3
3
|
include Connectable
|
4
4
|
|
5
|
-
attr_reader :connection, :handlers
|
5
|
+
attr_reader :connection, :handlers, :handler_pool
|
6
6
|
|
7
|
-
|
7
|
+
# url: - Optional URL to pass to `Bunny.new` to immediately connect
|
8
|
+
# concurrency: - Number of threads to handle messages on (default: 1)
|
9
|
+
def initialize(url: nil, concurrency: 1)
|
8
10
|
self.connect(url) if url
|
9
11
|
|
10
|
-
@handlers
|
11
|
-
@
|
12
|
+
@handlers = {}
|
13
|
+
@handler_pool = Concurrent::FixedThreadPool.new concurrency
|
12
14
|
|
13
15
|
self.class.default_server ||= self
|
14
16
|
end
|
@@ -31,38 +33,102 @@ module Fluffle
|
|
31
33
|
@channel = @connection.create_channel
|
32
34
|
@exchange = @channel.default_exchange
|
33
35
|
|
36
|
+
raise 'No handlers defined' if @handlers.empty?
|
37
|
+
|
34
38
|
@handlers.each do |name, handler|
|
35
39
|
qualified_name = Fluffle.request_queue_name name
|
36
40
|
queue = @channel.queue qualified_name
|
37
41
|
|
38
|
-
queue.subscribe do |
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
queue.subscribe do |_delivery_info, properties, payload|
|
43
|
+
@handler_pool.post do
|
44
|
+
self.handle_request handler: handler,
|
45
|
+
properties: properties,
|
46
|
+
payload: payload
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
self.wait_for_signal
|
52
|
+
end
|
53
|
+
|
54
|
+
# NOTE: Keeping this in its own method so its functionality can be more
|
55
|
+
# easily overwritten by `Fluffle::Testing`.
|
56
|
+
def wait_for_signal
|
57
|
+
signal_read, signal_write = IO.pipe
|
58
|
+
|
59
|
+
%w[INT TERM].each do |signal|
|
60
|
+
Signal.trap(signal) do
|
61
|
+
signal_write.puts signal
|
44
62
|
end
|
45
63
|
end
|
46
64
|
|
47
|
-
|
65
|
+
# Adapted from Sidekiq:
|
66
|
+
# https://github.com/mperham/sidekiq/blob/e634177/lib/sidekiq/cli.rb#L94-L97
|
67
|
+
while io = IO.select([signal_read])
|
68
|
+
readables = io.first
|
69
|
+
signal = readables.first.gets.strip
|
70
|
+
|
71
|
+
Fluffle.logger.info "Received #{signal}; shutting down..."
|
72
|
+
@channel.work_pool.shutdown
|
73
|
+
|
74
|
+
return
|
75
|
+
end
|
48
76
|
end
|
49
77
|
|
50
|
-
def handle_request(
|
51
|
-
id = nil
|
78
|
+
def handle_request(handler:, properties:, payload:)
|
52
79
|
reply_to = properties[:reply_to]
|
53
80
|
|
81
|
+
responses = []
|
82
|
+
|
54
83
|
begin
|
55
|
-
|
84
|
+
decoded = self.decode payload
|
85
|
+
|
86
|
+
requests =
|
87
|
+
if decoded.is_a? Hash
|
88
|
+
[ decoded ] # Single request
|
89
|
+
elsif decoded.is_a? Array
|
90
|
+
decoded # Batch request
|
91
|
+
else
|
92
|
+
raise Errors::InvalidRequestError.new('Payload was neither an Array nor an Object')
|
93
|
+
end
|
94
|
+
|
95
|
+
requests.each do |request|
|
96
|
+
response = self.call_handler handler: handler,
|
97
|
+
request: request
|
98
|
+
|
99
|
+
responses << response
|
100
|
+
end
|
101
|
+
rescue => err
|
102
|
+
responses << {
|
103
|
+
'jsonrpc' => '2.0',
|
104
|
+
'id' => nil,
|
105
|
+
'error' => self.build_error_response(err)
|
106
|
+
}
|
107
|
+
end
|
56
108
|
|
57
|
-
|
109
|
+
responses.each do |response|
|
110
|
+
@exchange.publish Oj.dump(response), routing_key: reply_to,
|
111
|
+
correlation_id: response['id']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# handler - Instance of a `Handler` that may receive `#call`
|
116
|
+
# request - `Hash` representing a decoded Request
|
117
|
+
def call_handler(handler:, request:)
|
118
|
+
begin
|
119
|
+
# We don't yet know if it's valid, so we have to be as cautious as
|
120
|
+
# possible about getting the ID
|
121
|
+
id = begin request['id']; rescue; nil end
|
122
|
+
|
123
|
+
self.validate_request request
|
58
124
|
|
59
125
|
result = handler.call id: id,
|
60
|
-
method: method,
|
61
|
-
params: params,
|
62
|
-
meta: {
|
63
|
-
reply_to: reply_to
|
64
|
-
}
|
126
|
+
method: request['method'],
|
127
|
+
params: request['params'],
|
128
|
+
meta: {}
|
65
129
|
rescue => err
|
130
|
+
log_error(err) if Fluffle.logger.error?
|
131
|
+
|
66
132
|
error = self.build_error_response err
|
67
133
|
end
|
68
134
|
|
@@ -74,25 +140,43 @@ module Fluffle
|
|
74
140
|
response['result'] = result
|
75
141
|
end
|
76
142
|
|
77
|
-
|
78
|
-
correlation_id: id
|
143
|
+
response
|
79
144
|
end
|
80
145
|
|
81
|
-
|
82
|
-
|
146
|
+
# Deserialize a JSON payload and extract its 3 members: id, method, params
|
147
|
+
#
|
148
|
+
# payload - `String` of the payload from the queue
|
149
|
+
#
|
150
|
+
# Returns a `Hash` from parsing the JSON payload (keys should be `String`)
|
83
151
|
def decode(payload)
|
84
|
-
|
85
|
-
|
86
|
-
id = payload['id']
|
87
|
-
method = payload['method']
|
88
|
-
params = payload['params']
|
89
|
-
|
90
|
-
[id, method, params]
|
152
|
+
Oj.load payload
|
91
153
|
end
|
92
154
|
|
93
|
-
# Raises if elements of the request do not comply with the spec
|
155
|
+
# Raises if elements of the request payload do not comply with the spec
|
156
|
+
#
|
157
|
+
# payload - Decoded `Hash` of the payload (`String` keys)
|
94
158
|
def validate_request(request)
|
95
|
-
raise Errors::InvalidRequestError.new("
|
159
|
+
raise Errors::InvalidRequestError.new("Improperly formatted Request (expected `Hash', got `#{request.class}')") unless request && request.is_a?(Hash)
|
160
|
+
raise Errors::InvalidRequestError.new("Missing `method' Request object member") unless request['method']
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
# Logs a nicely-formmated error to `Fluffle.logger` with the class,
|
166
|
+
# message, and backtrace (if available)
|
167
|
+
def log_error(err)
|
168
|
+
backtrace = Array(err.backtrace).flatten.compact
|
169
|
+
|
170
|
+
backtrace =
|
171
|
+
if backtrace.empty?
|
172
|
+
''
|
173
|
+
else
|
174
|
+
prefix = "\n "
|
175
|
+
prefix + backtrace.join(prefix)
|
176
|
+
end
|
177
|
+
|
178
|
+
message = "#{err.class}: #{err.message}#{backtrace}"
|
179
|
+
Fluffle.logger.error message
|
96
180
|
end
|
97
181
|
|
98
182
|
# Convert a Ruby error into a hash complying with the JSON-RPC spec
|
@@ -107,7 +191,7 @@ module Fluffle
|
|
107
191
|
else
|
108
192
|
response = {
|
109
193
|
'code' => 0,
|
110
|
-
'message' => err.message
|
194
|
+
'message' => "#{err.class}: #{err.message}"
|
111
195
|
}
|
112
196
|
|
113
197
|
response['data'] = err.data if err.respond_to? :data
|
data/lib/fluffle/testing.rb
CHANGED
@@ -2,6 +2,20 @@ require 'concurrent'
|
|
2
2
|
|
3
3
|
module Fluffle
|
4
4
|
module Testing
|
5
|
+
def self.setup!
|
6
|
+
# Inject our own custom `Connectable` implementation
|
7
|
+
[Fluffle::Client, Fluffle::Server].each do |mod|
|
8
|
+
mod.include Connectable
|
9
|
+
end
|
10
|
+
|
11
|
+
Fluffle::Server.class_eval do
|
12
|
+
# Overwriting this so that we don't actually block waiting for signal
|
13
|
+
def wait_for_signal
|
14
|
+
# pass
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
5
19
|
# Patch in a new `#connect` method that injects the loopback
|
6
20
|
module Connectable
|
7
21
|
def self.included(klass)
|
@@ -15,12 +29,6 @@ module Fluffle
|
|
15
29
|
end
|
16
30
|
end
|
17
31
|
|
18
|
-
def self.inject_connectable
|
19
|
-
[Fluffle::Client, Fluffle::Server].each do |mod|
|
20
|
-
mod.include Connectable
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
32
|
# Fake RabbitMQ server presented through a subset of the `Bunny`
|
25
33
|
# library's interface
|
26
34
|
class Loopback
|
@@ -119,14 +127,8 @@ module Fluffle
|
|
119
127
|
end
|
120
128
|
end
|
121
129
|
|
122
|
-
class WorkPool
|
123
|
-
# No-op in testing
|
124
|
-
def join
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
130
|
end # class LoopbackServer
|
129
131
|
end # module Testing
|
130
132
|
end # module Fluffle
|
131
133
|
|
132
|
-
Fluffle::Testing.
|
134
|
+
Fluffle::Testing.setup!
|
data/lib/fluffle/version.rb
CHANGED
data/lib/fluffle.rb
CHANGED
@@ -10,16 +10,22 @@ require 'fluffle/handlers/dispatcher'
|
|
10
10
|
require 'fluffle/server'
|
11
11
|
|
12
12
|
module Fluffle
|
13
|
-
|
14
|
-
|
15
|
-
"fluffle.requests.#{name}"
|
16
|
-
end
|
13
|
+
class << self
|
14
|
+
attr_writer :logger
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
def logger
|
17
|
+
@logger ||= Logger.new $stdout
|
18
|
+
end
|
21
19
|
|
22
|
-
|
23
|
-
|
20
|
+
# Expand a short name into a fully-qualified one
|
21
|
+
def request_queue_name(name)
|
22
|
+
"fluffle.requests.#{name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def response_queue_name(name)
|
26
|
+
"fluffle.responses.#{name}"
|
27
|
+
end
|
24
28
|
end
|
25
29
|
end
|
30
|
+
|
31
|
+
require 'fluffle/railtie' if defined? Rails
|
data/spec/server_spec.rb
CHANGED
@@ -36,9 +36,7 @@ describe Fluffle::Server do
|
|
36
36
|
payload['method'] = method unless payload.key? 'method'
|
37
37
|
payload['params'] = params unless payload.key? 'params'
|
38
38
|
|
39
|
-
subject.handle_request
|
40
|
-
handler: handler,
|
41
|
-
delivery_info: double('DeliveryInfo'),
|
39
|
+
subject.handle_request handler: handler,
|
42
40
|
properties: { reply_to: reply_to },
|
43
41
|
payload: Oj.dump(payload)
|
44
42
|
end
|
@@ -69,7 +67,7 @@ describe Fluffle::Server do
|
|
69
67
|
id: id,
|
70
68
|
method: method,
|
71
69
|
params: params,
|
72
|
-
meta: {
|
70
|
+
meta: {}
|
73
71
|
})
|
74
72
|
|
75
73
|
result
|
@@ -80,6 +78,37 @@ describe Fluffle::Server do
|
|
80
78
|
expect_response payload: { 'result' => result }
|
81
79
|
end
|
82
80
|
|
81
|
+
it 'responds with the correct individual results for a batch request' do
|
82
|
+
payload = [
|
83
|
+
{ 'jsonrpc' => '2.0', 'id' => 'first', 'method' => 'multiply', 'params' => [1] },
|
84
|
+
{ 'jsonrpc' => '2.0', 'id' => 'second', 'method' => 'multiply', 'params' => [2] }
|
85
|
+
]
|
86
|
+
|
87
|
+
handler = double 'Handler'
|
88
|
+
expect(handler).to receive(:call).twice do |args|
|
89
|
+
expect(args).to include({
|
90
|
+
id: anything,
|
91
|
+
method: 'multiply'
|
92
|
+
})
|
93
|
+
|
94
|
+
args[:params].first * 2
|
95
|
+
end
|
96
|
+
|
97
|
+
responses = []
|
98
|
+
|
99
|
+
allow(@exchange_spy).to receive(:publish).ordered do |payload_json, _opts|
|
100
|
+
responses << Oj.load(payload_json)
|
101
|
+
end
|
102
|
+
|
103
|
+
subject.handle_request handler: handler,
|
104
|
+
properties: { reply_to: reply_to },
|
105
|
+
payload: Oj.dump(payload)
|
106
|
+
|
107
|
+
expect(responses.length).to eq 2
|
108
|
+
expect(responses[0]).to include('id' => 'first', 'result' => 2)
|
109
|
+
expect(responses[1]).to include('id' => 'second', 'result' => 4)
|
110
|
+
end
|
111
|
+
|
83
112
|
it 'responds with the appropriate code and message when method not found' do
|
84
113
|
@method = 'notfound'
|
85
114
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluffle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dirk Gadsden
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bunny
|
@@ -134,6 +134,7 @@ files:
|
|
134
134
|
- lib/fluffle/handlers/base.rb
|
135
135
|
- lib/fluffle/handlers/delegator.rb
|
136
136
|
- lib/fluffle/handlers/dispatcher.rb
|
137
|
+
- lib/fluffle/railtie.rb
|
137
138
|
- lib/fluffle/server.rb
|
138
139
|
- lib/fluffle/testing.rb
|
139
140
|
- lib/fluffle/version.rb
|
@@ -152,7 +153,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
152
153
|
requirements:
|
153
154
|
- - ">="
|
154
155
|
- !ruby/object:Gem::Version
|
155
|
-
version: '2.
|
156
|
+
version: '2.1'
|
156
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
158
|
requirements:
|
158
159
|
- - ">="
|