mongo 1.4.0 → 1.5.0.rc0

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 (49) hide show
  1. data/docs/HISTORY.md +15 -0
  2. data/docs/REPLICA_SETS.md +19 -7
  3. data/lib/mongo.rb +1 -0
  4. data/lib/mongo/collection.rb +1 -1
  5. data/lib/mongo/connection.rb +29 -351
  6. data/lib/mongo/cursor.rb +88 -6
  7. data/lib/mongo/gridfs/grid.rb +4 -2
  8. data/lib/mongo/gridfs/grid_file_system.rb +4 -2
  9. data/lib/mongo/networking.rb +345 -0
  10. data/lib/mongo/repl_set_connection.rb +236 -191
  11. data/lib/mongo/util/core_ext.rb +45 -0
  12. data/lib/mongo/util/logging.rb +5 -0
  13. data/lib/mongo/util/node.rb +6 -4
  14. data/lib/mongo/util/pool.rb +73 -26
  15. data/lib/mongo/util/pool_manager.rb +100 -30
  16. data/lib/mongo/util/uri_parser.rb +29 -21
  17. data/lib/mongo/version.rb +1 -1
  18. data/test/bson/binary_test.rb +6 -8
  19. data/test/bson/bson_test.rb +1 -0
  20. data/test/bson/ordered_hash_test.rb +2 -0
  21. data/test/bson/test_helper.rb +0 -17
  22. data/test/collection_test.rb +22 -0
  23. data/test/connection_test.rb +1 -1
  24. data/test/cursor_test.rb +3 -3
  25. data/test/load/thin/load.rb +4 -7
  26. data/test/replica_sets/basic_test.rb +46 -0
  27. data/test/replica_sets/connect_test.rb +35 -58
  28. data/test/replica_sets/count_test.rb +15 -6
  29. data/test/replica_sets/insert_test.rb +6 -7
  30. data/test/replica_sets/query_test.rb +4 -6
  31. data/test/replica_sets/read_preference_test.rb +112 -8
  32. data/test/replica_sets/refresh_test.rb +66 -36
  33. data/test/replica_sets/refresh_with_threads_test.rb +55 -0
  34. data/test/replica_sets/replication_ack_test.rb +3 -6
  35. data/test/replica_sets/rs_test_helper.rb +12 -6
  36. data/test/replica_sets/threading_test.rb +111 -0
  37. data/test/test_helper.rb +9 -2
  38. data/test/threading_test.rb +14 -6
  39. data/test/tools/repl_set_manager.rb +55 -40
  40. data/test/unit/collection_test.rb +2 -1
  41. data/test/unit/connection_test.rb +8 -8
  42. data/test/unit/grid_test.rb +4 -2
  43. data/test/unit/pool_manager_test.rb +1 -0
  44. data/test/unit/read_test.rb +17 -5
  45. data/test/uri_test.rb +9 -4
  46. metadata +13 -28
  47. data/test/replica_sets/connection_string_test.rb +0 -29
  48. data/test/replica_sets/pooled_insert_test.rb +0 -58
  49. data/test/replica_sets/query_secondaries.rb +0 -109
@@ -26,7 +26,7 @@ module Mongo
26
26
  attr_reader :collection, :selector, :fields,
27
27
  :order, :hint, :snapshot, :timeout,
28
28
  :full_collection_name, :transformer,
29
- :options, :cursor_id
29
+ :options, :cursor_id, :show_disk_loc
30
30
 
31
31
  # Create a new cursor.
32
32
  #
@@ -98,6 +98,10 @@ module Mongo
98
98
  else
99
99
  @command = false
100
100
  end
101
+
102
+ @checkin_read_pool = false
103
+ @checkin_connection = false
104
+ @read_pool = nil
101
105
  end
102
106
 
103
107
  # Guess whether the cursor is alive on the server.
@@ -460,10 +464,17 @@ module Mongo
460
464
  def send_initial_query
461
465
  message = construct_query_message
462
466
  payload = instrument_payload if @logger
467
+ sock = @socket || checkout_socket_from_connection
463
468
  instrument(:find, payload) do
469
+ begin
464
470
  results, @n_received, @cursor_id = @connection.receive_message(
465
- Mongo::Constants::OP_QUERY, message, nil, @socket, @command,
466
- @read_preference, @options & OP_QUERY_EXHAUST != 0)
471
+ Mongo::Constants::OP_QUERY, message, nil, sock, @command,
472
+ nil, @options & OP_QUERY_EXHAUST != 0)
473
+ rescue ConnectionFailure, OperationFailure, OperationTimeout => ex
474
+ force_checkin_socket(sock)
475
+ raise ex
476
+ end
477
+ checkin_socket(sock) unless @socket
467
478
  @returned += @n_received
