fluffle 0.1.1 → 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/.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
|
+
[![Build Status](https://travis-ci.org/Everlane/fluffle.svg?branch=master)](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
|
![](fluffle.jpg)
|
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
|
- - ">="
|