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.

Files changed (50) hide show
  1. data/MIT_LICENSE +19 -0
  2. data/README.md +323 -0
  3. data/lib/moped.rb +19 -0
  4. data/lib/moped/bson.rb +25 -0
  5. data/lib/moped/bson/binary.rb +68 -0
  6. data/lib/moped/bson/code.rb +61 -0
  7. data/lib/moped/bson/document.rb +16 -0
  8. data/lib/moped/bson/extensions.rb +81 -0
  9. data/lib/moped/bson/extensions/array.rb +44 -0
  10. data/lib/moped/bson/extensions/boolean.rb +14 -0
  11. data/lib/moped/bson/extensions/false_class.rb +15 -0
  12. data/lib/moped/bson/extensions/float.rb +23 -0
  13. data/lib/moped/bson/extensions/hash.rb +49 -0
  14. data/lib/moped/bson/extensions/integer.rb +37 -0
  15. data/lib/moped/bson/extensions/nil_class.rb +20 -0
  16. data/lib/moped/bson/extensions/regexp.rb +40 -0
  17. data/lib/moped/bson/extensions/string.rb +35 -0
  18. data/lib/moped/bson/extensions/symbol.rb +25 -0
  19. data/lib/moped/bson/extensions/time.rb +21 -0
  20. data/lib/moped/bson/extensions/true_class.rb +15 -0
  21. data/lib/moped/bson/max_key.rb +21 -0
  22. data/lib/moped/bson/min_key.rb +21 -0
  23. data/lib/moped/bson/object_id.rb +123 -0
  24. data/lib/moped/bson/timestamp.rb +15 -0
  25. data/lib/moped/bson/types.rb +67 -0
  26. data/lib/moped/cluster.rb +193 -0
  27. data/lib/moped/collection.rb +67 -0
  28. data/lib/moped/cursor.rb +60 -0
  29. data/lib/moped/database.rb +76 -0
  30. data/lib/moped/errors.rb +61 -0
  31. data/lib/moped/indexes.rb +93 -0
  32. data/lib/moped/logging.rb +25 -0
  33. data/lib/moped/protocol.rb +20 -0
  34. data/lib/moped/protocol/command.rb +27 -0
  35. data/lib/moped/protocol/commands.rb +11 -0
  36. data/lib/moped/protocol/commands/authenticate.rb +54 -0
  37. data/lib/moped/protocol/delete.rb +92 -0
  38. data/lib/moped/protocol/get_more.rb +79 -0
  39. data/lib/moped/protocol/insert.rb +92 -0
  40. data/lib/moped/protocol/kill_cursors.rb +61 -0
  41. data/lib/moped/protocol/message.rb +320 -0
  42. data/lib/moped/protocol/query.rb +131 -0
  43. data/lib/moped/protocol/reply.rb +90 -0
  44. data/lib/moped/protocol/update.rb +107 -0
  45. data/lib/moped/query.rb +230 -0
  46. data/lib/moped/server.rb +73 -0
  47. data/lib/moped/session.rb +253 -0
  48. data/lib/moped/socket.rb +201 -0
  49. data/lib/moped/version.rb +4 -0
  50. metadata +108 -46
@@ -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
@@ -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