Isdeleted fix
This commit is contained in:
parent
d23c1a8dbc
commit
67532f5d0c
5 changed files with 140 additions and 98 deletions
|
|
@ -8,7 +8,6 @@ export interface DocumentMetadata {
|
||||||
parentVersionId: VaultUpdateId;
|
parentVersionId: VaultUpdateId;
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
hash: string;
|
hash: string;
|
||||||
isDeleted: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredDocumentMetadata {
|
export interface StoredDocumentMetadata {
|
||||||
|
|
@ -16,7 +15,6 @@ export interface StoredDocumentMetadata {
|
||||||
parentVersionId: VaultUpdateId;
|
parentVersionId: VaultUpdateId;
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
hash: string;
|
hash: string;
|
||||||
isDeleted: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoredDatabase {
|
export interface StoredDatabase {
|
||||||
|
|
@ -26,10 +24,11 @@ export interface StoredDatabase {
|
||||||
|
|
||||||
export interface DocumentRecord {
|
export interface DocumentRecord {
|
||||||
identity: symbol;
|
identity: symbol;
|
||||||
parallelVersion: number;
|
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
metadata: DocumentMetadata | undefined;
|
metadata: DocumentMetadata | undefined;
|
||||||
|
isDeleted: boolean;
|
||||||
updates: Promise<void>[];
|
updates: Promise<void>[];
|
||||||
|
parallelVersion: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
|
|
@ -48,6 +47,7 @@ export class Database {
|
||||||
relativePath,
|
relativePath,
|
||||||
identity: Symbol(),
|
identity: Symbol(),
|
||||||
metadata,
|
metadata,
|
||||||
|
isDeleted: false,
|
||||||
updates: [],
|
updates: [],
|
||||||
parallelVersion: 0
|
parallelVersion: 0
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
@ -68,9 +68,7 @@ export class Database {
|
||||||
public get resolvedDocuments(): DocumentRecord[] {
|
public get resolvedDocuments(): DocumentRecord[] {
|
||||||
const paths = new Map<string, DocumentRecord[]>();
|
const paths = new Map<string, DocumentRecord[]>();
|
||||||
this.documents
|
this.documents
|
||||||
.filter(
|
.filter(({ metadata }) => metadata !== undefined)
|
||||||
({ metadata }) => metadata !== undefined && !metadata.isDeleted
|
|
||||||
)
|
|
||||||
.forEach((record) =>
|
.forEach((record) =>
|
||||||
paths.set(record.relativePath, [
|
paths.set(record.relativePath, [
|
||||||
record,
|
record,
|
||||||
|
|
@ -120,32 +118,42 @@ export class Database {
|
||||||
documentId,
|
documentId,
|
||||||
relativePath,
|
relativePath,
|
||||||
parentVersionId,
|
parentVersionId,
|
||||||
hash,
|
hash
|
||||||
isDeleted
|
|
||||||
}: {
|
}: {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
parentVersionId: VaultUpdateId;
|
parentVersionId: VaultUpdateId;
|
||||||
hash: string;
|
hash: string;
|
||||||
isDeleted: boolean;
|
|
||||||
},
|
},
|
||||||
identity?: symbol
|
identity?: symbol
|
||||||
): void {
|
): void {
|
||||||
let entry: DocumentRecord | undefined;
|
let entry: DocumentRecord | undefined;
|
||||||
if (identity !== undefined) {
|
if (identity !== undefined) {
|
||||||
entry = this.getDocumentByIdentity(identity);
|
const entry = this.getDocumentByIdentity(identity);
|
||||||
|
|
||||||
if (entry !== undefined) {
|
|
||||||
this.documents = this.documents.filter(
|
this.documents = this.documents.filter(
|
||||||
({ identity }) => identity !== entry!.identity
|
({ identity }) => identity !== entry.identity
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.documents.push({
|
||||||
|
...entry,
|
||||||
|
relativePath,
|
||||||
|
metadata: {
|
||||||
|
documentId,
|
||||||
|
parentVersionId,
|
||||||
|
hash
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
|
|
||||||
|
this.save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We find a match based on relative path and we find one with a different document id
|
||||||
|
// meaning that two documents occupy the same path in terms of in-flight requests so we
|
||||||
|
// need to create a new parallel version.
|
||||||
entry = this.getLatestDocumentByRelativePath(relativePath);
|
entry = this.getLatestDocumentByRelativePath(relativePath);
|
||||||
if (
|
if (entry && entry.metadata?.documentId !== documentId) {
|
||||||
entry?.metadata?.documentId !== undefined &&
|
|
||||||
entry.metadata.documentId !== documentId
|
|
||||||
) {
|
|
||||||
this.documents.push({
|
this.documents.push({
|
||||||
// `entry` might be undefined if the document is new
|
// `entry` might be undefined if the document is new
|
||||||
identity: Symbol(),
|
identity: Symbol(),
|
||||||
|
|
@ -153,29 +161,27 @@ export class Database {
|
||||||
metadata: {
|
metadata: {
|
||||||
documentId,
|
documentId,
|
||||||
parentVersionId,
|
parentVersionId,
|
||||||
hash,
|
hash
|
||||||
isDeleted
|
|
||||||
},
|
},
|
||||||
|
isDeleted: false,
|
||||||
updates: [],
|
updates: [],
|
||||||
parallelVersion: entry?.parallelVersion + 1
|
parallelVersion: entry.parallelVersion + 1
|
||||||
});
|
});
|
||||||
}
|
|
||||||
this.save();
|
this.save();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.documents.push({
|
this.documents.push({
|
||||||
// `entry` might be undefined if the document is new
|
identity: Symbol(),
|
||||||
identity: entry?.identity ?? Symbol(),
|
|
||||||
relativePath,
|
relativePath,
|
||||||
metadata: {
|
metadata: {
|
||||||
documentId,
|
documentId,
|
||||||
parentVersionId,
|
parentVersionId,
|
||||||
hash,
|
hash
|
||||||
isDeleted
|
|
||||||
},
|
},
|
||||||
updates: entry?.updates ?? [],
|
isDeleted: false,
|
||||||
parallelVersion: entry?.parallelVersion ?? 0
|
updates: [],
|
||||||
|
parallelVersion: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
@ -208,6 +214,7 @@ export class Database {
|
||||||
relativePath,
|
relativePath,
|
||||||
identity: Symbol(),
|
identity: Symbol(),
|
||||||
metadata: undefined,
|
metadata: undefined,
|
||||||
|
isDeleted: false,
|
||||||
updates: [],
|
updates: [],
|
||||||
parallelVersion: 0
|
parallelVersion: 0
|
||||||
};
|
};
|
||||||
|
|
@ -257,10 +264,9 @@ export class Database {
|
||||||
const oldDocument =
|
const oldDocument =
|
||||||
this.getLatestDocumentByRelativePath(oldRelativePath);
|
this.getLatestDocumentByRelativePath(oldRelativePath);
|
||||||
if (oldDocument === undefined) {
|
if (oldDocument === undefined) {
|
||||||
|
// We can try moving a non-existent document if it hasn't yet got created becasue it's
|
||||||
|
// the result of an offline event while this move happens online before.
|
||||||
return;
|
return;
|
||||||
throw new Error(
|
|
||||||
`Document to be moved not found: ${oldRelativePath}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.documents = this.documents.filter(
|
this.documents = this.documents.filter(
|
||||||
|
|
@ -274,6 +280,7 @@ export class Database {
|
||||||
identity: oldDocument.identity,
|
identity: oldDocument.identity,
|
||||||
metadata: oldDocument.metadata,
|
metadata: oldDocument.metadata,
|
||||||
relativePath: newRelativePath,
|
relativePath: newRelativePath,
|
||||||
|
isDeleted: oldDocument.isDeleted,
|
||||||
updates: oldDocument.updates,
|
updates: oldDocument.updates,
|
||||||
// We're in a strange state where the target of the move has just got deleted,
|
// We're in a strange state where the target of the move has just got deleted,
|
||||||
// however, its metadata might already have a bunch of updates queued up for
|
// however, its metadata might already have a bunch of updates queued up for
|
||||||
|
|
@ -285,6 +292,15 @@ export class Database {
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public delete(relativePath: RelativePath): void {
|
||||||
|
const candidate = this.getLatestDocumentByRelativePath(relativePath);
|
||||||
|
if (candidate === undefined) {
|
||||||
|
// it's fine because the document to be deleted might not have been created yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
candidate.isDeleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
private save(): void {
|
private save(): void {
|
||||||
this.logger.debug(JSON.stringify(this.documents, null, 2));
|
this.logger.debug(JSON.stringify(this.documents, null, 2));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,8 @@ export class Syncer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.database.delete(relativePath);
|
||||||
|
|
||||||
const [promise, resolve, reject] = createPromise();
|
const [promise, resolve, reject] = createPromise();
|
||||||
|
|
||||||
await this.database.getResolvedDocumentByRelativePath(
|
await this.database.getResolvedDocumentByRelativePath(
|
||||||
|
|
|
||||||
|
|
@ -34,52 +34,57 @@ export class UnrestrictedSyncer {
|
||||||
getLatestDocument: () => DocumentRecord,
|
getLatestDocument: () => DocumentRecord,
|
||||||
updateTime?: Date
|
updateTime?: Date
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { relativePath, metadata } = getLatestDocument();
|
let latestDocument = getLatestDocument();
|
||||||
|
|
||||||
return this.executeSync(
|
return this.executeSync(
|
||||||
[relativePath],
|
[latestDocument.relativePath],
|
||||||
SyncType.CREATE,
|
SyncType.CREATE,
|
||||||
SyncSource.PUSH,
|
SyncSource.PUSH,
|
||||||
async () => {
|
async () => {
|
||||||
if (metadata !== undefined && !metadata.isDeleted) {
|
if (
|
||||||
|
latestDocument.metadata !== undefined &&
|
||||||
|
!latestDocument.isDeleted
|
||||||
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document ${relativePath} already exists in the database, no need to create it again`
|
`Document ${latestDocument.relativePath} already exists in the database, no need to create it again`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentBytes = await this.operations.read(relativePath); // this can throw FileNotFoundError
|
const contentBytes = await this.operations.read(
|
||||||
|
latestDocument.relativePath
|
||||||
|
); // this can throw FileNotFoundError
|
||||||
const contentHash = hash(contentBytes);
|
const contentHash = hash(contentBytes);
|
||||||
|
|
||||||
updateTime ??=
|
updateTime ??= await this.operations.getModificationTime(
|
||||||
await this.operations.getModificationTime(relativePath); // this can throw FileNotFoundError
|
latestDocument.relativePath
|
||||||
|
); // this can throw FileNotFoundError
|
||||||
|
|
||||||
const response = await this.syncService.create({
|
const response = await this.syncService.create({
|
||||||
relativePath,
|
relativePath: latestDocument.relativePath,
|
||||||
contentBytes,
|
contentBytes,
|
||||||
createdDate: updateTime
|
createdDate: updateTime
|
||||||
});
|
});
|
||||||
|
|
||||||
const { relativePath: currentRelativePath, identity } =
|
latestDocument = getLatestDocument();
|
||||||
getLatestDocument();
|
|
||||||
|
|
||||||
this.history.addHistoryEntry({
|
this.history.addHistoryEntry({
|
||||||
status: SyncStatus.SUCCESS,
|
status: SyncStatus.SUCCESS,
|
||||||
source: SyncSource.PUSH,
|
source: SyncSource.PUSH,
|
||||||
relativePath,
|
relativePath: latestDocument.relativePath,
|
||||||
message: `Successfully uploaded locally created file`,
|
message: `Successfully uploaded locally created file`,
|
||||||
type: SyncType.CREATE
|
type: SyncType.CREATE
|
||||||
});
|
});
|
||||||
|
|
||||||
const newMetadata = {
|
const newMetadata = {
|
||||||
relativePath: currentRelativePath,
|
relativePath: latestDocument.relativePath,
|
||||||
documentId: response.documentId,
|
documentId: response.documentId,
|
||||||
parentVersionId: response.vaultUpdateId,
|
parentVersionId: response.vaultUpdateId,
|
||||||
hash: contentHash,
|
hash: contentHash,
|
||||||
isDeleted: false
|
isDeleted: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.database.setDocument(newMetadata, identity);
|
this.database.setDocument(newMetadata, latestDocument.identity);
|
||||||
|
|
||||||
this.tryIncrementVaultUpdateId(response.vaultUpdateId);
|
this.tryIncrementVaultUpdateId(response.vaultUpdateId);
|
||||||
}
|
}
|
||||||
|
|
@ -95,12 +100,9 @@ export class UnrestrictedSyncer {
|
||||||
SyncType.DELETE,
|
SyncType.DELETE,
|
||||||
SyncSource.PUSH,
|
SyncSource.PUSH,
|
||||||
async () => {
|
async () => {
|
||||||
if (
|
if (document.metadata === undefined) {
|
||||||
document.metadata === undefined ||
|
|
||||||
document.metadata.isDeleted
|
|
||||||
) {
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document '${document.relativePath}' has been already deleted, no need to delete it again`
|
`Document '${document.relativePath}' has been created yet so deleting it remotely can be skipped`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -128,8 +130,7 @@ export class UnrestrictedSyncer {
|
||||||
relativePath: document.relativePath,
|
relativePath: document.relativePath,
|
||||||
documentId: response.documentId,
|
documentId: response.documentId,
|
||||||
parentVersionId: response.vaultUpdateId,
|
parentVersionId: response.vaultUpdateId,
|
||||||
hash: EMPTY_HASH,
|
hash: EMPTY_HASH
|
||||||
isDeleted: true
|
|
||||||
},
|
},
|
||||||
document.identity
|
document.identity
|
||||||
);
|
);
|
||||||
|
|
@ -155,12 +156,9 @@ export class UnrestrictedSyncer {
|
||||||
SyncType.UPDATE,
|
SyncType.UPDATE,
|
||||||
SyncSource.PUSH,
|
SyncSource.PUSH,
|
||||||
async () => {
|
async () => {
|
||||||
if (
|
if (document.metadata === undefined || document.isDeleted) {
|
||||||
document.metadata === undefined ||
|
|
||||||
document.metadata.isDeleted
|
|
||||||
) {
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document ${document.relativePath} has been already deleted, no need to update it, ${JSON.stringify(document)}, ${document.metadata?.isDeleted}`
|
`Document ${document.relativePath} has been already deleted, no need to update it`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +190,22 @@ export class UnrestrictedSyncer {
|
||||||
createdDate: updateTime
|
createdDate: updateTime
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update relativePath which is the only property that can change while this is running (due to a move)
|
||||||
|
document = getLatestDocument();
|
||||||
|
|
||||||
|
if (document.isDeleted) {
|
||||||
|
this.logger.info(
|
||||||
|
`Document ${document.relativePath} has been deleted before we could finish updating it`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.metadata) {
|
||||||
|
throw new Error(
|
||||||
|
`Document ${document.relativePath} no longer has metadata after updating it`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
document.metadata.parentVersionId >= response.vaultUpdateId
|
document.metadata.parentVersionId >= response.vaultUpdateId
|
||||||
) {
|
) {
|
||||||
|
|
@ -209,9 +223,6 @@ export class UnrestrictedSyncer {
|
||||||
type: SyncType.UPDATE
|
type: SyncType.UPDATE
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update relativePath which is the only property that can change while this is running (due to a move)
|
|
||||||
document = getLatestDocument();
|
|
||||||
|
|
||||||
if (response.isDeleted) {
|
if (response.isDeleted) {
|
||||||
await this.operations.delete(document.relativePath);
|
await this.operations.delete(document.relativePath);
|
||||||
|
|
||||||
|
|
@ -224,13 +235,13 @@ export class UnrestrictedSyncer {
|
||||||
type: SyncType.DELETE
|
type: SyncType.DELETE
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.database.delete(document.relativePath);
|
||||||
this.database.setDocument(
|
this.database.setDocument(
|
||||||
{
|
{
|
||||||
documentId: response.documentId,
|
documentId: response.documentId,
|
||||||
relativePath: document.relativePath,
|
relativePath: document.relativePath,
|
||||||
parentVersionId: response.vaultUpdateId,
|
parentVersionId: response.vaultUpdateId,
|
||||||
hash: EMPTY_HASH,
|
hash: EMPTY_HASH
|
||||||
isDeleted: true
|
|
||||||
},
|
},
|
||||||
document.identity
|
document.identity
|
||||||
);
|
);
|
||||||
|
|
@ -267,6 +278,8 @@ export class UnrestrictedSyncer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document = getLatestDocument();
|
||||||
|
|
||||||
this.database.setDocument(
|
this.database.setDocument(
|
||||||
{
|
{
|
||||||
documentId: response.documentId,
|
documentId: response.documentId,
|
||||||
|
|
@ -275,8 +288,7 @@ export class UnrestrictedSyncer {
|
||||||
? response.relativePath
|
? response.relativePath
|
||||||
: document.relativePath,
|
: document.relativePath,
|
||||||
parentVersionId: response.vaultUpdateId,
|
parentVersionId: response.vaultUpdateId,
|
||||||
hash: contentHash,
|
hash: contentHash
|
||||||
isDeleted: response.isDeleted
|
|
||||||
},
|
},
|
||||||
document.identity
|
document.identity
|
||||||
);
|
);
|
||||||
|
|
@ -321,6 +333,8 @@ export class UnrestrictedSyncer {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
} else if (remoteVersion.isDeleted) {
|
} else if (remoteVersion.isDeleted) {
|
||||||
|
// Either the doc hasn't made it to us before and therefore we don't need to delete it,
|
||||||
|
// or we already have it, in which case the preceeding if will deal with it
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document ${remoteVersion.relativePath} has been deleted remotely, no need to sync`
|
`Document ${remoteVersion.relativePath} has been deleted remotely, no need to sync`
|
||||||
);
|
);
|
||||||
|
|
@ -332,6 +346,20 @@ export class UnrestrictedSyncer {
|
||||||
documentId: remoteVersion.documentId
|
documentId: remoteVersion.documentId
|
||||||
})
|
})
|
||||||
).contentBase64;
|
).contentBase64;
|
||||||
|
|
||||||
|
const latestDocument =
|
||||||
|
getLatestDocument?.() ??
|
||||||
|
this.database.getDocumentByDocumentId(
|
||||||
|
remoteVersion.documentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (latestDocument?.isDeleted) {
|
||||||
|
this.logger.info(
|
||||||
|
`Document ${remoteVersion.relativePath} has been deleted locally before we could finish updating it`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const contentBytes = deserialize(content);
|
const contentBytes = deserialize(content);
|
||||||
|
|
||||||
await this.operations.create(
|
await this.operations.create(
|
||||||
|
|
@ -345,16 +373,9 @@ export class UnrestrictedSyncer {
|
||||||
documentId: remoteVersion.documentId,
|
documentId: remoteVersion.documentId,
|
||||||
relativePath: remoteVersion.relativePath,
|
relativePath: remoteVersion.relativePath,
|
||||||
parentVersionId: remoteVersion.vaultUpdateId,
|
parentVersionId: remoteVersion.vaultUpdateId,
|
||||||
hash: hash(contentBytes),
|
hash: hash(contentBytes)
|
||||||
isDeleted: remoteVersion.isDeleted
|
|
||||||
},
|
},
|
||||||
getLatestDocument?.()?.identity ??
|
latestDocument?.identity
|
||||||
this.database.getDocumentByDocumentId(
|
|
||||||
remoteVersion.documentId
|
|
||||||
)?.identity ??
|
|
||||||
this.database.getLatestDocumentByRelativePath(
|
|
||||||
remoteVersion.relativePath
|
|
||||||
)?.identity
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.history.addHistoryEntry({
|
this.history.addHistoryEntry({
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ print_failed_log() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo "Monitoring $process_count processes"
|
||||||
|
|
||||||
# Monitor processes
|
# Monitor processes
|
||||||
while true; do
|
while true; do
|
||||||
if print_failed_log; then
|
if print_failed_log; then
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ async function runTests(): Promise<void> {
|
||||||
const iterations = [50, 200];
|
const iterations = [50, 200];
|
||||||
const doDeletes = [true, false];
|
const doDeletes = [true, false];
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
for (const agentCount of agentCounts) {
|
for (const agentCount of agentCounts) {
|
||||||
for (const concurrency of concurrencies) {
|
for (const concurrency of concurrencies) {
|
||||||
for (const jitter of jitterScaleInSeconds) {
|
for (const jitter of jitterScaleInSeconds) {
|
||||||
|
|
@ -108,7 +109,7 @@ async function runTests(): Promise<void> {
|
||||||
doDeletes: deleteFiles,
|
doDeletes: deleteFiles,
|
||||||
jitterScaleInSeconds: jitter
|
jitterScaleInSeconds: jitter
|
||||||
});
|
});
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue