em-synchrony-moped 0.9.4 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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