monga 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +1 -0
  3. data/README.md +59 -3
  4. data/lib/monga/client.rb +51 -6
  5. data/lib/monga/clients/master_slave_client.rb +0 -5
  6. data/lib/monga/clients/replica_set_client.rb +32 -71
  7. data/lib/monga/clients/single_instance_client.rb +53 -0
  8. data/lib/monga/collection.rb +102 -41
  9. data/lib/monga/connection.rb +38 -13
  10. data/lib/monga/connection_pool.rb +6 -17
  11. data/lib/monga/connections/buffer.rb +33 -0
  12. data/lib/monga/connections/em_connection.rb +25 -56
  13. data/lib/monga/connections/em_proxy_connection.rb +80 -0
  14. data/lib/monga/connections/fibered_connection.rb +26 -0
  15. data/lib/monga/connections/fibered_proxy_connection.rb +23 -0
  16. data/lib/monga/connections/proxy_connection.rb +4 -0
  17. data/lib/monga/connections/tcp_connection.rb +57 -0
  18. data/lib/monga/cursor.rb +197 -95
  19. data/lib/monga/database.rb +175 -60
  20. data/lib/monga/{requests → protocol}/delete.rb +1 -2
  21. data/lib/monga/{requests → protocol}/get_more.rb +1 -1
  22. data/lib/monga/{requests → protocol}/insert.rb +1 -2
  23. data/lib/monga/{requests → protocol}/kill_cursors.rb +1 -1
  24. data/lib/monga/{requests → protocol}/query.rb +3 -3
  25. data/lib/monga/{requests → protocol}/update.rb +1 -1
  26. data/lib/monga/request.rb +27 -23
  27. data/lib/monga/utils/constants.rb +5 -0
  28. data/lib/monga/utils/exceptions.rb +11 -0
  29. data/lib/monga.rb +19 -11
  30. data/monga.gemspec +2 -2
  31. data/spec/helpers/mongodb.rb +115 -38
  32. data/spec/monga/block/collection_spec.rb +172 -0
  33. data/spec/monga/block/cursor_spec.rb +160 -0
  34. data/spec/monga/block/database_spec.rb +80 -0
  35. data/spec/monga/block/single_instance_client_spec.rb +31 -0
  36. data/spec/monga/em/collection_spec.rb +308 -0
  37. data/spec/monga/em/cursor_spec.rb +256 -0
  38. data/spec/monga/em/database_spec.rb +140 -0
  39. data/spec/monga/em/replica_set_client_spec.rb +86 -0
  40. data/spec/monga/em/single_instance_client_spec.rb +28 -0
  41. data/spec/monga/sync/collection_spec.rb +247 -0
  42. data/spec/monga/sync/cursor_spec.rb +211 -0
  43. data/spec/monga/sync/database_spec.rb +110 -0
  44. data/spec/monga/sync/replica_set_client_spec.rb +54 -0
  45. data/spec/monga/sync/single_instance_client_spec.rb +25 -0
  46. data/spec/spec_helper.rb +2 -20
  47. metadata +50 -38
  48. data/lib/monga/clients/client.rb +0 -24
  49. data/lib/monga/connections/primary.rb +0 -46
  50. data/lib/monga/connections/secondary.rb +0 -13
  51. data/lib/monga/exceptions.rb +0 -9
  52. data/lib/monga/miner.rb +0 -72
  53. data/lib/monga/response.rb +0 -11
  54. data/spec/helpers/truncate.rb +0 -15
  55. data/spec/monga/collection_spec.rb +0 -448
  56. data/spec/monga/connection_pool_spec.rb +0 -50
  57. data/spec/monga/connection_spec.rb +0 -64
  58. data/spec/monga/cursor_spec.rb +0 -186
  59. data/spec/monga/database_spec.rb +0 -67
  60. data/spec/monga/replica_set_client_spec.rb +0 -46
  61. data/spec/monga/requests/delete_spec.rb +0 -0
  62. data/spec/monga/requests/insert_spec.rb +0 -0
  63. data/spec/monga/requests/query_spec.rb +0 -28
