moped 1.3.2 → 1.4.0

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.

@@ -13,6 +13,18 @@ module Moped
13
13
  # @attribute [r] name The collection name.
14
14
  attr_reader :database, :name
15
15
 
16
+ # Return whether or not this collection is a capped collection.
17
+ #
18
+ # @example Is the collection capped?
19
+ # collection.capped?
20
+ #
21
+ # @return [ true, false ] If the collection is capped.
22
+ #
23
+ # @since 1.4.0
24
+ def capped?
25
+ database.command(collstats: name)["capped"]
26
+ end
27
+
16
28
  # Drop the collection.
17
29
  #
18
30
  # @example Drop the collection.
@@ -107,12 +119,10 @@ module Moped
107
119
  # @return [ Hash ] containing the result of aggregation
108
120
  #
109
121
  # @since 1.3.0
110
- def aggregate(pipeline)
111
- pipeline = [ pipeline ] unless pipeline.is_a?(Array)
122
+ def aggregate(*pipeline)
123
+ pipeline.flatten!
112
124
  command = { aggregate: name.to_s, pipeline: pipeline }
113
- database.session.with(consistency: :strong) do |sess|
114
- sess.command(command)["result"]
115
- end
125
+ database.session.command(command)["result"]
116
126
  end
117
127
  end
118
128
  end
@@ -54,7 +54,7 @@ module Moped
54
54
  #
55
55
  # @since 1.0.0
56
56
  def collection_names
