em-synchrony-moped 0.9.4 → 1.0.0.beta.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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWM1ZDFhNzU0ZWE3ZjM2MWQ2YTAwODlmOWMzOTliZTU5OTMwZTc2Yg==
5
+ data.tar.gz: !binary |-
6
+ MDIxYTY0M2Q1Zjg4NGRjNzBjYjMyZTlmOTEyN2FhOTlhNTk5MTQwOQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZGJhNzU3NmZlM2I3YTcxNWM5NmUzMDk4OWRiNTQwNDExMTA3M2U0NjVlYjFh
10
+ MDhkMjE5YmViNTZkNDIxY2MwMGFhNDhkZGJkMDBiNjc0ZjgwZmIyNTIzNDZi
11
+ MDY4Y2ZhZWIwNmE2YmRjZjUzMzE4MjcyMDczYzE2ZmI4MTE5MmI=
12
+ data.tar.gz: !binary |-
13
+ MTM0OTE2NTk1MzI4NGZhYmFiMGRmNzM2ZGQ3MjlmMzI0MjlkNWJhZmRlMmI1
14
+ MTU2YmE3ZDViOTA0MTVkYmIzMDk1M2U1NWJkNzYyNWQyZWIyMDBkNTI3OGU4
15
+ ZDEzMTBlNGJmNTMyY2RkYjI0MGY5MGNiODBlNjI4ODMyNjU0Yjc=
data/.gitignore CHANGED
@@ -1 +1,5 @@
1
1
  Gemfile.lock
2
+ pkg
3
+ tmp
4
+ .ruby-version
5
+ coverage
data/Guardfile ADDED
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ guard 'rspec' do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { "spec" }
7
+ end
8
+
9
+ guard 'rubocop' do
10
+ watch(%r{.+\.rb$})
11
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
12
+ end
13
+
14
+ guard 'bundler' do
15
+ watch('Gemfile')
16
+ watch(/^.+\.gemspec/)
17
+ end
data/Rakefile CHANGED
@@ -1,16 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
- begin
4
- require 'rspec/core'
5
- require 'rspec/core/rake_task'
3
+ require 'rspec/core'
4
+ require 'rspec/core/rake_task'
6
5
 
7
- task :default => [:spec]
8
- task :test => [:spec]
6
+ task :default => [:spec]
7
+ task :test => [:spec]
9
8
 
10
- desc "Run all RSpec tests"
11
- RSpec::Core::RakeTask.new(:spec)
12
-
13
-
14
- rescue LoadError
15
- # silent failure for when rspec is not installed (production mode)
16
- end
9
+ desc "Run all RSpec tests"
10
+ RSpec::Core::RakeTask.new(:spec)
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'em-synchrony-moped'
5
- s.version = "0.9.4"
5
+ s.version = "1.0.0.beta.1"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Adam Lebsack"]
@@ -18,11 +18,18 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_runtime_dependency 'eventmachine'
21
+ s.add_runtime_dependency 'eventmachine', '~> 1.0'
22
22
  s.add_runtime_dependency 'em-synchrony', '~> 1.0'
23
- s.add_runtime_dependency 'moped', '~> 1.4.5'
23
+ s.add_runtime_dependency 'moped', '~> 1.5.1'
24
24
  s.add_runtime_dependency 'em-resolv-replace', '~> 1.1.3'
25
25
 
26
26
  s.add_development_dependency 'rspec', '~> 2.12.0'
27
+ s.add_development_dependency 'guard'
28
+ s.add_development_dependency 'guard-rspec'
29
+ s.add_development_dependency 'guard-bundler'
30
+ s.add_development_dependency 'guard-rubocop'
31
+ s.add_development_dependency 'rubocop'
32
+ s.add_development_dependency 'spork'
33
+ s.add_development_dependency 'simplecov'
27
34
 
28
35
  end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require 'moped/cluster'
