Skip to content

Commit fa34de2

Browse files
authored
Merge branch 'main' into NODE-4854-minPoolSizeReconnections
2 parents 46a37d3 + 12cb82e commit fa34de2

13 files changed

+444
-534
lines changed

.evergreen/run-serverless-tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ npx mocha \
1919
test/integration/retryable-reads/retryable_reads.spec.test.js \
2020
test/integration/retryable-writes/retryable_writes.spec.test.ts \
2121
test/integration/sessions/sessions.spec.test.ts \
22+
test/integration/sessions/sessions.prose.test.ts \
2223
test/integration/sessions/sessions.test.ts \
2324
test/integration/transactions/transactions.spec.test.js \
2425
test/integration/transactions/transactions.test.ts \

src/cmap/connection.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
504504
if (err) {
505505
return callback(err);
506506
}
507+
} else if (session?.explicit) {
508+
return callback(new MongoCompatibilityError('Current topology does not support sessions'));
507509
}
508510

509511
// if we have a known cluster time, gossip it
@@ -620,7 +622,7 @@ export class CryptoConnection extends Connection {
620622
/** @internal */
621623
export function hasSessionSupport(conn: Connection): boolean {
622624
const description = conn.description;
623-
return description.logicalSessionTimeoutMinutes != null || !!description.loadBalanced;
625+
return description.logicalSessionTimeoutMinutes != null;
624626
}
625627

626628
function supportsOpMsg(conn: Connection) {

src/operations/execute_operation.ts

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -106,35 +106,18 @@ async function executeOperationAsync<
106106
throw new MongoRuntimeError('client.connect did not create a topology but also did not throw');
107107
}
108108

109-
if (topology.shouldCheckForSessionSupport()) {
110-
await topology.selectServerAsync(ReadPreference.primaryPreferred, {});
111-
}
112-
113109
// The driver sessions spec mandates that we implicitly create sessions for operations
114110
// that are not explicitly provided with a session.
115111
let session = operation.session;
116112
let owner: symbol | undefined;
117-
if (topology.hasSessionSupport()) {
118-
if (session == null) {
119-
owner = Symbol();
120-
session = client.startSession({ owner, explicit: false });
121-
} else if (session.hasEnded) {
122-
throw new MongoExpiredSessionError('Use of expired sessions is not permitted');
123-
} else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
124-
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
125-
}
126-
} else {
127-
// no session support
128-
if (session && session.explicit) {
129-
// If the user passed an explicit session and we are still, after server selection,
130-
// trying to run against a topology that doesn't support sessions we error out.
131-
throw new MongoCompatibilityError('Current topology does not support sessions');
132-
} else if (session && !session.explicit) {
133-
// We do not have to worry about ending the session because the server session has not been acquired yet
134-
delete operation.options.session;
135-
operation.clearSession();
136-
session = undefined;
137-
}
113+
114+
if (session == null) {
115+
owner = Symbol();
116+
session = client.startSession({ owner, explicit: false });
117+
} else if (session.hasEnded) {
118+
throw new MongoExpiredSessionError('Use of expired sessions is not permitted');
119+
} else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
120+
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
138121
}
139122

140123
const readPreference = operation.readPreference ?? ReadPreference.primary;

src/sdam/topology.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -574,26 +574,6 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
574574
processWaitQueue(this);
575575
}
576576

