monga 0.0.1

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.
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__)