mongo-find_replace 0.18.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/LICENSE.txt +202 -0
  2. data/README.rdoc +358 -0
  3. data/Rakefile +133 -0
  4. data/bin/bson_benchmark.rb +59 -0
  5. data/bin/fail_if_no_c.rb +11 -0
  6. data/examples/admin.rb +42 -0
  7. data/examples/capped.rb +22 -0
  8. data/examples/cursor.rb +48 -0
  9. data/examples/gridfs.rb +88 -0
  10. data/examples/index_test.rb +126 -0
  11. data/examples/info.rb +31 -0
  12. data/examples/queries.rb +70 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/strict.rb +35 -0
  15. data/examples/types.rb +36 -0
  16. data/lib/mongo.rb +61 -0
  17. data/lib/mongo/admin.rb +95 -0
  18. data/lib/mongo/collection.rb +664 -0
  19. data/lib/mongo/connection.rb +555 -0
  20. data/lib/mongo/cursor.rb +393 -0
  21. data/lib/mongo/db.rb +527 -0
  22. data/lib/mongo/exceptions.rb +60 -0
  23. data/lib/mongo/gridfs.rb +22 -0
  24. data/lib/mongo/gridfs/chunk.rb +90 -0
  25. data/lib/mongo/gridfs/grid_store.rb +555 -0
  26. data/lib/mongo/types/binary.rb +48 -0
  27. data/lib/mongo/types/code.rb +36 -0
  28. data/lib/mongo/types/dbref.rb +38 -0
  29. data/lib/mongo/types/min_max_keys.rb +58 -0
  30. data/lib/mongo/types/objectid.rb +219 -0
  31. data/lib/mongo/types/regexp_of_holding.rb +45 -0
  32. data/lib/mongo/util/bson_c.rb +18 -0
  33. data/lib/mongo/util/bson_ruby.rb +595 -0
  34. data/lib/mongo/util/byte_buffer.rb +222 -0
  35. data/lib/mongo/util/conversions.rb +97 -0
  36. data/lib/mongo/util/ordered_hash.rb +135 -0
  37. data/lib/mongo/util/server_version.rb +69 -0
  38. data/lib/mongo/util/support.rb +26 -0
  39. data/lib/mongo/util/xml_to_ruby.rb +112 -0
  40. data/mongo-ruby-driver.gemspec +28 -0
  41. data/test/replica/count_test.rb +34 -0
  42. data/test/replica/insert_test.rb +50 -0
  43. data/test/replica/pooled_insert_test.rb +54 -0
  44. data/test/replica/query_test.rb +39 -0
  45. data/test/test_admin.rb +67 -0
  46. data/test/test_bson.rb +397 -0
  47. data/test/test_byte_buffer.rb +81 -0
  48. data/test/test_chunk.rb +82 -0
  49. data/test/test_collection.rb +534 -0
  50. data/test/test_connection.rb +160 -0
  51. data/test/test_conversions.rb +120 -0
  52. data/test/test_cursor.rb +386 -0
  53. data/test/test_db.rb +254 -0
  54. data/test/test_db_api.rb +783 -0
  55. data/test/test_db_connection.rb +16 -0
  56. data/test/test_grid_store.rb +306 -0
  57. data/test/test_helper.rb +42 -0
  58. data/test/test_objectid.rb +156 -0
  59. data/test/test_ordered_hash.rb +168 -0
  60. data/test/test_round_trip.rb +114 -0
  61. data/test/test_slave_connection.rb +36 -0
  62. data/test/test_threading.rb +87 -0
  63. data/test/threading/test_threading_large_pool.rb +90 -0
  64. data/test/unit/collection_test.rb +52 -0
  65. data/test/unit/connection_test.rb +59 -0
  66. data/test/unit/cursor_test.rb +94 -0
  67. data/test/unit/db_test.rb +97 -0
  68. metadata +123 -0
