mongo 1.4.0 → 1.5.0.rc0

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