@@ -1,4 +1,4 @@
1
- module Monga::Requests
1
+ module Monga::Protocol
2
2
  class Update < Monga::Request
3
3
  op_name :update
4
4
 
data/lib/monga/request.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Monga
2
2
  class Request
3
- attr_reader :request_id
3
+ attr_reader :request_id, :connection
4
4
 
5
5
  OP_CODES = {
6
6
  reply: 1,
@@ -14,12 +14,13 @@ module Monga
14
14
  kill_cursors: 2007,
15
15
  }
16
16
 
17
- def initialize(db, collection_name, options = {})
18
- @db = db
17
+ def initialize(connection, db_name, collection_name, options = {})
18
+ @connection = connection
19
+ @db_name = db_name
19
20
  @collection_name = collection_name
20
21
  @options = options
22
+
21
23
  @request_id = self.class.request_id
22
- @connection = @db.client.aquire_connection
23
24
  end
24
25
 
25
26
  def command
@@ -37,23 +38,25 @@ module Monga
37
38
 
38
39
  # Fire and Forget
39
40
  def perform
40
- @connection.send_command(command)
41
- @request_id
41
+ @connection.send_command(command, @request_id)
42
+ self
42
43
  end
43
44
 
44
45
  # Fire and wait
45
46
  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)
47
+ @connection.send_command(command, @request_id) do |data|
48
+ err, resp = parse_response(data)
49
+ if block_given?
50
+ yield(err, resp)
51
+ else
52
+ err ? raise(err) : resp
50
53
  end
51
54
  end
52
55
  end
53
56
 
54
57
  def parse_response(data)
55
58
  if Exception === data
56
- data
59
+ [data, nil]
57
60
  else
58
61
  flags = data[4]
59
62
  number = data[7]
@@ -66,7 +69,7 @@ module Monga
66
69
  elsif docs.first && (docs.first["err"] || docs.first["errmsg"])
67
70
  Monga::Exceptions::QueryFailure.new(docs.first)
68
71
  else
69
- data
72
+ [nil, data]
70
73
  end
71
74
  end
72
75
  end
@@ -90,7 +93,7 @@ module Monga
90
93
  end
91
94
 
92
95
  def full_name
93
- [@db.name, @collection_name] * "."
96
+ [@db_name, @collection_name] * "."
94
97
  end
95
98
 
96
99
  def op_code
@@ -98,13 +101,14 @@ module Monga
98
101
  end
99
102
 
100
103
  def command_length
101
- HEADER_SIZE + body.size
104
+ Monga::HEADER_SIZE + body.size
102
105
  end
103
106
 
104
107
  def self.request_id
105
- @request_id ||= 0
106
- @request_id += 1
107
- @request_id >= 2**32 ? @request_id = 1 : @request_id
108
+ @@request_id ||= 0
109
+ @@request_id += 1
110
+ @@request_id >= 2**32 ? @@request_id = 1 : @@request_id
111
+ @@request_id
108
112
  end
109
113
 
110
114
  def self.op_name(op = nil)
@@ -114,9 +118,9 @@ module Monga
114
118
  end
115
119
 
116
120
 
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__)
121
+ require File.expand_path("../protocol/query", __FILE__)
122
+ require File.expand_path("../protocol/insert", __FILE__)
123
+ require File.expand_path("../protocol/delete", __FILE__)
124
+ require File.expand_path("../protocol/update", __FILE__)
125
+ require File.expand_path("../protocol/get_more", __FILE__)
126
+ require File.expand_path("../protocol/kill_cursors", __FILE__)
@@ -0,0 +1,5 @@
1
+ module Monga
2
+ DEFAULT_HOST = "127.0.0.1"
3
+ DEFAULT_PORT = 27017
4
+ HEADER_SIZE = 16
5
+ end
@@ -0,0 +1,11 @@
1
+ module Monga::Exceptions
2
+ class InvalidClientOption < StandardError; end
3
+ class UndefinedSocketType < StandardError; end
4
+ class WrongConnectionType < StandardError; end
5
+ class Disconnected < StandardError; end
6
+ class CouldNotConnect < StandardError; end
7
+ class CouldNotReconnect < StandardError; end
8
+ class QueryFailure < StandardError; end
9
+ class CursorNotFound < StandardError; end
10
+ class ClosedCursor < StandardError; end
11
+ end
data/lib/monga.rb CHANGED
@@ -1,12 +1,9 @@
1
- require "eventmachine"
1
+ require "em-synchrony"
2
2
  require "bson"
