moped 1.0.0.alpha → 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of moped might be problematic. Click here for more details.

@@ -56,12 +56,10 @@ module Moped
56
56
  # @param [Array<Hash>] documents the documents to insert
57
57
  def insert(documents)
58
58
  documents = [documents] unless documents.is_a? Array
59
- insert = Protocol::Insert.new(database.name, name, documents)
60
59
 
61
60
  database.session.with(consistency: :strong) do |session|
62
- session.execute insert
61
+ session.context.insert(database.name, name, documents)
63
62
  end
64
-
65
63
  end
66
64
  end
67
65
  end
@@ -0,0 +1,95 @@
1
+ require "timeout"
2
+
3
+ module Moped
4
+ class Connection
5
+
6
+ class TCPSocket < ::TCPSocket
7
+ def self.connect(host, port, timeout)
8
+ Timeout::timeout(timeout) do
9
+ new(host, port).tap do |sock|
10
+ sock.set_encoding 'binary'
11
+ end
12
+ end
13
+ end
14
+
15
+ def alive?
16
+ if Kernel::select([self], nil, nil, 0)
17
+ !eof? rescue false
18
+ else
19
+ true
20
+ end
21
+ end
22
+
23
+ def write(*args)
24
+ raise Errors::ConnectionFailure, "Socket connection was closed by remote host" unless alive?
25
+ super
26
+ end
27
+ end
28
+
29
+ def initialize
30
+ @sock = nil
31
+ @request_id = 0
32
+ end
33
+
34
+ def connect(host, port, timeout)
35
+ @sock = TCPSocket.connect host, port, timeout
36
+ end
37
+
38
+ def alive?
39
+ connected? ? @sock.alive? : false
40
+ end
41
+
42
+ def connected?
43
+ !!@sock
44
+ end
45
+
46
+ def disconnect
47
+ @sock.close
48
+ rescue
49
+ ensure
50
+ @sock = nil
51
+ end
52
+
53
+ def write(operations)
54
+ buf = ""
55
+
56
+ operations.each do |operation|
57
+ operation.request_id = (@request_id += 1)
58
+ operation.serialize(buf)
59
+ end
60
+
61
+ @sock.write buf
62
+ end
63
+
64
+ def receive_replies(operations)
65
+ operations.map do |operation|
66
+ read if operation.is_a?(Protocol::Query) || operation.is_a?(Protocol::GetMore)
67
+ end
68
+ end
69
+
70
+ def read
71
+ reply = Protocol::Reply.allocate
72
+
73
+ reply.length,
74
+ reply.request_id,
75
+ reply.response_to,
76
+ reply.op_code,
77
+ reply.flags,
78
+ reply.cursor_id,
79
+ reply.offset,
80
+ reply.count = @sock.read(36).unpack('l5<q<l2<')
81
+
82
+ if reply.count == 0
83
+ reply.documents = []
84
+ else
85
+ buffer = StringIO.new(@sock.read(reply.length - 36))
86
+
87
+ reply.documents = reply.count.times.map do
88
+ BSON::Document.deserialize(buffer)
89
+ end
90
+ end
91
+
92
+ reply
93
+ end
94
+ end
95
+ end
@@ -10,50 +10,69 @@ module Moped
10
10
 
11
11
  def initialize(session, query_operation)
12
12
  @session = session
13
- @query_op = query_operation.dup
14
13
 
15
- @get_more_op = Protocol::GetMore.new(
16
- @query_op.database,
17
- @query_op.collection,
18
- 0,
19
- @query_op.limit
20
- )
14
+ @database = query_operation.database
15
+ @collection = query_operation.collection
16
+ @selector = query_operation.selector
21
17
 
22
- @kill_cursor_op = Protocol::KillCursors.new([0])
18
+ @cursor_id = 0
19
+ @limit = query_operation.limit
20
+ @limited = @limit > 0
21
+
22
+ @options = {
23
+ request_id: query_operation.request_id,
24
+ flags: query_operation.flags,
25
+ limit: query_operation.limit,
26
+ skip: query_operation.skip,
27
+ fields: query_operation.fields
28
+ }
23
29
  end
