UNPKG

@itwin/core-backend

Version:
873 lines • 73.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { match as sinonMatch, restore as sinonRestore, spy as sinonSpy, stub as sinonStub } from "sinon"; import { Guid, Id64, IModelStatus } from "@itwin/core-bentley"; import { Code, IModel, IModelError, LockState } from "@itwin/core-common"; import { BriefcaseManager } from "../../BriefcaseManager"; import { PhysicalObject } from "../../domains/GenericElements"; import { BriefcaseDb, SnapshotDb } from "../../IModelDb"; import { IModelHost } from "../../IModelHost"; import { ElementOwnsChildElements } from "../../NavigationRelationship"; import { HubMock } from "../../internal/HubMock"; import { ExtensiveTestScenario, IModelTestUtils } from "../IModelTestUtils"; import { KnownTestLocations } from "../KnownTestLocations"; import { ChannelControl, LockConflict } from "../../core-backend"; import { _hubAccess, _releaseAllLocks } from "../../internal/Symbols"; import { EditTxn, withEditTxn } from "../../EditTxn"; const expect = chai.expect; const assert = chai.assert; chai.use(chaiAsPromised); describe("Server-based locks", () => { const createVersion0 = async () => { const dbName = IModelTestUtils.prepareOutputFile("ServerBasedLocks", "ServerBasedLocks.bim"); const sourceDb = SnapshotDb.createEmpty(dbName, { rootSubject: { name: "server lock test" } }); assert.isFalse(sourceDb.locks.isServerBased); await ExtensiveTestScenario.prepareDb(sourceDb); await ExtensiveTestScenario.populateDb(sourceDb); sourceDb.close(); return dbName; }; let iModelId; let accessToken1; let accessToken2; let briefcase1Props; let briefcase2Props; afterEach(() => sinonRestore()); before(async () => { HubMock.startup("ServerBasedLocks", KnownTestLocations.outputDir); const iModelProps = { iModelName: "server locks test", iTwinId: HubMock.iTwinId, version0: await createVersion0(), }; iModelId = await HubMock.createNewIModel(iModelProps); const args = { iTwinId: iModelProps.iTwinId, iModelId }; briefcase1Props = await BriefcaseManager.downloadBriefcase({ accessToken: "test token", ...args }); briefcase2Props = await BriefcaseManager.downloadBriefcase({ accessToken: "test token2", ...args }); }); after(() => { HubMock.shutdown(); }); const assertSharedLocks = (locks, ids) => { for (const id of Id64.iterable(ids)) assert.isTrue(locks.holdsSharedLock(id)); }; const assertExclusiveLocks = (locks, ids) => { assertSharedLocks(locks, ids); for (const id of Id64.iterable(ids)) assert.isTrue(locks.holdsSharedLock(id)); }; const assertLockCounts = (locks, shared, exclusive) => { assert.equal(locks.getLockCount(LockState.Shared), shared); assert.equal(locks.getLockCount(LockState.Exclusive), exclusive); }; it("Acquiring locks", async () => { const lockSpy = sinonSpy(IModelHost[_hubAccess], "acquireLocks"); let bc1 = await BriefcaseDb.open({ fileName: briefcase1Props.fileName }); assert.isTrue(bc1.locks.isServerBased); bc1.channels.addAllowedChannel(ChannelControl.sharedChannelName); let bc2 = await BriefcaseDb.open({ fileName: briefcase2Props.fileName }); assert.isTrue(bc2.locks.isServerBased); let bc1Locks = bc1.locks; let bc2Locks = bc2.locks; const child1 = IModelTestUtils.queryByUserLabel(bc1, "ChildObject1A"); const child2 = IModelTestUtils.queryByUserLabel(bc1, "ChildObject1B"); const childEl = bc1.elements.getElement(child1); const parentId = childEl.parent.id; const modelId = childEl.model; const modelProps = bc1.elements.getElementProps(modelId); const subjectId = modelProps.parent.id; const sharedLockError = "shared lock is held"; const exclusiveLockError = "lock is already held"; await bc1.acquireSchemaLock(); assert.equal(lockSpy.callCount, 1); assert.isTrue(bc1.holdsSchemaLock); assertLockCounts(bc1Locks, 0, 1); assertExclusiveLocks(bc1Locks, [IModel.dictionaryId, child1, parentId]); // even though we only hold 1 lock (the "schema" lock), every element should say it's locked await bc1Locks.acquireLocks({ exclusive: child1 }); // attempting to acquire exclusive lock on any element when schema lock is held shouldn't need a server request assert.equal(lockSpy.callCount, 1); assert.isFalse(bc2.holdsSchemaLock); await expect(bc2.acquireSchemaLock()).rejectedWith(IModelError, exclusiveLockError, "acquire schema exclusive"); await expect(bc2Locks.acquireLocks({ shared: childEl.model })).rejectedWith(IModelError, exclusiveLockError); await bc1Locks[_releaseAllLocks](); await bc1Locks.acquireLocks({ exclusive: parentId, shared: parentId }); assertLockCounts(bc1Locks, 3, 1); assertSharedLocks(bc1Locks, [modelId, IModel.rootSubjectId]); assertExclusiveLocks(bc1Locks, [parentId, child1, child2]); // acquiring exclusive lock on parent implicitly holds exclusive lock on children await bc1Locks[_releaseAllLocks](); await bc1Locks.acquireLocks({ exclusive: modelId }); assertLockCounts(bc1Locks, 2, 1); assertSharedLocks(bc1Locks, [modelId, IModel.rootSubjectId]); assertExclusiveLocks(bc1Locks, [modelId, parentId, child1, child2]); // acquiring exclusive lock on model implicitly holds exclusive lock on members await bc1Locks[_releaseAllLocks](); lockSpy.resetHistory(); await bc1Locks.acquireLocks({ shared: child1 }); assert.equal(lockSpy.callCount, 1); assert.equal(lockSpy.getCall(0).args[1].size, 5); assertSharedLocks(bc1Locks, [child1, parentId, modelId, subjectId, IModel.rootSubjectId]); assertLockCounts(bc1Locks, 5, 0); await bc1Locks.acquireLocks({ exclusive: child1 }); // upgrade lock from shared to exclusive assert.equal(lockSpy.callCount, 2); assertLockCounts(bc1Locks, 4, 1); assertExclusiveLocks(bc1Locks, child1); assert.equal(lockSpy.callCount, 2); // should not need to call server on a lock already held await expect(bc2.acquireSchemaLock()).rejectedWith(IModelError, sharedLockError); assert.equal(lockSpy.callCount, 3); await expect(bc2Locks.acquireLocks({ exclusive: parentId })).rejectedWith(IModelError, sharedLockError); assert.equal(lockSpy.callCount, 4); await bc2Locks.acquireLocks({ shared: parentId }); assert.equal(lockSpy.callCount, 5); assertSharedLocks(bc2Locks, [parentId, IModel.rootSubjectId]); await bc2Locks.acquireLocks({ shared: IModel.dictionaryId }); assert.equal(lockSpy.callCount, 6); assertSharedLocks(bc2Locks, IModel.dictionaryId); assertLockCounts(bc1Locks, 4, 1); assertLockCounts(bc2Locks, 5, 0); const childElJson = childEl.toJSON(); // if we close and reopen the briefcase, the local locks database should still be intact bc1.close(); bc2.close(); bc1 = await BriefcaseDb.open({ fileName: briefcase1Props.fileName }); bc1.channels.addAllowedChannel(ChannelControl.sharedChannelName); bc2 = await BriefcaseDb.open({ fileName: briefcase2Props.fileName }); bc2.channels.addAllowedChannel(ChannelControl.sharedChannelName); bc1Locks = bc1.locks; bc2Locks = bc2.locks; assertLockCounts(bc1Locks, 4, 1); assertLockCounts(bc2Locks, 5, 0); assertSharedLocks(bc1Locks, [parentId, childEl.model, IModel.rootSubjectId]); assertSharedLocks(bc2Locks, [parentId, IModel.rootSubjectId, IModel.dictionaryId]); assertExclusiveLocks(bc1Locks, child1); assert.isFalse(bc2Locks.holdsExclusiveLock(child1)); await bc2Locks[_releaseAllLocks](); // release all locks from bc2 so we can test expected failures below assertLockCounts(bc2Locks, 0, 0); await expect(bc2Locks.acquireLocks({ exclusive: [IModel.dictionaryId, parentId] })).rejectedWith(IModelError, sharedLockError); assertLockCounts(bc2Locks, 0, 0); // exclusive lock is available on dictionary, but not on parent - should get neither await bc2Locks.acquireLocks({ exclusive: IModel.dictionaryId }); // now attempt to get only dictionary assertExclusiveLocks(bc2Locks, IModel.dictionaryId); // that should work assertSharedLocks(bc2Locks, IModel.rootSubjectId); // it should also acquire the shared lock on the rootSubject assertLockCounts(bc2Locks, 1, 1); await bc1Locks[_releaseAllLocks](); await bc2Locks[_releaseAllLocks](); lockSpy.resetHistory(); const physicalProps = { classFullName: PhysicalObject.classFullName, model: modelId, parent: new ElementOwnsChildElements(parentId), category: childElJson.category, code: Code.createEmpty(), }; assert.throws(() => withEditTxn(bc1, (txn) => txn.insertElement(physicalProps)), IModelError, "shared lock"); // insert requires shared lock on model await bc1Locks.acquireLocks({ shared: parentId }); // also acquires shared lock on model const newElId = withEditTxn(bc1, (txn) => txn.insertElement(physicalProps)); assertExclusiveLocks(bc1Locks, newElId); childElJson.userLabel = "new user label"; assert.throws(() => withEditTxn(bc1, (txn) => txn.updateElement(childElJson)), "exclusive lock"); await bc1Locks.acquireLocks({ exclusive: child1 }); withEditTxn(bc1, (txn) => txn.updateElement(childElJson)); { const txn = new EditTxn(bc1, "verify delete lock behavior"); txn.start(); txn.deleteElement(child1); // make sure delete now works txn.end("abandon"); } assert.isTrue(bc1.locks.holdsSharedLock(IModel.repositoryModelId)); await bc1.pushChanges({ accessToken: accessToken1, description: "my changes" }); assert.isFalse(bc1.locks.holdsSharedLock(IModel.repositoryModelId)); assert.throws(() => withEditTxn(bc2, (txn) => txn.deleteElement(child1)), "exclusive lock"); // bc2 can't delete because it doesn't hold lock await expect(bc2Locks.acquireLocks({ exclusive: child1 })).rejectedWith(IModelError, "pull is required"); // can't get lock since other briefcase changed it await bc2.pullChanges({ accessToken: accessToken2 }); await bc2Locks.acquireLocks({ exclusive: child1, shared: child1 }); const child2El = bc2.elements.getElement(child1); assert.equal(child2El.userLabel, childElJson.userLabel); await bc1.locks.releaseAllLocks(); await bc2.locks.releaseAllLocks(); bc1.close(); bc2.close(); }); describe("releaseAllLocks", () => { let bc; let locks; let elemId; beforeEach(async () => { bc = await BriefcaseDb.open({ fileName: briefcase1Props.fileName }); expect(bc.locks.isServerBased).to.be.true; locks = bc.locks; bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); elemId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); expect(elemId).not.to.equal("0"); }); afterEach(() => bc.close()); function expectLocked() { expect(locks.getLockCount(LockState.Exclusive)).least(1); expect(locks.getLockCount(LockState.Shared)).to.equal(0); } function expectUnlocked() { expect(locks.getLockCount(LockState.Exclusive)).to.equal(0); expect(locks.getLockCount(LockState.Shared)).to.equal(0); } function write(txn) { const elem = bc.elements.getElement(elemId); elem.jsonProperties.testProp = Guid.createValue(); elem.update(txn); } async function push(retainLocks) { return bc.pushChanges({ retainLocks, accessToken: "token", description: "changes" }); } it("releases all locks", async () => { expectUnlocked(); await bc.acquireSchemaLock(); expectLocked(); await locks.releaseAllLocks(); expectUnlocked(); }); it("is called when pushChanges is called with no local changes", async () => { await bc.acquireSchemaLock(); expectLocked(); await push(); expectUnlocked(); }); it("is called when pushing changes", async () => { await bc.acquireSchemaLock(); expectLocked(); withEditTxn(bc, (txn) => write(txn)); await push(); expectUnlocked(); }); it("is not called when pushChanges is called with no local changes if retainLocks is specified", async () => { await bc.acquireSchemaLock(); expectLocked(); await push(true); expectLocked(); await locks.releaseAllLocks(); expectUnlocked(); }); it("is not called when pushing changes if retainLocks is specified", async () => { await bc.acquireSchemaLock(); expectLocked(); withEditTxn(bc, (txn) => write(txn)); await push(true); expectLocked(); await locks.releaseAllLocks(); expectUnlocked(); }); it("throws if briefcase has unpushed changes", async () => { expectUnlocked(); await bc.acquireSchemaLock(); expectLocked(); withEditTxn(bc, (txn) => write(txn)); await expect(locks.releaseAllLocks()).to.eventually.be.rejectedWith("local changes"); await push(); expectUnlocked(); }); it("throws if briefcase has unsaved changes", async () => { expectUnlocked(); await bc.acquireSchemaLock(); const txn = new EditTxn(bc, "releaseAllLocks unsaved changes"); txn.start(); write(txn); await expect(locks.releaseAllLocks()).to.eventually.be.rejectedWith("local changes"); expectLocked(); txn.end("abandon"); await locks.releaseAllLocks(); expectUnlocked(); }); }); describe("abandonLocksForReversedTxn", () => { let bc; let bc2; let locks; beforeEach(async () => { bc = await BriefcaseDb.open({ fileName: briefcase1Props.fileName }); expect(bc.locks.isServerBased).to.be.true; locks = bc.locks; bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); }); afterEach(async () => { sinonRestore(); await locks[_releaseAllLocks](); bc.close(); if (bc2 !== undefined) { await bc2.locks.releaseAllLocks(); bc2.close(); bc2 = undefined; } }); it("abandons locks acquired in the current, unsaved txn", async () => { const lockSpy = sinonSpy(IModelHost[_hubAccess], "abandonLocks"); const childId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: childId }); expect(locks.holdsExclusiveLock(childId)).to.be.true; expect(locks.getLockCount(LockState.Exclusive)).to.equal(1); expect(locks.getLockCount(LockState.Shared)).to.be.greaterThan(0); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(lockSpy.callCount).to.equal(1); const releasedLocks = lockSpy.getCall(0).args[1]; expect(releasedLocks.size).to.be.greaterThan(0); for (const state of releasedLocks.values()) expect(state).to.equal(LockState.None); expect(locks.holdsExclusiveLock(childId)).to.be.false; expect(locks.getLockCount(LockState.Exclusive)).to.equal(0); expect(locks.getLockCount(LockState.Shared)).to.equal(0); }); it("abandons locks acquired in the most recent saved txn", async () => { const lockSpy = sinonSpy(IModelHost[_hubAccess], "abandonLocks"); const childId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await withEditTxn(bc, async (txn) => { await locks.acquireLocks({ exclusive: childId }); const element = bc.elements.getElement(childId); element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); expect(locks.holdsExclusiveLock(childId)).to.be.true; expect(locks.getLockCount(LockState.Exclusive)).to.equal(1); expect(locks.getLockCount(LockState.Shared)).to.be.greaterThan(0); bc.txns.reverseSingleTxn(); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(lockSpy.callCount).to.equal(1); const releasedLocks = lockSpy.getCall(0).args[1]; expect(releasedLocks.size).to.be.greaterThan(0); for (const state of releasedLocks.values()) expect(state).to.equal(LockState.None); expect(locks.holdsExclusiveLock(childId)).to.be.false; expect(locks.getLockCount(LockState.Exclusive)).to.equal(0); expect(locks.getLockCount(LockState.Shared)).to.equal(0); }); it("does not release locks acquired by a different txn", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txn1 = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId1 }); await withEditTxn(bc, async (txn) => { const element = bc.elements.getElement(elementId1); element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); const txn2 = bc.txns.getCurrentTxnId(); expect(txn2).not.to.equal(txn1); await withEditTxn(bc, async (txn) => { await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; bc.txns.reverseTxns(1); expect(await locks.abandonLocksForReversedTxn(txn2)).to.be.true; expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; }); it("invalidates discovered locks", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const ownerModeltId = bc.elements.getElementProps(elementId).model; const ownersOwnerModelId = bc.elements.getElementProps(ownerModeltId).model; await locks.acquireLocks({ exclusive: ownersOwnerModelId }); expect(locks.holdsExclusiveLock(ownerModeltId)).to.be.true; expect(locks.holdsExclusiveLock(elementId)).to.be.true; const txnId = bc.txns.getCurrentTxnId(); bc.txns.reverseTxns(1); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(locks.holdsExclusiveLock(ownerModeltId)).to.be.false; expect(locks.holdsExclusiveLock(elementId)).to.be.false; }); it("restores lock to its previous state if it was upgraded by the reversed txn", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const firstTxnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId1, shared: elementId2 }); expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; // We must actually edit something in order to start a new Txn. await withEditTxn(bc, async (txn) => { const element = bc.elements.getElement(elementId1); element.setUserProperties("foo", { test: true }); element.update(txn); }); const secondTxnId = bc.txns.getCurrentTxnId(); expect(firstTxnId).not.to.equal(secondTxnId); await locks.acquireLocks({ exclusive: elementId2 }); // upgrade lock from shared to exclusive expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; expect(await locks.abandonLocksForReversedTxn(secondTxnId)).to.be.true; expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; }); it("does not update changesetid when releasing locks", async () => { bc2 = await BriefcaseDb.open({ fileName: briefcase2Props.fileName }); expect(bc2.locks.isServerBased).to.be.true; bc2.channels.addAllowedChannel(ChannelControl.sharedChannelName); // Make sure both briefcase initially have all changes. await bc.pullChanges({ accessToken: "token" }); await bc2.pullChanges({ accessToken: "token" }); const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); // Edit an element in the first briefcase and push the change. This will // create a new changeset. await locks.acquireLocks({ exclusive: elementId1 }); await withEditTxn(bc, async (txn) => { const element = bc.elements.getElement(elementId1); element.setUserProperties("foo", { test: true }); element.update(txn); }); await bc.pushChanges({ accessToken: "token", description: "changes" }); const secondTxnId = bc.txns.getCurrentTxnId(); // In that same briefcase, lock and edit a different element, but then reverse // the change and release the lock. await bc.locks.acquireLocks({ exclusive: elementId2 }); await withEditTxn(bc, async (txn) => { const element2 = bc.elements.getElement(elementId2); element2.setUserProperties("bar", { test: true }); element2.update(txn); }); bc.txns.reverseTxns(1); expect(await locks.abandonLocksForReversedTxn(secondTxnId)).to.be.true; // Now, in a separate briefcase, which has not yet pulled the changes pushed by the first, // attempt to lock the same element whose lock was just released. This should work because // the lock release by releaseLocksForReversedTxn should not have updated the changeset // associated with that lock. await bc2.locks.acquireLocks({ exclusive: elementId2 }); }); it("does not release on the server an implicit lock held for a new element", async () => { const childId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const childElement = bc.elements.getElement(childId); const parentId = childElement.parent.id; const modelId = childElement.model; const physicalProps = { classFullName: PhysicalObject.classFullName, model: modelId, parent: new ElementOwnsChildElements(parentId), category: childElement.category, code: Code.createEmpty(), }; const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ shared: [modelId, parentId] }); const newElementId = await withEditTxn(bc, async (txn) => { return txn.insertElement(physicalProps); }); expect(locks.holdsExclusiveLock(newElementId)).to.be.true; const lockSpy = sinonSpy(IModelHost[_hubAccess], "acquireLocks"); bc.txns.reverseTxns(1); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(lockSpy.calledWithMatch(sinonMatch.any, sinonMatch((lockMap) => lockMap.has(newElementId)))).to.be.false; }); it("releases locks for later txns, too", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txn1 = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element1.setUserProperties("foo", Guid.createValue()); element1.update(txn); }); const txn2 = bc.txns.getCurrentTxnId(); expect(txn2).not.to.equal(txn1); await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; // Reverse both txns, then abandon locks starting from the earlier one. // This will release locks for the later one, too. bc.txns.reverseTxns(2); expect(await locks.abandonLocksForReversedTxn(txn1)).to.be.true; expect(locks.holdsExclusiveLock(elementId1)).to.be.false; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; }); it("throws if asked to abandon locks for a txn that has not been reversed", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); // The txn has not been reversed, so abandonLocksForReversedTxn should throw. await expect(locks.abandonLocksForReversedTxn(txnId)).to.eventually.be.rejectedWith("has not been reversed"); expect(locks.holdsExclusiveLock(elementId)).to.be.true; }); it("throws if asked to abandon locks for the current txn and there are unsaved changes", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); // The current txn has unsaved changes, so abandonLocksForReversedTxn should throw. const txnId = bc.txns.getCurrentTxnId(); await expect(locks.abandonLocksForReversedTxn(txnId)).to.eventually.be.rejectedWith("unsaved changes"); expect(locks.holdsExclusiveLock(elementId)).to.be.true; }); }); it("throws if asked to abandon locks for a nonexistent txn", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); bc.txns.reverseSingleTxn(); // Use an ID that is not a valid txn. await expect(locks.abandonLocksForReversedTxn("0xffffffffffff")).to.eventually.be.rejectedWith("does not exist"); // The lock should still be held. expect(locks.holdsExclusiveLock(elementId)).to.be.true; }); it("returns false when called a second time for an already-abandoned txn", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); bc.txns.reverseSingleTxn(); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(locks.holdsExclusiveLock(elementId)).to.be.false; // Calling again should be a no-op and return false because the locks are already abandoned. expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.false; expect(locks.holdsExclusiveLock(elementId)).to.be.false; }); }); describe("acquireLocksForReinstatingTxn", () => { let bc; let bc2; let locks; beforeEach(async () => { bc = await BriefcaseDb.open({ fileName: briefcase1Props.fileName }); expect(bc.locks.isServerBased).to.be.true; locks = bc.locks; bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); }); afterEach(async () => { await locks[_releaseAllLocks](); bc.close(); if (bc2 !== undefined) { await bc2.locks[_releaseAllLocks](); bc2.close(); bc2 = undefined; } }); it("reacquires locks for reinstating a txn", async () => { const childId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: childId }); expect(locks.holdsExclusiveLock(childId)).to.be.true; // We must actually edit something in order to start a new Txn. const element = bc.elements.getElement(childId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", { test: true }); element.update(txn); }); bc.txns.reverseTxns(1); await locks.abandonLocksForReversedTxn(txnId); expect(locks.holdsExclusiveLock(childId)).to.be.false; expect(await locks.acquireLocksForReinstatingTxn(txnId)).to.be.true; bc.txns.reinstateTxn(); expect(locks.holdsExclusiveLock(childId)).to.be.true; }); it("reupgrades a shared lock to exclusive", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const firstTxnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId1, shared: elementId2 }); expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; // We must actually edit something in order to start a new Txn. const element = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", { test: true }); element.update(txn); }); const secondTxnId = bc.txns.getCurrentTxnId(); expect(firstTxnId).not.to.equal(secondTxnId); await locks.acquireLocks({ exclusive: elementId2 }); // upgrade lock from shared to exclusive const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", { test: true }); element2.update(txn); }); expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; bc.txns.reverseSingleTxn(); await locks.abandonLocksForReversedTxn(secondTxnId); expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; expect(await locks.acquireLocksForReinstatingTxn(secondTxnId)).to.be.true; bc.txns.reinstateTxn(); expect(locks.holdsSharedLock(elementId2)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; }); it("does not acquire on the server an implicit lock originally held for a new element", async () => { const childId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const childElement = bc.elements.getElement(childId); const parentId = childElement.parent.id; const modelId = childElement.model; const physicalProps = { classFullName: PhysicalObject.classFullName, model: modelId, parent: new ElementOwnsChildElements(parentId), category: childElement.category, code: Code.createEmpty(), }; await locks.acquireLocks({ shared: [modelId, parentId] }); const txnId = bc.txns.getCurrentTxnId(); const newElementId = await withEditTxn(bc, async (txn) => { return txn.insertElement(physicalProps); }); bc.txns.reverseTxns(1); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(await locks.acquireLocksForReinstatingTxn(txnId)).to.be.true; bc.txns.reinstateTxn(); expect(locks.holdsExclusiveLock(newElementId)).to.be.true; }); it("acquires locks for earlier txns, too", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txn1 = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element1.setUserProperties("foo", Guid.createValue()); element1.update(txn); }); const txn2 = bc.txns.getCurrentTxnId(); expect(txn2).not.to.equal(txn1); await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; // Reverse both txns and abandon locks starting from the earlier one. bc.txns.reverseTxns(2); expect(await locks.abandonLocksForReversedTxn(txn1)).to.be.true; expect(locks.holdsExclusiveLock(elementId1)).to.be.false; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; // Acquire locks for the later txn. This should also acquire locks for the earlier one. expect(await locks.acquireLocksForReinstatingTxn(txn2)).to.be.true; expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; }); it("cannot acquire locks for a reversed txn after new changes are made", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txn1 = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element1.setUserProperties("foo", Guid.createValue()); element1.update(txn); }); bc.txns.reverseTxns(1); expect(await locks.abandonLocksForReversedTxn(txn1)).to.be.true; // Make other changes, making the reversed txn inaccessible. const txn2 = bc.txns.getCurrentTxnId(); expect(txn1).to.equal(txn2); await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); // Attempting to reinstate the txn should not acquire any locks. expect(await locks.acquireLocksForReinstatingTxn(txn1)).to.be.false; // We should still not have a lock on elementId1, because txn1 is now a different txn after the original // was reversed and we started making further changes under the reused txn id. expect(locks.holdsExclusiveLock(elementId1)).to.be.false; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; }); it("throws if another briefcase holds a conflicting lock", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); // Reverse and abandon the lock so it's released on the server. bc.txns.reverseSingleTxn(); expect(await locks.abandonLocksForReversedTxn(txnId)).to.be.true; expect(locks.holdsExclusiveLock(elementId)).to.be.false; // Have a second briefcase acquire the same lock. bc2 = await BriefcaseDb.open({ fileName: briefcase2Props.fileName }); bc2.channels.addAllowedChannel(ChannelControl.sharedChannelName); await bc2.pullChanges(); await bc2.locks.acquireLocks({ exclusive: elementId }); // Attempting to reacquire the lock for reinstatement should fail because bc2 holds it. await expect(locks.acquireLocksForReinstatingTxn(txnId)).to.eventually.be.rejectedWith("lock is already held"); expect(locks.holdsExclusiveLock(elementId)).to.be.false; }); it("is a no-op when locks were never abandoned", async () => { const childId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const txnId = bc.txns.getCurrentTxnId(); await locks.acquireLocks({ exclusive: childId }); const element = bc.elements.getElement(childId); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); // Reverse the txn but do NOT abandon locks. bc.txns.reverseSingleTxn(); expect(locks.holdsExclusiveLock(childId)).to.be.true; // acquireLocksForReinstatingTxn should be a harmless no-op since locks were never abandoned. expect(await locks.acquireLocksForReinstatingTxn(txnId)).to.be.false; expect(locks.holdsExclusiveLock(childId)).to.be.true; }); it("throws for an invalid txnId", async () => { await expect(locks.acquireLocksForReinstatingTxn("0xffffffffffff")).to.eventually.be.rejectedWith("does not exist"); }); }); describe("TxnManager integration", () => { let bc; let locks; beforeEach(async () => { bc = await BriefcaseDb.open({ fileName: briefcase1Props.fileName }); expect(bc.locks.isServerBased).to.be.true; locks = bc.locks; bc.channels.addAllowedChannel(ChannelControl.sharedChannelName); }); afterEach(async () => { await locks[_releaseAllLocks](); bc.close(); }); describe("reverseTxnsAsync", () => { it("reverses a single txn and abandons its locks", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); const originalProps = element.getUserProperties("foo"); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); expect(locks.holdsExclusiveLock(elementId)).to.be.true; await bc.txns.reverseTxnsAsync(1); expect(locks.holdsExclusiveLock(elementId)).to.be.false; // Verify the element change was actually reversed. const revertedElement = bc.elements.getElement(elementId); expect(revertedElement.getUserProperties("foo")).to.deep.equal(originalProps); }); it("reverses multiple txns and abandons all their locks", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const orig1 = bc.elements.getElement(elementId1).getUserProperties("foo"); const orig2 = bc.elements.getElement(elementId2).getUserProperties("bar"); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element1.setUserProperties("foo", Guid.createValue()); element1.update(txn); }); await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; await bc.txns.reverseTxnsAsync(2); expect(locks.holdsExclusiveLock(elementId1)).to.be.false; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; // Verify the element changes were actually reversed. expect(bc.elements.getElement(elementId1).getUserProperties("foo")).to.deep.equal(orig1); expect(bc.elements.getElement(elementId2).getUserProperties("bar")).to.deep.equal(orig2); }); it("when an earlier txn is reversed using reverseTxns, and a later one with reverseTxnsAsync, the latter call abandons the earlier locks, too", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element1.setUserProperties("foo", Guid.createValue()); element1.update(txn); }); await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; // Reverse the later txn using reverseTxns (no lock abandonment). bc.txns.reverseTxns(1); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; // Reverse the earlier txn using reverseTxnsAsync. // This should also abandon locks for the already-reversed later txn. await bc.txns.reverseTxnsAsync(1); expect(locks.holdsExclusiveLock(elementId1)).to.be.false; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; }); }); describe("reverseSingleTxnAsync", () => { it("reverses a single txn and abandons its locks", async () => { const elementId = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); await locks.acquireLocks({ exclusive: elementId }); const element = bc.elements.getElement(elementId); const originalProps = element.getUserProperties("foo"); await withEditTxn(bc, async (txn) => { element.setUserProperties("foo", Guid.createValue()); element.update(txn); }); expect(locks.holdsExclusiveLock(elementId)).to.be.true; await bc.txns.reverseSingleTxnAsync(); expect(locks.holdsExclusiveLock(elementId)).to.be.false; // Verify the element change was actually reversed. const revertedElement = bc.elements.getElement(elementId); expect(revertedElement.getUserProperties("foo")).to.deep.equal(originalProps); }); }); describe("reverseAllTxnsAsync", () => { it("reverses all txns and abandons all their locks", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const orig1 = bc.elements.getElement(elementId1).getUserProperties("foo"); const orig2 = bc.elements.getElement(elementId2).getUserProperties("bar"); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditTxn(bc, async (txn) => { element1.setUserProperties("foo", Guid.createValue()); element1.update(txn); }); await locks.acquireLocks({ exclusive: elementId2 }); const element2 = bc.elements.getElement(elementId2); await withEditTxn(bc, async (txn) => { element2.setUserProperties("bar", Guid.createValue()); element2.update(txn); }); expect(locks.holdsExclusiveLock(elementId1)).to.be.true; expect(locks.holdsExclusiveLock(elementId2)).to.be.true; await bc.txns.reverseAllTxnsAsync(); expect(locks.holdsExclusiveLock(elementId1)).to.be.false; expect(locks.holdsExclusiveLock(elementId2)).to.be.false; // Verify the element changes were actually reversed. expect(bc.elements.getElement(elementId1).getUserProperties("foo")).to.deep.equal(orig1); expect(bc.elements.getElement(elementId2).getUserProperties("bar")).to.deep.equal(orig2); }); }); describe("reverseToTxnAsync", () => { it("reverses to a specific txn and abandons locks for all reversed txns", async () => { const elementId1 = IModelTestUtils.queryByUserLabel(bc, "PhysicalObject2"); const elementId2 = IModelTestUtils.queryByUserLabel(bc, "ChildObject1B"); const origProps2 = bc.elements.getElement(elementId2).getUserProperties("bar"); await locks.acquireLocks({ exclusive: elementId1 }); const element1 = bc.elements.getElement(elementId1); await withEditT