3
3
  require "logger"
4
+ require "forwardable"
4
5
 
5
6
  module Monga
6
- DEFAULT_HOST = "127.0.0.1"
7
- DEFAULT_PORT = 27017
8
- HEADER_SIZE = 16
9
-
10
7
  extend self
11
8
 
12
9
  def logger
@@ -18,13 +15,24 @@ module Monga
18
15
  end
19
16
  end
20
17
 
18
+ # It is strange, but cursor should be required befor fibered_connection
19
+ require File.expand_path("../monga/cursor", __FILE__)
20
+
21
+ require File.expand_path("../monga/clients/single_instance_client", __FILE__)
22
+ require File.expand_path("../monga/clients/replica_set_client", __FILE__)
23
+ require File.expand_path("../monga/connections/em_connection", __FILE__)
24
+ require File.expand_path("../monga/connections/fibered_connection", __FILE__)
25
+ require File.expand_path("../monga/connections/tcp_connection", __FILE__)
26
+ require File.expand_path("../monga/connections/em_proxy_connection", __FILE__)
27
+ require File.expand_path("../monga/connections/fibered_proxy_connection", __FILE__)
28
+ require File.expand_path("../monga/connections/proxy_connection", __FILE__)
29
+ require File.expand_path("../monga/connections/buffer", __FILE__)
30
+
31
+ require File.expand_path("../monga/client", __FILE__)
21
32
  require File.expand_path("../monga/connection", __FILE__)
22
33
  require File.expand_path("../monga/connection_pool", __FILE__)
23
- require File.expand_path("../monga/client", __FILE__)
24
34
  require File.expand_path("../monga/database", __FILE__)
25
35
  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__)
36
+ require File.expand_path("../monga/request", __FILE__)
37
+ require File.expand_path("../monga/utils/exceptions", __FILE__)
38
+ require File.expand_path("../monga/utils/constants", __FILE__)
data/monga.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "monga"
7
- spec.version = "0.0.2"
7
+ spec.version = "0.0.3"
8
8
  spec.authors = ["Petr Yanovich"]
9
9
  spec.email = ["fl00r@yandex.ru"]
10
10
  spec.description = %q{MongoDB Ruby Evented Driver on EventMachine}
@@ -19,8 +19,8 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_development_dependency "bundler", "~> 1.3"
21
21
  spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "em-synchrony"
22
23
 
23
- spec.add_dependency "eventmachine"
24
24
  spec.add_dependency "bson"
25
25
  spec.add_dependency "bson_ext"
26
26
  end
