monga 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +9 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +110 -0
  5. data/Rakefile +13 -0
  6. data/lib/monga/client.rb +8 -0
  7. data/lib/monga/clients/client.rb +24 -0
  8. data/lib/monga/clients/master_slave_client.rb +5 -0
  9. data/lib/monga/clients/replica_set_client.rb +101 -0
  10. data/lib/monga/collection.rb +108 -0
  11. data/lib/monga/connection.rb +23 -0
  12. data/lib/monga/connection_pool.rb +38 -0
  13. data/lib/monga/connections/em_connection.rb +152 -0
  14. data/lib/monga/connections/primary.rb +46 -0
  15. data/lib/monga/connections/secondary.rb +13 -0
  16. data/lib/monga/cursor.rb +175 -0
  17. data/lib/monga/database.rb +97 -0
  18. data/lib/monga/exceptions.rb +9 -0
  19. data/lib/monga/miner.rb +72 -0
  20. data/lib/monga/request.rb +122 -0
  21. data/lib/monga/requests/delete.rb +23 -0
  22. data/lib/monga/requests/get_more.rb +19 -0
  23. data/lib/monga/requests/insert.rb +29 -0
  24. data/lib/monga/requests/kill_cursors.rb +25 -0
  25. data/lib/monga/requests/query.rb +49 -0
  26. data/lib/monga/requests/update.rb +25 -0
  27. data/lib/monga/response.rb +11 -0
  28. data/lib/monga.rb +30 -0
  29. data/monga.gemspec +26 -0
  30. data/spec/helpers/mongodb.rb +59 -0
  31. data/spec/helpers/truncate.rb +15 -0
  32. data/spec/monga/collection_spec.rb +448 -0
  33. data/spec/monga/connection_pool_spec.rb +50 -0
  34. data/spec/monga/connection_spec.rb +64 -0
  35. data/spec/monga/cursor_spec.rb +186 -0
  36. data/spec/monga/database_spec.rb +67 -0
  37. data/spec/monga/replica_set_client_spec.rb +46 -0
  38. data/spec/monga/requests/delete_spec.rb +0 -0
  39. data/spec/monga/requests/insert_spec.rb +0 -0
  40. data/spec/monga/requests/query_spec.rb +28 -0
  41. data/spec/spec_helper.rb +29 -0
  42. metadata +185 -0
@@ -0,0 +1,175 @@
1
+ module Monga
2
+ class Cursor < EM::DefaultDeferrable
3
+ attr_reader :cursor_id
4
+
5
+ CURSORS = {}
6
+ CLOSED_CURSOR = 0
7
+ # Batch kill cursors marked to be killed each CLOSE_TIMEOUT seconds
8
+ CLOSE_TIMEOUT = 1
9
+
10
+ def initialize(db, collection_name, options = {}, flags = {})
11
+ @keep_alive = true if flags.delete :keep_alive
12
+
13
+ @db = db
14
+ @connection = @db.client.aquire_connection
15
+ @collection_name = collection_name
16
+ @options = options
17
+ @options.merge!(flags)
18
+
19
+ @fetched_docs = []
20
+ @count = 0
21
+ @total_count = 0
22
+ @limit = @options[:limit] ||= 0
23
+ @batch_size = @options[:batch_size]
24
+ end
25
+
26
+ def next_document
27
+ Monga::Response.surround do |resp|
28
+ if doc = @fetched_docs.shift
29
+ resp.succeed doc
30
+ else
31
+ req = next_batch
32
+ req.callback do |docs|
33
+ @fetched_docs = docs
34
+ if doc = @fetched_docs.shift
35
+ @count =+ 1
36
+ resp.succeed doc
37
+ end
38
+ end
39
+ req.errback{ |err| resp.fail err }
40
+ end
41
+ end
42
+ end
43
+
44
+ def each_doc(&blk)
45
+ if more?
46
+ req = next_batch
47
+ req.callback do |batch|
48
+ if batch.any?
49
+ batch.each do |doc|
50
+ @count += 1
51
+ blk.call(doc)
52
+ end
53
+ each_doc(&blk)
54
+ else
55
+ succeed
56
+ end
57
+ end
58
+ req.errback{ |err| fail err }
59
+ else
60
+ succeed
61
+ end
62
+ self
63
+ end
64
+
65
+ def kill
66
+ return unless @cursor_id > 0
67
+ self.class.kill_cursors(@connection, @cursor_id)
68
+ CURSORS.delete @cursor_id
69
+ @cursor_id = 0
70
+ end
71
+
72
+ def self.batch_kill(conn)
73
+ cursors = CURSORS.select{ |k,v| v }
74
+ cursor_ids = cursors.keys
75
+ if cursor_ids.any?
76
+ Monga.logger.debug("Following cursors are going to be deleted: #{cursor_ids}")
77
+ kill_cursors(conn, cursor_ids)
78
+ CURSORS.delete_if{|k,v| cursor_ids.include?(k) }
79
+ end
80
+ end
81
+
82
+ # Sometime in future all marked cursors will be killed in batch
83
+ def mark_to_kill
84
+ CURSORS[@cursor_id] = true if @cursor_id && alive?
85
+ @cursor_id = 0
86
+ end
87
+
88
+ # Cursor is alive and we need more minerals
89
+ def more?
90
+ alive? && !satisfied?
91
+ end
92
+
93
+ private
94
+
95
+ def get_more(batch_size)
96
+ Monga::Response.surround do |resp|
97
+ req = if @cursor_id
98
+ opts = { cursor_id: @cursor_id, batch_size: batch_size }
99
+ Monga::Requests::GetMore.new(@db, @collection_name, opts).callback_perform
100
+ else
101
+ Monga::Requests::Query.new(@db, @collection_name, @options).callback_perform
102
+ end
103
+ req.callback do |data|
104
+ @cursor_id = data[5]
105
+ fetched_docs = data.last
106
+ @total_count += fetched_docs.count
107
+ mark_to_kill unless cursor_more?
108
+
109
+ resp.succeed fetched_docs
110
+ end
111
+ req.errback do |err|
112
+ mark_to_kill
113
+ resp.fail err
114
+ end
115
+ end
116
+ end
117
+
118
+ def next_batch
119
+ Monga::Response.surround do |resp|
120
+ if more?
121
+ batch_size = get_batch_size
122
+ req = get_more(batch_size)
123
+ req.callback{ |res| resp.succeed res }
124
+ req.errback{ |err| resp.fail err }
125
+ else
126
+ mark_to_kill
127
+ if !alive?
128
+ resp.fail Monga::Exceptions::CursorIsClosed.new("Cursor is already closed. Check `cursor.more?` before calling cursor")
129
+ elsif satisfied?
130
+ resp.fail Monga::Exceptions::CursorLimit.new("You've already fetched #{@limit} docs you asked. Check `cursor.more?` before calling cursor")
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def cursor_more?
137
+ alive? && !cursor_satisfied?
138
+ end
139
+
140
+ # If cursor_id is not setted, or if isn't CLOSED_CURSOR - cursor is alive
141
+ def alive?
142
+ @cursor_id != CLOSED_CURSOR
143
+ end
144
+
145
+ # If global limit is setted
146
+ # we will be satisfied when we will get limit amount of documents.
147
+ # Otherwise we are not satisfied untill crsor is alive
148
+ def satisfied?
149
+ @limit > 0 && @count >= @limit
150
+ end
151
+
152
+ def cursor_satisfied?
153
+ @limit > 0 && @total_count >= @limit
154
+ end
155
+
156
+ # How many docs should be returned
157
+ def rest
158
+ @limit - @count if @limit > 0
159
+ end
160
+
161
+ # Cursor will get exact amount of docs as user passed with `limit` opr
162
+ def get_batch_size
163
+ if @limit > 0 && @batch_size
164
+ rest < @batch_size ? rest : @batch_size
165
+ else @batch_size
166
+ @batch_size
167
+ end
168
+ end
169
+
170
+ def self.kill_cursors(connection, cursor_ids)
171
+ Monga::Requests::KillCursors.new(connection, cursor_ids: [*cursor_ids]).perform
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,97 @@
1
+ module Monga
2
+ class Database
3
+ attr_reader :client, :name
4
+
5
+ def initialize(client, name)
6
+ @client = client
7
+ @name = name
8
+ end
9
+
10
+ def [](collection_name)
11
+ Monga::Collection.new(self, collection_name)
12
+ end
13
+
14
+ def cmd(cmd, opts={})
15
+ options = {}
16
+ options[:query] = cmd
17
+ options.merge! opts
18
+ Monga::Miner.new(self, "$cmd", options).limit(1)
19
+ end
20
+
21
+ def eval(js)
22
+ with_response do
23
+ cmd(eval: js)
24
+ end
25
+ end
26
+
27
+ # Be carefull with using get_last_error with connection pool.
28
+ # In most cases you need to use #safe methods
29
+ # and don't access to #get_last_error directky
30
+ def get_last_error
31
+ with_response do
32
+ cmd(getLastError: 1)
33
+ end
34
+ end
35
+
36
+ def drop_collection(collection_name)
37
+ with_response do
38
+ cmd(drop: collection_name)
39
+ end
40
+ end
41
+
42
+ def create_collection(collection_name, opts = {})
43
+ with_response do
44
+ cmd({create: collection_name}.merge(opts))
45
+ end
46
+ end
47
+
48
+ def count(collection_name)
49
+ Monga::Response.surround do |resp|
50
+ req = with_response do
51
+ cmd(count: collection_name)
52
+ end
53
+ req.callback do |data|
54
+ cnt = data.first["n"].to_i
55
+ resp.succeed cnt
56
+ end
57
+ req.errback{ |err| resp.fail err }
58
+ end
59
+ end
60
+
61
+ def drop_indexes(collection_name, indexes)
62
+ with_response do
63
+ cmd(dropIndexes: collection_name, index: indexes)
64
+ end
65
+ end
66
+
67
+ # Just helper
68
+ def list_collections
69
+ Monga::Response.surround do |resp|
70
+ req = eval("db.getCollectionNames()")
71
+ req.callback do |data|
72
+ resp.succeed(data.first["retval"])
73
+ end
74
+ req.errback do |err|
75
+ resp.fail(err)
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def with_response
83
+ Monga::Response.surround do |resp|
84
+ req = yield(resp)
85
+ req.callback do |data|
86
+ if data.any?
87
+ resp.succeed(data)
88
+ else
89
+ exception = Monga::Exceptions::QueryFailure.new("Nothing was returned for your query: #{req.options[:query]}")
90
+ resp.fail(exception)
91
+ end
92
+ end
93
+ req.errback{ |err| resp.fail err }
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ module Monga::Exceptions
2
+ class LostConnection < StandardError; end
3
+ class CursorNotFound < StandardError; end
4
+ class CursorIsClosed < StandardError; end
5
+ class CursorLimit < StandardError; end
6
+ class QueryFailure < StandardError; end
7
+ class UndefinedIndexVersion < StandardError; end
8
+ class NoAvailableServers < StandardError; end
9
+ end
@@ -0,0 +1,72 @@
1
+ # Miner is a "proxy" object to Cursor.
2
+ # It dinamically stores Cursor options and at any moment can return cursor.
3
+ # Also it hides Deferrable that could return all objects that cursor do.
4
+ module Monga
5
+ class Miner < EM::DefaultDeferrable
6
+ attr_reader :options
7
+
8
+ def initialize(db, collection_name, options={})
9
+ @db = db
10
+ @collection_name = collection_name
11
+ @options = options
12
+
13
+ # Defaults
14
+ @options[:query] ||= {}
15
+ @options[:limit] ||= 0
16
+ @options[:skip] ||= 0
17
+ end
18
+
19
+ def cursor(flags = {})
20
+ @cursor = Monga::Cursor.new(@db, @collection_name, @options, flags)
21
+ end
22
+
23
+ def explain
24
+ @options[:explain] = true
25
+ end
26
+
27
+ def hint
28
+ @options[:hint] = true
29
+ end
30
+
31
+ def sort(val)
32
+ @options[:sort] = val
33
+ end
34
+
35
+ def limit(count)
36
+ @options[:limit] = count and self
37
+ end
38
+
39
+ def skip(count)
40
+ @options[:skip] = count and self
41
+ end
42
+
43
+ def batch_size(count)
44
+ @options[:batch_size] = count and self
45
+ end
46
+
47
+ # Lazy operation execution
48
+ [:callback, :errback, :timeout].each do |meth|
49
+ class_eval <<-EOS
50
+ def #{meth}(*args)
51
+ mine! && @deferred = true unless @deferred
52
+ super
53
+ end
54
+ EOS
55
+ end
56
+
57
+ private
58
+
59
+ def mine!
60
+ docs = []
61
+ itrator = cursor.each_doc do |doc|
62
+ docs << doc
63
+ end
64
+ itrator.callback do |resp|
65
+ succeed docs
66
+ end
67
+ itrator.errback do |err|
68
+ fail err
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,122 @@
1
+ module Monga
2
+ class Request
3
+ attr_reader :request_id
4
+
5
+ OP_CODES = {
6
+ reply: 1,
7
+ msg: 1000,
8
+ update: 2001,
9
+ insert: 2002,
10
+ reserved: 2003,
11
+ query: 2004,
12
+ get_more: 2005,
13
+ delete: 2006,
14
+ kill_cursors: 2007,
15
+ }
16
+
17
+ def initialize(db, collection_name, options = {})
18
+ @db = db
19
+ @collection_name = collection_name
20
+ @options = options
21
+ @request_id = self.class.request_id
22
+ @connection = @db.client.aquire_connection
23
+ end
24
+
25
+ def command
26
+ header.append!(body)
27
+ end
28
+
29
+ def header
30
+ headers = BSON::ByteBuffer.new
31
+ headers.put_int(command_length)
32
+ headers.put_int(@request_id)
33
+ headers.put_int(0)
34
+ headers.put_int(op_code)
35
+ headers
36
+ end
37
+
38
+ # Fire and Forget
39
+ def perform
40
+ @connection.send_command(command)
41
+ @request_id
42
+ end
43
+
44
+ # Fire and wait
45
+ def callback_perform
46
+ Monga::Response.surround do |response|
47
+ @connection.send_command(command, @request_id) do |data|
48
+ resp = parse_response(data)
49
+ Exception === resp ? response.fail(resp) : response.succeed(resp)
50
+ end
51
+ end
52
+ end
53
+
54
+ def parse_response(data)
55
+ if Exception === data
56
+ data
57
+ else
58
+ flags = data[4]
59
+ number = data[7]
60
+ docs = unpack_docs(data.last, number)
61
+ data[-1] = docs
62
+ if flags & 2**0 > 0
63
+ Monga::Exceptions::CursorNotFound.new(docs.first)
64
+ elsif flags & 2**1 > 0
65
+ Monga::Exceptions::QueryFailure.new(docs.first)
66
+ elsif docs.first && (docs.first["err"] || docs.first["errmsg"])
67
+ Monga::Exceptions::QueryFailure.new(docs.first)
68
+ else
69
+ data
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def unpack_docs(data, number)
77
+ number.times.map do
78
+ size = data.slice(0, 4).unpack("L").first
79
+ d = data.slice!(0, size)
80
+ BSON.deserialize(d)
81
+ end
82
+ end
83
+
84
+ def flags
85
+ flags = 0
86
+ self.class::FLAGS.each do |k, byte|
87
+ flags = flags | 1 << byte if @options[k]
88
+ end
89
+ flags
90
+ end
91
+
92
+ def full_name
93
+ [@db.name, @collection_name] * "."
94
+ end
95
+
96
+ def op_code
97
+ OP_CODES[self.class.op_name]
98
+ end
99
+
100
+ def command_length
101
+ HEADER_SIZE + body.size
102
+ end
103
+
104
+ def self.request_id
105
+ @request_id ||= 0
106
+ @request_id += 1
107
+ @request_id >= 2**32 ? @request_id = 1 : @request_id
108
+ end
109
+
110
+ def self.op_name(op = nil)
111
+ op ? @op_name = op : @op_name
112
+ end
113
+ end
114
+ end
115
+
116
+
117
+ require File.expand_path("../requests/query", __FILE__)
118
+ require File.expand_path("../requests/insert", __FILE__)
119
+ require File.expand_path("../requests/delete", __FILE__)
120
+ require File.expand_path("../requests/update", __FILE__)
121
+ require File.expand_path("../requests/get_more", __FILE__)
122
+ require File.expand_path("../requests/kill_cursors", __FILE__)
@@ -0,0 +1,23 @@
1
+ module Monga::Requests
2
+ class Delete < Monga::Request
3
+ op_name :delete
4
+
5
+ FLAGS = {
6
+ single_remove: 0,
7
+ }
8
+
9
+ def body
10
+ @body ||= begin
11
+ query = @options[:query]
12
+
13
+ b = BSON::ByteBuffer.new
14
+ b.put_int(0)
15
+ BSON::BSON_RUBY.serialize_cstr(b, full_name)
16
+ b.put_int(flags)
17
+ b.append!(BSON::BSON_C.serialize(query).to_s)
18
+ b
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Monga::Requests
2
+ class GetMore < Monga::Request
3
+ op_name :get_more
4
+
5
+ def body
6
+ @body ||= begin
7
+ batch_size = @options[:batch_size] || 0
8
+ cursor_id = @options[:cursor_id]
9
+
10
+ b = BSON::ByteBuffer.new
11
+ b.put_int(0)
12
+ BSON::BSON_RUBY.serialize_cstr(b, full_name)
13
+ b.put_int(batch_size)
14
+ b.put_long(cursor_id)
15
+ b
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Monga::Requests
2
+ class Insert < Monga::Request
3
+ op_name :insert
4
+
5
+ FLAGS = {
6
+ continue_on_error: 0,
7
+ }
8
+
9
+ def body
10
+ @body ||= begin
11
+ documents = @options[:documents]
12
+
13
+ b = BSON::ByteBuffer.new
14
+ b.put_int(flags)
15
+ BSON::BSON_RUBY.serialize_cstr(b, full_name)
16
+ case documents
17
+ when Array
18
+ documents.each do |doc|
19
+ b.append!(BSON::BSON_C.serialize(doc).to_s)
20
+ end
21
+ when Hash
22
+ b.append!(BSON::BSON_C.serialize(documents).to_s)
23
+ end
24
+ b
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Monga::Requests
2
+ class KillCursors < Monga::Request
3
+ op_name :kill_cursors
4
+
5
+ def initialize(connection, options = {})
6
+ @options = options
7
+ @request_id = self.class.request_id
8
+ @connection = connection
9
+ end
10
+
11
+ def body
12
+ @body ||= begin
13
+ cursor_ids = @options[:cursor_ids]
14
+
15
+ b = BSON::ByteBuffer.new
16
+ b.put_int(0)
17
+ b.put_int(cursor_ids.size)
18
+ cursor_ids.each do |cursor_id|
19
+ b.put_long(cursor_id)
20
+ end
21
+ b
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ module Monga::Requests
2
+ class Query < Monga::Request
3
+ op_name :query
4
+
5
+ FLAGS = {
6
+ tailable_cursor: 1,
7
+ slave_ok: 2,
8
+ no_cursor_timeout: 4,
9
+ await_data: 5,
10
+ exhaust: 6,
11
+ partial: 7,
12
+ }
13
+
14
+ def body
15
+ @body ||= begin
16
+ skip = @options[:skip] || 0
17
+ limit = get_limit
18
+ fields = @options[:fields] || {}
19
+
20
+ query = {}
21
+ query["$query"] = @options[:query] || {}
22
+ query["$hint"] = @options[:hint] if @options[:hint]
23
+ query["$orderby"] = @options[:sort] if @options[:sort]
24
+ query["$explain"] = @options[:explain] if @options[:explain]
25
+
26
+ b = BSON::ByteBuffer.new
27
+ b.put_int(flags)
28
+ BSON::BSON_RUBY.serialize_cstr(b, full_name)
29
+ b.put_int(skip)
30
+ b.put_int(limit)
31
+ b.append!(BSON::BSON_C.serialize(query).to_s)
32
+ b.append!(BSON::BSON_C.serialize(fields).to_s) if fields.any?
33
+ b
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def get_limit
40
+ if @options[:batch_size]
41
+ @options[:batch_size]
42
+ elsif @options[:limit]
43
+ -@options[:limit]
44
+ else
45
+ 0
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ module Monga::Requests
2
+ class Update < Monga::Request
3
+ op_name :update
4
+
5
+ FLAGS = {
6
+ upsert: 0,
7
+ multi_update: 1,
8
+ }
9
+
10
+ def body
11
+ @body ||= begin
12
+ query = @options[:query]
13
+ update = @options[:update]
14
+
15
+ b = BSON::ByteBuffer.new
16
+ b.put_int(0)
17
+ BSON::BSON_RUBY.serialize_cstr(b, full_name)
18
+ b.put_int(flags)
19
+ b.append!(BSON::BSON_C.serialize(query).to_s)
20
+ b.append!(BSON::BSON_C.serialize(update).to_s)
21
+ b
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module Monga
2
+ class Response
3
+ include EM::Deferrable
4
+
5
+ def self.surround
6
+ resp = new
7
+ yield(resp)
8
+ resp
9
+ end
10
+ end
11
+ end
data/lib/monga.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "eventmachine"
2
+ require "bson"
3
+ require "logger"
4
+
5
+ module Monga
6
+ DEFAULT_HOST = "127.0.0.1"
7
+ DEFAULT_PORT = 27017
8
+ HEADER_SIZE = 16
9
+
10
+ extend self
11
+
12
+ def logger
13
+ @logger ||= begin
14
+ l = Logger.new(STDOUT)
15
+ l.level = Logger::DEBUG
16
+ l
17
+ end
18
+ end
19
+ end
20
+
21
+ require File.expand_path("../monga/connection", __FILE__)
22
+ require File.expand_path("../monga/connection_pool", __FILE__)
23
+ require File.expand_path("../monga/client", __FILE__)
24
+ require File.expand_path("../monga/database", __FILE__)
25
+ require File.expand_path("../monga/collection", __FILE__)
26
+ require File.expand_path("../monga/miner", __FILE__)
27
+ require File.expand_path("../monga/cursor", __FILE__)
28
+ require File.expand_path("../monga/exceptions", __FILE__)
29
+ require File.expand_path("../monga/response", __FILE__)
30
+ require File.expand_path("../monga/request", __FILE__)