UNPKG

@itwin/core-backend

Version:
279 lines • 14.6 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 { restore as sinonRestore, spy as sinonSpy } from "sinon"; import { Guid, Id64 } 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 } from "../../core-backend"; import { _hubAccess, _releaseAllLocks } from "../../internal/Symbols"; 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.saveChanges(); 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(() => bc1.elements.insertElement(physicalProps), IModelError, "shared lock"); // insert requires shared lock on model await bc1Locks.acquireLocks({ shared: parentId }); // also acquires shared lock on model const newElId = bc1.elements.insertElement(physicalProps); assertExclusiveLocks(bc1Locks, newElId); childElJson.userLabel = "new user label"; assert.throws(() => bc1.elements.updateElement(childElJson), "exclusive lock"); await bc1Locks.acquireLocks({ exclusive: child1 }); bc1.elements.updateElement(childElJson); bc1.saveChanges(); bc1.elements.deleteElement(child1); // make sure delete now works bc1.abandonChanges(); assert.isTrue(bc1.locks.holdsSharedLock(IModel.repositoryModelId)); await bc1.pushChanges({ accessToken: accessToken1, description: "my changes" }); assert.isFalse(bc1.locks.holdsSharedLock(IModel.repositoryModelId)); assert.throws(() => bc2.elements.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() { const elem = bc.elements.getElement(elemId); elem.jsonProperties.testProp = Guid.createValue(); elem.update(); } 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(); write(); bc.saveChanges(); 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(); write(); bc.saveChanges(); await push(true); expectLocked(); await locks.releaseAllLocks(); expectUnlocked(); }); it("throws if briefcase has unpushed changes", async () => { expectUnlocked(); await bc.acquireSchemaLock(); expectLocked(); write(); bc.saveChanges(); 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(); write(); await expect(locks.releaseAllLocks()).to.eventually.be.rejectedWith("local changes"); expectLocked(); bc.abandonChanges(); await locks.releaseAllLocks(); expectUnlocked(); }); }); }); //# sourceMappingURL=ServerBasedLocks.test.js.map