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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c192b7069e5371d2a5d9dfa867030ace9a8a0b90
4
- data.tar.gz: 4c53d4d29dc63263d475538aa6683581cb6c4b9d
3
+ metadata.gz: 2de3913524a4ba8c3270b96d5272f806c1b307e1
4
+ data.tar.gz: f2d2d81a77fed5dd7ed95a79e70e986c53c5bca4
5
5
  SHA512:
6
- metadata.gz: 1634ca417c9043b76ec5d6349a6a395f4412014519317bf39b54c8ebbbb0dbe0c9d7a80a6d73f1301d627d7a34525397af9921fa72520c1009e5aaf3b7309f98
7
- data.tar.gz: 2ccac8567795f76520072b914764f6620c9dd8f49182b4e6d7df72fbe0f98535d9acf4d5edff6817f4988ec8cd7c8cad4f35fcc945fbc7a3021b6b355a311645
6
+ metadata.gz: b33a3af2b4958527059766387e030d491d56ee05dbfc6e3141f57255f0c664680e575319fd2aa4cd38bbe7ae1466e6e00da17d82c111c81fc29bfa7c33b9a263
7
+ data.tar.gz: a92a7984a455c82071dd492b50fa87ad9d81aa86bb7a6059fdc51a707a056e4fc9b505aa62678d2c1689cfad228cbb1a1953459f01143a93fa55ae5fdae32032
data/.travis.yml CHANGED
@@ -2,9 +2,8 @@ sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
- - 2.2.1
6
- - 2.2.2
7
- - 2.2.3
5
+ - 2.1.10
6
+ - 2.2.5
8
7
  - 2.3.0
9
8
  - 2.3.1
10
9
  services:
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.0'
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'
@@ -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 in time
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']&.length || 0
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
 
@@ -14,7 +14,7 @@ module Fluffle
14
14
  end
15
15
 
16
16
  def connected?
17
- @connection&.connected?
17
+ @connection && @connection.connected?
18
18
  end
19
19
  end
20
20
  end
@@ -0,0 +1,8 @@
1
+ module Fluffle
2
+ class Railtie < Rails::Railtie
3
+ # Inherit the application's logger
4
+ initializer 'fluffle.configure_logger', after: 'initialize_logger' do |app|
5
+ Fluffle.logger = app.config.logger
6
+ end
7
+ end
8
+ end
@@ -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
- def initialize(url: nil)
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
- @queues = {}
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 |delivery_info, properties, payload|
39
- self.handle_request queue_name: name,
40
- handler: handler,
41
- delivery_info: delivery_info,
42
- properties: properties,
43
- payload: payload
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
- @channel.work_pool.join
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(queue_name:, handler:, delivery_info:, properties:, payload:)
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
- id, method, params = self.decode payload
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
- validate_request method: method
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
- @exchange.publish Oj.dump(response), routing_key: reply_to,
78
- correlation_id: id
143
+ response
79
144
  end
80
145
 
81
- protected
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
- payload = Oj.load payload
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("Missing `method' Request object member") unless request[:method]
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
@@ -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.inject_connectable
134
+ Fluffle::Testing.setup!
@@ -1,3 +1,3 @@
1
1
  module Fluffle
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
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
- # Expand a short name into a fully-qualified one
14
- def self.request_queue_name(name)
15
- "fluffle.requests.#{name}"
16
- end
13
+ class << self
14
+ attr_writer :logger
17
15
 
18
- def self.response_queue_name(name)
19
- "fluffle.responses.#{name}"
20
- end
16
+ def logger
17
+ @logger ||= Logger.new $stdout
18
+ end
21
19
 
22
- def self.logger
23
- @logger ||= Logger.new $stdout
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 queue_name: 'fakequeue',
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: { reply_to: reply_to }
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
@@ -1,3 +1,6 @@
1
1
  require 'bundler/setup'
2
+ require 'pry'
2
3
 
3
4
  require 'fluffle'
5
+
6
+ Fluffle.logger.level = Logger::Severity::FATAL
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.1.1
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 00:00:00.000000000 Z
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.0'
156
+ version: '2.1'
156
157
  required_rubygems_version: !ruby/object:Gem::Requirement
157
158
  requirements:
158
159
  - - ">="