24
30
 
25
31
  def each
26
- documents = query @query_op
32
+ documents = load
27
33
  documents.each { |doc| yield doc }
28
34
 
29
35
  while more?
30
- return kill if limited? && @get_more_op.limit <= 0
36
+ return kill if limited? && @limit <= 0
31
37
 
32
- documents = query @get_more_op
38
+ documents = get_more
33
39
  documents.each { |doc| yield doc }
34
40
  end
35
41
  end
36
42
 
37
- def query(operation)
38
- reply = session.query operation
43
+ def load
44
+ consistency = session.consistency
45
+ @options[:flags] |= [:slave_ok] if consistency == :eventual
46
+
47
+ reply, @node = session.context.with_node do |node|
48
+ [node.query(@database, @collection, @selector, @options), node]
49
+ end
39
50
 
40
- @get_more_op.limit -= reply.count if limited?
41
- @get_more_op.cursor_id = reply.cursor_id
42
- @kill_cursor_op.cursor_ids = [reply.cursor_id]
51
+ @limit -= reply.count if limited?
52
+ @cursor_id = reply.cursor_id
43
53
 
44
54
  reply.documents
45
55
  end
46
56
 
47
57
  def limited?
48
- @query_op.limit > 0
58
+ @limited
49
59
  end
50
60
 
51
61
  def more?
52
- @get_more_op.cursor_id != 0
62
+ @cursor_id != 0
63
+ end
64
+
65
+ def get_more
66
+ reply = @node.get_more @database, @collection, @cursor_id, @limit
67
+
68
+ @limit -= reply.count if limited?
69
+ @cursor_id = reply.cursor_id
70
+
71
+ reply.documents
53
72
  end
54
73
 
55
74
  def kill
56
- session.execute kill_cursor_op
75
+ @node.kill_cursors [@cursor_id]
57
76
  end
58
77
  end
59
78
 
@@ -29,7 +29,9 @@ module Moped
29
29
 
30
30
  # Drop the database.
31
31
  def drop
32
- command dropDatabase: 1
32
+ session.with(consistency: :strong) do |session|
33
+ session.context.command name, dropDatabase: 1
34
+ end
33
35
  end
34
36
 
35
37
  # Log in with +username+ and +password+ on the current database.
@@ -37,12 +39,12 @@ module Moped
37
39
  # @param [String] username the username
38
40
  # @param [String] password the password
39
41
  def login(username, password)
40
- session.cluster.login(name, username, password)
42
+ session.context.login(name, username, password)
41
43
  end
42
44
 
43
45
  # Log out from the current database.
44
46
  def logout
45
- session.cluster.logout(name)
47
+ session.context.logout(name)
46
48
  end
47
49
 
48
50
  # Run +command+ on the database.
@@ -54,17 +56,7 @@ module Moped
54
56
  # @param [Hash] command the command to run
55
57
  # @return [Hash] the result of the command
56
58
  def command(command)
57
- operation = Protocol::Command.new(name, command)
58
-
59
- result = session.with(consistency: :strong) do |session|
60
- session.simple_query(operation)
61
- end
62
-
63
- raise Errors::OperationFailure.new(
64
- operation, result
65
- ) unless result["ok"] == 1.0
66
-
67
- result
59
+ session.context.command name, command
68
60
  end
69
61
 
70
62
  # @param [Symbol, String] collection the collection name
@@ -1,6 +1,4 @@
1
1
  module Moped
2
-
3
- # The namespace for all errors generated by Moped.
4
2
  module Errors
5
3
 
6
4
  # Mongo's exceptions are sparsely documented, but this is the most accurate
@@ -10,14 +8,12 @@ module Moped
10
8
  # Generic error class for exceptions related to connection failures.
11
9
  class ConnectionFailure < StandardError; end
12
10
 
11
+ # Tag applied to unhandled exceptions on a node.
12
+ module SocketError end
13
+
13
14
  # Generic error class for exceptions generated on the remote MongoDB
14
15
  # server.
15
- class MongoError < StandardError; end
16
-
17
- # Exception class for exceptions generated as a direct result of an
18
- # operation, such as a failed insert or an invalid command.
19
- class OperationFailure < MongoError
20
-
16
+ class MongoError < StandardError
21
17
  # @return the command that generated the error
22
18
  attr_reader :command
23
19
 
@@ -53,9 +49,28 @@ module Moped
53
49
  end
54
50
  end
55
51
 
56
- # A special kind of OperationFailure, raised when Mongo sets the
57
- # :query_failure flag on a query response.
58
- class QueryFailure < OperationFailure; end
52
+ # Exception class for exceptions generated as a direct result of an
53
+ # operation, such as a failed insert or an invalid command.
54
+ class OperationFailure < MongoError; end
55
+
56
+ # Exception raised on invalid queries.
57
+ class QueryFailure < MongoError; end
58
+
59
+ # Exception raised when authentication fails.
60
+ class AuthenticationFailure < MongoError; end
61
+
62
+ # Raised when providing an invalid string from an object id.
63
+ class InvalidObjectId < StandardError
64
+ def initialize(string)
65
+ super("'#{string}' is not a valid object id.")
66
+ end
67
+ end
68
+
69
+ # @api private
70
+ #
71
+ # Internal exception raised by Node#ensure_primary and captured by
72
+ # Cluster#with_primary.
73
+ class ReplicaSetReconfigured < StandardError; end
59
74
 
60
75
  end
61
76
  end