4
+
5
+ module Moped
6
+ # Our patches to Moped::Cluster
7
+ module EventedCluster
8
+ def sleep(seconds)
9
+ if EventMachine.reactor_thread?
10
+ EM::Synchrony.sleep(seconds)
11
+ else
12
+ Kernel.sleep(seconds)
13
+ end
14
+ end
15
+ end
16
+
17
+ class Cluster
18
+ include EventedCluster
19
+ end
20
+ end
@@ -0,0 +1,122 @@
1
+ # encoding: utf-8
2
+
3
+ module Moped
4
+ class Connection
5
+ def connect
6
+ if EventMachine.reactor_thread?
7
+ if !!options[:ssl]
8
+ @sock = Sockets::EmSSL.em_connect(host, port, timeout, options)
9
+ else
10
+ @sock = Sockets::EmTCP.em_connect(host, port, timeout, options)
11
+ end
12
+ else # use old driver
13
+ if !!options[:ssl]
14
+ @sock = Sockets::SSL.connect(host, port, timeout)
15
+ else
16
+ @sock = Sockets::TCP.connect(host, port, timeout)
17
+ end
18
+ end
19
+ end
20
+ end # class Cnnection
21
+
22
+ module Sockets
23
+ module Connectable
24
+ module ClassMethods
25
+ def em_connect(host, port, timeout, options)
26
+ socket = EventMachine.connect(host, port, self) do |c|
27
+ c.pending_connect_timeout = timeout
28
+ c.options = options
29
+ end
30
+ # In TCPSocket, new against a closed port raises Errno::ECONNREFUSED.
31
+ # In EM, connect against a closed port result in a call to unbind
32
+ # with a reason param of Errno::ECONNREFUSED as a class, not an
33
+ # instance.
34
+ unless socket.sync(:in) # wait for connection
35
+ fail socket.unbind_reason.new if socket.unbind_reason.is_a? Class
36
+ fail SocketError, socket.unbind_reason
37
+ end
38
+ socket
39
+
40
+ rescue Errno::ETIMEDOUT
41
+ fail Errors::ConnectionFailure,
42
+ "Timed out connection to Mongo on #{host}:#{port}"
43
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EPIPE,
44
+ Errno::ECONNRESET, IOError => error
45
+ fail Errors::ConnectionFailure,
46
+ "#{host}:#{port}: #{error.class.name} (#{error.errno}): " +
47
+ "#{error.message}"
48
+ rescue SocketError => error
49
+ fail Errors::ConnectionFailure,
50
+ "#{host}:#{port}: #{error.class.name}: #{error.message}"
51
+ rescue OpenSSL::SSL::SSLError => error
52
+ fail Errors::ConnectionFailure,
53
+ "#{host}:#{port}: #{error.class.name} (#{error.errno}): " +
54
+ "#{error.message}"
55
+ end
56
+ end
57
+ end
58
+
59
+ # The EM-Synchrony flavor of Moped::Sockets::TCP
60
+ class EmTCP < EventMachine::Synchrony::TCPSocket
61
+ include Connectable
62
+
63
+ # TODO: re-evaluate the options call. Can't we pass the caller
64
+ # up to the connection or something?
65
+ attr_accessor :options
66
+
67
+ def alive?
68
+ !closed?
69
+ end
70
+ end
71
+
72
+ # The EM-Synchrony flavor of Moped::Sockets::SSL
73
+ class EmSSL < EmTCP
74
+ def connection_completed
75
+ @verified = false
76
+ if @options[:ssl].is_a?(Hash)
77
+ start_tls(@options[:ssl])
78
+ else
79
+ start_tls
80
+ end
81
+ end
82
+
83
+ def ssl_verify_peer(pem)
84
+ unless (cert_store = @options[:ssl][:cert_store])
85
+ cert_store = OpenSSL::X509::Store.new
86
+ cert_store.add_file(@options[:ssl][:verify_cert])
87
+ end
88
+
89
+ if (cert = OpenSSL::X509::Certificate.new(pem) rescue nil)
90
+ if cert_store.verify(cert)
91
+
92
+ cert.extensions.each do |e|
93
+ if e.oid == 'basicConstraints' && e.value == 'CA:TRUE'
94
+ return true
95
+ end
96
+ end
97
+
98
+ host = @options[:ssl][:verify_host]
99
+ if OpenSSL::SSL.verify_certificate_identity(cert, host)
100
+ @verified = true
101
+ return true
102
+ end
103
+ end
104
+ end
105
+
106
+ true
107
+ rescue
108
+ unbind 'Failed to verify SSL certificate of peer'
109
+ false
110
+ end
111
+
112
+ def ssl_handshake_completed
113
+ if @options[:ssl][:verify_peer] && !@verified
114
+ unbind 'Failed to verify SSL certificate of peer'
115
+ else
116
+ @opening = false
117
+ @in_req.succeed self
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ require 'moped/node'
4
+ require 'em-dns-resolver'
5
+ require 'fiber'
6
+
7
+ module Moped
8
+ # Our monkey patches to Moped::Node
9
+ module EventedNode
10
+ # Override to support non-blocking DNS requests
11
+ def parse_address
12
+ return super if EM.reactor_thread?
13
+ host, port = address.split(':')
14
+ @port = (port || 27_017).to_i
15
+
16
+ @ip_address = em_lookup_address(host)
17
+ fail SocketError unless @ip_address
18
+ @resolved_address = "#{@ip_address}:#{@port}"
19
+ rescue Resolv::ResolvError
20
+ raise SocketError
21
+ end
22
+
23
+ def em_lookup_address(value)
24
+ # Lookup in /etc/hosts
25
+ result = []
26
+ @hosts ||= Resolv::Hosts.new
27
+ @hosts.send(:each_address, value) { |x| result << x.to_s }
28
+ return result unless result.empty?
29
+
30
+ # Nothing, hit DNS
31
+ fiber = Fiber.current
32
+ df = EM::DnsResolver.send(:resolve, value)
33
+ df.callback do |a|
34
+ fiber.resume(a)
35
+ end
36
+ df.errback do |*a|
37
+ fiber.resume(ResolvError.new(a.inspect))
38
+ end
39
+ result = Fiber.yield
40
+ raise result if result.is_a?(StandardError)
41
+ result
42
+ end
43
+ end
44
+
45
+ class Node
46
+ include EventedNode
47
+ end
48
+ end
@@ -1,182 +1,6 @@
1
- require "moped/connection"
2
- require "moped/node"
3
- require 'em-resolv-replace'
1
+ # encoding: utf-8
4
2
 
