import * as assert from 'power-assert' import tcb from '../../../src/index' import * as Config from '../../config.local' import * as common from '../../common/index' const app = tcb.init(Config) const db = app.database() const _ = db.command const offset = 60 * 1000 describe('transaction', () => { let collection = null const collectionName = 'db-test-transactions' const date = new Date() const data = [ { _id: '1', category: 'Web', tags: ['JavaScript', 'C#'], date, geo: new db.Geo.Point(90, 23), num: 2 }, { _id: '2', category: 'Web', tags: ['Go', 'C#'] }, { _id: '3', category: 'Life', tags: ['Go', 'Python', 'JavaScript'] }, { _id: '4', serverDate1: db.serverDate(), serverDate2: db.serverDate({ offset }) } ] beforeEach(async () => { collection = await common.safeCollection(db, collectionName) // 测试环境不稳定, 清除之前的影响 await collection.remove() const success = await collection.create(data) assert.strictEqual(success, true) }) afterEach(async () => { const success = await collection.remove() assert.strictEqual(success, true) }) it('发起事务', async () => { const transaction = await db.startTransaction() assert.strictEqual(typeof transaction._id, 'string') }) it('提交事务', async () => { const transaction = await db.startTransaction() const res = await transaction.commit() assert.strictEqual(typeof res.requestId, 'string') }) it('runTransaction', async () => { await db.runTransaction(async function(transaction) { // 事务内插入 含date geo类型文档 const insertDoc = { _id: 'lluke', date: date, geo: new db.Geo.Point(90, 23) } const insertRes = await transaction.collection(collectionName).add(insertDoc) assert(insertRes.inserted === 1) const doc = await transaction .collection(collectionName) .doc('lluke') .get() assert(doc.data, insertDoc) // 事务外插入含date geo serverDate 类型文档 const doc1 = await transaction .collection(collectionName) .doc('1') .get() const doc4 = await transaction .collection(collectionName) .doc('4') .get() assert.deepStrictEqual(doc1.data, data[0]) assert.deepStrictEqual( doc4.data.serverDate1.getTime() + offset === doc4.data.serverDate2.getTime(), true ) // 事务内插入 含 serverDate 类型文档 报错不支持 // const insertRes = await transaction.collection(collectionName).add({_id: 'lluke', serverDate1: db.serverDate(), serverDate2: db.serverDate({ offset })}) // console.log('insertRes:', insertRes) // const lukeData = await transaction.collection(collectionName).doc('lluke').get() // assert(lukeData.data.serverDate1.getTime() + offset === lukeData.data.serverDate2.getTime(), true) }) }) it('事务内批量插入 条件查询', async () => { const transaction = await db.startTransaction() const addRes = await transaction .collection(collectionName) .add([{ name: 'a' }, { name: 'b' }]) assert(addRes.ids.length === 2) const queryRes = await transaction .collection(collectionName) .where(_.or([{ name: 'a' }, { name: 'b' }])) .get() assert(queryRes.data.length === 2) const result = await transaction.commit() assert.strictEqual(typeof result.requestId, 'string') }) it('事务内批量插入1000条文档', async () => { const transaction = await db.startTransaction() // 构造1001条数据 const mockData = [] let i = 0 while (i++ <= 1000) { mockData.push({ name: `luketest${i}` }) } const addRes = await transaction.collection(collectionName).add(mockData) assert(addRes.ids.length === 1001) const queryRes = await transaction .collection(collectionName) .where({ name: /^luketest/ }) .limit(1000) .get() // console.log('queryRes:', queryRes) assert(queryRes.data.length === 1000) const result = await transaction.commit() assert.strictEqual(typeof result.requestId, 'string') }) it('事务内更新含特殊类型 字段文档', async () => { await db.runTransaction(async function(transaction) { const newDate = new Date() const newGeo = new db.Geo.Point(90, 23) const updateRes = await transaction .collection(collectionName) .doc('1') .update({ num: _.inc(1), date: newDate, geo: newGeo }) assert(updateRes.updated === 1) const res = await transaction .collection(collectionName) .doc('1') .get() assert(res.data.num === 3, true) assert(res.data.date, newDate) assert(res.data.geo, newGeo) }) }) it('insert', async () => { const transaction = await db.startTransaction() const res = await transaction .collection(collectionName) .add({ category: 'Web', tags: ['JavaScript', 'C#'], date }) assert(res.id !== undefined && res.inserted === 1) const result = await transaction.commit() assert.strictEqual(typeof result.requestId, 'string') }) it('insert with custom docid', async () => { const docId = +new Date() const transaction = await db.startTransaction() const res = await transaction .collection(collectionName) .add({ _id: docId, category: 'Web', tags: ['JavaScript', 'C#'], date }) assert(res.id == docId && res.inserted === 1) const deleteRes = await transaction .collection(collectionName) .doc(docId) .remove() assert(deleteRes.deleted === 1) const result = await transaction.commit() assert.strictEqual(typeof result.requestId, 'string') }) it('get', async () => { // const docRef = db.collection(collectionName).doc('1') const transaction = await db.startTransaction() // const doc = await transaction.get(docRef) const doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, data[0]) const res = await transaction.commit() assert(res.requestId) }) it('get不存在的文档,返回null', async () => { // const docRef = db.collection(collectionName).doc('114514') const transaction = await db.startTransaction() // const doc = await transaction.get(docRef) const doc = await transaction .collection(collectionName) .doc('114514') .get() assert.strictEqual(doc.data, null) const res = await transaction.commit() assert(res.requestId) }) it('事务回滚', async () => { const transaction = await db.startTransaction() // const docRef = db.collection(collectionName).doc('1') // const doc = await transaction.get(docRef) const doc = await transaction .collection(collectionName) .doc('1') .get() const addDoc = await transaction .collection(collectionName) .add({ _id: '5', category: 'hope' }) assert.deepStrictEqual(addDoc.id, '5') assert.deepStrictEqual(doc.data, data[0]) const res = await transaction.rollback() assert(res.requestId) }) it('update', async () => { // const docRef = db.collection(collectionName).doc('1') const transaction = await db.startTransaction() // let doc = await transaction.get(docRef) let doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, data[0]) const date = new Date() // const updateResult = await transaction.update(docRef, { // category: 'Node.js', // date // }) const updateResult = await transaction .collection(collectionName) .doc('1') .update({ category: 'Node.js', date }) assert.strictEqual(updateResult.updated, 1) // doc = await transaction.get(docRef) doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, { ...data[0], date, category: 'Node.js' }) const res = await transaction.commit() assert(res.requestId) }) it('modifyAndReturnDoc', async () => { const transaction = await db.startTransaction() let modifyAndReturnRes = await transaction .collection(collectionName) .where({ category: 'Web' }) .updateAndReturn({ category: 'web' }) const res = await transaction.commit() assert(modifyAndReturnRes.doc.category === 'web') }) it('set doc', async () => { // const docRef = db.collection(collectionName).doc('1') const transaction = await db.startTransaction() // let doc = await transaction.get(docRef) let doc = await transaction .collection(collectionName) .doc('1') .get() const date = new Date() // const updateResult = await transaction.set(docRef, { // ...data[0], // date, // category: 'Node.js' // }) const newData = { ...data[0] } delete newData['_id'] const updateResult = await transaction .collection(collectionName) .doc('1') .set({ ...newData, date, category: 'Node.js' }) assert.strictEqual(updateResult.updated, 1) // doc = await transaction.get(docRef) doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, { ...data[0], date, category: 'Node.js' }) const res = await transaction.commit() assert(res.requestId) }) it('upsert doc', async () => { // const docRef = db.collection(collectionName).doc('114514') const transaction = await db.startTransaction() // let doc = await transaction.get(docRef) let doc = await transaction .collection(collectionName) .doc('114514') .get() assert.deepStrictEqual(doc.data, null) const date = new Date() const data = { category: 'Node.js', date } // const updateResult = await transaction.set(docRef, data) const updateResult = await transaction .collection(collectionName) .doc('114514') .set(data) assert.strictEqual(updateResult.upserted.length, 1) // doc = await transaction.get(docRef) doc = await transaction .collection(collectionName) .doc('114514') .get() assert.deepStrictEqual(doc.data, { _id: '114514', ...data }) const res = await transaction.rollback() assert(res.requestId) }) it('delete doc', async () => { // 前面测试用例更改过 _id = 1 的数据 // const docRef = db.collection(collectionName).doc('2') const transaction = await db.startTransaction() // let doc = await transaction.get(docRef) let doc = await transaction .collection(collectionName) .doc('2') .get() assert.deepStrictEqual(doc.data, data[1]) // const deleteResult = await transaction.delete(docRef) const deleteResult = await transaction .collection(collectionName) .doc('2') .delete() assert.strictEqual(deleteResult.deleted, 1) // doc = await transaction.get(docRef) doc = await transaction .collection(collectionName) .doc('2') .get() assert.deepStrictEqual(doc.data, null) await transaction.commit() }) it('runTransaction with customResult', async () => { // 验证自定义成功返回 const result = await db.runTransaction(async function(transaction) { const doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, data[0]) // assert(doc.data) return 'luke' }) assert(result === 'luke') }) // runTransaction rollback it('rollback within runTransaction', async () => { try { await db.runTransaction(async function(transaction) { const doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, data[0]) await transaction.rollback('luke') }) } catch (err) { assert(err === 'luke') } try { await db.runTransaction(async function(transaction) { const doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, data[0]) await transaction.rollback() }) } catch (err) { assert(err === undefined) } try { await db.runTransaction(async transaction => { const doc = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(doc.data, data[0]) // mock 事务冲突 throw { code: 'DATABASE_TRANSACTION_CONFLICT', message: '[ResourceUnavailable.TransactionConflict] Transaction is conflict, maybe resource operated by others. Please check your request, but if the problem persists, contact us.' } }) } catch (e) { assert(e.code === 'DATABASE_TRANSACTION_CONFLICT') } try { const docRef1 = db.collection(collectionName).doc('1') await db.runTransaction(async transaction => { const doc = await transaction .collection(collectionName) .doc('1') .get() await docRef1.set({ category: 'wwwwwwwwwwwwwwwww' }) const res = await transaction .collection(collectionName) .doc('1') .update({ category: 'transactiontransactiontransaction' }) }) } catch (e) { // 因为mongo write conflict时会自动rollback,兜底的rollback会报错 非conflict错误 assert(e.code) // assert(e.code === 'DATABASE_TRANSACTION_CONFLICT') } }) it('delete doc and abort', async () => { // 前面测试用例删除了 _id = 2 的数据 const docRef = db.collection(collectionName).doc('3') const transaction = await db.startTransaction() // let doc = await transaction.get(docRef) let doc = await transaction .collection(collectionName) .doc('3') .get() // const deleteResult = await transaction.delete(docRef) const deleteResult = await transaction .collection(collectionName) .doc('3') .delete() assert.strictEqual(deleteResult.deleted, 1) // doc = await transaction.get(docRef) doc = await transaction .collection(collectionName) .doc('3') .get() assert.deepStrictEqual(doc.data, null) await transaction.rollback() const res = await docRef.get() // const res = await transaction.collection(collectionName).doc('3').get() assert.deepStrictEqual(res.data[0], data[2]) }) it('事务提交后, 不能进行其它操作', async () => { // const docRef = db.collection(collectionName).doc('1') const transaction = await db.startTransaction() await transaction.commit() await assert.rejects( async () => { // await transaction.set(docRef, { // category: 'Node.js' // }) await transaction .collection(collectionName) .doc('1') .set({ category: 'Node.js' }) }, { code: 'DATABASE_TRANSACTION_FAIL', message: '[ResourceUnavailable.TransactionNotExist] Transaction does not exist on the server, transaction must be commit or abort in 30 seconds. Please check your request, but if the problem persists, contact us.' } ) }) it('冲突检测', async () => { // const docRef = db.collection(collectionName).doc('1') const transaction1 = await db.startTransaction(), transaction2 = await db.startTransaction() // 事务1先读取数据 // const doc = await transaction1.get(docRef) const doc = await transaction1 .collection(collectionName) .doc('1') .get() // 事务2更改之前事务1读取的数据,并且提交 // await transaction2.update(docRef, { // category: '冲突检测' // }) await transaction2 .collection(collectionName) .doc('1') .update({ category: '冲突检测' }) // 由于事务1读取的数据没有时效性,故报错 await assert.rejects( async () => { // await transaction1.update(docRef, { // category: doc.data.category + '冲突检测' // }) await transaction1 .collection(collectionName) .doc('1') .update({ category: doc.data.category + '冲突检测' }) }, { code: 'DATABASE_TRANSACTION_CONFLICT' } ) await transaction2.commit() }) it('读快照', async () => { const docRef = db.collection(collectionName).doc('1') // 启动事务 const transaction = await db.startTransaction() // 修改数据 // await transaction.update(docRef, { // category: 'update in transaction' // }) await transaction .collection(collectionName) .doc('1') .update({ category: 'update in transaction' }) const result = await docRef.get() // 事务读,读的是开始时刻的快照 // const doc_new = await transaction.get(docRef) const doc_new = await transaction .collection(collectionName) .doc('1') .get() assert.deepStrictEqual(result.data[0], data[0]) assert.deepStrictEqual(doc_new.data, { ...data[0], category: 'update in transaction' }) await transaction.rollback() }) it('读偏', async () => { const docRef1 = db.collection(collectionName).doc('1') const docRef2 = db.collection(collectionName).doc('2') // 启动事务 const transaction = await db.startTransaction() // 修改数据 // console.log(await transaction.get(docRef1)) await transaction .collection(collectionName) .doc('1') .get() await docRef1.set({ category: 'wwwwwwwwwwwwwwwww' }) await docRef2.set({ category: 'hhhhhhhhhhhhhhh' }) // 事务读,读的是开始时刻的快照 // const snapshot1 = await transaction.get(docRef1) // const snapshot2 = await transaction.get(docRef2) const snapshot1 = await transaction .collection(collectionName) .doc('1') .get() const snapshot2 = await transaction .collection(collectionName) .doc('2') .get() assert.deepStrictEqual(snapshot1.data, data[0]) assert.deepStrictEqual(snapshot2.data, data[1]) // 外部已经修改了数据,事务内修改应该失败 await assert.rejects( async () => { // await transaction.update(docRef1, { // category: 'transactiontransactiontransaction' // }) await transaction .collection(collectionName) .doc('1') .update({ category: 'transactiontransactiontransaction' }) }, { code: 'DATABASE_TRANSACTION_CONFLICT', message: '[ResourceUnavailable.TransactionConflict] Transaction is conflict, maybe resource operated by others. Please check your request, but if the problem persists, contact us.' } ) }) it('write skew', async () => { const docRef1 = db.collection(collectionName).doc('1') const docRef2 = db.collection(collectionName).doc('2') const transaction1 = await db.startTransaction() const transaction2 = await db.startTransaction() // 事务1:读1写2 // 事务2:读2写1 // const doc1 = await transaction1.get(docRef1) // const doc2 = await transaction2.get(docRef2) const doc1 = await transaction1 .collection(collectionName) .doc('1') .get() const doc2 = await transaction2 .collection(collectionName) .doc('2') .get() // await transaction1.set(docRef2, { // category: doc1.data.category + 'doc1' // }) // await transaction2.set(docRef1, { // category: doc2.data.category + 'doc2' // }) await transaction1 .collection(collectionName) .doc('2') .set({ category: doc1.data.category + 'doc1' }) await transaction2 .collection(collectionName) .doc('1') .set({ category: doc2.data.category + 'doc2' }) // 由于事务2读取的数据没有时效性,故报错 try { await transaction1.commit() await transaction2.commit() } catch (error) { console.log(error) } console.log(await docRef1.get()) console.log(await docRef2.get()) }) })