feat(transactions): add single-node transaction support with session-aware reads, commits, aborts, and transaction metrics
This commit is contained in:
+60
-15
@@ -44,7 +44,7 @@ tap.test('transactions: should still support explicit sessions', async () => {
|
||||
expect(end.ok).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('transactions: should reject raw transaction-scoped writes before mutation', async () => {
|
||||
tap.test('transactions: should reject transaction-scoped writes without txnNumber before mutation', async () => {
|
||||
const db = client.db('txntest');
|
||||
const coll = db.collection('docs');
|
||||
await coll.insertOne({ key: 'outside', value: 1 });
|
||||
@@ -59,8 +59,8 @@ tap.test('transactions: should reject raw transaction-scoped writes before mutat
|
||||
});
|
||||
} catch (err: any) {
|
||||
threw = true;
|
||||
expect(err.code).toEqual(20);
|
||||
expect(err.codeName).toEqual('IllegalOperation');
|
||||
expect(err.code).toEqual(14);
|
||||
expect(err.codeName).toEqual('TypeMismatch');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
|
||||
@@ -68,44 +68,89 @@ tap.test('transactions: should reject raw transaction-scoped writes before mutat
|
||||
expect(await coll.countDocuments({ key: 'outside' })).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('transactions: official driver transaction should fail without committing writes', async () => {
|
||||
tap.test('transactions: official driver transaction should commit buffered writes', async () => {
|
||||
const coll = client.db('txntest').collection('driverdocs');
|
||||
await coll.insertOne({ key: 'outside-driver', value: 0 });
|
||||
const session = client.startSession();
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
session.startTransaction();
|
||||
await coll.insertOne({ key: 'inside-driver', value: 1 }, { session });
|
||||
const inTxn = await coll.findOne({ key: 'inside-driver' }, { session });
|
||||
expect(inTxn).toBeTruthy();
|
||||
expect(await coll.countDocuments({ key: 'inside-driver' })).toEqual(0);
|
||||
await session.commitTransaction();
|
||||
} catch (err: any) {
|
||||
threw = true;
|
||||
expect(err.code).toEqual(20);
|
||||
expect(err.codeName).toEqual('IllegalOperation');
|
||||
await session.abortTransaction().catch(() => undefined);
|
||||
} finally {
|
||||
await session.endSession();
|
||||
}
|
||||
|
||||
expect(threw).toBeTrue();
|
||||
expect(await coll.countDocuments({ key: 'inside-driver' })).toEqual(0);
|
||||
expect(await coll.countDocuments({ key: 'inside-driver' })).toEqual(1);
|
||||
expect(await coll.countDocuments({ key: 'outside-driver' })).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('transactions: commit and abort commands should be explicit unsupported errors', async () => {
|
||||
tap.test('transactions: abort should discard buffered writes', async () => {
|
||||
const coll = client.db('txntest').collection('abortdocs');
|
||||
const session = client.startSession();
|
||||
|
||||
try {
|
||||
session.startTransaction();
|
||||
await coll.insertOne({ key: 'abort-me', value: 1 }, { session });
|
||||
expect(await coll.findOne({ key: 'abort-me' }, { session })).toBeTruthy();
|
||||
await session.abortTransaction();
|
||||
} finally {
|
||||
await session.endSession();
|
||||
}
|
||||
|
||||
expect(await coll.findOne({ key: 'abort-me' })).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('transactions: update and delete should commit atomically', async () => {
|
||||
const coll = client.db('txntest').collection('mutations');
|
||||
await coll.insertMany([
|
||||
{ key: 'update-me', value: 1 },
|
||||
{ key: 'delete-me', value: 2 },
|
||||
]);
|
||||
const session = client.startSession();
|
||||
|
||||
try {
|
||||
session.startTransaction();
|
||||
await coll.updateOne({ key: 'update-me' }, { $set: { value: 10 } }, { session });
|
||||
await coll.deleteOne({ key: 'delete-me' }, { session });
|
||||
expect((await coll.findOne({ key: 'update-me' }, { session }))!.value).toEqual(10);
|
||||
expect(await coll.findOne({ key: 'delete-me' }, { session })).toBeNull();
|
||||
expect((await coll.findOne({ key: 'update-me' }))!.value).toEqual(1);
|
||||
expect(await coll.findOne({ key: 'delete-me' })).toBeTruthy();
|
||||
await session.commitTransaction();
|
||||
} finally {
|
||||
await session.endSession();
|
||||
}
|
||||
|
||||
expect((await coll.findOne({ key: 'update-me' }))!.value).toEqual(10);
|
||||
expect(await coll.findOne({ key: 'delete-me' })).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('transactions: commit and abort without active transaction should be explicit errors', async () => {
|
||||
for (const command of [{ commitTransaction: 1 }, { abortTransaction: 1 }]) {
|
||||
let threw = false;
|
||||
try {
|
||||
await client.db('admin').command(command);
|
||||
} catch (err: any) {
|
||||
threw = true;
|
||||
expect(err.code).toEqual(20);
|
||||
expect(err.codeName).toEqual('IllegalOperation');
|
||||
expect(err.code).toEqual(251);
|
||||
expect(err.codeName).toEqual('NoSuchTransaction');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('transactions: serverStatus should expose transaction and oplog metrics', async () => {
|
||||
const status = await client.db('admin').command({ serverStatus: 1 });
|
||||
expect(status.ok).toEqual(1);
|
||||
expect(status.transactions.currentActive).toEqual(0);
|
||||
expect(status.logicalSessionRecordCache.activeSessionsCount).toBeGreaterThanOrEqual(0);
|
||||
expect(status.oplog.totalEntries).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('transactions: cleanup', async () => {
|
||||
await client.close();
|
||||
await server.stop();
|
||||
|
||||
Reference in New Issue
Block a user