@@ -1,59 +1,136 @@
1
- module Mongodb
2
- MONGOD_PATH = "mongod"
3
-
4
- class Instance
5
- def initialize(opts)
6
- @opts = opts
7
- @cmd = [MONGOD_PATH]
8
- opts.each do |k,v|
9
- @cmd << "--#{k}"
10
- @cmd << v.to_s
11
- end
12
- clean_lock
13
- shtdwn
14
- start
1
+ module Fake
2
+
3
+ # Fake Response.
4
+ # It could be `ok`, or `primary?` reply.
5
+ class Response
6
+ def initialize(data, primary)
7
+ @data = data
8
+ @primary = primary
9
+ end
10
+
11
+ def ok(doc = nil)
12
+ document = doc || { ok: 1.0 }
13
+ [flags, cursor_id, starting_from, number_returned].pack("LQLL")
14
+ b = BSON::ByteBuffer.new
15
+ b.put_int(flags)
16
+ b.put_long(cursor_id)
17
+ b.put_int(starting_from)
18
+ b.put_int(number_returned)
19
+ b.append!(BSON::BSON_C.serialize(document).to_s)
20
+
21
+ header(b) + b.to_s
22
+ end
23
+
24
+ def primary?
25
+ doc = { ismaster: @primary }
26
+ ok(doc)
27
+ end
28
+
29
+ private
30
+
31
+ def header(body)
32
+ length = 16 + body.to_s.bytesize
33
+ request_id = 0
34
+ op_code = 0
35
+
36
+ h = BSON::ByteBuffer.new
37
+ h.put_int(length)
38
+ h.put_int(request_id)
39
+ h.put_int(response_to)
40
+ h.put_int(op_code)
41
+ h.to_s
42
+ end
43
+
44
+ def response_to
45
+ @data.unpack("LLLL")[1]
15
46
  end
16
47
 
17
- def clean_lock
18
- lock = @opts[:dbpath] + "/mongod.lock"
19
- File.rm(lock)
20
- rescue
21
- # ok
48
+ def flags; 0; end
49
+ def cursor_id; 0; end
50
+ def starting_from; 0; end
51
+ def number_returned; 1; end
52
+ end
53
+
54
+ # Fake MongoDB server.
55
+ # Replies with `ok` message for all 2004 op queries instead `isMaster`.
56
+ # For other ops it sends no answer.
57
+ class Node < EM::Connection
58
+ def initialize(si)
59
+ @si = si
60
+ @si.server = self
22
61
  end
23
62
 
24
- def shtdwn
25
- system "#{@cmd * ' '} --shutdown"
63
+ def primary
64
+ @si.rs.primary == @si if @si.rs
65
+ end
66
+
67
+ def receive_data(data)
68
+ begin
69
+ length, req_id, resp_to, op_code = data.unpack("LLLL")
70
+ piece = data.slice!(0, length)
71
+ if op_code == 2004
72
+ if piece["isMaster"]
73
+ send_data Fake::Response.new(piece, primary).primary?
74
+ else
75
+ send_data Fake::Response.new(piece, primary).ok
76
+ end
77
+ end
78
+ end while data != ""
79
+ end
80
+ end
81
+
82
+ # Single instance binded on one port.
83
+ # Could be stopped or started.
84
+ class SingleInstance
85
+ attr_reader :rs
86
+ attr_accessor :server
87
+
88
+ def initialize(port, rs=nil)
89
+ @rs = rs
90
+ @port = port
26
91
  end
27
92
 
28
93
  def start
29
- return @pid if @pid
30
- @pid = spawn(*@cmd, out: "/dev/null")
31
- sleep(1)
94
+ @connected = true
95
+ @sign = EM.start_server '127.0.0.1', @port, Fake::Node, self
32
96
  end
33
97
 
34
98
  def stop
35
- return unless @pid
36
- Process.kill("SIGINT", @pid)
37
- Process.waitpid(@pid)
38
- @pid = nil
99
+ @server.close_connection
100
+ EM.stop_server @sign
101
+ @connected = false
102
+ end
103
+
104
+ def connected?
105
+ @connected
39
106
  end
40
107
  end
41
108
 
109
+ # Fake Replica set with a number of Single Instances.
110
+ # You could stop/start primary/secondary.
42
111
  class ReplicaSet
43
- def initialize(ports, opts={})
44
- opts[:replSet] ||= "rs1"
45
- opts[:dbpath] ||= "/tmp/mongodb/rs0"
46
- @instances = {}
47
- ports.each.with_index do |prt, i|
48
- dbpath = opts[:dbpath] + "-#{i}"
49
- o = opts.merge({ port: prt[:port], dbpath: dbpath })
50
- @instances[prt[:port]] = Instance.new(o)
112
+ def initialize(ports)
113
+ @instances = ports.map do |port|
114
+ SingleInstance.new(port, self)
51
115
  end