@@ -0,0 +1,555 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ require 'set'
18
+ require 'socket'
19
+ require 'thread'
20
+
21
+ module Mongo
22
+
23
+ # Instantiates and manages connections to MongoDB.
24
+ class Connection
25
+
26
+ # Abort connections if a ConnectionError is raised.
27
+ Thread.abort_on_exception = true
28
+
29
+ DEFAULT_PORT = 27017
30
+ STANDARD_HEADER_SIZE = 16
31
+ RESPONSE_HEADER_SIZE = 20
32
+
33
+ attr_reader :logger, :size, :host, :port, :nodes, :sockets, :checked_out
34
+
35
+ # Counter for generating unique request ids.
36
+ @@current_request_id = 0
37
+
38
+ # Create a connection to MongoDB. Specify either one or a pair of servers,
39
+ # along with a maximum connection pool size and timeout.
40
+ #
41
+ # If connecting to just one server, you may specify whether connection to slave is permitted.
42
+ # In all cases, the default host is "localhost" and the default port is 27017.
43
+ #
44
+ # When specifying a pair, +pair_or_host+, is a hash with two keys: :left and :right. Each key maps to either
45
+ # * a server name, in which case port is 27017,
46
+ # * a port number, in which case the server is "localhost", or
47
+ # * an array containing [server_name, port_number]
48
+ #
49
+ # Note that there are a few issues when using connection pooling with Ruby 1.9 on Windows. These
50
+ # should be resolved in the next release.
51
+ #
52
+ # @param [String, Hash] pair_or_host See explanation above.
53
+ # @param [Integer] port specify a port number here if only one host is being specified. Leave nil if
54
+ # specifying a pair of servers in +pair_or_host+.
55
+ #
56
+ # @option options [Boolean] :slave_ok (false) Must be set to +true+ when connecting
57
+ # to a single, slave node.
58
+ # @option options [Logger, #debug] :logger (nil) Logger instance to receive driver operation log.
59
+ # @option options [Boolean] :auto_reconnect DEPRECATED. See http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby
60
+ # @option options [Integer] :pool_size (1) The maximum number of socket connections that can be opened to the database.
61
+ # @option options [Float] :timeout (5.0) When all of the connections to the pool are checked out,
62
+ # this is the number of seconds to wait for a new connection to be released before throwing an exception.
63
+ #
64
+ #
65
+ # @example localhost, 27017
66
+ # Connection.new
67
+ #
68
+ # @example localhost, 27017
69
+ # Connection.new("localhost")
70
+ #
71
+ # @example localhost, 3000, max 5 connections, with max 5 seconds of wait time.
72
+ # Connection.new("localhost", 3000, :pool_size => 5, :timeout => 5)
73
+ #
74
+ # @example localhost, 3000, where this node may be a slave
75
+ # Connection.new("localhost", 3000, :slave_ok => true)
76
+ #
77
+ # @example A pair of servers. The driver will always talk to master.
78
+ # # On connection errors, Mongo::ConnectionFailure will be raised.
79
+ # Connection.new({:left => ["db1.example.com", 27017],
80
+ # :right => ["db2.example.com", 27017]})
81
+ #
82
+ # @example A pair of servers with connection pooling enabled. Note the nil param placeholder for port.
83
+ # Connection.new({:left => ["db1.example.com", 27017],
84
+ # :right => ["db2.example.com", 27017]}, nil,
85
+ # :pool_size => 20, :timeout => 5)
86
+ #
87
+ # @see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby Replica pairs in Ruby
88
+ def initialize(pair_or_host=nil, port=nil, options={})
89
+ @nodes = format_pair(pair_or_host, port)
90
+
91
+ # Host and port of current master.
92
+ @host = @port = nil
93
+
94
+ # Lock for request ids.
95
+ @id_lock = Mutex.new
96
+
97
+ # Pool size and timeout.
98
+ @size = options[:pool_size] || 1
99
+ @timeout = options[:timeout] || 5.0
100
+
101
+ # Mutex for synchronizing pool access
102
+ @connection_mutex = Mutex.new
103
+ @safe_mutex = Mutex.new
104
+
105
+ # Condition variable for signal and wait
106
+ @queue = ConditionVariable.new
107
+
108
+ @sockets = []
109
+ @checked_out = []
110
+
111
+ if options[:auto_reconnect]
112
+ warn(":auto_reconnect is deprecated. see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby")
113
+ end
114
+
115
+ # slave_ok can be true only if one node is specified
116
+ @slave_ok = options[:slave_ok] && @nodes.length == 1
117
+ @logger = options[:logger] || nil
118
+ @options = options
119
+
120
+ should_connect = options[:connect].nil? ? true : options[:connect]
121
+ connect_to_master if should_connect
122
+ end
123
+
124
+ # Return a hash with all database names
125
+ # and their respective sizes on disk.
126
+ #
127
+ # @return [Hash]
128
+ def database_info
129
+ doc = self['admin'].command(:listDatabases => 1)
130
+ returning({}) do |info|
131
+ doc['databases'].each { |db| info[db['name']] = db['sizeOnDisk'].to_i }
132
+ end
133
+ end
134
+
135
+ # Return an array of database names.
136
+ #
137
+ # @return [Array]
138
+ def database_names
139
+ database_info.keys
140
+ end
141
+
142
+ # Return a database with the given name.
143
+ # See DB#new for valid options hash parameters.
144
+ #
145
+ # @param [String] db_name a valid database name.
146
+ #
147
+ # @return [Mongo::DB]
148
+ def db(db_name, options={})
149
+ DB.new(db_name, self, options.merge(:logger => @logger))
150
+ end
151
+
152
+ # Shortcut for returning a database. Use DB#db to accept options.
153
+ #
154
+ # @param [String] db_name a valid database name.
155
+ #
156
+ # @return [Mongo::DB]
157
+ def [](db_name)
158
+ DB.new(db_name, self, :logger => @logger)
159
+ end
160
+
161
+ # Drop a database.
162
+ #
163
+ # @param [String] name name of an existing database.
164
+ def drop_database(name)
165
+ self[name].command(:dropDatabase => 1)
166
+ end
167
+
168
+ # Copy the database +from+ on the local server to +to+ on the specified +host+.
169
+ # +host+ defaults to 'localhost' if no value is provided.
170
+ #
171
+ # @param [String] from name of the database to copy from.
172
+ # @param [String] to name of the database to copy to.
173
+ # @param [String] from_host host of the 'from' database.
174
+ def copy_database(from, to, from_host="localhost")
175
+ oh = OrderedHash.new
176
+ oh[:copydb] = 1
177
+ oh[:fromhost] = from_host
178
+ oh[:fromdb] = from
179
+ oh[:todb] = to
180
+ self["admin"].command(oh)
181
+ end
182
+
183
+ # Increment and return the next available request id.
184
+ #
185
+ # return [Integer]
186
+ def get_request_id
187
+ request_id = ''
188
+ @id_lock.synchronize do
189
+ request_id = @@current_request_id += 1
190
+ end
191
+ request_id
192
+ end
193
+
194
+ # Get the build information for the current connection.
195
+ #
196
+ # @return [Hash]
197
+ def server_info
198
+ db("admin").command({:buildinfo => 1}, {:admin => true, :check_response => true})
199
+ end
200
+
201
+ # Get the build version of the current server.
202
+ #
203
+ # @return [Mongo::ServerVersion]
204
+ # object allowing easy comparability of version.
205
+ def server_version
206
+ ServerVersion.new(server_info["version"])
207
+ end
208
+
209
+ # Is it okay to connect to a slave?
210
+ #
211
+ # @return [Boolean]
212
+ def slave_ok?
213
+ @slave_ok
214
+ end
215
+
216
+
217
+ ## Connections and pooling ##
218
+
219
+ # Send a message to MongoDB, adding the necessary headers.
220
+ #
221
+ # @param [Integer] operation a MongoDB opcode.
222
+ # @param [ByteBuffer] message a message to send to the database.
223
+ # @param [String] log_message text version of +message+ for logging.
224
+ #
225
+ # @return [True]
226
+ def send_message(operation, message, log_message=nil)
227
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
228
+ begin
229
+ packed_message = add_message_headers(operation, message).to_s
230
+ socket = checkout
231
+ send_message_on_socket(packed_message, socket)
232
+ ensure
233
+ checkin(socket)
234
+ end
235
+ end
236
+
237
+ # Sends a message to the database, waits for a response, and raises
238
+ # an exception if the operation has failed.
239
+ #
240
+ # @param [Integer] operation a MongoDB opcode.
241
+ # @param [ByteBuffer] message a message to send to the database.
242
+ # @param [String] db_name the name of the database. used on call to get_last_error.
243
+ # @param [String] log_message text version of +message+ for logging.
244
+ #
245
+ # @return [Array]
246
+ # An array whose indexes include [0] documents returned, [1] number of document received,
247
+ # and [3] a cursor_id.
248
+ def send_message_with_safe_check(operation, message, db_name, log_message=nil)
249
+ message_with_headers = add_message_headers(operation, message)
250
+ message_with_check = last_error_message(db_name)
251
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
252
+ begin
253
+ sock = checkout
254
+ packed_message = message_with_headers.append!(message_with_check).to_s
255
+ docs = num_received = cursor_id = ''
256
+ @safe_mutex.synchronize do
257
+ send_message_on_socket(packed_message, sock)
258
+ docs, num_received, cursor_id = receive(sock)
259
+ end
260
+ ensure
261
+ checkin(sock)
262
+ end
263
+ if num_received == 1 && error = docs[0]['err']
264
+ raise Mongo::OperationFailure, error
265
+ end
266
+ [docs, num_received, cursor_id]
267
+ end
268
+
269
+ # Sends a message to the database and waits for the response.
270
+ #
271
+ # @param [Integer] operation a MongoDB opcode.
272
+ # @param [ByteBuffer] message a message to send to the database.
273
+ # @param [String] log_message text version of +message+ for logging.
274
+ # @param [Socket] socket a socket to use in lieu of checking out a new one.
275
+ #
276
+ # @return [Array]
277
+ # An array whose indexes include [0] documents returned, [1] number of document received,
278
+ # and [3] a cursor_id.
279
+ def receive_message(operation, message, log_message=nil, socket=nil)
280
+ packed_message = add_message_headers(operation, message).to_s
281
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
282
+ begin
283
+ sock = socket || checkout
284
+
285
+ result = ''
286
+ @safe_mutex.synchronize do
287
+ send_message_on_socket(packed_message, sock)
288
+ result = receive(sock)
289
+ end
290
+ ensure
291
+ checkin(sock)
292
+ end
293
+ result
294
+ end
295
+
296
+ # Create a new socket and attempt to connect to master.
297
+ # If successful, sets host and port to master and returns the socket.
298
+ #
299
+ # @raise [ConnectionFailure] if unable to connect to any host or port.
300
+ def connect_to_master
301
+ close
302
+ @host = @port = nil
303
+ for node_pair in @nodes
304
+ host, port = *node_pair
305
+ begin
306
+ socket = TCPSocket.new(host, port)
307
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
308
+
309
+ # If we're connected to master, set the @host and @port
310
+ result = self['admin'].command({:ismaster => 1}, false, false, socket)
311
+ if result['ok'] == 1 && ((is_master = result['ismaster'] == 1) || @slave_ok)
312
+ @host, @port = host, port
313
+ end
314
+
315
+ # Note: slave_ok can be true only when connecting to a single node.
316
+ if @nodes.length == 1 && !is_master && !@slave_ok
317
+ raise ConfigurationError, "Trying to connect directly to slave; " +
318
+ "if this is what you want, specify :slave_ok => true."
319
+ end
320
+
321
+ break if is_master || @slave_ok
322
+ rescue SocketError, SystemCallError, IOError => ex
323
+ socket.close if socket
324
+ false
325
+ end
326
+ end
327
+ raise ConnectionFailure, "failed to connect to any given host:port" unless socket
328
+ end
329
+
330
+ # Are we connected to MongoDB? This is determined by checking whether
331
+ # host and port have values, since they're set to nil on calls to #close.
332
+ def connected?
333
+ @host && @port
334
+ end
335
+
336
+ # Close the connection to the database.
337
+ def close
338
+ @sockets.each do |sock|
339
+ sock.close
340
+ end
341
+ @host = @port = nil
342
+ @sockets.clear
343
+ @checked_out.clear
344
+ end
345
+
346
+ private
347
+
348
+ # Return a socket to the pool.
349
+ def checkin(socket)
350
+ @connection_mutex.synchronize do
351
+ @checked_out.delete(socket)
352
+ @queue.signal
353
+ end
354
+ true
355
+ end
356
+
357
+ # Adds a new socket to the pool and checks it out.
358
+ #
359
+ # This method is called exclusively from #checkout;
360
+ # therefore, it runs within a mutex.
361
+ def checkout_new_socket
362
+ begin
363
+ socket = TCPSocket.new(@host, @port)
364
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
365
+ rescue => ex
366
+ raise ConnectionFailure, "Failed to connect socket: #{ex}"
367
+ end
368
+ @sockets << socket
369
+ @checked_out << socket
370
+ socket
371
+ end
372
+
373
+ # Checks out the first available socket from the pool.
374
+ #
375
+ # This method is called exclusively from #checkout;
376
+ # therefore, it runs within a mutex.
377
+ def checkout_existing_socket
378
+ socket = (@sockets - @checked_out).first
379
+ @checked_out << socket
380
+ socket
381
+ end
382
+
383
+ # Check out an existing socket or create a new socket if the maximum
384
+ # pool size has not been exceeded. Otherwise, wait for the next
385
+ # available socket.
386
+ def checkout
387
+ connect_to_master if !connected?
388
+ start_time = Time.now
389
+ loop do
390
+ if (Time.now - start_time) > @timeout
391
+ raise ConnectionTimeoutError, "could not obtain connection within " +
392
+ "#{@timeout} seconds. The max pool size is currently #{@size}; " +
393
+ "consider increasing the pool size or timeout."
394
+ end
395
+
396
+ @connection_mutex.synchronize do
397
+ socket = if @checked_out.size < @sockets.size
398
+ checkout_existing_socket
399
+ elsif @sockets.size < @size
400
+ checkout_new_socket
401
+ end
402
+
403
+ return socket if socket
404
+
405
+ # Otherwise, wait
406
+ if @logger
407
+ @logger.warn "Waiting for available connection; #{@checked_out.size} of #{@size} connections checked out."
408
+ end
409
+ @queue.wait(@connection_mutex)
410
+ end
411
+ end
412
+ end
413
+
414
+ def receive(sock)
415
+ receive_header(sock)
416
+ number_received, cursor_id = receive_response_header(sock)
417
+ read_documents(number_received, cursor_id, sock)
418
+ end
419
+
420
+ def receive_header(sock)
421
+ header = ByteBuffer.new
422
+ header.put_array(receive_message_on_socket(16, sock).unpack("C*"))
423
+ unless header.size == STANDARD_HEADER_SIZE
424
+ raise "Short read for DB response header: " +
425
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
426
+ end
427
+ header.rewind
428
+ size = header.get_int
429
+ request_id = header.get_int
430
+ response_to = header.get_int
431
+ op = header.get_int
432
+ end
433
+
434
+ def receive_response_header(sock)
435
+ header_buf = ByteBuffer.new
436
+ header_buf.put_array(receive_message_on_socket(RESPONSE_HEADER_SIZE, sock).unpack("C*"))
437
+ if header_buf.length != RESPONSE_HEADER_SIZE
438
+ raise "Short read for DB response header; " +
439
+ "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
440
+ end
441
+ header_buf.rewind
442
+ result_flags = header_buf.get_int
443
+ cursor_id = header_buf.get_long
444
+ starting_from = header_buf.get_int
445
+ number_remaining = header_buf.get_int
446
+ [number_remaining, cursor_id]
447
+ end
448
+
449
+ def read_documents(number_received, cursor_id, sock)
450
+ docs = []
451
+ number_remaining = number_received
452
+ while number_remaining > 0 do
453
+ buf = ByteBuffer.new
454
+ buf.put_array(receive_message_on_socket(4, sock).unpack("C*"))
455
+ buf.rewind
456
+ size = buf.get_int
457
+ buf.put_array(receive_message_on_socket(size - 4, sock).unpack("C*"), 4)
458
+ number_remaining -= 1
459
+ buf.rewind
460
+ docs << BSON.deserialize(buf)
461
+ end
462
+ [docs, number_received, cursor_id]
463
+ end
464
+
465
+ def last_error_message(db_name)
466
+ message = ByteBuffer.new
467
+ message.put_int(0)
468
+ BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
469
+ message.put_int(0)
470
+ message.put_int(-1)
471
+ message.put_array(BSON.serialize({:getlasterror => 1}, false).unpack("C*"))
472
+ add_message_headers(Mongo::Constants::OP_QUERY, message)
473
+ end
474
+
475
+ # Prepares a message for transmission to MongoDB by
476
+ # constructing a valid message header.
477
+ def add_message_headers(operation, message)
478
+ headers = ByteBuffer.new
479
+
480
+ # Message size.
481
+ headers.put_int(16 + message.size)
482
+
483
+ # Unique request id.
484
+ headers.put_int(get_request_id)
485
+
486
+ # Response id.
487
+ headers.put_int(0)
488
+
489
+ # Opcode.
490
+ headers.put_int(operation)
491
+ message.prepend!(headers)
492
+ end
493
+
494
+ # Low-level method for sending a message on a socket.
495
+ # Requires a packed message and an available socket,
496
+ def send_message_on_socket(packed_message, socket)
497
+ begin
498
+ socket.send(packed_message, 0)
499
+ rescue => ex
500
+ close
501
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
502
+ end
503
+ end
504
+
505
+ # Low-level method for receiving data from socket.
506
+ # Requires length and an available socket.
507
+ def receive_message_on_socket(length, socket)
508
+ message = ""
509
+ begin
510
+ while message.length < length do
511
+ chunk = socket.recv(length - message.length)
512
+ raise ConnectionFailure, "connection closed" unless chunk.length > 0
513
+ message += chunk
514
+ end
515
+ rescue => ex
516
+ raise ConnectionFailure, "Operation failed with the following exception: #{ex}"
517
+ end
518
+ message
519
+ end
520
+
521
+
522
+ ## Private helper methods
523
+
524
+ # Returns an array of host-port pairs.
525
+ def format_pair(pair_or_host, port)
526
+ case pair_or_host
527
+ when String
528
+ [[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
529
+ when Hash
530
+ connections = []
531
+ connections << pair_val_to_connection(pair_or_host[:left])
532
+ connections << pair_val_to_connection(pair_or_host[:right])
533
+ connections
534
+ when nil
535
+ [['localhost', DEFAULT_PORT]]
536
+ end
537
+ end
538
+
539
+ # Turns an array containing a host name string and a
540
+ # port number integer into a [host, port] pair array.
541
+ def pair_val_to_connection(a)
542
+ case a
543
+ when nil
544
+ ['localhost', DEFAULT_PORT]
545
+ when String
546
+ [a, DEFAULT_PORT]
547
+ when Integer
548
+ ['localhost', a]
549
+ when Array
550
+ a
551
+ end
552
+ end
553
+
554
+ end
555
+ end