57
- namespaces = Collection.new(self, "system.namespaces").find(name: { "$not" => /#{name}\.system|\$/ })
57
+ namespaces = Collection.new(self, "system.namespaces").find(name: { "$not" => /#{name}\.system\.|\$/ })
58
58
  namespaces.map do |doc|
59
59
  _name = doc["name"]
60
60
  _name[name.length + 1, _name.length]
data/lib/moped/errors.rb CHANGED
@@ -93,18 +93,32 @@ module Moped
93
93
  end
94
94
  end
95
95
 
96
+ # Classes of errors that should not disconnect connections.
97
+ class DoNotDisconnect < MongoError; end
98
+
99
+ # Classes of errors that could be caused by a replica set reconfiguration.
100
+ class PotentialReconfiguration < MongoError
101
+
102
+ # Replica set reconfigurations can be either in the form of an operation
103
+ # error with code 13435, or with an error message stating the server is
104
+ # not a master. (This encapsulates codes 10054, 10056, 10058)
105
+ def reconfiguring_replica_set?
106
+ details["code"] == 13435 || details["err"] == "not master"
107
+ end
108
+ end
109
+
96
110
  # Exception raised when authentication fails.
97
- class AuthenticationFailure < MongoError; end
111
+ class AuthenticationFailure < DoNotDisconnect; end
98
112
 
99
113
  # Exception class for exceptions generated as a direct result of an
100
114
  # operation, such as a failed insert or an invalid command.
101
- class OperationFailure < MongoError; end
115
+ class OperationFailure < PotentialReconfiguration; end
102
116
 
103
117
  # Exception raised on invalid queries.
104
- class QueryFailure < MongoError; end
118
+ class QueryFailure < PotentialReconfiguration; end
105
119
 
106
120
  # Exception raised if the cursor could not be found.
107
- class CursorNotFound < MongoError
121
+ class CursorNotFound < DoNotDisconnect
108
122
  def initialize(operation, cursor_id)
109
123
  super(operation, {"errmsg" => "cursor #{cursor_id} not found"})
110
124
  end
@@ -114,7 +128,7 @@ module Moped
114
128
  #
115
129
  # Internal exception raised by Node#ensure_primary and captured by
116
130
  # Cluster#with_primary.
117
- class ReplicaSetReconfigured < StandardError; end
131
+ class ReplicaSetReconfigured < DoNotDisconnect; end
118
132
 
119
133
  # Tag applied to unhandled exceptions on a node.
120
134
  module SocketError; end
data/lib/moped/node.rb CHANGED
@@ -127,32 +127,17 @@ module Moped
127
127
  begin
128
128
  connect unless connected?
129
129
  yield
130
- rescue Errors::ReplicaSetReconfigured
131
- # Someone else wrapped this in an #ensure_primary block, so let the
132
- # reconfiguration exception bubble up.
133
- raise
134
- rescue Errors::QueryFailure => e
135
- # We might have a replica set change with:
136
- # "failed with error 13435: "not master and slaveOk=false"
137
- if e.details['code'] == 13435
130
+ rescue Errors::PotentialReconfiguration => e
131
+ if e.reconfiguring_replica_set?
138
132
  raise Errors::ReplicaSetReconfigured
139
133
  end
140
134
  raise
141
- rescue Errors::OperationFailure => e
142
- # We might have a replica set change with:
143
- # MongoDB uses 3 different error codes for "not master", [10054, 10056, 10058]
144
- # thus it is easier to capture the "err"
145
- if e.details["err"] == "not master"
146
- raise Errors::ReplicaSetReconfigured
147
- end
148
- raise
149
- rescue Errors::AuthenticationFailure, Errors::CursorNotFound
135
+ rescue Errors::DoNotDisconnect
150
136
  # These exceptions are "expected" in the normal course of events, and
151
137
  # don't necessitate disconnecting.
152
138
  raise
153
139
  rescue Errors::ConnectionFailure
154
140
  disconnect
155
-
156
141
  if retry_on_failure
157
142
  # Maybe there was a hiccup -- try reconnecting one more time
158
143
  retry_on_failure = false
@@ -387,7 +372,7 @@ module Moped
387
372
  # node.refresh
388
373
  #
389
374
  # @raise [ ConnectionFailure ] If the node cannot be reached.
390
-
375
+ #
391
376
  # @raise [ ReplicaSetReconfigured ] If the node is no longer a primary node and
392
377
  # refresh was called within an +#ensure_primary+ block.
393
378
  #
@@ -398,18 +383,11 @@ module Moped
398
383
  if resolve_address
399
384
  begin
400
385
  info = command("admin", ismaster: 1)
401
-
402
386
  @refreshed_at = Time.now
403
- primary = true if info["ismaster"]
387
+ primary = true if info["ismaster"]
404
388
  secondary = true if info["secondary"]
389
+ generate_peers(info)
405
390
 
406
- peers = []
407
- peers.push(info["primary"]) if info["primary"]
408
- peers.concat(info["hosts"]) if info["hosts"]
409
- peers.concat(info["passives"]) if info["passives"]
410
- peers.concat(info["arbiters"]) if info["arbiters"]
411
-
412
- @peers = peers.map { |peer| Node.new(peer, options) }
413
391
  @primary, @secondary = primary, secondary
414
392
  @arbiter = info["arbiterOnly"]
415
393
  @passive = info["passive"]
@@ -475,28 +453,42 @@ module Moped
475
453
  process(Protocol::Update.new(database, collection, selector, change, options))
476
454
  end
477
455
 
456
+ # Get the node as a nice formatted string.
457
+ #
458
+ # @example Inspect the node.
459
+ # node.inspect
460
+ #
461
+ # @return [ String ] The string inspection.
462
+ #
463
+ # @since 1.0.0
478
464
  def inspect
479
465
  "<#{self.class.name} resolved_address=#{@resolved_address.inspect}>"
480
466
  end
481
467
 
482
-
483
468
  private
484
469
 
485
470
  def auth
486
471
  @auth ||= {}
487
472
  end
488
473
 
474
+ def generate_peers(info)
475
+ peers = []
476
+ peers.push(info["primary"]) if info["primary"]
477
+ peers.concat(info["hosts"]) if info["hosts"]
478
+ peers.concat(info["passives"]) if info["passives"]
479
+ peers.concat(info["arbiters"]) if info["arbiters"]
480
+ @peers = peers.map { |peer| Node.new(peer, options) }.uniq
481
+ end
482
+
489
483
  def login(database, username, password)
490
484
  getnonce = Protocol::Command.new(database, getnonce: 1)
491
485
  connection.write [getnonce]
492
486
  result = connection.read.documents.first
493
487
  raise Errors::OperationFailure.new(getnonce, result) unless result["ok"] == 1
494
-
495
488
  authenticate = Protocol::Commands::Authenticate.new(database, username, password, result["nonce"])
496
489
  connection.write [authenticate]
497
490
  result = connection.read.documents.first
498
491
  raise Errors::AuthenticationFailure.new(authenticate, result) unless result["ok"] == 1
499
-
500
492
  auth[database] = [username, password]
501
493
  end
502
494
 
@@ -134,6 +134,21 @@ module Moped
134
134
  f.join(" ") % v
135
135
  end
136
136
 
137
+ # Get the basic selector.
138
+ #
139
+ # @example Get the basic selector.
140
+ # query.basic_selector
141
+ #
142
+ # @note Sometimes, like in cases of deletion we need this since MongoDB
143
+ # does not understand $query in operations like DELETE.
144
+ #
145
+ # @return [ Hash ] The basic selector.
146
+ #
147
+ # @since 2.0.0
148
+ def basic_selector
149
+ selector["$query"] || selector
150
+ end
151
+
137
152
  # Receive replies to the message.
138
153
  #
139
154
  # @example Receive replies.
data/lib/moped/query.rb CHANGED
@@ -32,11 +32,10 @@ module Moped
32
32
  # @return [ Integer ] The number of documents that match the selector.
33
33
  #
34
34
  # @since 1.0.0
35
- def count
36
- result = collection.database.command(
37
- count: collection.name,
38
- query: selector
39
- )
35
+ def count(limit = false)
36
+ command = { count: collection.name, query: selector }
37
+ command.merge!(skip: operation.skip, limit: operation.limit) if limit
38
+ result = collection.database.command(command)
40
39
  result["n"].to_i
41
40
  end
42
41
 
@@ -93,12 +92,14 @@ module Moped
93
92
  explanation = operation.selector.dup
94
93
  hint = explanation["$hint"]
95
94
  sort = explanation["$orderby"]
95
+ max_scan = explanation["$maxScan"]
96
96
  explanation = {
97
97
  "$query" => selector,
98
98
  "$explain" => true,
99
99
  }
100
100
  explanation["$orderby"] = sort if sort
101
101
  explanation["$hint"] = hint if hint
102
+ explanation["$maxScan"] = max_scan if max_scan
102
103
  Query.new(collection, explanation).limit(-(operation.limit.abs)).each { |doc| return doc }
103
104
  end
104
105
 
@@ -135,11 +136,27 @@ module Moped
135
136
  #
136
137
  # @since 1.0.0
137
138
  def hint(hint)
138
- operation.selector = { "$query" => selector } unless operation.selector["$query"]
139
+ upgrade_to_advanced_selector
139
140
  operation.selector["$hint"] = hint
140
141
  self
141
142
  end
142
143
 
144
+ # Apply a max scan limit to the query.
145
+ #
146
+ # @example Limit the query to only scan up to 100 documents
147
+ # db[:people].find.max_scan(100)
148
+ #
149
+ # @param [ Integer ] max The maximum number of documents to scan
150
+ #
151
+ # @return [ Query ] self
152
+ #
153
+ # @since 1.4.0
154
+ def max_scan(max)
155
+ upgrade_to_advanced_selector
156
+ operation.selector["$maxScan"] = max
157
+ self
158
+ end
159
+
143
160
  # Initialize the query.
144
161
  #
145
162
  # @example Initialize the query.
@@ -256,7 +273,7 @@ module Moped
256
273
  session.context.remove(
257
274
  operation.database,
258
275
  operation.collection,
259
- operation.selector,
276
+ operation.basic_selector,
260
277
  flags: [ :remove_first ]
261
278
  )
262
279
  end
@@ -275,7 +292,7 @@ module Moped
275
292
  session.context.remove(
276
293
  operation.database,
277
294
  operation.collection,
278
- operation.selector
295
+ operation.basic_selector
279
296
  )
280
297
  end
281
298
  end
@@ -321,7 +338,8 @@ module Moped
321
338
  #
322
339
  # @since 1.0.0
323
340
  def sort(sort)
324
- operation.selector = { "$query" => selector, "$orderby" => sort }
341
+ upgrade_to_advanced_selector
342
+ operation.selector["$orderby"] = sort
325
343
  self
326
344
  end
327
345
 
@@ -404,5 +422,9 @@ module Moped
404
422
  def session
405
423
  collection.database.session
406
424
  end
425
+
426
+ def upgrade_to_advanced_selector
427
+ operation.selector = { "$query" => selector } unless operation.selector["$query"]
428
+ end
407
429
  end
408
430
  end
data/lib/moped/session.rb CHANGED
@@ -130,6 +130,18 @@ module Moped
130
130
  current_database.drop
131
131
  end
132
132
 
133
+ # Provide a string inspection for the session.
134
+ #
135
+ # @example Inspect the session.
136
+ # session.inspect
137
+ #
138
+ # @return [ String ] The string inspection.
139
+ #
140
+ # @since 1.4.0
141
+ def inspect
142
+ "<#{self.class.name} seeds=#{cluster.seeds} database=#{current_database_name}>"
143
+ end
144
+
133
145
  # Log in with +username+ and +password+ on the current database.
134
146
  #
135
147
  # @param (see Moped::Database#login)
@@ -229,7 +241,7 @@ module Moped
229
241
  session = with(options)
230
242
  session.instance_variable_set(:@cluster, cluster.dup)
231
243
  if block_given?
232
- yield session
244
+ yield(session)
233
245
  else
234
246
  session
235
247
  end
@@ -271,7 +283,7 @@ module Moped
271
283
  # @since 1.0.0
272
284
  def use(database)
273
285
  options[:database] = database
274
- set_current_database database
286
+ set_current_database(database)
275
287
  end
276
288
 
277
289
  # Create a new session with +options+ reusing existing connections.
@@ -309,7 +321,7 @@ module Moped
309
321
  session = dup
310
322
  session.options.update(options)
311
323
  if block_given?
312
- yield session
324
+ yield(session)
313
325
  else
314
326
  session
315
327
  end
@@ -338,8 +350,7 @@ module Moped
338
350
  private
339
351
 
340
352
  def current_database
341
- return @current_database if defined? @current_database
342
-
353
+ return @current_database if defined?(@current_database)
343
354
  if database = options[:database]
344
355
  set_current_database(database)
345
356
  else
@@ -347,12 +358,15 @@ module Moped
347
358
  end
348
359
  end
349
360
 
361
+ def current_database_name
362
+ defined?(@current_database) ? current_database.name : :none
363
+ end
364
+
350
365
  def initialize_copy(_)
351
366
  @context = Context.new(self)
352
367
  @options = @options.dup
353
-
354
- if defined? @current_database
355
- remove_instance_variable :@current_database
368
+ if defined?(@current_database)
369
+ remove_instance_variable(:@current_database)
356
370
  end
357
371
  end
358
372
 
@@ -13,7 +13,7 @@ module Moped
13
13
  #
14
14
  # @since 1.0.0
15
15
  def alive?
16
- if Kernel::select([ self ], nil, nil, 0)
16
+ if Kernel::select([ self ], nil, [ self ], 0)
17
17
  !eof? rescue false
18
18
  else
19
19
  true
@@ -42,6 +42,7 @@ module Moped
42
42
  #
43
43
  # @since 1.2.0
44
44
  def read(length)
45
+ check_if_alive!
45
46
  handle_socket_errors { super }
46
47
  end
47
48
 
@@ -56,30 +57,77 @@ module Moped
56
57
  #
57
58
  # @since 1.0.0
58
59
  def write(*args)
59
- raise Errors::ConnectionFailure, "Socket connection was closed by remote host" unless alive?
60
+ check_if_alive!
60
61
  handle_socket_errors { super }
61
62
  end
62
63
 
63
64
  private
64
65
 
66
+ # Before performing a read or write operating, ping the server to check
67
+ # if it is alive.
68
+ #
69
+ # @api private
70
+ #
71
+ # @example Check if the connection is alive.
72
+ # connectable.check_if_alive!
73
+ #
74
+ # @raise [ ConnectionFailure ] If the connectable is not alive.
75
+ #
76
+ # @since 1.4.0
77
+ def check_if_alive!
78
+ unless alive?
79
+ raise Errors::ConnectionFailure, "Socket connection was closed by remote host"
80
+ end
81
+ end
82
+
83
+ # Generate the message for the connection failure based of the system
84
+ # call error, with some added information.
85
+ #
86
+ # @api private
87
+ #
88
+ # @example Generate the error message.
89
+ # connectable.generate_message(error)
90
+ #
91
+ # @param [ SystemCallError ] error The error.
92
+ #
93
+ # @return [ String ] The error message.
94
+ #
95
+ # @since 1.4.0
96
+ def generate_message(error)
97
+ "#{host}:#{port}: #{error.class.name} (#{error.errno}): #{error.message}"
98
+ end
99
+
65
100
  # Handle the potential socket errors that can occur.
66
101
  #
67
102
  # @api private
68
103
  #
69
104
  # @example Handle the socket errors while executing the block.
70
105
  # handle_socket_errors do
71
- # #...
106
+ # socket.read(128)
72
107
  # end
73
108
  #
109
+ # @raise [ Moped::Errors::ConnectionFailure ] If a system call error or
110
+ # IOError occured which can be retried.
111
+ # @raise [ Moped::Errors::Unrecoverable ] If a system call error occured
112
+ # which cannot be retried and should be re-raised.
113
+ #
74
114
  # @return [ Object ] The result of the yield.
75
115
  #
76
116
  # @since 1.0.0
77
117
  def handle_socket_errors
78
118
  yield
79
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EPIPE
80
- raise Errors::ConnectionFailure, "Could not connect to Mongo on #{host}:#{port}"
81
- rescue Errno::ECONNRESET
82
- raise Errors::ConnectionFailure, "Connection reset to Mongo on #{host}:#{port}"
119
+ rescue Errno::ECONNREFUSED => e
120
+ raise Errors::ConnectionFailure, generate_message(e)
121
+ rescue Errno::EHOSTUNREACH => e
122
+ raise Errors::ConnectionFailure, generate_message(e)
123
+ rescue Errno::EPIPE => e
124
+ raise Errors::ConnectionFailure, generate_message(e)
125
+ rescue Errno::ECONNRESET => e
126
+ raise Errors::ConnectionFailure, generate_message(e)
127
+ rescue Errno::ETIMEDOUT => e
128
+ raise Errors::ConnectionFailure, generate_message(e)
129
+ rescue IOError
130
+ raise Errors::ConnectionFailure, "Connection timed out to Mongo on #{host}:#{port}"
83
131
  rescue OpenSSL::SSL::SSLError => e
84
132
  raise Errors::ConnectionFailure, "SSL Error '#{e.to_s}' for connection to Mongo on #{host}:#{port}"
85
133
  end
@@ -103,7 +151,10 @@ module Moped
103
151
  Timeout::timeout(timeout) do
104
152
  sock = new(host, port)
105
153
  sock.set_encoding('binary')
154
+ timeout_val = [ timeout, 0 ].pack("l_2")
106
155
  sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
156
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeout_val)
157
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeout_val)
107
158
  sock
108
159
  end
109
160
  rescue Timeout::Error