bones-rpc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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