Skip to content
This repository was archived by the owner on Dec 22, 2021. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/MongoDB/_Server.pm
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use Types::Standard qw(
HashRef
Str
Num
Maybe
);
use List::Util qw/first/;
use Time::HiRes qw/time/;
Expand Down Expand Up @@ -251,6 +252,30 @@ sub _build_is_writable {
return !! grep { $type eq $_ } qw/Standalone RSPrimary Mongos/;
}

has is_data_bearing => (
is => 'lazy',
isa => Bool,
builder => "_build_is_data_bearing",
);

sub _build_is_data_bearing {
my ( $self ) = @_;
my $type = $self->type;
return !! grep { $type eq $_ } qw/Standalone RSPrimary RSSecondary Mongos/;
}

# logicalSessionTimeoutMinutes can be not set by a client
has logical_session_timeout_minutes => (
is => 'lazy',
isa => Maybe [NonNegNum],
builder => "_build_logical_session_timeout_minutes",
);

sub _build_logical_session_timeout_minutes {
my ( $self ) = @_;
return $self->is_master->{logicalSessionTimeoutMinutes} || undef;
}

sub updated_since {
my ( $self, $time ) = @_;
return( ($self->last_update_time - $time) > 0 );
Expand Down
29 changes: 28 additions & 1 deletion lib/MongoDB/_Topology.pm
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use Types::Standard qw(
);
use MongoDB::_Server;
use Config;
use List::Util qw/first max/;
use List::Util qw/first max min/;
use Safe::Isa;
use Time::HiRes qw/time usleep/;
use Try::Tiny;
Expand Down Expand Up @@ -127,6 +127,13 @@ has local_threshold_sec => (
isa => Num,
);

has logical_session_timeout_minutes => (
is => 'ro',
default => undef,
writer => '_set_logical_session_timeout_minutes',
isa => Maybe [NonNegNum],
);

has next_scan_time => (
is => 'ro',
default => sub { time() },
Expand Down Expand Up @@ -552,6 +559,24 @@ sub _check_wire_versions {
return;
}

sub _update_ls_timeout_minutes {
my ( $self, $new_server ) = @_;

my @data_bearing_servers = grep { $_->is_data_bearing } $self->all_servers;
my $timeout = min map {
# use -1 as a flag to prevent undefined warnings
defined $_->logical_session_timeout_minutes
? $_->logical_session_timeout_minutes
: -1
} @data_bearing_servers;
# min will return undef if the array is empty
if ( defined $timeout && $timeout < 0 ) {
$timeout = undef;
}
$self->_set_logical_session_timeout_minutes( $timeout );
return;
}

sub _check_staleness_compatibility {
my ($self, $read_pref) = @_;
my $max_staleness_sec = $read_pref ? $read_pref->max_staleness_seconds : -1;
Expand Down Expand Up @@ -997,6 +1022,8 @@ sub _update_topology_from_server_desc {
# if link is still around, tag it with server specifics
$self->_update_link_metadata( $address, $new_server );

$self->_update_ls_timeout_minutes( $new_server );

return $new_server;
}

Expand Down
55 changes: 34 additions & 21 deletions t/data/SDAM/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,6 @@ The YAML and JSON files in this directory tree are platform-independent tests
that drivers can use to prove their conformance to the
Server Discovery And Monitoring Spec.

Converting to JSON
------------------

The tests are written in YAML
because it is easier for humans to write and read,
and because YAML includes a standard comment format.
A JSONified version of each YAML file is included in this repository.
Whenever you change the YAML, re-convert to JSON.
One method to convert to JSON is with
`jsonwidget-python <http://jsonwidget.org/wiki/Jsonwidget-python>`_::

pip install PyYAML urwid jsonwidget
make

Or instead of "make":

for i in `find . -iname '*.yml'`; do
echo "${i%.*}"
jwc yaml2json $i > ${i%.*}.json
done

Version
-------

Expand Down Expand Up @@ -71,6 +50,8 @@ processing the responses in the phases so far. It has the following keys:
- setName: A string with the expected replica set name, or null.
- servers: An object whose keys are addresses like "a:27017", and whose values
are "server" objects.
- logicalSessionTimeoutMinutes: null or an integer.
- compatible: absent or a bool.

A "server" object represents a correct ServerDescription within the client's
current TopologyDescription. It has the following keys:
Expand All @@ -79,24 +60,56 @@ current TopologyDescription. It has the following keys:
- setName: A string with the expected replica set name, or null.
- setVersion: absent or an integer.
- electionId: absent, null, or an ObjectId.
- logicalSessionTimeoutMinutes: absent, null, or an integer.
- minWireVersion: absent or an integer.
- maxWireVersion: absent or an integer.

Use as unittests
----------------

Mocking
~~~~~~~

Drivers should be able to test their server discovery and monitoring logic
without any network I/O, by parsing ismaster responses from the test file
and passing them into the driver code. Parts of the client and monitoring
code may need to be mocked or subclassed to achieve this. `A reference
implementation for PyMongo 3.x is available here
<https://github.com/mongodb/mongo-python-driver/blob/3.0-dev/test/test_discovery_and_monitoring.py>`_.

Initialization
~~~~~~~~~~~~~~

For each file, create a fresh client object initialized with the file's "uri".

All files in the "single" directory include a connection string with one host
and no "replicaSet" option.
Set the client's initial TopologyType to Single, however that is achieved using the client's API.
(The spec says "The user MUST be able to set the initial TopologyType to Single"
without specifying how.)

All files in the "sharded" directory include a connection string with multiple hosts
and no "replicaSet" option.
Set the client's initial TopologyType to Unknown or Sharded, depending on the client's API.

All files in the "rs" directory include a connection string with a "replicaSet" option.
Set the client's initial TopologyType to ReplicaSetNoPrimary.
(For most clients, parsing a connection string with a "replicaSet" option
automatically sets the TopologyType to ReplicaSetNoPrimary.)

Test Phases
~~~~~~~~~~~

For each phase in the file, parse the "responses" array.
Pass in the responses in order to the driver code.
If a response is the empty object `{}`, simulate a network error.

Once all responses are processed, assert that the phase's "outcome" object
is equivalent to the driver's current TopologyDescription.

Some fields such as "logicalSessionTimeoutMinutes" or "compatible" were added
later and haven't been added to all test files. If these fields are present,
test that they are equivalent to the fields of the driver's current
TopologyDescription.

Continue until all phases have been executed.
55 changes: 55 additions & 0 deletions t/data/SDAM/rs/compatible.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"description": "Replica set member with large maxWireVersion",
"uri": "mongodb://a,b/?replicaSet=rs",
"phases": [
{
"responses": [
[
"a:27017",
{
"ok": 1,
"ismaster": true,
"setName": "rs",
"hosts": [
"a:27017",
"b:27017"
],
"minWireVersion": 0,
"maxWireVersion": 6
}
],
[
"b:27017",
{
"ok": 1,
"ismaster": false,
"secondary": true,
"setName": "rs",
"hosts": [
"a:27017",
"b:27017"
],
"minWireVersion": 0,
"maxWireVersion": 1000
}
]
],
"outcome": {
"servers": {
"a:27017": {
"type": "RSPrimary",
"setName": "rs"
},
"b:27017": {
"type": "RSSecondary",
"setName": "rs"
}
},
"topologyType": "ReplicaSetWithPrimary",
"setName": "rs",
"logicalSessionTimeoutMinutes": null,
"compatible": true
}
}
]
}
41 changes: 41 additions & 0 deletions t/data/SDAM/rs/compatible.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
description: "Replica set member with large maxWireVersion"
uri: "mongodb://a,b/?replicaSet=rs"
phases: [
{
responses: [
["a:27017", {
ok: 1,
ismaster: true,
setName: "rs",
hosts: ["a:27017", "b:27017"],
minWireVersion: 0,
maxWireVersion: 6
}],
["b:27017", {
ok: 1,
ismaster: false,
secondary: true,
setName: "rs",
hosts: ["a:27017", "b:27017"],
minWireVersion: 0,
maxWireVersion: 1000
}]
],
outcome: {
servers: {
"a:27017": {
type: "RSPrimary",
setName: "rs"
},
"b:27017": {
type: "RSSecondary",
setName: "rs"
}
},
topologyType: "ReplicaSetWithPrimary",
setName: "rs",
logicalSessionTimeoutMinutes: null,
compatible: true
}
}
]
75 changes: 39 additions & 36 deletions t/data/SDAM/rs/discover_arbiters.json
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
{
"description": "Discover arbiters",
"phases": [
{
"outcome": {
"servers": {
"a:27017": {
"setName": "rs",
"type": "RSPrimary"
},
"b:27017": {
"setName": null,
"type": "Unknown"
}
},
"setName": "rs",
"topologyType": "ReplicaSetWithPrimary"
},
"responses": [
[
"a:27017",
{
"arbiters": [
"b:27017"
],
"hosts": [
"a:27017"
],
"ismaster": true,
"ok": 1,
"setName": "rs"
}
]
]
}
],
"uri": "mongodb://a/?replicaSet=rs"
"description": "Discover arbiters",
"uri": "mongodb://a/?replicaSet=rs",
"phases": [
{
"responses": [
[
"a:27017",
{
"ok": 1,
"ismaster": true,
"hosts": [
"a:27017"
],
"arbiters": [
"b:27017"
],
"setName": "rs",
"minWireVersion": 0,
"maxWireVersion": 6
}
]
],
"outcome": {
"servers": {
"a:27017": {
"type": "RSPrimary",
"setName": "rs"
},
"b:27017": {
"type": "Unknown",
"setName": null
}
},
"topologyType": "ReplicaSetWithPrimary",
"logicalSessionTimeoutMinutes": null,
"setName": "rs"
}
}
]
}
6 changes: 4 additions & 2 deletions t/data/SDAM/rs/discover_arbiters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ phases: [
ismaster: true,
hosts: ["a:27017"],
arbiters: ["b:27017"],
setName: "rs"
setName: "rs",
minWireVersion: 0,
maxWireVersion: 6
}]
],

Expand All @@ -33,8 +35,8 @@ phases: [
setName:
}
},

topologyType: "ReplicaSetWithPrimary",
logicalSessionTimeoutMinutes: null,
setName: "rs"
}
}
Expand Down
Loading