mongo-find_replace 0.18.3
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.
- data/LICENSE.txt +202 -0
- data/README.rdoc +358 -0
- data/Rakefile +133 -0
- data/bin/bson_benchmark.rb +59 -0
- data/bin/fail_if_no_c.rb +11 -0
- data/examples/admin.rb +42 -0
- data/examples/capped.rb +22 -0
- data/examples/cursor.rb +48 -0
- data/examples/gridfs.rb +88 -0
- data/examples/index_test.rb +126 -0
- data/examples/info.rb +31 -0
- data/examples/queries.rb +70 -0
- data/examples/simple.rb +24 -0
- data/examples/strict.rb +35 -0
- data/examples/types.rb +36 -0
- data/lib/mongo.rb +61 -0
- data/lib/mongo/admin.rb +95 -0
- data/lib/mongo/collection.rb +664 -0
- data/lib/mongo/connection.rb +555 -0
- data/lib/mongo/cursor.rb +393 -0
- data/lib/mongo/db.rb +527 -0
- data/lib/mongo/exceptions.rb +60 -0
- data/lib/mongo/gridfs.rb +22 -0
- data/lib/mongo/gridfs/chunk.rb +90 -0
- data/lib/mongo/gridfs/grid_store.rb +555 -0
- data/lib/mongo/types/binary.rb +48 -0
- data/lib/mongo/types/code.rb +36 -0
- data/lib/mongo/types/dbref.rb +38 -0
- data/lib/mongo/types/min_max_keys.rb +58 -0
- data/lib/mongo/types/objectid.rb +219 -0
- data/lib/mongo/types/regexp_of_holding.rb +45 -0
- data/lib/mongo/util/bson_c.rb +18 -0
- data/lib/mongo/util/bson_ruby.rb +595 -0
- data/lib/mongo/util/byte_buffer.rb +222 -0
- data/lib/mongo/util/conversions.rb +97 -0
- data/lib/mongo/util/ordered_hash.rb +135 -0
- data/lib/mongo/util/server_version.rb +69 -0
- data/lib/mongo/util/support.rb +26 -0
- data/lib/mongo/util/xml_to_ruby.rb +112 -0
- data/mongo-ruby-driver.gemspec +28 -0
- data/test/replica/count_test.rb +34 -0
- data/test/replica/insert_test.rb +50 -0
- data/test/replica/pooled_insert_test.rb +54 -0
- data/test/replica/query_test.rb +39 -0
- data/test/test_admin.rb +67 -0
- data/test/test_bson.rb +397 -0
- data/test/test_byte_buffer.rb +81 -0
- data/test/test_chunk.rb +82 -0
- data/test/test_collection.rb +534 -0
- data/test/test_connection.rb +160 -0
- data/test/test_conversions.rb +120 -0
- data/test/test_cursor.rb +386 -0
- data/test/test_db.rb +254 -0
- data/test/test_db_api.rb +783 -0
- data/test/test_db_connection.rb +16 -0
- data/test/test_grid_store.rb +306 -0
- data/test/test_helper.rb +42 -0
- data/test/test_objectid.rb +156 -0
- data/test/test_ordered_hash.rb +168 -0
- data/test/test_round_trip.rb +114 -0
- data/test/test_slave_connection.rb +36 -0
- data/test/test_threading.rb +87 -0
- data/test/threading/test_threading_large_pool.rb +90 -0
- data/test/unit/collection_test.rb +52 -0
- data/test/unit/connection_test.rb +59 -0
- data/test/unit/cursor_test.rb +94 -0
- data/test/unit/db_test.rb +97 -0
- 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
|