bones-rpc 0.0.1

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +29 -0
  8. data/Rakefile +1 -0
  9. data/bones-rpc.gemspec +29 -0
  10. data/lib/bones-rpc.rb +2 -0
  11. data/lib/bones/rpc.rb +23 -0
  12. data/lib/bones/rpc/adapter.rb +49 -0
  13. data/lib/bones/rpc/adapter/base.rb +41 -0
  14. data/lib/bones/rpc/adapter/erlang.rb +28 -0
  15. data/lib/bones/rpc/adapter/json.rb +23 -0
  16. data/lib/bones/rpc/adapter/msgpack.rb +52 -0
  17. data/lib/bones/rpc/adapter/parser.rb +37 -0
  18. data/lib/bones/rpc/address.rb +167 -0
  19. data/lib/bones/rpc/cluster.rb +266 -0
  20. data/lib/bones/rpc/connection.rb +146 -0
  21. data/lib/bones/rpc/connection/reader.rb +49 -0
  22. data/lib/bones/rpc/connection/socket.rb +4 -0
  23. data/lib/bones/rpc/connection/socket/connectable.rb +196 -0
  24. data/lib/bones/rpc/connection/socket/ssl.rb +35 -0
  25. data/lib/bones/rpc/connection/socket/tcp.rb +28 -0
  26. data/lib/bones/rpc/connection/writer.rb +51 -0
  27. data/lib/bones/rpc/context.rb +48 -0
  28. data/lib/bones/rpc/errors.rb +33 -0
  29. data/lib/bones/rpc/failover.rb +38 -0
  30. data/lib/bones/rpc/failover/disconnect.rb +33 -0
  31. data/lib/bones/rpc/failover/ignore.rb +31 -0
  32. data/lib/bones/rpc/failover/retry.rb +39 -0
  33. data/lib/bones/rpc/future.rb +26 -0
  34. data/lib/bones/rpc/instrumentable.rb +41 -0
  35. data/lib/bones/rpc/instrumentable/log.rb +45 -0
  36. data/lib/bones/rpc/instrumentable/noop.rb +33 -0
  37. data/lib/bones/rpc/loggable.rb +112 -0
  38. data/lib/bones/rpc/node.rb +317 -0
  39. data/lib/bones/rpc/node/registry.rb +32 -0
  40. data/lib/bones/rpc/parser.rb +114 -0
  41. data/lib/bones/rpc/parser/buffer.rb +80 -0
  42. data/lib/bones/rpc/protocol.rb +106 -0
  43. data/lib/bones/rpc/protocol/acknowledge.rb +82 -0
  44. data/lib/bones/rpc/protocol/adapter_helper.rb +164 -0
  45. data/lib/bones/rpc/protocol/binary_helper.rb +431 -0
  46. data/lib/bones/rpc/protocol/ext_message.rb +86 -0
  47. data/lib/bones/rpc/protocol/notify.rb +38 -0
  48. data/lib/bones/rpc/protocol/request.rb +45 -0
  49. data/lib/bones/rpc/protocol/response.rb +58 -0
  50. data/lib/bones/rpc/protocol/synchronize.rb +70 -0
  51. data/lib/bones/rpc/read_preference.rb +43 -0
  52. data/lib/bones/rpc/read_preference/nearest.rb +57 -0
  53. data/lib/bones/rpc/read_preference/selectable.rb +81 -0
  54. data/lib/bones/rpc/readable.rb +57 -0
  55. data/lib/bones/rpc/session.rb +195 -0
  56. data/lib/bones/rpc/uri.rb +222 -0
  57. data/lib/bones/rpc/version.rb +6 -0
  58. metadata +198 -0
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ module Bones
3
+ module RPC
4
+ module Failover
5
+
6
+ # Ignore is for the case when we get exceptions we deem are proper user
7
+ # or datbase errors and should be re-raised.
8
+ #
9
+ # @since 2.0.0
10
+ module Ignore
11
+ extend self
12
+
13
+ # Executes the failover strategy. In the case of ignore, we just re-raise
14
+ # the exception that was thrown previously.
15
+ #
16
+ # @example Execute the ignore strategy.
17
+ # Bones::RPC::Failover::Ignore.execute(exception, node)
18
+ #
19
+ # @param [ Exception ] exception The raised exception.
20
+ # @param [ Node ] node The node the exception got raised on.
21
+ #
22
+ # @raise [ Exception ] The exception that was previously thrown.
23
+ #
24
+ # @since 2.0.0
25
+ def execute(exception, node)
26
+ raise(exception)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ module Bones
3
+ module RPC
4
+ module Failover
5
+
6
+ # Retry is for the case when we get exceptions around the connection, and
7
+ # want to make another attempt to try and resolve the issue.
8
+ #
9
+ # @since 2.0.0
10
+ module Retry
11
+ extend self
12
+
13
+ # Executes the failover strategy. In the case of retyr, we disconnect and
14
+ # reconnect, then try the operation one more time.
15
+ #
16
+ # @example Execute the retry strategy.
17
+ # Bones::RPC::Failover::Retry.execute(exception, node)
18
+ #
19
+ # @param [ Exception ] exception The raised exception.
20
+ # @param [ Node ] node The node the exception got raised on.
21
+ #
22
+ # @raise [ Errors::ConnectionFailure ] If the retry fails.
23
+ #
24
+ # @return [ Object ] The result of the block yield.
25
+ #
26
+ # @since 2.0.0
27
+ def execute(exception, node)
28
+ node.disconnect
29
+ begin
30
+ yield if block_given?
31
+ rescue Exception => e
32
+ node.down
33
+ raise(e)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ module Bones
3
+ module RPC
4
+ class Future < ::Celluloid::Future
5
+
6
+ def initialize(*args, &block)
7
+ @start = Time.now
8
+ super
9
+ end
10
+
11
+ def signal(*args, &block)
12
+ @stop = Time.now
13
+ super
14
+ end
15
+
16
+ def runtime
17
+ if @stop
18
+ @stop - @start
19
+ else
20
+ Time.now - @start
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ require 'bones/rpc/instrumentable/log'
3
+ require 'bones/rpc/instrumentable/noop'
4
+
5
+ module Bones
6
+ module RPC
7
+ module Instrumentable
8
+
9
+ # The name of the topic of operations for Bones::RPC.
10
+ #
11
+ # @since 2.0.0
12
+ TOPIC = "bones-rpc.operations"
13
+
14
+ # Topic for warning instrumentation.
15
+ #
16
+ # @since 2.0.0
17
+ WARN = "bones-rpc.warn"
18
+
19
+ # @!attribute instrumenter
20
+ # @return [ Object ] The instrumenter
21
+ attr_reader :instrumenter
22
+
23
+ # Instrument and execute the provided block.
24
+ #
25
+ # @example Instrument and execute.
26
+ # instrument("bones-rpc.noop") do
27
+ # node.connect
28
+ # end
29
+ #
30
+ # @param [ String ] name The name of the operation.
31
+ # @param [ Hash ] payload The payload.
32
+ #
33
+ # @return [ Object ] The result of the yield.
34
+ #
35
+ # @since 2.0.0
36
+ def instrument(name, payload = {}, &block)
37
+ instrumenter.instrument(name, payload, &block)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+ module Bones
3
+ module RPC
4
+ module Instrumentable
5
+
6
+ # Provides logging instrumentation for compatibility with active support
7
+ # notifications.
8
+ #
9
+ # @since 2.0.0
10
+ class Log
11
+
12
+ class << self
13
+
14
+ # Instrument the log payload.
15
+ #
16
+ # @example Instrument the log payload.
17
+ # Log.instrument("bones-rpc.ops", {})
18
+ #
19
+ # @param [ String ] name The name of the logging type.
20
+ # @param [ Hash ] payload The log payload.
21
+ #
22
+ # @return [ Object ] The result of the yield.
23
+ #
24
+ # @since 2.0.0
25
+ def instrument(name, payload = {})
26
+ started = Time.new
27
+ begin
28
+ yield if block_given?
29
+ rescue Exception => e
30
+ payload[:exception] = [ e.class.name, e.message ]
31
+ raise e
32
+ ensure
33
+ runtime = ("%.4fms" % (1000 * (Time.now.to_f - started.to_f)))
34
+ if name == TOPIC
35
+ Bones::RPC::Loggable.log_operations(payload[:prefix], payload[:ops], runtime)
36
+ else
37
+ Bones::RPC::Loggable.debug(payload[:prefix], payload.reject { |k,v| k == :prefix }, runtime)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ module Bones
3
+ module RPC
4
+ module Instrumentable
5
+
6
+ # Does not instrument anything, just yields.
7
+ #
8
+ # @since 2.0.0
9
+ class Noop
10
+
11
+ class << self
12
+
13
+ # Do not instrument anything.
14
+ #
15
+ # @example Do not instrument.
16
+ # Noop.instrument("bones-rpc.noop") do
17
+ # node.connect
18
+ # end
19
+ #
20
+ # @param [ String ] name The name of the operation.
21
+ # @param [ Hash ] payload The payload.
22
+ #
23
+ # @return [ Object ] The result of the yield.
24
+ #
25
+ # @since 2.0.0
26
+ def instrument(name, payload = {})
27
+ yield payload if block_given?
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: utf-8
2
+ module Bones
3
+ module RPC
4
+
5
+ # Contains behaviour for logging.
6
+ #
7
+ # @since 1.0.0
8
+ module Loggable
9
+
10
+ # Log the provided operations.
11
+ #
12
+ # @example Log the operations.
13
+ # Loggable.log_operations("BONES-RPC", {}, 30)
14
+ #
15
+ # @param [ String ] prefix The prefix for all operations in the log.
16
+ # @param [ Array ] ops The operations.
17
+ # @param [ String ] runtime The runtime in formatted ms.
18
+ #
19
+ # @since 2.0.0
20
+ def self.log_operations(prefix, ops, runtime)
21
+ indent = " "*prefix.length
22
+ if ops.length == 1
23
+ Bones::RPC.logger.debug([ prefix, ops.first.log_inspect, "runtime: #{runtime}" ].join(' '))
24
+ else
25
+ first, *middle, last = ops
26
+ Bones::RPC.logger.debug([ prefix, first.log_inspect ].join(' '))
27
+ middle.each { |m| Bones::RPC.logger.debug([ indent, m.log_inspect ].join(' ')) }
28
+ Bones::RPC.logger.debug([ indent, last.log_inspect, "runtime: #{runtime}" ].join(' '))
29
+ end
30
+ end
31
+
32
+ # Log the payload to debug.
33
+ #
34
+ # @example Log to debug.
35
+ # Loggable.debug("BONES-RPC", payload "30.012ms")
36
+ #
37
+ # @param [ String ] prefix The log prefix.
38
+ # @param [ String ] payload The log operations.
39
+ # @param [ String ] runtime The runtime in formatted ms.
40
+ #
41
+ # @since 2.0.0
42
+ def self.debug(prefix, payload, runtime)
43
+ Bones::RPC.logger.debug([ prefix, payload, "runtime: #{runtime}" ].join(' '))
44
+ end
45
+
46
+ # Log the payload to warn.
47
+ #
48
+ # @example Log to warn.
49
+ # Loggable.warn("BONES-RPC", payload "30.012ms")
50
+ #
51
+ # @param [ String ] prefix The log prefix.
52
+ # @param [ String ] payload The log operations.
53
+ # @param [ String ] runtime The runtime in formatted ms.
54
+ #
55
+ # @since 2.0.0
56
+ def self.warn(prefix, payload, runtime)
57
+ Bones::RPC.logger.warn([ prefix, payload, "runtime: #{runtime}" ].join(' '))
58
+ end
59
+
60
+ # Get the logger.
61
+ #
62
+ # @example Get the logger.
63
+ # Loggable.logger
64
+ #
65
+ # @return [ Logger ] The logger.
66
+ #
67
+ # @since 1.0.0
68
+ def logger
69
+ return @logger if defined?(@logger)
70
+ @logger = rails_logger || default_logger
71
+ end
72
+
73
+ # Get the rails logger.
74
+ #
75
+ # @example Get the rails logger.
76
+ # Loggable.rails_logger
77
+ #
78
+ # @return [ Logger ] The Rails logger.
79
+ #
80
+ # @since 1.0.0
81
+ def rails_logger
82
+ defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
83
+ end
84
+
85
+ # Get the default logger.
86
+ #
87
+ # @example Get the default logger.
88
+ # Loggable.default_logger
89
+ #
90
+ # @return [ Logger ] The default logger.
91
+ #
92
+ # @since 1.0.0
93
+ def default_logger
94
+ logger = Logger.new(STDOUT)
95
+ logger.level = Logger::INFO
96
+ logger
97
+ end
98
+
99
+ # Set the logger.
100
+ #
101
+ # @example Set the logger.
102
+ # Loggable.logger = logger
103
+ #
104
+ # @return [ Logger ] The logger.
105
+ #
106
+ # @since 1.0.0
107
+ def logger=(logger)
108
+ @logger = logger
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,317 @@
1
+ # encoding: utf-8
2
+ require 'bones/rpc/address'
3
+ require 'bones/rpc/connection'
4
+ require 'bones/rpc/failover'
5
+ require 'bones/rpc/future'
6
+ require 'bones/rpc/instrumentable'
7
+ require 'bones/rpc/node/registry'
8
+
9
+ module Bones
10
+ module RPC
11
+
12
+ # Represents a client to a node in a server cluster.
13
+ #
14
+ # @since 1.0.0
15
+ class Node
16
+ include Celluloid
17
+ include Instrumentable
18
+
19
+ # @!attribute address
20
+ # @return [ Address ] The address.
21
+ # @!attribute down_at
22
+ # @return [ Time ] The time the node was marked as down.
23
+ # @!attribute latency
24
+ # @return [ Integer ] The latency in milliseconds.
25
+ # @!attribute options
26
+ # @return [ Hash ] The node options.
27
+ # @!attribute refreshed_at
28
+ # @return [ Time ] The last time the node did a refresh.
29
+ attr_reader :cluster, :address, :down_at, :latency, :refreshed_at
30
+
31
+ # Is this node equal to another?
32
+ #
33
+ # @example Is the node equal to another.
34
+ # node == other
35
+ #
36
+ # @param [ Node ] other The other node.
37
+ #
38
+ # @return [ true, false ] If the addresses are equal.
39
+ #
40
+ # @since 1.0.0
41
+ def ==(other)
42
+ return false unless other.is_a?(Node)
43
+ id == other.id
44
+ end
45
+ alias :eql? :==
46
+
47
+ def adapter
48
+ @adapter ||= Adapter.get(options[:adapter] || :msgpack)
49
+ end
50
+
51
+ def attach(channel, id, future)
52
+ @registry.set(channel, id, future)
53
+ end
54
+
55
+ def cleanup_socket(socket)
56
+ @registry.flush
57
+ end
58
+
59
+ # Connect the node on the underlying connection.
60
+ #
61
+ # @example Connect the node.
62
+ # node.connect
63
+ #
64
+ # @raise [ Errors::ConnectionFailure ] If connection failed.
65
+ #
66
+ # @return [ true ] If the connection suceeded.
67
+ #
68
+ # @since 2.0.0
69
+ def connect
70
+ start = Time.now
71
+ @connection.connect
72
+ @latency = Time.now - start
73
+ @down_at = nil
74
+ true
75
+ end
76
+
77
+ # Is the node currently connected?
78
+ #
79
+ # @example Is the node connected?
80
+ # node.connected?
81
+ #
82
+ # @return [ true, false ] If the node is connected or not.
83
+ #
84
+ # @since 2.0.0
85
+ def connected?
86
+ @connection.alive?
87
+ end
88
+
89
+ def detach(channel, id)
90
+ @registry.get(channel, id)
91
+ end
92
+
93
+ # Force the node to disconnect from the server.
94
+ #
95
+ # @example Disconnect the node.
96
+ # node.disconnect
97
+ #
98
+ # @return [ true ] If the disconnection succeeded.
99
+ #
100
+ # @since 1.2.0
101
+ def disconnect
102
+ @connection.disconnect
103
+ true
104
+ end
105
+
106
+ # Is the node down?
107
+ #
108
+ # @example Is the node down?
109
+ # node.down?
110
+ #
111
+ # @return [ Time, nil ] The time the node went down, or nil if up.
112
+ #
113
+ # @since 1.0.0
114
+ def down?
115
+ !!@down_at
116
+ end
117
+
118
+ # Mark the node as down.
119
+ #
120
+ # @example Mark the node as down.
121
+ # node.down
122
+ #
123
+ # @return [ nil ] Nothing.
124
+ #
125
+ # @since 2.0.0
126
+ def down
127
+ @down_at = Time.new
128
+ @latency = nil
129
+ disconnect if connected?
130
+ end
131
+
132
+ # Yields the block if a connection can be established, retrying when a
133
+ # connection error is raised.
134
+ #
135
+ # @example Ensure we are connection.
136
+ # node.ensure_connected do
137
+ # #...
138
+ # end
139
+ #
140
+ # @raises [ ConnectionFailure ] When a connection cannot be established.
141
+ #
142
+ # @return [ nil ] nil.
143
+ #
144
+ # @since 1.0.0
145
+ def ensure_connected(&block)
146
+ begin
147
+ connect unless connected?
148
+ yield(current_actor)
149
+ rescue Exception => e
150
+ Failover.get(e).execute(e, current_actor, &block)
151
+ end
152
+ end
153
+
154
+ def handle_message(message)
155
+ logging(message) do
156
+ if future = message.get(current_actor)
157
+ message.signal(future)
158
+ end
159
+ end
160
+ end
161
+
162
+ def initialize(cluster, address)
163
+ @cluster = cluster
164
+ @address = address
165
+ @connection = Connection.new(current_actor)
166
+ @down_at = nil
167
+ @refreshed_at = nil
168
+ @latency = nil
169
+ @instrumenter = @cluster.options[:instrumenter] || Instrumentable::Log
170
+ @registry = Node::Registry.new
171
+ @request_id = 0
172
+ @synack_id = 0
173
+ @address.resolve(self)
174
+ end
175
+
176
+ # Get the node as a nice formatted string.
177
+ #
178
+ # @example Inspect the node.
179
+ # node.inspect
180
+ #
181
+ # @return [ String ] The string inspection.
182
+ #
183
+ # @since 1.0.0
184
+ def inspect
185
+ "<#{self.class.name} resolved_address=#{address.resolved.inspect}>"
186
+ end
187
+
188
+ # Does the node need to be refreshed?
189
+ #
190
+ # @example Does the node require refreshing?
191
+ # node.needs_refresh?(time)
192
+ #
193
+ # @param [ Time ] time The next referesh time.
194
+ #
195
+ # @return [ true, false] Whether the node needs to be refreshed.
196
+ #
197
+ # @since 1.0.0
198
+ def needs_refresh?(time)
199
+ !refreshed_at || refreshed_at < time
200
+ end
201
+
202
+ def notify(method, params)
203
+ without_future(Protocol::Notify.new(method, params))
204
+ end
205
+
206
+ def options
207
+ cluster.options
208
+ end
209
+
210
+ # Refresh information about the node, such as it's status in the replica
211
+ # set and it's known peers.
212
+ #
213
+ # @example Refresh the node.
214
+ # node.refresh
215
+ #
216
+ # @raise [ ConnectionFailure ] If the node cannot be reached.
217
+ #
218
+ # @raise [ ReplicaSetReconfigured ] If the node is no longer a primary node and
219
+ # refresh was called within an +#ensure_primary+ block.
220
+ #
221
+ # @return [ nil ] nil.
222
+ #
223
+ # @since 1.0.0
224
+ def refresh
225
+ if address.resolve(self)
226
+ begin
227
+ @refreshed_at = Time.now
228
+ if synchronize.value(timeout)
229
+ cluster.handle_refresh(current_actor)
230
+ else
231
+ down
232
+ end
233
+ rescue Timeout::Error
234
+ down
235
+ end
236
+ end
237
+ end
238
+
239
+ def request(method, params)
240
+ with_future(Protocol::Request.new(next_request_id, method, params))
241
+ end
242
+
243
+ def synchronize
244
+ with_future(Protocol::Synchronize.new(next_synack_id, adapter))
245
+ end
246
+
247
+ # Get the timeout, in seconds, for this node.
248
+ #
249
+ # @example Get the timeout in seconds.
250
+ # node.timeout
251
+ #
252
+ # @return [ Integer ] The configured timeout or the default of 5.
253
+ #
254
+ # @since 1.0.0
255
+ def timeout
256
+ @timeout ||= (options[:timeout] || 5)
257
+ end
258
+
259
+ private
260
+
261
+ # Yield the block with logging.
262
+ #
263
+ # @api private
264
+ #
265
+ # @example Yield with logging.
266
+ # logging(operations) do
267
+ # node.command(ismaster: 1)
268
+ # end
269
+ #
270
+ # @param [ Array<Message> ] operations The operations.
271
+ #
272
+ # @return [ Object ] The result of the yield.
273
+ #
274
+ # @since 2.0.0
275
+ def logging(message)
276
+ instrument(TOPIC, prefix: " BONES-RPC: #{address.resolved}", ops: [message]) do
277
+ yield if block_given?
278
+ end
279
+ end
280
+
281
+ def next_request_id
282
+ @request_id += 1
283
+ if @request_id >= 1 << 31
284
+ @request_id = 0
285
+ end
286
+ @request_id
287
+ end
288
+
289
+ def next_synack_id
290
+ @synack_id += 1
291
+ if @synack_id >= (1 << 32) - 1
292
+ @synack_id = 0
293
+ end
294
+ @synack_id
295
+ end
296
+
297
+ def process(message, future = nil)
298
+ logging(message) do
299
+ ensure_connected do
300
+ @connection.write([[message, future]])
301
+ end
302
+ end
303
+ return future
304
+ rescue Exception => e
305
+ abort(e)
306
+ end
307
+
308
+ def with_future(message)
309
+ process(message, Future.new)
310
+ end
311
+
312
+ def without_future(message)
313
+ process(message, nil)
314
+ end
315
+ end
316
+ end
317
+ end