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
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in monga.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem "minitest"
8
+ gem "minitest-reporters"
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Petr Yanovich
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Monga
2
+
3
+ [MongoDB](http://www.mongodb.org/) Ruby Client on [EventMachine](https://github.com/eventmachine/eventmachine).
4
+
5
+ This client is under development. You can try [em-mongo](https://github.com/bcg/em-mongo).
6
+
7
+ Client supports MongoDB 2.4. Some features won't work in lower versions.
8
+
9
+ ## To Do List
10
+
11
+ * [ ] Write a Wiki
12
+ * [ ] Write comments
13
+ * [ ] Grammar improvement ;)
14
+
15
+ ### Clients
16
+ * [x] Client (Single instance connection)
17
+ * [IN PROCESS] ReplicaSetClient
18
+ * [ ] MasterSlaveClient
19
+ * [ ] ReadPref
20
+ * [ ] Sharding Support
21
+
22
+ ### Connection
23
+ * [x] Connection
24
+ * [x] Autoreconnect
25
+ * [x] Connection Pool
26
+
27
+ ### Protocol
28
+ * [x] OP_QUERY
29
+ * [x] OP_GET_MORE
30
+ * [x] OP_KILL_CURSORS
31
+ * [x] OP_INSERT
32
+ * [x] OP_UPDATE
33
+ * [x] OP_DELETE
34
+ * [x] OP_REPLY
35
+
36
+ ### Database
37
+ * [x] create_collection
38
+ * [x] drop_collection
39
+ * [x] get_last_error
40
+ * [x] drop_indexes
41
+ * [x] get_indexes
42
+ * Authentication
43
+ * [ ] login
44
+ * [ ] logout
45
+ * [ ] add_user
46
+ * [ ] check maxBsonSize / validate
47
+ * [x] cmd
48
+ * [x] eval
49
+ * [ ] where
50
+
51
+ ### Collection
52
+ * QUERY_OP
53
+ * [x] find
54
+ * [x] find_one (first)
55
+ * [ ] sorting
56
+ * INSERT_OP
57
+ * [x] insert (single)
58
+ * [x] insert (batch)
59
+ * [x] safe_insert
60
+ * FLAGS
61
+ * [x] continue_on_error
62
+ * UPDATE_OP
63
+ * [x] update
64
+ * [x] safe_update
65
+ * FLAGS
66
+ * [x] upsert
67
+ * [x] multi_update
68
+ * DELETE_OP
69
+ * [x] delete
70
+ * [x] safe_delete
71
+ * FLAGS
72
+ * [x] single_remove
73
+ * INDEXES
74
+ * [x] ensure_index
75
+ * [x] drop_index
76
+ * [x] drop_indexes
77
+ * [x] get_indexes
78
+ * [x] count
79
+ * [x] all
80
+ * [x] cursor
81
+ * [ ] DBRef
82
+
83
+ ### Cursor
84
+ * [x] limit
85
+ * [x] skip
86
+ * [x] batch_size
87
+ * [x] get_more
88
+ * [x] next_document
89
+ * [x] next_batch
90
+ * [x] each_doc
91
+ * [x] kill
92
+ * [x] mark_to_kill
93
+ * [x] batch_kill
94
+ * [x] explain
95
+ * [x] hint
96
+ * Flags
97
+ * [x] tailable_cursor
98
+ * [x] slave_ok
99
+ * [x] no_cursor_timeout
100
+ * [x] await_data
101
+ * [x] exhaust
102
+ * [x] partial
103
+
104
+ # ISSUES handled with
105
+
106
+ Some commands, such as `db.getLastError`, `db.count` and other `db.commands` requires `numberToReturn` in OP_QUERY to be setted as `-1`. Also this commands should return a response. If nothing returned it should be interpreted as an Exception. Also, in contrast to the other queries it could return `errmsg` which should be interpreted as an Exception too. Other query methods could return `err` response.
107
+
108
+ To create index you can't call any `db.ensureIndex` command but you should insert a document into `sytem.indexes` collection manually. To get list of indexes you should fetch all documents from this collection. But to drop index you should call specific `db.dropIndexes` command.
109
+
110
+ `multi_update` flag works only with `$` commands (i.e. `$set: { title: "blahblah" }`)
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :default => :spec
5
+ Rake::TestTask.new(:spec) do |t|
6
+ t.libs << 'spec'
7
+ if spec = ENV['spec']
8
+ t.pattern = "spec/**/#{spec}*_spec.rb"
9
+ else
10
+ t.pattern = 'spec/**/*_spec.rb'
11
+ end
12
+ t.verbose = false
13
+ end
@@ -0,0 +1,8 @@
1
+ require File.expand_path("../clients/client", __FILE__)
2
+ require File.expand_path("../clients/replica_set_client", __FILE__)
3
+ require File.expand_path("../clients/master_slave_client", __FILE__)
4
+ module Monga
5
+ Client = Monga::Clients::Client
6
+ ReplicaSetClient = Monga::Clients::ReplicaSetClient
7
+ MasterSlaveClient = Monga::Clients::MasterSlaveClient
8
+ end
@@ -0,0 +1,24 @@
1
+ module Monga::Clients
2
+ class Client
3
+ extend Forwardable
4
+
5
+ def_delegators :@connection_pool, :aquire_connection, :send_command, :primary?, :connected?
6
+
7
+ attr_reader :connection_pool
8
+
9
+ def initialize(opts={})
10
+ opts[:pool_size] ||= 1
11
+ @connection_pool = Monga::ConnectionPool.new(opts)
12
+ end
13
+
14
+ def [](db_name)
15
+ Monga::Database.new(self, db_name)
16
+ end
17
+
18
+ def find_primary!
19
+ @connection_pool.connections.each do |conn|
20
+ conn.is_master?(self)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module Monga::Clients
2
+ class MasterSlaveClient
3
+
4
+ end
5
+ end
@@ -0,0 +1,101 @@
1
+ # How it works
2
+ # Replica Set tries to establish connections to all passed servers.
3
+ # Till no connection established it queues al queries inside it's
4
+
5
+ module Monga::Clients
6
+ class ReplicaSetClient
7
+ class ProxyConnection
8
+ include EM::Deferrable
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ def send_command(msg, request_id=nil, &cb)
14
+ callback do
15
+ connection = @client.aquire_connection
16
+ connection.send_command(msg, request_id, &cb)
17
+ end
18
+ end
19
+ end
20
+
21
+ include EM::Deferrable
22
+
23
+ attr_reader :servers, :clients
24
+
25
+ def initialize(opts = {})
26
+ @read_pref = opts.delete(:read_pref) || :primary
27
+ @servers = opts.delete(:servers)
28
+ raise ArgumentError, "servers option is not passed or empty" if @servers.empty?
29
+
30
+ @clients = @servers.map do |server|
31
+ Monga::Client.new(server.merge(opts))
32
+ end
33
+
34
+ @proxy_connection = ProxyConnection.new(self)
35
+ end
36
+
37
+ def [](db_name)
38
+ Monga::Database.new(self, db_name)
39
+ end
40
+
41
+ def aquire_connection
42
+ server ||= case @read_pref
43
+ when :primary
44
+ primary
45
+ when :secondary
46
+ secondary
47
+ when :primary_preferred
48
+ primary || secondary
49
+ when :secondary_preferred
50
+ secondary || primary
51
+ when :nearest
52
+ fail "unimplemented read_pref mode"
53
+ else
54
+ fail "read_pref is undefined"
55
+ end
56
+
57
+
58
+ if server
59
+ if @deferred_status != :succeeded
60
+ set_deferred_status :succeeded
61
+ @proxy_connection.set_deferred_status :succeeded
62
+ end
63
+ else
64
+ if @deferred_status == :succeeded
65
+ set_deferred_status nil
66
+ @proxy_connection.set_deferred_status nil
67
+ end
68
+ end
69
+
70
+ server || @proxy_connection
71
+ end
72
+
73
+ def primary
74
+ prim = @clients.detect{ |c| c.primary? && c.connected? }
75
+ unless prim
76
+ find_primary!
77
+ end
78
+ prim
79
+ end
80
+
81
+ def secondary
82
+ @clients.select{ |c| !c.primary? && c.connected? }.sample
83
+ end
84
+
85
+ def find_primary!
86
+ unless @pending_primary
87
+ @pending_primary = true
88
+ @clients.each{ |c| c.find_primary! }
89
+ EM.add_timer(0.1) do
90
+ if primary
91
+ aquire_connection
92
+ @pending_primary = false
93
+ else
94
+ @pending_primary = false
95
+ find_primary!
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,108 @@
1
+ module Monga
2
+ class Collection
3
+ attr_reader :name, :db
4
+
5
+ def initialize(db, name)
6
+ @db = db
7
+ @name = name
8
+ end
9
+
10
+ def query(query = {}, fields = {}, opts = {})
11
+ options = {}
12
+ options[:query] = query
13
+ options[:fields] = fields
14
+ options.merge! opts
15
+ Monga::Miner.new(db, name, options)
16
+ end
17
+ alias :find :query
18
+
19
+ def find_one(query = {}, fields = {}, opts = {})
20
+ options = {}
21
+ options[:query] = query
22
+ options[:fields] = fields
23
+ options.merge! opts
24
+
25
+ Monga::Response.surround do |resp|
26
+ req = Monga::Miner.new(db, name, options).limit(1)
27
+ req.callback{ |data| resp.succeed data.first }
28
+ req.errback{ |err| resp.fail err }
29
+ end
30
+ end
31
+ alias :first :find_one
32
+
33
+ def insert(documents, opts = {})
34
+ options = {}
35
+ options[:documents] = documents
36
+ options.merge!(opts)
37
+ Monga::Requests::Insert.new(@db, @name, options).perform
38
+ end
39
+
40
+ def update(query = {}, update = {}, flags = {})
41
+ options = {}
42
+ options[:query] = query
43
+ options[:update] = update
44
+ options.merge!(flags)
45
+ Monga::Requests::Update.new(@db, @name, options).perform
46
+ end
47
+
48
+ def delete(query = {}, opts = {})
49
+ options = {}
50
+ options[:query] = query
51
+ options.merge!(opts)
52
+ Monga::Requests::Delete.new(@db, @name, options).perform
53
+ end
54
+ alias :remove :delete
55
+
56
+ def ensure_index(keys, opts={})
57
+ doc = {}
58
+ doc.merge!(opts)
59
+ # Read docs about naming
60
+ doc[:name] ||= keys.to_a.flatten * "_"
61
+ doc[:key] = keys
62
+ doc[:ns] = "#{db.name}.#{name}"
63
+ Monga::Requests::Insert.new(@db, "system.indexes", {documents: doc}).perform
64
+ end
65
+
66
+ def drop_index(indexes)
67
+ @db.drop_indexes(@name, indexes)
68
+ end
69
+
70
+ def drop_indexes
71
+ @db.drop_indexes(@name, "*")
72
+ end
73
+
74
+ def get_indexes
75
+ Monga::Miner.new(@db, "system.indexes")
76
+ end
77
+
78
+ def drop
79
+ @db.drop_collection(@name)
80
+ end
81
+
82
+ def count
83
+ @db.count(@name)
84
+ end
85
+
86
+ # Safe methods
87
+ [:update, :insert, :delete, :remove, :ensure_index].each do |meth|
88
+ class_eval <<-EOS
89
+ def safe_#{meth}(*args)
90
+ safe do
91
+ #{meth}(*args)
92
+ end
93
+ end
94
+ EOS
95
+ end
96
+
97
+ def safe
98
+ response = Monga::Response.new
99
+ request_id = yield
100
+ req = @db.get_last_error
101
+ req.callback do |data|
102
+ response.succeed(request_id)
103
+ end
104
+ req.errback{ |err| response.fail(err) }
105
+ response
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path("../connections/em_connection", __FILE__)
2
+ require File.expand_path("../connections/primary", __FILE__)
3
+ require File.expand_path("../connections/secondary", __FILE__)
4
+
5
+ module Monga
6
+ class Connection
7
+ extend Forwardable
8
+
9
+ def_delegators :@connection, :connected?, :reconnect, :responses, :send_command, :master?, :is_master?, :host, :port
10
+
11
+ CONNECTION_TYPES = {
12
+ default: Monga::Connections::EMConnection,
13
+ primary: Monga::Connections::Primary,
14
+ secondary: Monga::Connections::Secondary,
15
+ }
16
+
17
+ def initialize(opts={})
18
+ conn_type = opts.delete(:connection_type) || :default
19
+ conn_class = CONNECTION_TYPES[conn_type]
20
+ @connection = conn_class.connect(opts)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module Monga
2
+ class ConnectionPool < Monga::Connection
3
+ extend Forwardable
4
+
5
+ def_delegators :aquire_connection, :send_command
6
+
7
+ attr_reader :connections
8
+
9
+ def initialize(opts={})
10
+ @connections = []
11
+ pool_size = opts.delete :pool_size
12
+
13
+ pool_size.times do
14
+ @connections << Monga::Connection.new(opts)
15
+ end
16
+ end
17
+
18
+ def aquire_connection
19
+ connected = @connections.select(&:connected?)
20
+ if connected.any?
21
+ min = connected.min_by{ |c| c.responses.size }.responses.size
22
+ conns = connected.select{ |c| c.responses.size == min }
23
+ conns.sample
24
+ else
25
+ @connections.sample
26
+ end
27
+ end
28
+
29
+ def primary?
30
+ conn = aquire_connection
31
+ conn ? conn.master? : false
32
+ end
33
+
34
+ def connected?
35
+ @connections.any?(&:connected?)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,152 @@
1
+ module Monga::Connections
2
+ class EMConnection < EM::Connection
3
+ include EM::Deferrable
4
+
5
+ class Buffer
6
+ include Enumerable
7
+
8
+ attr_reader :buffer
9
+
10
+ def initialize
11
+ @buffer = ""
12
+ @positon = 0
13
+ end
14
+
15
+ def append(data)
16
+ @buffer += data
17
+ end
18
+
19
+ def each
20
+ while true
21
+ size = @buffer.size
22
+ if size > Monga::HEADER_SIZE
23
+ msg_length = @buffer[0, 4].unpack("L").first
24
+ if msg_length && size >= msg_length
25
+ data = @buffer.slice!(0, size)
26
+ yield data.unpack("LLLLLQLLa*")
27
+ else
28
+ break
29
+ end
30
+ else
31
+ break
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ attr_reader :responses, :host, :port
38
+
39
+ def initialize(opts = {})
40
+ @host = opts[:host]
41
+ @port = opts[:port]
42
+ @reactor_running = true
43
+ @responses = {}
44
+ end
45
+
46
+ def self.connect(opts = {})
47
+ host = opts[:host] ||= Monga::DEFAULT_HOST
48
+ port = opts[:port] ||= Monga::DEFAULT_PORT
49
+
50
+ EM.connect(host, port, self, opts)
51
+ end
52
+
53
+ def send_command(msg, request_id=nil, &cb)
54
+ reconnect unless @connected
55
+
56
+ callback do
57
+ send_data msg
58
+ end
59
+
60
+ @responses[request_id] = cb if cb
61
+ end
62
+
63
+ def receive_data(data)
64
+ @buffer.append(data)
65
+ @buffer.each do |message|
66
+ request_id = message[2]
67
+ cb = @responses.delete request_id
68
+ cb.call(message) if cb
69
+ end
70
+ end
71
+
72
+ def connection_completed
73
+ Monga.logger.debug("Connection is established #{@host}:#{@port}")
74
+
75
+ EM.add_shutdown_hook do
76
+ close
77
+ end
78
+
79
+ unless @reactor_running
80
+ EM.add_periodic_timer(Monga::Cursor::CLOSE_TIMEOUT){ Monga::Cursor.batch_kill(self) }
81
+ end
82
+
83
+ @connected = true
84
+ @pending_for_reconnect = false
85
+ @buffer = Buffer.new
86
+ @reactor_running = true
87
+
88
+ succeed
89
+ end
90
+
91
+ def reconnect
92
+ unless @connected && @pending_for_reconnect
93
+ if @reactor_running
94
+ super(@host, @port)
95
+ else
96
+ EM.schedule{ super(@host, @port) }
97
+ end
98
+ @pending_for_reconnect = true
99
+ end
100
+ end
101
+
102
+ def force_reconnect(host, port)
103
+ @connected = false
104
+ @host = host
105
+ @port = port
106
+ end
107
+
108
+ def connected?
109
+ EM.schedule { reconnect } unless @reactor_running
110
+ @connected || false
111
+ end
112
+
113
+ def unbind
114
+ Monga.logger.debug("Lost connection #{@host}:#{@port}")
115
+
116
+ @responses.each{ |k, cb| cb.call(Monga::Exceptions::LostConnection.new("Mongo has lost connection"))}
117
+ @connected = false
118
+ @primary = false
119
+ @pending_for_reconnect = false
120
+ set_deferred_status(nil)
121
+
122
+ if @reactor_running
123
+ EM.add_timer(0.1){ reconnect }
124
+ end
125
+ end
126
+
127
+ def close
128
+ Monga.logger.debug("EventMachine is stopped, closing connection")
129
+ @reactor_running = false
130
+ end
131
+
132
+ def master?
133
+ @primary || false
134
+ end
135
+
136
+ def is_master?(client)
137
+ db = client["admin"]
138
+ req = Monga::Requests::Query.new(db, "$cmd", query: {"isMaster" => 1}, limit: 1)
139
+ command = req.command
140
+ request_id = req.request_id
141
+ @responses[request_id] = proc do |data|
142
+ resp = req.parse_response(data)
143
+ if Exception === resp
144
+ @primary = false
145
+ else
146
+ @primary = resp.last.first["ismaster"]
147
+ end
148
+ end
149
+ send_data command
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,46 @@
1
+ module Monga::Connections
2
+ class Primary < Monga::Connections::EMConnection
3
+ def initialize(opts)
4
+ @client = opts.delete :client
5
+ @ser
6
+ super
7
+ end
8
+
9
+ def connection_completed
10
+ check_master do |master|
11
+ if master
12
+ super
13
+ else
14
+ reconnect
15
+ end
16
+ end
17
+ end
18
+
19
+ def check_master
20
+ db = @client["admin"]
21
+ request = Monga::Request.new(db, "$cmd", query: { "isMaster" => 1 })
22
+ request_id = request.request_id
23
+ @responses[request_id] = proc do |data|
24
+ request.parse_response(data)
25
+ if Exception === data
26
+ Monga.logger.debug("Error on connecting Primary to #{@host}:#{@port}, #{data.class}, #{data.message}")
27
+ @host, @port = @client.next_addr(@host, @port)
28
+ yield false
29
+ else
30
+ doc = data.last.first
31
+ if doc["ismaster"]
32
+ Monga.logger.debug("Primary has connected to #{@host}:#{@port}")
33
+ @client.inform(:primary, @host, @port)
34
+ yield true
35
+ else
36
+ Monga.logger.debug("#{@host}:#{@port} is not a primary")
37
+ @host, @port = @client.select_addr(doc)
38
+ yield false
39
+ end
40
+ end
41
+ end
42
+ command = request.command
43
+ send_data(command)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module Monga::Connections
2
+ class Secondary < Monga::Connections::EMConnection
3
+ def initialize(opts)
4
+ @client = opts.delete :client
5
+ super
6
+ end
7
+
8
+ def connection_completed
9
+ @client.inform(:secondary, @host, @port)
10
+ super()
11
+ end
12
+ end
13
+ end