moped 0.0.0.beta → 1.0.0.alpha
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.
- data/MIT_LICENSE +19 -0
- data/README.md +323 -0
- data/lib/moped.rb +19 -0
- data/lib/moped/bson.rb +25 -0
- data/lib/moped/bson/binary.rb +68 -0
- data/lib/moped/bson/code.rb +61 -0
- data/lib/moped/bson/document.rb +16 -0
- data/lib/moped/bson/extensions.rb +81 -0
- data/lib/moped/bson/extensions/array.rb +44 -0
- data/lib/moped/bson/extensions/boolean.rb +14 -0
- data/lib/moped/bson/extensions/false_class.rb +15 -0
- data/lib/moped/bson/extensions/float.rb +23 -0
- data/lib/moped/bson/extensions/hash.rb +49 -0
- data/lib/moped/bson/extensions/integer.rb +37 -0
- data/lib/moped/bson/extensions/nil_class.rb +20 -0
- data/lib/moped/bson/extensions/regexp.rb +40 -0
- data/lib/moped/bson/extensions/string.rb +35 -0
- data/lib/moped/bson/extensions/symbol.rb +25 -0
- data/lib/moped/bson/extensions/time.rb +21 -0
- data/lib/moped/bson/extensions/true_class.rb +15 -0
- data/lib/moped/bson/max_key.rb +21 -0
- data/lib/moped/bson/min_key.rb +21 -0
- data/lib/moped/bson/object_id.rb +123 -0
- data/lib/moped/bson/timestamp.rb +15 -0
- data/lib/moped/bson/types.rb +67 -0
- data/lib/moped/cluster.rb +193 -0
- data/lib/moped/collection.rb +67 -0
- data/lib/moped/cursor.rb +60 -0
- data/lib/moped/database.rb +76 -0
- data/lib/moped/errors.rb +61 -0
- data/lib/moped/indexes.rb +93 -0
- data/lib/moped/logging.rb +25 -0
- data/lib/moped/protocol.rb +20 -0
- data/lib/moped/protocol/command.rb +27 -0
- data/lib/moped/protocol/commands.rb +11 -0
- data/lib/moped/protocol/commands/authenticate.rb +54 -0
- data/lib/moped/protocol/delete.rb +92 -0
- data/lib/moped/protocol/get_more.rb +79 -0
- data/lib/moped/protocol/insert.rb +92 -0
- data/lib/moped/protocol/kill_cursors.rb +61 -0
- data/lib/moped/protocol/message.rb +320 -0
- data/lib/moped/protocol/query.rb +131 -0
- data/lib/moped/protocol/reply.rb +90 -0
- data/lib/moped/protocol/update.rb +107 -0
- data/lib/moped/query.rb +230 -0
- data/lib/moped/server.rb +73 -0
- data/lib/moped/session.rb +253 -0
- data/lib/moped/socket.rb +201 -0
- data/lib/moped/version.rb +4 -0
- metadata +108 -46
data/lib/moped/server.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Moped
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
#
|
5
|
+
# The internal class for storing information about a server.
|
6
|
+
class Server
|
7
|
+
|
8
|
+
# @return [String] the original host:port address provided
|
9
|
+
attr_reader :address
|
10
|
+
|
11
|
+
# @return [String] the resolved host:port address
|
12
|
+
attr_reader :resolved_address
|
13
|
+
|
14
|
+
# @return [String] the resolved ip address
|
15
|
+
attr_reader :ip_address
|
16
|
+
|
17
|
+
# @return [Integer] the resolved port
|
18
|
+
attr_reader :port
|
19
|
+
|
20
|
+
attr_writer :primary
|
21
|
+
attr_writer :secondary
|
22
|
+
|
23
|
+
def initialize(address)
|
24
|
+
@address = address
|
25
|
+
|
26
|
+
host, port = address.split(":")
|
27
|
+
port = port ? port.to_i : 27017
|
28
|
+
|
29
|
+
ip_address = ::Socket.getaddrinfo(host, nil, ::Socket::AF_INET, ::Socket::SOCK_STREAM).first[3]
|
30
|
+
|
31
|
+
@primary = @secondary = false
|
32
|
+
@ip_address = ip_address
|
33
|
+
@port = port
|
34
|
+
@resolved_address = "#{ip_address}:#{port}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def primary?
|
38
|
+
!!@primary
|
39
|
+
end
|
40
|
+
|
41
|
+
def secondary?
|
42
|
+
!!@secondary
|
43
|
+
end
|
44
|
+
|
45
|
+
def merge(other)
|
46
|
+
@primary = other.primary?
|
47
|
+
@secondary = other.secondary?
|
48
|
+
|
49
|
+
other.close
|
50
|
+
end
|
51
|
+
|
52
|
+
def close
|
53
|
+
if @socket
|
54
|
+
@socket.close
|
55
|
+
@socket = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def socket
|
60
|
+
@socket ||= Socket.new(ip_address, port)
|
61
|
+
end
|
62
|
+
|
63
|
+
def ==(other)
|
64
|
+
self.class === other && hash == other.hash
|
65
|
+
end
|
66
|
+
alias eql? ==
|
67
|
+
|
68
|
+
def hash
|
69
|
+
[ip_address, port].hash
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
module Moped
|
2
|
+
|
3
|
+
# A session in moped is root for all interactions with a MongoDB server or
|
4
|
+
# replica set.
|
5
|
+
#
|
6
|
+
# It can talk to a single default database, or dynamically speak to multiple
|
7
|
+
# databases.
|
8
|
+
#
|
9
|
+
# @example Single database (console-style)
|
10
|
+
# session = Moped::Session.new(["127.0.0.1:27017"])
|
11
|
+
# session.use :moped
|
12
|
+
# session[:users].find.one # => { name: "John" }
|
13
|
+
#
|
14
|
+
# @example Multiple databases
|
15
|
+
# session = Moped::Session.new(["127.0.0.1:27017"])
|
16
|
+
#
|
17
|
+
# session.with(database: :admin) do |admin|
|
18
|
+
# admin.command ismaster: 1
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# session.with(database: :moped) do |moped|
|
22
|
+
# moped[:users].find.one # => { name: "John" }
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @example Authentication
|
26
|
+
#
|
27
|
+
# session = Moped::Session.new %w[127.0.0.1:27017],
|
28
|
+
# session.with(database: "admin").login("admin", "s3cr3t")
|
29
|
+
#
|
30
|
+
class Session
|
31
|
+
extend Forwardable
|
32
|
+
|
33
|
+
# @return [Hash] this session's options
|
34
|
+
attr_reader :options
|
35
|
+
|
36
|
+
# @private
|
37
|
+
# @return [Cluster] this session's cluster
|
38
|
+
attr_reader :cluster
|
39
|
+
|
40
|
+
# @param [Array] seeds an of host:port pairs
|
41
|
+
# @param [Hash] options
|
42
|
+
# @option options [Boolean] :safe (false) ensure writes are persisted
|
43
|
+
# @option options [Hash] :safe ensure writes are persisted with the
|
44
|
+
# specified safety level e.g., "fsync: true", or "w: 2, wtimeout: 5"
|
45
|
+
# @option options [Symbol, String] :database the database to use
|
46
|
+
# @option options [:strong, :eventual] :consistency (:eventual)
|
47
|
+
def initialize(seeds, options = {})
|
48
|
+
@cluster = Cluster.new(seeds)
|
49
|
+
@options = options
|
50
|
+
@options[:consistency] ||= :eventual
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Boolean] whether the current session requires safe operations.
|
54
|
+
def safe?
|
55
|
+
!!safety
|
56
|
+
end
|
57
|
+
|
58
|
+
# Switch the session's current database.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# session.use :moped
|
62
|
+
# session[:people]. john, mary = session[:people].find.one # => { :name => "John" }
|
63
|
+
#
|
64
|
+
# @param [String] database the database to use
|
65
|
+
def use(database)
|
66
|
+
options[:database] = database
|
67
|
+
set_current_database database
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create a new session with +options+ reusing existing connections.
|
71
|
+
#
|
72
|
+
# @example Change safe mode
|
73
|
+
# session.with(safe: { w: 2 })[:people].insert(name: "Joe")
|
74
|
+
#
|
75
|
+
# @example Change safe mode with block
|
76
|
+
# session.with(safe: { w: 2 }) do |session|
|
77
|
+
# session[:people].insert(name: "Joe")
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @example Temporarily change database
|
81
|
+
# session.with(database: "admin") do |admin|
|
82
|
+
# admin.command ismaster: 1
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# @example Copy between databases
|
86
|
+
# session.use "moped"
|
87
|
+
# session.with(database: "backup") do |backup|
|
88
|
+
# session[:people].each do |person|
|
89
|
+
# backup[:people].insert person
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# @yieldparam [Moped::Session] session the new session
|
94
|
+
# @return [Moped::Session, Object] the new session, or the value returned
|
95
|
+
# by the block if provided.
|
96
|
+
def with(options = {})
|
97
|
+
session = dup
|
98
|
+
session.options.update options
|
99
|
+
|
100
|
+
if block_given?
|
101
|
+
yield session
|
102
|
+
else
|
103
|
+
session
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Create a new session with +options+ and use new socket connections.
|
108
|
+
#
|
109
|
+
# @see #with
|
110
|
+
# @yieldparam [Moped::Session] session the new session
|
111
|
+
# @return [Moped::Session] the new session
|
112
|
+
def new(options = {})
|
113
|
+
session = with(options)
|
114
|
+
session.cluster.reconnect
|
115
|
+
|
116
|
+
if block_given?
|
117
|
+
yield session
|
118
|
+
else
|
119
|
+
session
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# @method [](collection)
|
124
|
+
# Return +collection+ from the current database.
|
125
|
+
#
|
126
|
+
# @param (see Moped::Database#[])
|
127
|
+
# @return (see Moped::Database#[])
|
128
|
+
delegate :"[]" => :current_database
|
129
|
+
|
130
|
+
# @method command(command)
|
131
|
+
# Run +command+ on the current database.
|
132
|
+
#
|
133
|
+
# @param (see Moped::Database#command)
|
134
|
+
# @return (see Moped::Database#command)
|
135
|
+
delegate :command => :current_database
|
136
|
+
|
137
|
+
# @method drop
|
138
|
+
# Drop the current database.
|
139
|
+
#
|
140
|
+
# @param (see Moped::Database#drop)
|
141
|
+
# @return (see Moped::Database#drop)
|
142
|
+
delegate :drop => :current_database
|
143
|
+
|
144
|
+
# @method login(username, password)
|
145
|
+
# Log in with +username+ and +password+ on the current database.
|
146
|
+
#
|
147
|
+
# @param (see Moped::Database#login)
|
148
|
+
# @raise (see Moped::Database#login)
|
149
|
+
delegate :login => :current_database
|
150
|
+
|
151
|
+
# @method logout
|
152
|
+
# Log out from the current database.
|
153
|
+
#
|
154
|
+
# @param (see Moped::Database#logout)
|
155
|
+
# @raise (see Moped::Database#login)
|
156
|
+
delegate :logout => :current_database
|
157
|
+
|
158
|
+
# @private
|
159
|
+
def current_database
|
160
|
+
return @current_database if defined? @current_database
|
161
|
+
|
162
|
+
if database = options[:database]
|
163
|
+
set_current_database(database)
|
164
|
+
else
|
165
|
+
raise "No database set for session. Call #use or #with before accessing the database"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @private
|
170
|
+
def simple_query(query)
|
171
|
+
query.limit = -1
|
172
|
+
|
173
|
+
query(query).documents.first
|
174
|
+
end
|
175
|
+
|
176
|
+
# @private
|
177
|
+
def query(query)
|
178
|
+
if options[:consistency] == :eventual
|
179
|
+
query.flags |= [:slave_ok] if query.respond_to? :flags
|
180
|
+
mode = :read
|
181
|
+
else
|
182
|
+
mode = :write
|
183
|
+
end
|
184
|
+
|
185
|
+
reply = socket_for(mode).execute(query)
|
186
|
+
|
187
|
+
reply.tap do |reply|
|
188
|
+
if reply.flags.include?(:query_failure)
|
189
|
+
raise Errors::QueryFailure.new(query, reply.documents.first)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# @private
|
195
|
+
def execute(op)
|
196
|
+
mode = options[:consistency] == :eventual ? :read : :write
|
197
|
+
socket = socket_for(mode)
|
198
|
+
|
199
|
+
if safe?
|
200
|
+
last_error = Protocol::Command.new(
|
201
|
+
"admin", { getlasterror: 1 }.merge(safety)
|
202
|
+
)
|
203
|
+
|
204
|
+
socket.execute(op, last_error).documents.first.tap do |result|
|
205
|
+
raise Errors::OperationFailure.new(
|
206
|
+
op, result
|
207
|
+
) if result["err"] || result["errmsg"]
|
208
|
+
end
|
209
|
+
else
|
210
|
+
socket.execute(op)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
# @return [Boolean, Hash] the safety level for this session
|
217
|
+
def safety
|
218
|
+
safe = options[:safe]
|
219
|
+
|
220
|
+
case safe
|
221
|
+
when false
|
222
|
+
false
|
223
|
+
when true
|
224
|
+
{ safe: true }
|
225
|
+
else
|
226
|
+
safe
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def socket_for(mode)
|
231
|
+
if options[:retain_socket]
|
232
|
+
@socket ||= cluster.socket_for(mode)
|
233
|
+
else
|
234
|
+
cluster.socket_for(mode)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def set_current_database(database)
|
239
|
+
@current_database = Database.new(self, database)
|
240
|
+
end
|
241
|
+
|
242
|
+
def dup
|
243
|
+
session = super
|
244
|
+
session.instance_variable_set :@options, options.dup
|
245
|
+
|
246
|
+
if defined? @current_database
|
247
|
+
session.send(:remove_instance_variable, :@current_database)
|
248
|
+
end
|
249
|
+
|
250
|
+
session
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
data/lib/moped/socket.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
require "timeout"
|
2
|
+
|
3
|
+
module Moped
|
4
|
+
|
5
|
+
# @api private
|
6
|
+
#
|
7
|
+
# The internal class wrapping a socket connection.
|
8
|
+
class Socket
|
9
|
+
|
10
|
+
# Thread-safe atomic integer.
|
11
|
+
class RequestId
|
12
|
+
def initialize
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@id = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def next
|
18
|
+
@mutex.synchronize { @id += 1 }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :connection
|
23
|
+
|
24
|
+
attr_reader :host
|
25
|
+
attr_reader :port
|
26
|
+
|
27
|
+
def initialize(host, port)
|
28
|
+
@host = host
|
29
|
+
@port = port
|
30
|
+
|
31
|
+
@mutex = Mutex.new
|
32
|
+
@request_id = RequestId.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [true, false] whether the connection was successful
|
36
|
+
# @note The connection timeout is currently just 0.5 seconds, which should
|
37
|
+
# be sufficient, but may need to be raised or made configurable for
|
38
|
+
# high-latency situations. That said, if connecting to the remote server
|
39
|
+
# takes that long, we may not want to use the node any way.
|
40
|
+
def connect
|
41
|
+
return true if connection
|
42
|
+
|
43
|
+
Timeout::timeout 0.5 do
|
44
|
+
@connection = TCPSocket.new(host, port)
|
45
|
+
end
|
46
|
+
rescue Errno::ECONNREFUSED, Timeout::Error
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [true, false] whether this socket connection is alive
|
51
|
+
def alive?
|
52
|
+
if connection
|
53
|
+
return false if connection.closed?
|
54
|
+
|
55
|
+
@mutex.synchronize do
|
56
|
+
if select([connection], nil, nil, 0)
|
57
|
+
!connection.eof? rescue false
|
58
|
+
else
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Execute the operations on the connection.
|
68
|
+
def execute(*ops)
|
69
|
+
instrument(ops) do
|
70
|
+
buf = ""
|
71
|
+
|
72
|
+
last = ops.each do |op|
|
73
|
+
op.request_id = @request_id.next
|
74
|
+
op.serialize buf
|
75
|
+
end.last
|
76
|
+
|
77
|
+
if Protocol::Query === last || Protocol::GetMore === last
|
78
|
+
length = nil
|
79
|
+
|
80
|
+
@mutex.synchronize do
|
81
|
+
connection.write buf
|
82
|
+
|
83
|
+
length, = connection.read(4).unpack('l<')
|
84
|
+
|
85
|
+
# Re-use the already allocated buffer used for writing the command.
|
86
|
+
connection.read(length - 4, buf)
|
87
|
+
end
|
88
|
+
|
89
|
+
parse_reply length, buf
|
90
|
+
else
|
91
|
+
@mutex.synchronize do
|
92
|
+
connection.write buf
|
93
|
+
end
|
94
|
+
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_reply(length, data)
|
101
|
+
buffer = StringIO.new data
|
102
|
+
|
103
|
+
reply = Protocol::Reply.allocate
|
104
|
+
|
105
|
+
reply.length = length
|
106
|
+
|
107
|
+
reply.request_id,
|
108
|
+
reply.response_to,
|
109
|
+
reply.op_code,
|
110
|
+
reply.flags,
|
111
|
+
reply.cursor_id,
|
112
|
+
reply.offset,
|
113
|
+
reply.count = buffer.read(32).unpack('l4<q<l2<')
|
114
|
+
|
115
|
+
reply.documents = reply.count.times.map do
|
116
|
+
BSON::Document.deserialize(buffer)
|
117
|
+
end
|
118
|
+
|
119
|
+
reply
|
120
|
+
end
|
121
|
+
|
122
|
+
# Executes a simple (one result) query and returns the first document.
|
123
|
+
#
|
124
|
+
# @return [Hash] the first document in a result set.
|
125
|
+
def simple_query(query)
|
126
|
+
query = query.dup
|
127
|
+
query.limit = -1
|
128
|
+
|
129
|
+
execute(query).documents.first
|
130
|
+
end
|
131
|
+
|
132
|
+
# Manually closes the connection
|
133
|
+
def close
|
134
|
+
@mutex.synchronize do
|
135
|
+
connection.close if connection && !connection.closed?
|
136
|
+
@connection = nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def auth
|
141
|
+
@auth ||= {}
|
142
|
+
end
|
143
|
+
|
144
|
+
def apply_auth(credentials)
|
145
|
+
return if auth == credentials
|
146
|
+
logouts = auth.keys - credentials.keys
|
147
|
+
|
148
|
+
logouts.each do |database|
|
149
|
+
logout database
|
150
|
+
end
|
151
|
+
|
152
|
+
credentials.each do |database, (username, password)|
|
153
|
+
login(database, username, password) unless auth[database] == [username, password]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def login(database, username, password)
|
158
|
+
getnonce = Protocol::Command.new(database, getnonce: 1)
|
159
|
+
result = simple_query getnonce
|
160
|
+
|
161
|
+
raise Errors::OperationFailure.new(getnonce, result) unless result["ok"] == 1
|
162
|
+
|
163
|
+
authenticate = Protocol::Commands::Authenticate.new(database, username, password, result["nonce"])
|
164
|
+
result = simple_query authenticate
|
165
|
+
raise Errors::OperationFailure.new(authenticate, result) unless result["ok"] == 1
|
166
|
+
|
167
|
+
auth[database.to_s] = [username, password]
|
168
|
+
end
|
169
|
+
|
170
|
+
def logout(database)
|
171
|
+
command = Protocol::Command.new(database, logout: 1)
|
172
|
+
result = simple_query command
|
173
|
+
raise Errors::OperationFailure.new(command, result) unless result["ok"] == 1
|
174
|
+
auth.delete(database.to_s)
|
175
|
+
end
|
176
|
+
|
177
|
+
def instrument(ops)
|
178
|
+
instrument_start = (logger = Moped.logger) && logger.debug? && Time.now
|
179
|
+
yield
|
180
|
+
ensure
|
181
|
+
log_operations(logger, ops, Time.now - instrument_start) if instrument_start && !$!
|
182
|
+
end
|
183
|
+
|
184
|
+
def log_operations(logger, ops, duration)
|
185
|
+
prefix = " MOPED: #{host}:#{port} "
|
186
|
+
indent = " "*prefix.length
|
187
|
+
runtime = (" (%.1fms)" % duration)
|
188
|
+
|
189
|
+
if ops.length == 1
|
190
|
+
logger.debug prefix + ops.first.log_inspect + runtime
|
191
|
+
else
|
192
|
+
first, *middle, last = ops
|
193
|
+
|
194
|
+
logger.debug prefix + first.log_inspect
|
195
|
+
middle.each { |m| logger.debug indent + m.log_inspect }
|
196
|
+
logger.debug indent + last.log_inspect + runtime
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|