5
- silence_warnings do
6
- module Moped
7
- class Cluster
8
- def sleep(seconds)
9
- EM::Synchrony.sleep(seconds)
10
- end
11
-
12
- # MONKEY PATCH: if all available connections are down, use the seeds
13
- # again to determine, where the application should connect. This is a
14
- # implementation detail and unlikely to be main line. Therefore no pull
15
- # request here. The assumtion is, that if we can't connect to any mongodb
16
- # that they are relocated and available under a different DNS address.
17
- def nodes(opts = {})
18
- current_time = Time.new
19
- down_boundary = current_time - down_interval
20
- refresh_boundary = current_time - refresh_interval
21
-
22
- # Find the nodes that were down but are ready to be refreshed, or those
23
- # with stale connection information.
24
- needs_refresh, available = @nodes.partition do |node|
25
- node.down? ? (node.down_at < down_boundary) : node.needs_refresh?(refresh_boundary)
26
- end
27
-
28
- # Refresh those nodes.
29
- available.concat refresh(needs_refresh)
30
-
31
- # Now return all the nodes that are available and participating in the
32
- # replica set.
33
- avail_not_down = available.reject do |node|
34
- node.down? || !member?(node) || (!opts[:include_arbiters] && node.arbiter?)
35
- end
36
-
37
- if avail_not_down.empty?
38
- if logger = Moped.logger
39
- logger.warn " MOPED: reinitialize cluster because all nodes " \
40
- "are down with #{@seeds.inspect}"
41
- end
42
- @nodes = @seeds.map { |host| Node.new(host, @options) }
43
- end
44
-
45
- avail_not_down
46
- end
47
- end
48
-
49
- class Node
50
- # Override to support non-blocking DNS requests
51
- def parse_address
52
- host, port = address.split(":")
53
- @port = (port || 27017).to_i
54
-
55
- @resolver ||= Resolv.new([Resolv::Hosts.new, Resolv::DNS.new])
56
-
57
- # For now, limit the IPs only to IPv4 hosts. In order to support IPv6,
58
- # the node should be able to handle fallback connections.
59
- @resolver.getaddresses(host).each do |ip|
60
- if ip =~ Resolv::IPv4::Regex
61
- @ip_address = ip
62
- break
63
- end
64
- end
65
- raise SocketError unless @ip_address
66
- @resolved_address = "#{@ip_address}:#{@port}"
67
- rescue Resolv::ResolvError
68
- raise SocketError
69
- end
70
- end
71
-
72
-
73
- class Connection
74
- def connected?
75
- @sock && !@sock.instance_variable_get("@remote_closed") && !@sock.closed?
76
- end
77
-
78
- def connect
79
- @sock = if !!options[:ssl]
80
- Sockets::SSL.connect(host, port, timeout, options)
81
- else
82
- Sockets::TCP.connect(host, port, timeout, options)
83
- end
84
- end
85
- end
86
-
87
- Sockets.send(:remove_const, :TCP)
88
- Sockets.send(:remove_const, :SSL)
89
- module Sockets
90
- module Connectable
91
- attr_accessor :options
92
-
93
- def alive?
94
- !closed?
95
- end
96
-
97
- module ClassMethods
98
-
99
- def connect(host, port, timeout, options={})
100
- socket = EM.connect(host, port, self) do |c|
101
- c.pending_connect_timeout = timeout
102
- c.options = options.merge(:host => host)
103
- end
104
- # In TCPSocket, new against a closed port raises Errno::ECONNREFUSED.
105
- # In EM, connect against a closed port result in a call to unbind with
106
- # a reason param of Errno::ECONNREFUSED as a class, not an instance.
107
- unless socket.sync(:in) # wait for connection
108
- raise socket.unbind_reason.new if socket.unbind_reason.is_a? Class
109
- raise SocketError, socket.unbind_reason
110
- end
111
- socket
112
-
113
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EPIPE, Errno::ECONNRESET, Errno::ETIMEDOUT, IOError => error
114
- raise Errors::ConnectionFailure, "#{host}:#{port}: #{error.class.name} (#{error.errno}): #{error.message}"
115
- rescue SocketError => error
116
- raise Errors::ConnectionFailure, "#{host}:#{port}: #{error.class.name}: #{error.message}"
117
- rescue OpenSSL::SSL::SSLError => error
118
- raise Errors::ConnectionFailure, "#{host}:#{port}: #{error.class.name} (#{error.errno}): #{error.message}"
119
- end
120
-
121
-
122
- end
123
- end
124
-
125
- class TCP < EventMachine::Synchrony::TCPSocket
126
- include Connectable
127
- end
128
-
129
- class SSL < EventMachine::Synchrony::TCPSocket
130
- include Connectable
131
-
132
- def connection_completed
133
- @verified = false
134
- if @options[:ssl].is_a?(Hash)
135
- start_tls(@options[:ssl])
136
- else
137
- start_tls
138
- end
139
- end
140
-
141
- def ssl_verify_peer(pem)
142
- unless cert_store = @options[:ssl][:cert_store]
143
- cert_store = OpenSSL::X509::Store.new
144
- cert_store.add_file(@options[:ssl][:verify_cert])
145
- end
146
-
147
- if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
148
- if cert_store.verify(cert)
149
-
150
- cert.extensions.each do |e|
151
- if e.oid == 'basicConstraints' && e.value == 'CA:TRUE'
152
- return true
153
- end
154
- end
155
-
156
- host = @options[:ssl][:verify_host]
157
- if OpenSSL::SSL.verify_certificate_identity(cert, host)
158
- @verified = true
159
- return true
160
- end
161
- end
162
- end
163
-
164
- true
165
- rescue => e
166
- unbind "Failed to verify SSL certificate of peer"
167
- false
168
- end
169
-
170
- def ssl_handshake_completed
171
- if @options[:ssl][:verify_peer] && !@verified
172
- unbind "Failed to verify SSL certificate of peer"
173
- else
174
- @opening = false
175
- @in_req.succeed self
176
- end
177
- end
178
-
179
- end
180
- end
181
- end
182
- end
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/moped/cluster'
5
+ require 'em-synchrony/moped/node'
6
+ require 'em-synchrony/moped/connection'
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'moped'
6
+ require 'em-synchrony/moped'
7
+
8
+ describe Moped::Cluster do
9
+ let(:mongod_options) { {} }
10
+ let(:server1) { FakeMongodServer.new(mongod_options) }
11
+ let(:server1_port) { server1.port }
12
+
13
+ let(:server2) { FakeMongodServer.new(mongod_options.merge(master: 0)) }
14
+ let(:server2_port) { server2.port }
15
+
16
+ after do
17
+ server1.stop if server1
18
+ server2.stop if server2
19
+ end
20
+
21
+ shared_context 'common cluster' do
22
+ let(:options) { {} }
23
+ let(:seeds) do
24
+ ["#{server1.host}:#{server1_port}", "#{server2.host}:#{server2_port}"]
25
+ end
26
+ let(:cluster) { Moped::Cluster.new(seeds, options.merge(timeout: 1)) }
27
+
28
+ describe '#with_primary' do
29
+ it 'should get a primary node' do
30
+ cluster.with_primary do |node|
31
+ expect(node).to be_primary
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#with_secondary' do
37
+ it 'should get a secondary node' do
38
+ cluster.with_secondary do |node|
39
+ expect(node).to be_secondary
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ context 'evented' do
46
+ include_context 'with em-synchrony'
47
+ include_context 'common cluster'
48
+ end
49
+ context 'threaded' do
50
+ include_context 'without em-synchrony'
51
+ include_context 'common cluster'
52
+ end
53
+ end
@@ -0,0 +1,137 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ require 'moped'
5
+ require 'em-synchrony/moped'
6
+
7
+ # TODO: clean up a bit. we're basically testing Connection through Node.
8
+ # we should be able to test Connection by itself.
9
+
10
+ describe Moped::Connection do
11
+ it 'should have patches included' do
12
+ expect { Moped::Sockets::EmTCP }.not_to raise_error(NameError)
13
+ end
14
+
15
+ let(:mongod_options) { {} }
16
+ let(:server) { FakeMongodServer.new(mongod_options) }
17
+ let(:server_port) { server.port }
18
+ after { server.stop }
19
+
20
+ let(:node_options) { {} }
21
+ let(:node) do
22
+ options = node_options.merge(timeout: 1)
23
+ host = options.delete(:host) || 'localhost'
24
+ Moped::Node.new("#{host}:#{server_port}", options)
25
+ end
26
+
27
+ shared_context 'common connection' do
28
+ context 'with a running server' do
29
+ it 'should connect' do
30
+ node.refresh
31
+ node.should be_primary
32
+ node.should be_connected
33
+ end
34
+
35
+ it 'should detect a disconnect' do
36
+ node.refresh
37
+ node.should be_primary
38
+ node.should be_connected
39
+ server.stop
40
+ expect do
41
+ node.command('admin', ismaster: 1)
42
+ end.to raise_error(
43
+ Moped::Errors::ConnectionFailure # TODO: check the message
44
+ )
45
+ node.should_not be_connected
46
+ end
47
+ end
48
+
49
+ context 'with an unpresponsive host' do
50
+ # 127.0.0.2 seems to timeout for my tests...
51
+ let(:node_options) { { host: '127.0.0.2' } }
52
+ it 'should raise a timeout error' do
53
+ expect { node.refresh }.to raise_error(
54
+ Moped::Errors::ConnectionFailure,
55
+ /^Timed out connection to Mongo on/)
56
+ end
57
+ end
58
+
59
+ context 'without a server' do
60
+ it 'should raise a connection error on connection refused' do
61
+ server.stop
62
+ expect { node.refresh }.to raise_error(
63
+ Moped::Errors::ConnectionFailure, /ECONNREFUSED/)
64
+ end
65
+ end
66
+ end
67
+
68
+ shared_context 'common connection ssl' do
69
+ context 'with ssl server' do
70
+ let(:mongod_options) do
71
+ {
72
+ ssl: {
73
+ private_key_file: "#{SSL_DIR}/server.key",
74
+ cert_chain_file: "#{SSL_DIR}/server.crt",
75
+ verify_peer: false
76
+ }
77
+ }
78
+ end
79
+
80
+ context 'without verifying peer' do
81
+ let(:node_options) { { ssl: { verify_peer: false } } }
82
+ it 'should connect' do
83
+ node.refresh
84
+ node.should be_primary
85
+ end
86
+ end
87
+
88
+ context 'when verifying peer' do
89
+ let(:node_options) do
90
+ { ssl: {
91
+ verify_peer: true,
92
+ verify_cert: "#{SSL_DIR}/ca_cert.pem",
93
+ verify_host: 'localhost'
94
+ }
95
+ }
96
+ end
97
+ it 'should connect' do
98
+ node.refresh
99
+ node.should be_primary
100
+ end
101
+
102
+ context 'with untrusted key on server' do
103
+ let(:mongod_options) do
104
+ {
105
+ ssl: {
106
+ private_key_file: "#{SSL_DIR}/untrusted.key",
107
+ cert_chain_file: "#{SSL_DIR}/untrusted.crt",
108
+ verify_peer: false
109
+ }
110
+ }
111
+ end
112
+
113
+ it 'should connect and fail to verify peer' do
114
+ expect do
115
+ node.refresh
116
+ node.should be_primary
117
+ end.to raise_error(
118
+ Moped::Errors::ConnectionFailure,
119
+ /Failed to verify SSL certificate of peer/
120
+ )
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ context 'evented' do
128
+ include_context 'with em-synchrony'
129
+ include_context 'common connection'
130
+ include_context 'common connection ssl'
131
+ end
132
+ context 'threaded' do
133
+ include_context 'without em-synchrony'
134
+ include_context 'common connection'
135
+ end
136
+
137
+ end