468
479
  @cache += results
469
480
  @query_run = true
@@ -491,13 +502,83 @@ module Mongo
491
502
  # Cursor id.
492
503
  message.put_long(@cursor_id)
493
504
  log(:debug, "cursor.refresh() for cursor #{@cursor_id}") if @logger
505
+ sock = @socket || checkout_socket_for_op_get_more
506
+
507
+ begin
494
508
  results, @n_received, @cursor_id = @connection.receive_message(
495
- Mongo::Constants::OP_GET_MORE, message, nil, @socket, @command, @read_preference)
509
+ Mongo::Constants::OP_GET_MORE, message, nil, sock, @command, nil)
510
+ rescue ConnectionFailure, OperationFailure, OperationTimeout => ex
511
+ force_checkin_socket(sock)
512
+ raise ex
513
+ end
514
+ checkin_socket(sock) unless @socket
496
515
  @returned += @n_received
497
516
  @cache += results
498
517
  close_cursor_if_query_complete
499
518
  end
500
519
 
520
+ def checkout_socket_from_connection
521
+ @checkin_connection = true
522
+ if @command || @read_preference == :primary
523
+ @connection.checkout_writer
524
+ else
525
+ @read_pool = @connection.read_pool
526
+ @connection.checkout_reader
527
+ end
528
+ end
529
+
530
+ def checkout_socket_for_op_get_more
531
+ if @read_pool && (@read_pool != @connection.read_pool)
532
+ checkout_socket_from_read_pool
533
+ else
534
+ checkout_socket_from_connection
535
+ end
536
+ end
537
+
538
+ def checkout_socket_from_read_pool
539
+ new_pool = @connection.secondary_pools.detect do |pool|
540
+ pool.host == @read_pool.host && pool.port == @read_pool.port
541
+ end
542
+ if new_pool
543
+ @read_pool = new_pool
544
+ sock = new_pool.checkout
545
+ @checkin_read_pool = true
546
+ return sock
547
+ else
548
+ raise Mongo::OperationFailure, "Failure to continue iterating " +
549
+ "cursor because the the replica set member persisting this " +
550
+ "cursor at #{@read_pool.host_string} cannot be found."
551
+ end
552
+ end
553
+
554
+ def checkin_socket(sock)
555
+ if @checkin_read_pool
556
+ @read_pool.checkin(sock)
557
+ @checkin_read_pool = false
558
+ elsif @checkin_connection
559
+ if @command || @read_preference == :primary
560
+ @connection.checkin_writer(sock)
561
+ else
562
+ @connection.checkin_reader(sock)
563
+ end
564
+ @checkin_connection = false
565
+ end
566
+ end
567
+
568
+ def force_checkin_socket(sock)
569
+ if @checkin_read_pool
570
+ @read_pool.checkin(sock)
571
+ @checkin_read_pool = false
572
+ else
573
+ if @command || @read_preference == :primary
574
+ @connection.checkin_writer(sock)
575
+ else
576
+ @connection.checkin_reader(sock)
577
+ end
578
+ @checkin_connection = false
579
+ end
580
+ end
581
+
501
582
  def construct_query_message
502
583
  message = BSON::ByteBuffer.new
503
584
  message.put_int(@options)
@@ -527,7 +608,7 @@ module Mongo
527
608
  spec['$hint'] = @hint if @hint && @hint.length > 0
528
609
  spec['$explain'] = true if @explain
529
610
  spec['$snapshot'] = true if @snapshot
530
- spec['$maxscan'] = @max_scan if @max_scan
611
+ spec['$maxScan'] = @max_scan if @max_scan
531
612
  spec['$returnKey'] = true if @return_key
532
613
  spec['$showDiskLoc'] = true if @show_disk_loc
533
614
  spec
@@ -535,7 +616,8 @@ module Mongo
535
616
 
536
617
  # Returns true if the query contains order, explain, hint, or snapshot.
537
618
  def query_contains_special_fields?
538
- @order || @explain || @hint || @snapshot
619
+ @order || @explain || @hint || @snapshot || @show_disk_loc ||
620
+ @max_scan || @return_key
539
621
  end
540
622
 
541
623
  def close_cursor_if_query_complete
@@ -38,8 +38,10 @@ module Mongo
38
38
  @chunks = @db["#{fs_name}.chunks"]
39
39
  @fs_name = fs_name