577-
// Sessions related methods
578-
579-
/**
580-
* @returns Whether the topology should initiate selection to determine session support
581-
*/
582-
shouldCheckForSessionSupport(): boolean {
583-
if (this.description.type === TopologyType.Single) {
584-
return !this.description.hasKnownServers;
585-
}
586-
587-
return !this.description.hasDataBearingServers;
588-
}
589-
590-
/**
591-
* @returns Whether sessions are supported on the current topology
592-
*/
593-
hasSessionSupport(): boolean {
594-
return this.loadBalanced || this.description.logicalSessionTimeoutMinutes != null;
595-
}
596-
597577
/**
598578
* Update the internal TopologyDescription with a ServerDescription
599579
*
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { expect } from 'chai';
2+
import { ChildProcess, spawn } from 'child_process';
3+
import { once } from 'events';
4+
5+
import { Collection, CommandStartedEvent, MongoClient, MongoDriverError } from '../../mongodb';
6+
import { sleep } from '../../tools/utils';
7+
8+
describe('Sessions Prose Tests', () => {
9+
describe('14. Implicit sessions only allocate their server session after a successful connection checkout', () => {
10+
let client: MongoClient;
11+
let testCollection: Collection<{ _id: number; a?: number }>;
12+
beforeEach(async function () {
13+
const configuration = this.configuration;
14+
client = await configuration.newClient({ maxPoolSize: 1, monitorCommands: true }).connect();
15+
16+
// reset test collection
17+
testCollection = client.db('test').collection('too.many.sessions');
18+
await testCollection.drop().catch(() => null);
19+
});
20+
21+
afterEach(async () => {
22+
await client?.close(true);
23+
});
24+
25+
/**
26+
* Create a MongoClient with the following options: maxPoolSize=1 and retryWrites=true
27+
* Attach a command started listener that collects each command's lsid
28+
* Drivers MUST assert that exactly one session is used for all operations at least once across the retries of this test.
29+
* Note that it's possible, although rare, for greater than 1 server session to be used because the session is not released until after the connection is checked in.
30+
* Drivers MUST assert that the number of allocated sessions is strictly less than the number of concurrent operations in every retry of this test. In this instance it would be less than (but NOT equal to) 8.
31+
*/
32+
it('released server sessions are correctly reused', async () => {
33+
const events: CommandStartedEvent[] = [];
34+
client.on('commandStarted', ev => events.push(ev));
35+
36+
const operations = [
37+
testCollection.insertOne({ _id: 1 }),
38+
testCollection.deleteOne({ _id: 2 }),
39+
testCollection.updateOne({ _id: 3 }, { $set: { a: 1 } }),
40+
testCollection.bulkWrite([
41+
{ updateOne: { filter: { _id: 4 }, update: { $set: { a: 1 } } } }
42+
]),
43+
testCollection.findOneAndDelete({ _id: 5 }),
44+
testCollection.findOneAndUpdate({ _id: 6 }, { $set: { a: 1 } }),
45+
testCollection.findOneAndReplace({ _id: 7 }, { a: 8 }),
46+
testCollection.find().toArray()
47+
];
48+
49+
const allResults = await Promise.all(operations);
50+
51+
expect(allResults).to.have.lengthOf(operations.length);
52+
expect(events).to.have.lengthOf(operations.length);
53+
54+
// This is a guarantee in node, unless you are performing a transaction (which is not being done in this test)
55+
expect(new Set(events.map(ev => ev.command.lsid.id.toString('hex')))).to.have.lengthOf(2);
56+
});
57+
});
58+
59+
describe('When sessions are not supported', () => {
60+
/**
61+
* Since all regular 3.6+ servers support sessions, the prose tests which test for
62+
* session non-support SHOULD use a mongocryptd server as the test server
63+
* (available with server versions 4.2+)
64+
*
65+
* As part of the test setup for these cases, create a MongoClient pointed at the test server
66+
* with the options specified in the test case and verify that the test server does NOT define a
67+
* value for logicalSessionTimeoutMinutes by sending a hello command and checking the response.
68+
*/
69+
const mongocryptdTestPort = '27022';
70+
let client: MongoClient;
71+
let childProcess: ChildProcess;
72+
before(() => {
73+
childProcess = spawn('mongocryptd', ['--port', mongocryptdTestPort, '--ipv6'], {
74+
stdio: 'ignore',
75+
detached: true
76+
});
77+
78+
childProcess.on('error', err => {
79+
console.warn('Sessions prose mongocryptd error:', err);
80+
});
81+
});
82+
83+
beforeEach(async () => {
84+
client = new MongoClient(`mongodb://localhost:${mongocryptdTestPort}`, {
85+
monitorCommands: true
86+
});
87+
88+
const hello = await client.db().command({ hello: true });
89+
expect(hello).to.have.property('iscryptd', true); // sanity check
90+
expect(hello).to.not.have.property('logicalSessionTimeoutMinutes');
91+
});
92+
93+
afterEach(async () => {
94+
await client?.close();
95+
});
96+
97+
after(() => {
98+
childProcess.kill();
99+
});
100+
101+
it(
102+
'18. Implicit session is ignored if connection does not support sessions',
103+
{
104+
requires: {
105+
clientSideEncryption: true,
106+
mongodb: '>=4.2.0'
107+
}
108+
},
109+
async function () {
110+
/**
111+
* 1. Send a read command to the server (e.g., `findOne`), ignoring any errors from the server response
112+
* 2. Check the corresponding `commandStarted` event: verify that `lsid` is not set
113+
*/
114+
const readCommandEventPromise = once(client, 'commandStarted').then(res => res[0]);
115+
await client
116+
.db()
117+
.collection('test')
118+
.findOne({})
119+
.catch(() => null);
120+
const readCommandEvent = await Promise.race([readCommandEventPromise, sleep(500)]);
121+
expect(readCommandEvent).to.have.property('commandName', 'find');
122+
expect(readCommandEvent).to.not.have.property('lsid');
123+
124+
/**
125+
* 3. Send a write command to the server (e.g., `insertOne`), ignoring any errors from the server response
126+
* 4. Check the corresponding `commandStarted` event: verify that `lsid` is not set
127+
*/
128+
const writeCommandEventPromise = once(client, 'commandStarted').then(res => res[0]);
129+
await client
130+
.db()
131+
.collection('test')
132+
.insertOne({})
133+
.catch(() => null);
134+
const writeCommandEvent = await Promise.race([writeCommandEventPromise, sleep(500)]);
135+
expect(writeCommandEvent).to.have.property('commandName', 'insert');
136+
expect(writeCommandEvent).to.not.have.property('lsid');
137+
}
138+
);
139+
140+
it(
141+
'19. Explicit session raises an error if connection does not support sessions',
142+
{
143+
requires: {
144+
clientSideEncryption: true,
145+
mongodb: '>=4.2.0'
146+
}
147+
},
148+
async function () {
149+
/**
150+
* 1. Create a new explicit session by calling `startSession` (this MUST NOT error)
151+
*/
152+
const session = client.startSession();
153+
154+
/**
155+
* 2. Attempt to send a read command to the server (e.g., `findOne`) with the explicit session passed in
156+
* 3. Assert that a client-side error is generated indicating that sessions are not supported
157+
*/
158+
const readOutcome = await client
159+
.db()
160+
.collection('test')
161+
.findOne({}, { session })
162+
.catch(err => err);
163+
expect(readOutcome).to.be.instanceOf(MongoDriverError);
164+
expect(readOutcome.message).to.match(/does not support sessions/);
165+
166+
/**
167+
* 4. Attempt to send a write command to the server (e.g., `insertOne`) with the explicit session passed in
168+
* 5. Assert that a client-side error is generated indicating that sessions are not supported
169+
*/
170+
const writeOutcome = await client
171+
.db()
172+
.collection('test')
173+
.insertOne({}, { session })
174+
.catch(err => err);
175+
expect(writeOutcome).to.be.instanceOf(MongoDriverError);
176+
expect(writeOutcome.message).to.match(/does not support sessions/);
177+
}
178+
);
179+
});
180+
});

test/integration/sessions/sessions.spec.prose.test.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)