File: connection_spec.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (353 lines) | stat: -rw-r--r-- 12,089 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# frozen_string_literal: true
# rubocop:todo all

require 'spec_helper'

describe 'Connections' do
  clean_slate

  let(:client) do
    ClientRegistry.instance.global_client('authorized').tap do |client|
      stop_monitoring(client)
    end
  end

  let(:server) { client.cluster.servers.first }

  describe '#connect!' do

    let(:connection) do
      Mongo::Server::Connection.new(server, server.options)
    end

    context 'network error during handshake' do
      # On JRuby 9.2.7.0, this line:
      # expect_any_instance_of(Mongo::Socket).to receive(:write).and_raise(exception)
      # ... appears to produce a moment in which Mongo::Socket#write is undefined
      # entirely, resulting in this failure:
      # RSpec::Expectations::ExpectationNotMetError: expected Mongo::Error::SocketError, got #<NameError: undefined method `write' for class `Mongo::Socket'>
      fails_on_jruby

      # 4.4 has two monitors and thus our socket mocks get hit twice
      max_server_version '4.2'

      let(:exception) { Mongo::Error::SocketError }

      let(:error) do
        connection
        expect_any_instance_of(Mongo::Socket).to receive(:write).and_raise(exception)
        expect do
          connection.connect!
        end.to raise_error(exception)
      end

      it 'sets server type to unknown' do
        expect(server).not_to be_unknown
        error

        expect(server).to be_unknown
      end

      context 'with sdam event subscription' do

        let(:subscriber) { Mrss::EventSubscriber.new }
        let(:client) do
          ClientRegistry.instance.global_client('authorized').with(app_name: 'connection_integration').tap do |client|
            client.subscribe(Mongo::Monitoring::SERVER_OPENING, subscriber)
            client.subscribe(Mongo::Monitoring::SERVER_CLOSED, subscriber)
            client.subscribe(Mongo::Monitoring::SERVER_DESCRIPTION_CHANGED, subscriber)
            client.subscribe(Mongo::Monitoring::TOPOLOGY_OPENING, subscriber)
            client.subscribe(Mongo::Monitoring::TOPOLOGY_CHANGED, subscriber)
          end
        end

        it 'publishes server description changed event' do
          expect(subscriber.succeeded_events).to be_empty

          wait_for_all_servers(client.cluster)

          connection
          subscriber.succeeded_events.clear
          error

          event = subscriber.first_event('server_description_changed_event')
          expect(event).not_to be_nil
          expect(event.address).to eq(server.address)
          expect(event.new_description).to be_unknown
        end

        it 'marks server unknown' do
          expect(server).not_to be_unknown

          connection
          error

          expect(server).to be_unknown
        end

        context 'in replica set topology' do
          require_topology :replica_set

          # need to use the primary here, otherwise a secondary will be
          # changed to unknown which wouldn't alter topology
          let(:server) { client.cluster.next_primary }

          it 'changes topology type' do
            # wait for topology to get discovered
            client.cluster.next_primary

            expect(client.cluster.topology.class).to eql(Mongo::Cluster::Topology::ReplicaSetWithPrimary)

            # stop background monitoring to prevent it from racing with the test
            client.cluster.servers_list.each do |server|
              server.monitor.stop!
            end

            connection
            error

            expect(client.cluster.topology.class).to eql(Mongo::Cluster::Topology::ReplicaSetNoPrimary)
          end
        end
      end

      context 'error during handshake to primary in a replica set' do
        require_topology :replica_set

        let(:server) { client.cluster.next_primary }

        before do
          # insert to perform server selection and get topology to primary
          client.cluster.next_primary
        end

        it 'sets cluster type to replica set without primary' do
          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::ReplicaSetWithPrimary)
          error

          expect(client.cluster.topology).to be_a(Mongo::Cluster::Topology::ReplicaSetNoPrimary)
        end
      end

      describe 'number of sockets created' do

        before do
          server
        end

        shared_examples_for 'is 1 per connection' do
          it 'is 1 per connection' do
            # Instantiating a connection object should not create any sockets
            RSpec::Mocks.with_temporary_scope do
              expect(socket_cls).not_to receive(:new)

              connection
            end

            # When the connection connects, exactly one socket should be created
            # (and subsequently connected)
            RSpec::Mocks.with_temporary_scope do
              expect(socket_cls).to receive(:new).and_call_original

              connection.connect!
            end
          end
        end

        let(:socket_cls) { ::Socket }

        it_behaves_like 'is 1 per connection'

        context 'connection to Unix domain socket' do
          # Server does not allow Unix socket connections when TLS is enabled
          require_no_tls

          let(:port) { SpecConfig.instance.any_port }

          let(:client) do
            new_local_client(["/tmp/mongodb-#{port}.sock"], connect: :direct).tap do |client|
              stop_monitoring(client)
            end
          end

          let(:socket_cls) { ::UNIXSocket }

          it_behaves_like 'is 1 per connection'
        end
      end

      context 'when socket connection fails' do

        before do
          server
        end

        let(:socket_cls) { ::Socket }

        let(:socket) do
          double('socket').tap do |socket|
            allow(socket).to receive(:setsockopt)
            allow(socket).to receive(:set_encoding)
            allow(socket).to receive(:getsockopt)
            expect(socket).to receive(:connect).and_raise(IOError, 'test error')

            # This test is testing for the close call:
            expect(socket).to receive(:close)
          end
        end

        it 'closes the socket' do
          RSpec::Mocks.with_temporary_scope do
            expect(::Socket).to receive(:new).with(
              Socket::AF_INET, Socket::SOCK_STREAM, 0).and_return(socket)

            lambda do
              connection.connect!
            end.should raise_error(Mongo::Error::SocketError, /test error/)
          end
        end

        context 'with tls' do
          require_tls

          let(:socket) do
            double('socket').tap do |socket|
              allow(socket).to receive(:hostname=)
              allow(socket).to receive(:sync_close=)
              expect(socket).to receive(:connect).and_raise(IOError, 'test error')

              # This test is testing for the close call:
              expect(socket).to receive(:close)
            end
          end

          it 'closes the SSL socket' do
            RSpec::Mocks.with_temporary_scope do
              expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(socket)

              lambda do
                connection.connect!
              end.should raise_error(Mongo::Error::SocketError, /test error/)
            end
          end
        end
      end
    end

    describe 'wire protocol version range update' do
      require_no_required_api_version

      # 3.2 wire protocol is 4.
      # Wire protocol < 2 means only scram auth is available,
      # which is not supported by modern mongos.
      # Instead of mucking with this we just limit this test to 3.2+
      # so that we can downgrade protocol range to 0..3 instead of 0..1.
      min_server_fcv '3.2'

      let(:client) { ClientRegistry.instance.global_client('authorized').with(app_name: 'wire_protocol_update') }

      context 'non-lb' do
        require_topology :single, :replica_set, :sharded

        it 'updates on handshake response from non-monitoring connections' do
          # connect server
          client['test'].insert_one(test: 1)

          # kill background threads so that they are not interfering with
          # our mocked hello response
          client.cluster.servers.each do |server|
            server.monitor.stop!
          end

          server = client.cluster.servers.first
          expect(server.features.server_wire_versions.max >= 4).to be true
          max_version = server.features.server_wire_versions.max

          # Depending on server version, handshake here may return a
          # description that compares equal to the one we got from a
          # monitoring connection (pre-4.2) or not (4.2+).
          # Since we do run SDAM flow on handshake responses on
          # non-monitoring connections, force descriptions to be different
          # by setting the existing description here to unknown.
          server.monitor.instance_variable_set('@description',
            Mongo::Server::Description.new(server.address))

          RSpec::Mocks.with_temporary_scope do
            # now pretend a handshake returned a different range
            features = Mongo::Server::Description::Features.new(0..3)
            # One Features instantiation is for SDAM event publication, this
            # one always happens. The second one happens on servers
            # where we do not negotiate auth mechanism.
            expect(Mongo::Server::Description::Features).to receive(:new).at_least(:once).and_return(features)

            connection = Mongo::Server::Connection.new(server, server.options)
            expect(connection.connect!).to be true

            # hello response should update server description via sdam flow,
            # which includes wire version range
            expect(server.features.server_wire_versions.max).to eq(3)
          end
        end
      end

      context 'lb' do
        require_topology :load_balanced

        it 'does not update on handshake response from non-monitoring connections since there are not any' do
          # connect server
          client['test'].insert_one(test: 1)

          server = client.cluster.servers.first
          server.load_balancer?.should be true
          server.features.server_wire_versions.max.should be 0
        end
      end
    end

    describe 'SDAM flow triggered by hello on non-monitoring thread' do
      # replica sets can transition between having and not having a primary
      require_topology :replica_set

      let(:client) do
        # create a new client because we make manual state changes
        ClientRegistry.instance.global_client('authorized').with(app_name: 'non-monitoring thread sdam')
      end

      it 'performs SDAM flow' do
        client['foo'].insert_one(bar: 1)
        client.cluster.servers_list.each do |server|
          server.monitor.stop!
        end
        expect(client.cluster.topology.class).to eq(Mongo::Cluster::Topology::ReplicaSetWithPrimary)

        # need to connect to the primary for topology to change
        server = client.cluster.servers.detect do |server|
          server.primary?
        end

        # overwrite server description
        server.instance_variable_set('@description', Mongo::Server::Description.new(
          server.address))

        # overwrite topology
        client.cluster.instance_variable_set('@topology',
          Mongo::Cluster::Topology::ReplicaSetNoPrimary.new(
            client.cluster.topology.options, client.cluster.topology.monitoring, client.cluster))

        # now create a connection.
        connection = Mongo::Server::Connection.new(server, server.options)

        # verify everything once again
        expect(server).to be_unknown
        expect(client.cluster.topology.class).to eq(Mongo::Cluster::Topology::ReplicaSetNoPrimary)

        # this should dispatch the sdam event
        expect(connection.connect!).to be true

        # back to primary
        expect(server).to be_primary
        expect(client.cluster.topology.class).to eq(Mongo::Cluster::Topology::ReplicaSetWithPrimary)
      end
    end
  end
end