@@ -0,0 +1,334 @@
1
+ module Moped
2
+ class Node
3
+
4
+ attr_reader :address
5
+ attr_reader :resolved_address
6
+ attr_reader :ip_address
7
+ attr_reader :port
8
+
9
+ attr_reader :peers
10
+ attr_reader :timeout
11
+
12
+ def initialize(address)
13
+ @address = address
14
+
15
+ host, port = address.split(":")
16
+ @ip_address = ::Socket.getaddrinfo(host, nil, ::Socket::AF_INET, ::Socket::SOCK_STREAM).first[3]
17
+ @port = port.to_i
18
+ @resolved_address = "#{@ip_address}:#{@port}"
19
+
20
+ @timeout = 5
21
+ end
22
+
23
+ def command(database, cmd, options = {})
24
+ operation = Protocol::Command.new(database, cmd, options)
25
+
26
+ process(operation) do |reply|
27
+ result = reply.documents[0]
28
+
29
+ raise Errors::OperationFailure.new(
30
+ operation, result
31
+ ) if result["ok"] != 1 || result["err"] || result["errmsg"]
32
+
33
+ result
34
+ end
35
+ end
36
+
37
+ def kill_cursors(cursor_ids)
38
+ process Protocol::KillCursors.new(cursor_ids)
39
+ end
40
+
41
+ def get_more(database, collection, cursor_id, limit)
42
+ process Protocol::GetMore.new(database, collection, cursor_id, limit)
43
+ end
44
+
45
+ def remove(database, collection, selector, options = {})
46
+ process Protocol::Delete.new(database, collection, selector, options)
47
+ end
48
+
49
+ def update(database, collection, selector, change, options = {})
50
+ process Protocol::Update.new(database, collection, selector, change, options)
51
+ end
52
+
53
+ def insert(database, collection, documents)
54
+ process Protocol::Insert.new(database, collection, documents)
55
+ end
56
+
57
+ def query(database, collection, selector, options = {})
58
+ operation = Protocol::Query.new(database, collection, selector, options)
59
+
60
+ process operation do |reply|
61
+ if reply.flags.include? :query_failure
62
+ raise Errors::QueryFailure.new(operation, reply.documents.first)
63
+ end
64
+
65
+ reply
66
+ end
67
+ end
68
+
69
+ # @return [true/false] whether the node needs to be refreshed.
70
+ def needs_refresh?(time)
71
+ !@refreshed_at || @refreshed_at < time
72
+ end
73
+
74
+ def primary?
75
+ @primary
76
+ end
77
+
78
+ def secondary?
79
+ @secondary
80
+ end
81
+
82
+ # Refresh information about the node, such as it's status in the replica
83
+ # set and it's known peers.
84
+ #
85
+ # Returns nothing.
86
+ # Raises Errors::ConnectionFailure if the node cannot be reached
87
+ # Raises Errors::ReplicaSetReconfigured if the node is no longer a primary node and
88
+ # refresh was called within an +#ensure_primary+ block.
89
+ def refresh
90
+ info = command "admin", ismaster: 1
91
+
92
+ @refreshed_at = Time.now
93
+ primary = true if info["ismaster"]
94
+ secondary = true if info["secondary"]
95
+
96
+ peers = []
97
+ peers.push info["primary"] if info["primary"]
98
+ peers.concat info["hosts"] if info["hosts"]
99
+ peers.concat info["passives"] if info["passives"]
100
+ peers.concat info["arbiters"] if info["arbiters"]
101
+
102
+ @peers = peers.map { |peer| Node.new(peer) }
103
+ @primary, @secondary = primary, secondary
104
+
105
+ if !primary && Threaded.executing?(:ensure_primary)
106
+ raise Errors::ReplicaSetReconfigured, "#{inspect} is no longer the primary node."
107
+ end
108
+ end
109
+
110
+ attr_reader :down_at
111
+
112
+ def down?
113
+ @down_at
114
+ end
115
+
116
+ # Set a flag on the node for the duration of provided block so that an
117
+ # exception is raised if the node is no longer the primary node.
118
+ #
119
+ # Returns nothing.
120
+ def ensure_primary
121
+ Threaded.begin :ensure_primary
122
+ yield
123
+ ensure
124
+ Threaded.end :ensure_primary
125
+ end
126
+
127
+ # Yields the block if a connection can be established, retrying when a
128
+ # connection error is raised.
129
+ #
130
+ # @raises ConnectionFailure when a connection cannot be established.
131
+ def ensure_connected
132
+ # Don't run the reconnection login if we're already inside an
133
+ # +ensure_connected+ block.
134
+ return yield if Threaded.executing? :connection
135
+ Threaded.begin :connection
136
+
137
+ retry_on_failure = true
138
+
139
+ begin
140
+ connect unless connected?
141
+ yield
142
+ rescue Errors::ReplicaSetReconfigured
143
+ # Someone else wrapped this in an #ensure_primary block, so let the
144
+ # reconfiguration exception bubble up.
145
+ raise
146
+ rescue Errors::OperationFailure, Errors::AuthenticationFailure, Errors::QueryFailure
147
+ # These exceptions are "expected" in the normal course of events, and
148
+ # don't necessitate disconnecting.
149
+ raise
150
+ rescue Errors::ConnectionFailure
151
+ disconnect
152
+
153
+ if retry_on_failure
154
+ # Maybe there was a hiccup -- try reconnecting one more time
155
+ retry_on_failure = false
156
+ retry
157
+ else
158
+ # Nope, we failed to connect twice. Flag the node as down and re-raise
159
+ # the exception.
160
+ down!
161
+ raise
162
+ end
163
+ rescue
164
+ # Looks like we got an unexpected error, so we'll clean up the connection
165
+ # and re-raise the exception.
166
+ disconnect
167
+ raise $!.extend(Errors::SocketError)
168
+ end
169
+ ensure
170
+ Threaded.end :connection
171
+ end
172
+
173
+ def pipeline
174
+ Threaded.begin :pipeline
175
+
176
+ begin
177
+ yield
178
+ ensure
179
+ Threaded.end :pipeline
180
+ end
181
+
182
+ flush unless Threaded.executing? :pipeline
183
+ end
184
+
185
+ def apply_auth(credentials)
186
+ unless auth == credentials
187
+ logouts = auth.keys - credentials.keys
188
+
189
+ logouts.each do |database|
190
+ logout database
191
+ end
192
+
193
+ credentials.each do |database, (username, password)|
194
+ login(database, username, password) unless auth[database] == [username, password]
195
+ end
196
+ end
197
+
198
+ self
199
+ end
200
+
201
+ def ==(other)
202
+ resolved_address == other.resolved_address
203
+ end
204
+ alias eql? ==
205
+
206
+ def hash
207
+ [ip_address, port].hash
208
+ end
209
+
210
+ private
211
+
212
+ def auth
213
+ @auth ||= {}
214
+ end
215
+
216
+ def login(database, username, password)
217
+ getnonce = Protocol::Command.new(database, getnonce: 1)
218
+ connection.write [getnonce]
219
+ result = connection.read.documents.first
220
+ raise Errors::OperationFailure.new(getnonce, result) unless result["ok"] == 1
221
+
222
+ authenticate = Protocol::Commands::Authenticate.new(database, username, password, result["nonce"])
223
+ connection.write [authenticate]
224
+ result = connection.read.documents.first
225
+ raise Errors::AuthenticationFailure.new(authenticate, result) unless result["ok"] == 1
226
+
227
+ auth[database] = [username, password]
228
+ end
229
+
230
+ def logout(database)
231
+ command = Protocol::Command.new(database, logout: 1)
232
+ connection.write [command]
233
+ result = connection.read.documents.first
234
+ raise Errors::OperationFailure.new(command, result) unless result["ok"] == 1
235
+
236
+ auth.delete(database)
237
+ end
238
+
239
+ def initialize_copy(_)
240
+ @connection = nil
241
+ end
242
+
243
+ def connection
244
+ @connection ||= Connection.new
245
+ end
246
+
247
+ def disconnect
248
+ auth.clear
249
+ connection.disconnect
250
+ end
251
+
252
+ def connected?
253
+ connection.connected?
254
+ end
255
+
256
+ # Mark the node as down.
257
+ #
258
+ # Returns nothing.
259
+ def down!
260
+ @down_at = Time.new
261
+
262
+ disconnect
263
+ end
264
+
265
+ # Connect to the node.
266
+ #
267
+ # Returns nothing.
268
+ # Raises Moped::ConnectionError if the connection times out.
269
+ # Raises Moped::ConnectionError if the server is unavailable.
270
+ def connect
271
+ connection.connect ip_address, port, timeout
272
+ @down_at = nil
273
+
274
+ refresh
275
+ rescue Timeout::Error
276
+ raise Errors::ConnectionFailure, "Timed out connection to Mongo on #{address}"
277
+ rescue Errno::ECONNREFUSED
278
+ raise Errors::ConnectionFailure, "Could not connect to Mongo on #{address}"
279
+ end
280
+
281
+ def process(operation, &callback)
282
+ if Threaded.executing? :pipeline
283
+ queue.push [operation, callback]
284
+ else
285
+ flush([[operation, callback]])
286
+ end
287
+ end
288
+
289
+ def queue
290
+ Threaded.stack(:pipelined_operations)
291
+ end
292
+
293
+ def flush(ops = queue)
294
+ operations, callbacks = ops.transpose
295
+
296
+ logging(operations) do
297
+ ensure_connected do
298
+ connection.write operations
299
+ replies = connection.receive_replies(operations)
300
+
301
+ replies.zip(callbacks).map do |reply, callback|
302
+ callback ? callback[reply] : reply
303
+ end.last
304
+ end
305
+ end
306
+ ensure
307
+ ops.clear
308
+ end
309
+
310
+ def logging(operations)
311
+ instrument_start = (logger = Moped.logger) && logger.debug? && Time.new
312
+ yield
313
+ ensure
314
+ log_operations(logger, operations, Time.new - instrument_start) if instrument_start && !$!
315
+ end
316
+
317
+ def log_operations(logger, ops, duration)
318
+ prefix = " MOPED: #{address} "
319
+ indent = " "*prefix.length
320
+ runtime = (" (%.1fms)" % duration)
321
+
322
+ if ops.length == 1
323
+ logger.debug prefix + ops.first.log_inspect + runtime
324
+ else
325
+ first, *middle, last = ops
326
+
327
+ logger.debug prefix + first.log_inspect
328
+ middle.each { |m| logger.debug indent + m.log_inspect }
329
+ logger.debug indent + last.log_inspect + runtime
330
+ end
331
+ end
332
+
333
+ end
334
+ end