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 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
  - - ">="