1#  Copyright 2015 - present MongoDB, Inc.
2#
3#  Licensed under the Apache License, Version 2.0 (the "License");
4#  you may not use this file except in compliance with the License.
5#  You may obtain a copy of the License at
6#
7#  http://www.apache.org/licenses/LICENSE-2.0
8#
9#  Unless required by applicable law or agreed to in writing, software
10#  distributed under the License is distributed on an "AS IS" BASIS,
11#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#  See the License for the specific language governing permissions and
13#  limitations under the License.
14
15use strict;
16use warnings;
17use Test::More 0.96;
18use Test::Fatal;
19use JSON::MaybeXS;
20use Path::Tiny 0.054; # basename with suffix
21
22use MongoDB;
23use MongoDB::ReadPreference;
24use MongoDB::_Credential;
25use MongoDB::_Server;
26use MongoDB::_Topology;
27use MongoDB::_URI;
28
29subtest "rtt tests" => sub {
30    my $iterator = path('t/data/SS/rtt')->iterator( { recurse => 1 } );
31
32    for my $path ( exhaust_sort($iterator) ) {
33        next unless -f $path && $path =~ /\.json$/;
34        my $plan = eval { decode_json( $path->slurp_utf8 ) };
35        if ($@) {
36            die "Error decoding $path: $@";
37        }
38        run_rtt_test( $path->basename(".json"), $plan );
39    }
40
41    like(
42        exception { create_mock_server( "localhost:27017", -1 ) },
43        qr/non-negative number/,
44        "negative RTT times throw an excepton"
45    );
46};
47
48subtest "server selection tests" => sub {
49    my $source = path('t/data/SS/server_selection');
50    my $iterator = $source->iterator( { recurse => 1 } );
51
52    for my $path ( exhaust_sort($iterator) ) {
53        next unless -f $path && $path =~ /\.json$/;
54        my $plan = eval { decode_json( $path->slurp_utf8 ) };
55        if ($@) {
56            die "Error decoding $path: $@";
57        }
58        run_ss_test( $path->relative($source), $plan );
59    }
60};
61
62subtest "random selection" => sub {
63
64    my $topo = create_mock_topology( "mongodb://localhost", { type => 'Sharded' } );
65    $topo->_remove_address("localhost:27017");
66
67    for my $n ( "a" .. "z" ) {
68        my $address = "$n:27017";
69        my $server = create_mock_server( $address, 10, type => 'Mongos' );
70        $topo->servers->{$server->address} = $server;
71        $topo->_update_ewma( $server->address, $server );
72    }
73
74    # try up to 20
75    my $first = $topo->_find_available_server;
76
77    my $different = 0;
78    for ( 1 .. 20 ) {
79        my $another = $topo->_find_available_server;
80        if ( $first->address ne $another->address ) {
81            $different = 1;
82            last;
83        }
84    }
85
86    ok( $different, "servers randomly selected" );
87};
88
89subtest "server_selector" => sub {
90
91    my $topo = create_mock_topology(
92      "mongodb://localhost", { type => 'Sharded', server_selector => sub { shift } }
93    );
94    $topo->_remove_address("localhost:27017");
95
96    for my $n ( "a" .. "z" ) {
97        my $address = "$n:27017";
98        my $server = create_mock_server( $address, 10, type => 'Mongos' );
99        $topo->servers->{$server->address} = $server;
100        $topo->_update_ewma( $server->address, $server );
101    }
102
103    my $first = $topo->_find_available_server;
104    for ( 1 .. 5 ) {
105        my $another = $topo->_find_available_server;
106        is($first->address, $another->address,
107            'same server always, since server_selector is set'
108        );
109    }
110};
111
112sub exhaust_sort {
113    my $iter = shift;
114    my @result;
115    while ( defined( my $i = $iter->() ) ) {
116        push @result, $i;
117    }
118    return sort @result;
119}
120
121sub create_mock_topology {
122    my ( $uri, $options ) = @_;
123    $options->{'type'} ||= 'Single';
124
125    return MongoDB::_Topology->new(
126        uri                    => MongoDB::_URI->new( uri              => $uri ),
127        min_server_version     => "0.0.0",
128        max_wire_version       => 3,
129        min_wire_version       => 0,
130        heartbeat_frequency_ms => 3600000,
131        last_scan_time         => time + 60,
132        credential             => MongoDB::_Credential->new(
133            mechanism => 'NONE',
134            monitoring_callback => undef,
135        ),
136        monitoring_callback    => undef,
137        %$options,
138    );
139}
140
141sub create_mock_server {
142    my ( $address, $rtt, @args ) = @_;
143    return MongoDB::_Server->new(
144        address          => $address,
145        last_update_time => 0,
146        rtt_sec          => $rtt,
147        is_master        => { ismaster => 1, ok => 1 },
148        @args,
149    );
150}
151
152sub run_rtt_test {
153    my ( $name, $plan ) = @_;
154
155    my $topo = create_mock_topology("mongodb://localhost");
156
157    if ( $plan->{avg_rtt_ms} ne 'NULL' ) {
158        $topo->rtt_ewma_sec->{"localhost:27017"} = $plan->{avg_rtt_ms}/1000;
159    }
160
161    my $server = create_mock_server( "localhost:2707", $plan->{new_rtt_ms}/1000 );
162
163    $topo->_update_topology_from_server_desc( 'localhost:27017', $server );
164
165    is( $topo->rtt_ewma_sec->{"localhost:27017"}, $plan->{new_avg_rtt}/1000, $name );
166}
167
168sub run_ss_test {
169    my ( $name, $plan ) = @_;
170
171    $name =~ s{\.json$}{};
172
173    my $topo_desc = $plan->{topology_description};
174    my $topo = create_mock_topology( "mongodb://localhost", { type => $topo_desc->{type} } );
175    $topo->_remove_address("localhost:27017");
176    for my $s ( @{ $topo_desc->{servers} } ) {
177        my $address = $s->{address};
178        my $server  = create_mock_server(
179            $address,
180            $s->{avg_rtt_ms}/1000,
181            type => $s->{type},
182            tags => $s->{tags} || {},
183        );
184        $topo->servers->{$server->address} = $server;
185        $topo->_update_ewma( $server->address, $server );
186    }
187
188    my $got;
189    if ( $plan->{operation} eq 'read' ) {
190        my $read_pref = MongoDB::ReadPreference->new(
191            mode     => $plan->{read_preference}{mode},
192            tag_sets => $plan->{read_preference}{tag_sets} || {},
193        );
194        my $mode = $read_pref ? lc $read_pref->mode : 'primary';
195        my $method =
196            $topo->type eq "Single"  ? '_find_available_server'
197          : $topo->type eq "Sharded" ? '_find_readable_mongos_server'
198          :                            "_find_${mode}_server";
199
200        $got = $topo->$method($read_pref);
201    }
202    else {
203        my $method =
204          $topo->type eq 'Single' || $topo->type eq 'Sharded'
205          ? '_find_available_server'
206          : "_find_primary_server";
207
208        $got = $topo->$method;
209    }
210
211    if ( my @expect = @{ $plan->{in_latency_window} } ) {
212        if ( defined($got) ) {
213            my $got_address = $got->address;
214            my $found = grep { $got_address eq $_->{address} } @expect;
215            ok( $found, $name );
216        }
217        else {
218            fail( "expected @expect, but got nothing" );
219        }
220    }
221    else {
222        ok( !defined($got), $name );
223    }
224}
225
226done_testing;
227