52
116
  end
53
117
 
118
+ def start_all
119
+ @instances.each(&:start)
120
+ end
121
+
122
+ def vote
123
+ @primary = @instances.select{|inst| inst.connected? }.sample
124
+ end
125
+
54
126
  def primary
55
- pr = RS_CLIENT.primary
56
- @instances[pr.aquire_connection.port] if pr
127
+ @primary ||= begin
128
+ @instances.select{|inst| inst.connected? }.sample
129
+ end
130
+ end
131
+
132
+ def secondaries
133
+ @instances - [@primary]
57
134
  end
58
135
  end
59
136
  end
@@ -0,0 +1,172 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monga::Collection do
4
+ before do
5
+ @client = Monga::Client.new(type: :block, pool_size: 10)
6
+ @db = @client["dbTest"]
7
+ @collection = @db["testCollection"]
8
+ @collection.safe_remove
9
+ docs = []
10
+ 10.times do |i|
11
+ docs << { artist: "Madonna", title: "Track #{i+1}" }
12
+ docs << { artist: "Radiohead", title: "Track #{i+1}" }
13
+ end
14
+ @collection.safe_insert(docs)
15
+ end
16
+
17
+ # QUERY
18
+
19
+ describe "query" do
20
+ it "should fetch all documents" do
21
+ docs = @collection.find.all
22
+ docs.size.must_equal 20
23
+ end
24
+
25
+ it "should fetch all docs with skip and limit" do
26
+ docs = @collection.find.skip(10).limit(4).all
27
+ docs.size.must_equal 4
28
+ end
29
+
30
+ it "should fetch first" do
31
+ doc = @collection.first
32
+ doc.keys.must_equal ["_id", "artist", "title"]
33
+ end
34
+ end
35
+
36
+ # INSERT
37
+
38
+ describe "insert" do
39
+ before do
40
+ @collection.safe_ensure_index({ "personal_id" => 1 }, { unique: true, sparse: true })
41
+ end
42
+
43
+ after do
44
+ @collection.drop_index( personal_id: 1 )
45
+ end
46
+
47
+ it "should insert single doc" do
48
+ doc = { name: "Peter", age: 18 }
49
+ @collection.safe_insert(doc)
50
+ resp = @collection.find(name: "Peter").all
51
+ resp.size.must_equal 1
52
+ resp.first["age"].must_equal 18
53
+ end
54
+
55
+ it "should insert batch of docs" do
56
+ docs = [{ name: "Peter", age: 18 }, {name: "Jhon", age: 18}]
57
+ @collection.safe_insert(docs)
58
+ resp = @collection.find(age: 18).all
59
+ resp.size.must_equal 2
60
+ end
61
+
62
+ it "should fail on uniq index" do
63
+ docs = [{ name: "Peter", age: 18, personal_id: 20 }, {name: "Jhon", age: 18, personal_id: 20}, {name: "Rebeca", age: 21, personal_id: 5}]
64
+ proc{ @collection.safe_insert(docs) }.must_raise Monga::Exceptions::QueryFailure
65
+ @collection.count.must_equal 21
66
+ end
67
+
68
+ it "should continue_on_error" do
69
+ docs = [{ name: "Peter", age: 18, personal_id: 20 }, {name: "Jhon", age: 18, personal_id: 20}, {name: "Rebeca", age: 21, personal_id: 5}]
70
+ proc{ @collection.safe_insert(docs, continue_on_error: true) }.must_raise Monga::Exceptions::QueryFailure
71
+ @collection.count.must_equal 22
72
+ end
73
+ end
74
+
75
+ # UPDATE
76
+
77
+ describe "update" do
78
+ it "should make simple update (first matching)" do
79
+ @collection.safe_update({ artist: "Madonna" }, { "$set" => { country: "USA" } })
80
+ @collection.count( query: { artist: "Madonna", country: "USA" }).must_equal 1
81
+ end
82
+
83
+ it "should create non existing item (upsert)" do
84
+ @collection.safe_update({ artist: "Bjork" }, { "$set" => { country: "Iceland" } }, { upsert: true })
85
+ @collection.count(query: { artist: "Bjork" }).must_equal 1
86
+ end
87
+
88
+ it "should update all matching data (multi_update)" do
89
+ @collection.safe_update({ artist: "Madonna" }, { "$set" => { country: "USA" } }, {multi_update: true})
90
+ docs = @collection.find(artist: "Madonna").all
91
+ docs.each{ |d| d["country"].must_equal "USA" }
92
+ end
93
+ end
94
+
95
+ # REMOVE
96
+
97
+ describe "remove" do
98
+ it "should delete all matching docs" do
99
+ @collection.safe_delete(artist: "Madonna")
100
+ @collection.count(query: { artist: "Madonna" }).must_equal 0
101
+ end
102
+
103
+ it "should delete first matching doc (single_remove)" do
104
+ @collection.safe_delete({ artist: "Madonna" }, single_remove: true)
105
+ @collection.count(query: { artist: "Madonna" }).must_equal 9
106
+ end
107
+ end
108
+
109
+ # COUNT
110
+
111
+ describe "count" do
112
+ it "should count all docs" do
113
+ @collection.count.must_equal 20
114
+ end
115
+
116
+ it "should count all docs with query" do
117
+ @collection.count(query: { artist: "Madonna" }).must_equal 10
118
+ end
119
+
120
+ it "should count all docs with limit" do
121
+ @collection.count(query: { artist: "Madonna" }, limit: 5).must_equal 5
122
+ end
123
+
124
+ it "should count all docs with limit and skip" do
125
+ @collection.count(query: { artist: "Madonna" }, limit: 5, skip: 6).must_equal 4
126
+ end
127
+ end
128
+
129
+ # ENSURE/DROP INDEX
130
+
131
+ describe "ensure_index" do
132
+ before do
133
+ @collection.drop_indexes
134
+ end
135
+
136
+ it "should create index" do
137
+ @collection.safe_ensure_index(title: 1)
138
+ docs = @collection.get_indexes
139
+ docs.any?{ |doc| doc["key"] == {"title" => 1}}.must_equal true
140
+ end
141
+
142
+ it "should create sparse index" do
143
+ @collection.safe_ensure_index({ title: 1 }, sparse: true)
144
+ docs = @collection.get_indexes
145
+ docs.any?{ |doc| doc["key"] == {"title" => 1} && doc["sparse"] == true }.must_equal true
146
+ end
147
+
148
+ it "should create unique index" do
149
+ @collection.safe_ensure_index({ some_field: 1 }, unique: true, sparse: true)
150
+ docs = @collection.get_indexes
151
+ docs.any?{ |doc| doc["key"] == {"some_field" => 1} && doc["unique"] == true }.must_equal true
152
+ end
153
+
154
+ it "should drop single index" do
155
+ @collection.safe_ensure_index(title: 1)
156
+ docs = @collection.get_indexes
157
+ docs.any?{ |doc| doc["key"] == {"title" => 1}}.must_equal true
158
+ @collection.drop_index(title: 1)
159
+ docs = @collection.get_indexes
160
+ docs.any?{ |doc| doc["key"] == {"title" => 1}}.must_equal false
161
+ end
162
+
163
+ it "should drop all indexes (except primary on _id)" do
164
+ @collection.safe_ensure_index(title: 1)
165
+ docs = @collection.get_indexes
166
+ docs.any?{ |doc| doc["key"] == {"title" => 1}}.must_equal true
167
+ @collection.drop_indexes
168
+ docs = @collection.get_indexes
169
+ docs.select{ |d| d["ns"] == "dbTest.testCollection" }.size.must_equal 1
170
+ end
171
+ end
172
+ end