Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fallback to use V1 checkpoints when a V2 checkpoint contains invalid dbGuid #1042

Merged
merged 7 commits into from
Mar 25, 2021
1 change: 1 addition & 0 deletions common/api/imodeljs-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ export class CheckpointManager {
// (undocumented)
static readonly onDownload: BeEvent<(job: DownloadJob) => void>;
static tryOpenLocalFile(request: DownloadRequest): SnapshotDb | undefined;
static validateCheckpointGuids(checkpoint: CheckpointProps, nativeDb: IModelJsNative.DgnDb): void;
// (undocumented)
static verifyCheckpoint(checkpoint: CheckpointProps, fileName: string): boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/imodeljs-backend",
"comment": "",
"type": "none"
}
],
"packageName": "@bentley/imodeljs-backend",
"email": "33036725+wgoehrig@users.noreply.github.com"
}
35 changes: 23 additions & 12 deletions core/backend/src/CheckpointManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "@bentley/bentleyjs-core";
import { CheckpointQuery, CheckpointV2Query } from "@bentley/imodelhub-client";
import { BriefcaseIdValue, DownloadBriefcaseStatus, IModelError } from "@bentley/imodeljs-common";
import { BlobDaemon, BlobDaemonCommandArg } from "@bentley/imodeljs-native";
import { BlobDaemon, BlobDaemonCommandArg, IModelJsNative } from "@bentley/imodeljs-native";
import { AuthorizedClientRequestContext, ProgressCallback, UserCancelledError } from "@bentley/itwin-client";
import { BackendLoggerCategory } from "./BackendLoggerCategory";
import { BriefcaseManager } from "./BriefcaseManager";
Expand Down Expand Up @@ -271,17 +271,7 @@ export class V1CheckpointManager {
if (dbChangeSetId !== checkpoint.mergedChangeSetId)
throw new IModelError(IModelStatus.ValidationFailed, "ParentChangeSetId of the checkpoint was not correctly setup", Logger.logError, loggerCategory, () => ({ ...traceInfo, ...checkpoint, dbChangeSetId }));

const dbGuid = Guid.normalize(nativeDb.getDbGuid());
if (dbGuid !== Guid.normalize(requestedCkp.iModelId)) {
Logger.logWarning(loggerCategory, "iModelId is not properly setup in the checkpoint. Updated checkpoint to the correct iModelId.", () => ({ ...traceInfo, ...checkpoint, dbGuid }));
nativeDb.setDbGuid(Guid.normalize(requestedCkp.iModelId));
// Required to reset the ChangeSetId because setDbGuid clears the value.
nativeDb.saveLocalValue("ParentChangeSetId", dbChangeSetId);
}

const dbContextGuid = Guid.normalize(nativeDb.queryProjectGuid());
if (dbContextGuid !== Guid.normalize(requestedCkp.contextId))
throw new IModelError(IModelStatus.ValidationFailed, "ContextId was not properly setup in the checkpoint", Logger.logError, loggerCategory, () => ({ ...traceInfo, dbContextGuid }));
CheckpointManager.validateCheckpointGuids(requestedCkp, nativeDb);

// Apply change sets if necessary
if (dbChangeSetId !== requestedCkp.changeSetId) {
Expand Down Expand Up @@ -344,6 +334,27 @@ export class CheckpointManager {
return V1CheckpointManager.downloadCheckpoint(request);
}

/** checks a file's dbGuid & contextId for consistency, and updates the dbGuid when possible */
public static validateCheckpointGuids(checkpoint: CheckpointProps, nativeDb: IModelJsNative.DgnDb) {
const traceInfo = { contextId: checkpoint.contextId, iModelId: checkpoint.iModelId };

const dbChangeSetId = nativeDb.getParentChangeSetId();
const dbGuid = Guid.normalize(nativeDb.getDbGuid());
if (dbGuid !== Guid.normalize(checkpoint.iModelId)) {
if (nativeDb.isReadonly())
throw new IModelError(IModelStatus.ValidationFailed, "iModelId is not properly setup in the checkpoint", Logger.logError, loggerCategory, () => ({ ...traceInfo, dbGuid }));

Logger.logWarning(loggerCategory, "iModelId is not properly setup in the checkpoint. Updated checkpoint to the correct iModelId.", () => ({ ...traceInfo, dbGuid }));
nativeDb.setDbGuid(Guid.normalize(checkpoint.iModelId));
// Required to reset the ChangeSetId because setDbGuid clears the value.
nativeDb.saveLocalValue("ParentChangeSetId", dbChangeSetId);
}

const dbContextGuid = Guid.normalize(nativeDb.queryProjectGuid());
if (dbContextGuid !== Guid.normalize(checkpoint.contextId))
throw new IModelError(IModelStatus.ValidationFailed, "ContextId was not properly setup in the checkpoint", Logger.logError, loggerCategory, () => ({ ...traceInfo, dbContextGuid }));
}

/** @returns true if the file is the checkpoint requested */
public static verifyCheckpoint(checkpoint: CheckpointProps, fileName: string): boolean {
if (!IModelJsFs.existsSync(fileName))
Expand Down
6 changes: 6 additions & 0 deletions core/backend/src/IModelDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2605,6 +2605,12 @@ export class SnapshotDb extends IModelDb {
const filePath = await V2CheckpointManager.attach(checkpoint);
const snapshot = SnapshotDb.openFile(filePath, { lazyBlockCache: true, key: CheckpointManager.getKey(checkpoint) });
snapshot._contextId = checkpoint.contextId;
try {
CheckpointManager.validateCheckpointGuids(checkpoint, snapshot.nativeDb);
} catch (err) {
snapshot.close();
throw err;
}
return snapshot;
}

Expand Down
13 changes: 11 additions & 2 deletions core/backend/src/rpc-impl/RpcBriefcaseUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BriefcaseProps, IModelConnectionProps, IModelError, IModelRpcOpenProps,
import { AuthorizedClientRequestContext } from "@bentley/itwin-client";
import { BackendLoggerCategory } from "../BackendLoggerCategory";
import { BriefcaseManager } from "../BriefcaseManager";
import { CheckpointProps, V1CheckpointManager } from "../CheckpointManager";
import { CheckpointManager, CheckpointProps, V1CheckpointManager } from "../CheckpointManager";
import { BriefcaseDb, IModelDb, SnapshotDb } from "../IModelDb";
import { IModelHost } from "../IModelHost";
import { IModelJsFs } from "../IModelJsFs";
Expand Down Expand Up @@ -127,12 +127,21 @@ export class RpcBriefcaseUtility {

// opening a checkpoint, readonly.
let db: SnapshotDb | void;
// first check if it's already open
db = SnapshotDb.tryFindByKey(CheckpointManager.getKey(checkpoint));
if (db) {
Logger.logTrace(loggerCategory, "Checkpoint was already open", () => ({ ...tokenProps }));
return db;
}

try {
// first try V2 checkpoint
// now try V2 checkpoint
db = await SnapshotDb.openCheckpointV2(checkpoint);
requestContext.enter();
Logger.logTrace(loggerCategory, "using V2 checkpoint briefcase", () => ({ ...tokenProps }));
} catch (e) {
Logger.logTrace(loggerCategory, "unable to open V2 checkpoint - falling back to V1 checkpoint", () => ({ ...tokenProps }));

// this isn't a v2 checkpoint. Set up a race between the specified timeout period and the open. Throw an RpcPendingResponse exception if the timeout happens first.
const request = {
checkpoint,
Expand Down
38 changes: 38 additions & 0 deletions core/backend/src/test/standalone/IModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,7 @@ describe("iModel", () => {
const iModelId = snapshot.getGuid();
const contextId = Guid.createValue();
const changeSetId = generateChangeSetId();
snapshot.nativeDb.saveProjectGuid(Guid.normalize(contextId));
snapshot.nativeDb.saveLocalValue("ParentChangeSetId", changeSetId); // even fake checkpoints need a changeSetId!
snapshot.saveChanges();
snapshot.close();
Expand Down Expand Up @@ -1755,6 +1756,43 @@ describe("iModel", () => {
checkpoint.close();
});

it("should throw for invalid dbGuid in v2 checkpoint", async () => {
const dbPath = IModelTestUtils.prepareOutputFile("IModel", "TestCheckpoint.bim");
const snapshot = SnapshotDb.createEmpty(dbPath, { rootSubject: { name: "test" } });
const iModelId = Guid.createValue(); // This is wrong - it should be `snapshot.getGuid()`!
const contextId = Guid.createValue();
const changeSetId = generateChangeSetId();
snapshot.nativeDb.saveProjectGuid(Guid.normalize(contextId));
snapshot.nativeDb.saveLocalValue("ParentChangeSetId", changeSetId);
snapshot.saveChanges();
snapshot.close();

// Mock iModelHub
const mockCheckpointV2: CheckpointV2 = {
wsgId: "INVALID",
ecId: "INVALID",
changeSetId,
containerAccessKeyAccount: "testAccount",
containerAccessKeyContainer: `imodelblocks-${iModelId}`,
containerAccessKeySAS: "testSAS",
containerAccessKeyDbName: "testDb",
};
const checkpointsV2Handler = IModelHost.iModelClient.checkpointsV2;
sinon.stub(checkpointsV2Handler, "get").callsFake(async () => [mockCheckpointV2]);
sinon.stub(IModelHost.iModelClient, "checkpointsV2").get(() => checkpointsV2Handler);

// Mock blockcacheVFS daemon
sinon.stub(BlobDaemon, "getDbFileName").callsFake(() => dbPath);
const daemonSuccessResult = { result: DbResult.BE_SQLITE_OK, errMsg: "" };
sinon.stub(BlobDaemon, "command").callsFake(async () => daemonSuccessResult);

process.env.BLOCKCACHE_DIR = "/foo/";
const ctx = ClientRequestContext.current as AuthorizedClientRequestContext;

const error = await getIModelError(SnapshotDb.openCheckpointV2({ requestContext: ctx, contextId, iModelId, changeSetId }));
expectIModelError(IModelStatus.ValidationFailed, error);
});

it("should throw when opening checkpoint without blockcache dir env", async () => {
process.env.BLOCKCACHE_DIR = "";
const ctx = ClientRequestContext.current as AuthorizedClientRequestContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe("IModelConnection (#integration)", () => {
assert.isNotNull(noVersionsIModel3);
});

it("should send a usage log everytime an iModel is opened", async () => {
it.skip("should send a usage log everytime an iModel is opened", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this stop working again?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the change made to lookup whether the checkpoint already exists locally short-circuits the path that would make the call to open. I plan to remove this test shortly anyway as we'll be removing the default workflow.

const projectId = await TestUtility.getTestProjectId("iModelJsIntegrationTest");
const iModelId = await TestUtility.getTestIModelId(projectId, "ReadOnlyTest");

Expand Down