40
40
 
41
- # Ensure indexes only if not connected to slave.
42
- unless db.connection.slave_ok?
41
+ # Create indexes only if we're connected to a primary node.
42
+ connection = @db.connection
43
+ if (connection.class == Connection && connection.read_primary?) ||
44
+ (connection.class == ReplSetConnection && connection.primary)
43
45
  @chunks.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
44
46
  end
45
47
  end
@@ -39,8 +39,10 @@ module Mongo
39
39
 
40
40
  @default_query_opts = {:sort => [['filename', 1], ['uploadDate', -1]], :limit => 1}
41
41
 
42
- # Ensure indexes only if not connected to slave.
43
- unless db.connection.slave_ok?
42
+ # Create indexes only if we're connected to a primary node.
43
+ connection = @db.connection
44
+ if (connection.class == Connection && connection.read_primary?) ||
45
+ (connection.class == ReplSetConnection && connection.primary)
44
46
  @files.create_index([['filename', 1], ['uploadDate', -1]])
45
47
  @chunks.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
46
48
  end
@@ -0,0 +1,345 @@
1
+ module Mongo
2
+ module Networking
3
+
4
+ STANDARD_HEADER_SIZE = 16
5
+ RESPONSE_HEADER_SIZE = 20
6
+
7
+ # Counter for generating unique request ids.
8
+ @@current_request_id = 0
9
+
10
+ # Send a message to MongoDB, adding the necessary headers.
11
+ #
12
+ # @param [Integer] operation a MongoDB opcode.
13
+ # @param [BSON::ByteBuffer] message a message to send to the database.
14
+ #
15
+ # @option opts [Symbol] :connection (:writer) The connection to which
16
+ # this message should be sent. Valid options are :writer and :reader.
17
+ #
18
+ # @return [Integer] number of bytes sent
19
+ def send_message(operation, message, opts={})
20
+ if opts.is_a?(String)
21
+ warn "Connection#send_message no longer takes a string log message. " +
22
+ "Logging is now handled within the Collection and Cursor classes."
23
+ opts = {}
24
+ end
25
+
26
+ connection = opts.fetch(:connection, :writer)
27
+
28
+ add_message_headers(message, operation)
29
+ packed_message = message.to_s
30
+
31
+ if connection == :writer
32
+ sock = checkout_writer
33
+ else
34
+ sock = checkout_reader
35
+ end
36
+
37
+ begin
38
+ send_message_on_socket(packed_message, sock)
39
+ ensure
40
+ if connection == :writer
41
+ checkin_writer(sock)
42
+ else
43
+ checkin_reader(sock)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Sends a message to the database, waits for a response, and raises
49
+ # an exception if the operation has failed.
50
+ #
51
+ # @param [Integer] operation a MongoDB opcode.
52
+ # @param [BSON::ByteBuffer] message a message to send to the database.
53
+ # @param [String] db_name the name of the database. used on call to get_last_error.
54
+ # @param [Hash] last_error_params parameters to be sent to getLastError. See DB#error for
55
+ # available options.
56
+ #
57
+ # @see DB#get_last_error for valid last error params.
58
+ #
59
+ # @return [Hash] The document returned by the call to getlasterror.
60
+ def send_message_with_safe_check(operation, message, db_name, log_message=nil, last_error_params=false)
61
+ docs = num_received = cursor_id = ''
62
+ add_message_headers(message, operation)
63
+
64
+ last_error_message = BSON::ByteBuffer.new
65
+ build_last_error_message(last_error_message, db_name, last_error_params)
66
+ last_error_id = add_message_headers(last_error_message, Mongo::Constants::OP_QUERY)
67
+
68
+ packed_message = message.append!(last_error_message).to_s
69
+ sock = checkout_writer
70
+ begin
71
+ send_message_on_socket(packed_message, sock)
72
+ docs, num_received, cursor_id = receive(sock, last_error_id)
73
+ checkin_writer(sock)
74
+ rescue ConnectionFailure, OperationFailure, OperationTimeout => ex
75
+ checkin_writer(sock)
76
+ raise ex
77
+ end
78
+
79
+ if num_received == 1 && (error = docs[0]['err'] || docs[0]['errmsg'])
80
+ close if error == "not master"
81
+ error = "wtimeout" if error == "timeout"
82
+ raise OperationFailure.new(docs[0]['code'].to_s + ': ' + error, docs[0]['code'], docs[0])
83
+ end
84
+
85
+ docs[0]
86
+ end
87
+
88
+ # Sends a message to the database and waits for the response.
89
+ #
90
+ # @param [Integer] operation a MongoDB opcode.
91
+ # @param [BSON::ByteBuffer] message a message to send to the database.
92
+ # @param [String] log_message this is currently a no-op and will be removed.
93
+ # @param [Socket] socket a socket to use in lieu of checking out a new one.
94
+ # @param [Boolean] command (false) indicate whether this is a command. If this is a command,
95
+ # the message will be sent to the primary node.
96
+ # @param [Boolean] command (false) indicate whether the cursor should be exhausted. Set
97
+ # this to true only when the OP_QUERY_EXHAUST flag is set.
98
+ #
99
+ # @return [Array]
100
+ # An array whose indexes include [0] documents returned, [1] number of document received,
101
+ # and [3] a cursor_id.
102
+ def receive_message(operation, message, log_message=nil, socket=nil, command=false,
103
+ read=:primary, exhaust=false)
104
+ request_id = add_message_headers(message, operation)
105
+ packed_message = message.to_s
106
+ if socket
107
+ sock = socket
108
+ should_checkin = false
109
+ else
110
+ if command || read == :primary
111
+ sock = checkout_writer
112
+ elsif read == :secondary
113
+ sock = checkout_reader
114
+ else
115
+ sock = checkout_tagged(read)
116
+ end
117
+ should_checkin = true
118
+ end
119
+
120
+ result = ''
121
+ begin
122
+ send_message_on_socket(packed_message, sock)
123
+ result = receive(sock, request_id, exhaust)
124
+ ensure
125
+ if should_checkin
126
+ if command || read == :primary
127
+ checkin_writer(sock)
128
+ elsif read == :secondary
129
+ checkin_reader(sock)
130
+ else
131
+ # TODO: sock = checkout_tagged(read)
132
+ end
133
+ end
134
+ end
135
+ result
136
+ end
137
+
138
+ private
139
+
140
+ def receive(sock, cursor_id, exhaust=false)
141
+ begin
142
+ if exhaust
143
+ docs = []
144
+ num_received = 0
145
+
146
+ while(cursor_id != 0) do
147
+ receive_header(sock, cursor_id, exhaust)
148
+ number_received, cursor_id = receive_response_header(sock)
149
+ new_docs, n = read_documents(number_received, sock)
150
+ docs += new_docs
151
+ num_received += n
152
+ end
153
+
154
+ return [docs, num_received, cursor_id]
155
+ else
156
+ receive_header(sock, cursor_id, exhaust)
157
+ number_received, cursor_id = receive_response_header(sock)
158
+ docs, num_received = read_documents(number_received, sock)
159
+
160
+ return [docs, num_received, cursor_id]
161
+ end
162
+ rescue Mongo::ConnectionFailure => ex
163
+ close
164
+ raise ex
165
+ end
166
+ end
167
+
168
+ def receive_header(sock, expected_response, exhaust=false)
169
+ header = receive_message_on_socket(16, sock)
170
+ size, request_id, response_to = header.unpack('VVV')
171
+ if !exhaust && expected_response != response_to
172
+ raise Mongo::ConnectionFailure, "Expected response #{expected_response} but got #{response_to}"
173
+ end
174
+
175
+ unless header.size == STANDARD_HEADER_SIZE
176
+ raise "Short read for DB response header: " +
177
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
178
+ end
179
+ nil
180
+ end
181
+
182
+ def receive_response_header(sock)
183
+ header_buf = receive_message_on_socket(RESPONSE_HEADER_SIZE, sock)
184
+ if header_buf.length != RESPONSE_HEADER_SIZE
185
+ raise "Short read for DB response header; " +
186
+ "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
187
+ end
188
+ flags, cursor_id_a, cursor_id_b, starting_from, number_remaining = header_buf.unpack('VVVVV')
189
+ check_response_flags(flags)
190
+ cursor_id = (cursor_id_b << 32) + cursor_id_a
191
+ [number_remaining, cursor_id]
192
+ end
193
+
194
+ def check_response_flags(flags)
195
+ if flags & Mongo::Constants::REPLY_CURSOR_NOT_FOUND != 0
196
+ raise Mongo::OperationFailure, "Query response returned CURSOR_NOT_FOUND. " +
197
+ "Either an invalid cursor was specified, or the cursor may have timed out on the server."
198
+ elsif flags & Mongo::Constants::REPLY_QUERY_FAILURE != 0
199
+ # Getting odd failures when a exception is raised here.
200
+ end
201
+ end
202
+
203
+ def read_documents(number_received, sock)
204
+ docs = []
205
+ number_remaining = number_received
206
+ while number_remaining > 0 do
207
+ buf = receive_message_on_socket(4, sock)
208
+ size = buf.unpack('V')[0]
209
+ buf << receive_message_on_socket(size - 4, sock)
210
+ number_remaining -= 1
211
+ docs << BSON::BSON_CODER.deserialize(buf)
212
+ end
213
+ [docs, number_received]
214
+ end
215
+
216
+ # Constructs a getlasterror message. This method is used exclusively by
217
+ # Connection#send_message_with_safe_check.
218
+ #
219
+ # Because it modifies message by reference, we don't need to return it.
220
+ def build_last_error_message(message, db_name, opts)
221
+ message.put_int(0)
222
+ BSON::BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
223
+ message.put_int(0)
224
+ message.put_int(-1)
225
+ cmd = BSON::OrderedHash.new
226
+ cmd[:getlasterror] = 1
227
+ if opts.is_a?(Hash)
228
+ opts.assert_valid_keys(:w, :wtimeout, :fsync)
229
+ cmd.merge!(opts)
230
+ end
231
+ message.put_binary(BSON::BSON_CODER.serialize(cmd, false).to_s)
232
+ nil
233
+ end
234
+
235
+ # Prepares a message for transmission to MongoDB by
236
+ # constructing a valid message header.
237
+ #
238
+ # Note: this method modifies message by reference.
239
+ #
240
+ # @return [Integer] the request id used in the header
241
+ def add_message_headers(message, operation)
242
+ headers = [
243
+ # Message size.
244
+ 16 + message.size,
245
+
246
+ # Unique request id.
247
+ request_id = get_request_id,
248
+
249
+ # Response id.
250
+ 0,
251
+
252
+ # Opcode.
253
+ operation
254
+ ].pack('VVVV')
255
+
256
+ message.prepend!(headers)
257
+
258
+ request_id
259
+ end
260
+
261
+ # Increment and return the next available request id.
262
+ #
263
+ # return [Integer]
264
+ def get_request_id
265
+ request_id = ''
266
+ @id_lock.synchronize do
267
+ request_id = @@current_request_id += 1
268
+ end
269
+ request_id
270
+ end
271
+
272
+ # Low-level method for sending a message on a socket.
273
+ # Requires a packed message and an available socket,
274
+ #
275
+ # @return [Integer] number of bytes sent
276
+ def send_message_on_socket(packed_message, socket)
277
+ begin
278
+ total_bytes_sent = socket.send(packed_message, 0)
279
+ if total_bytes_sent != packed_message.size
280
+ packed_message.slice!(0, total_bytes_sent)
281
+ while packed_message.size > 0
282
+ byte_sent = socket.send(packed_message, 0)
283
+ total_bytes_sent += byte_sent
284
+ packed_message.slice!(0, byte_sent)
285
+ end
286
+ end
287
+ total_bytes_sent
288
+ rescue => ex
289
+ close
290
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}:#{ex.message}"
291
+ end
292
+ end
293
+
294
+ # Low-level method for receiving data from socket.
295
+ # Requires length and an available socket.
296
+ def receive_message_on_socket(length, socket)
297
+ begin
298
+ if @op_timeout
299
+ message = nil
300
+ Mongo::TimeoutHandler.timeout(@op_timeout, OperationTimeout) do
301
+ message = receive_data(length, socket)
302
+ end
303
+ else
304
+ message = receive_data(length, socket)
305
+ end
306
+ rescue => ex
307
+ close
308
+
309
+ if ex.class == OperationTimeout
310
+ raise OperationTimeout, "Timed out waiting on socket read."
311
+ else
312
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
313
+ end
314
+ end
315
+ message
316
+ end
317
+
318
+ def receive_data(length, socket)
319
+ message = new_binary_string
320
+ socket.read(length, message)
321
+ raise ConnectionFailure, "connection closed" unless message && message.length > 0
322
+ if message.length < length
323
+ chunk = new_binary_string
324
+ while message.length < length
325
+ socket.read(length - message.length, chunk)
326
+ raise ConnectionFailure, "connection closed" unless chunk.length > 0
327
+ message << chunk
328
+ end
329
+ end
330
+ message
331
+ end
332
+
333
+ if defined?(Encoding)
334
+ BINARY_ENCODING = Encoding.find("binary")
335
+
336
+ def new_binary_string
337
+ "".force_encoding(BINARY_ENCODING)
338
+ end
339
+ else
340
+ def new_binary_string
341
+ ""
342
+ end
343
+ end
344
+ end
345
+ end