@itwin/core-backend
Version:
iTwin.js backend components
279 lines • 14.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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