-
Notifications
You must be signed in to change notification settings - Fork 1
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
Use Hashing #1027
Use Hashing #1027
Conversation
|
18e1723
to
5ba00b3
Compare
7a6888d
to
3cc236a
Compare
d4344ed
to
442a101
Compare
3cc236a
to
12436c0
Compare
442a101
to
f27ae38
Compare
12436c0
to
f6b7ed5
Compare
64d840d
to
6d39212
Compare
2d269f1
to
496773c
Compare
496773c
to
f30f499
Compare
f30f499
to
309da10
Compare
src/services/filesync/filesync.ts
Outdated
this.editGraphQL.query({ query: FILE_SYNC_HASHES_QUERY }).then((data) => ({ | ||
gadgetFilesVersion: BigInt(data.fileSyncHashes.filesVersion), | ||
gadgetHashes: data.fileSyncHashes.hashes, | ||
})), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If your local files version matches the files version in Gadget, do we generate the hashes twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this was just one graphQL query where we passed along the localFilesVersion
we could skip generating 50% of the hashes a lot of the time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, yeah we can turn this into a single query 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this here: Use fileSyncComparisonHashes
throw new TooManySyncAttemptsError(attempt); | ||
} | ||
|
||
const { filesVersionHashes, localHashes, gadgetHashes, gadgetFilesVersion } = await this._getHashes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I getting this right? If you start a sync and there is 1 file that has been modified we will hash the project server side 4 times?
- Attempt = 0, filesVersionHashes
- Attempt = 0, gadgetFilesVersionHashes
- Attempt = 1, filesVersionHashes
- Attempt = 1, gadgetFilesVersionHashes
Because we always iterate through this at least once if there has been any change either locally or upstream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's right.
src/services/filesync/filesync.ts
Outdated
choices: [Action.CANCEL, Action.MERGE, Action.RESET], | ||
}); | ||
localChanges = withoutUnnecessaryChanges({ changes: localChanges, existing: gadgetHashes }); | ||
gadgetChanges = withoutUnnecessaryChanges({ changes: gadgetChanges, existing: localHashes }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we want to remove unnecessary changes before generating the conflicts between them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The end result shouldn't change, but yes we could do this before checking for conflicts.
In fact, we could do this inside of getChanges
if we made it take existing
:
let localChanges = getChanges({ from: filesVersionHashes, to: localHashes, existing: gadetHashes, ignore: [".gadget/"] });
let gadgetChanges = getChanges({ from: filesVersionHashes, to: gadgetHashes, existing: localHashes });
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think moving it into getChanges
is a good call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that we wanted to drop unnecessary changes before asking the user about conflicts in case an unnecessary change was a conflict - but moving this into getConflicts
achieves the same thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this here: Use withoutUnnecessaryChanges inside getChanges
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! I am a little unfamiliar but I couldnt see any issues. The core sync loop is nice and terse and digestable, great job!
process.exit(0); | ||
} | ||
// recursively call this function until we're in sync | ||
return this.sync({ attempt: ++attempt }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The attempt limit is low enough that this won't ever stack overflow right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested this, but I can't think of a scenario where this would run into a stack overflow with 100 attempts let alone 10.
Although we could prevent a stack overflow from ever happening if we change the function to this:
async sync(): Promise<void> {
const maxAttempts = 10;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const hashes = await this._getHashes();
this.log.debug("syncing", hashes);
if (isEqualHashes(hashes.localHashes, hashes.gadgetHashes)) {
this.log.info("filesystem is in sync");
await this._save(hashes.gadgetFilesVersion);
return;
}
await this._sync(hashes);
}
throw new TooManySyncAttemptsError(maxAttempts);
}
private async _sync({
filesVersionHashes,
localHashes,
gadgetHashes,
gadgetFilesVersion,
}: {
filesVersionHashes: Hashes;
localHashes: Hashes;
gadgetHashes: Hashes;
gadgetFilesVersion: bigint;
}): Promise<void> {
let localChanges = getChanges({ from: filesVersionHashes, to: localHashes, existing: gadgetHashes, ignore: [".gadget/"] });
let gadgetChanges = getChanges({ from: filesVersionHashes, to: gadgetHashes, existing: localHashes });
if (localChanges.size === 0 && gadgetChanges.size === 0) {
// the local filesystem is missing .gadget/ files
gadgetChanges = getChanges({ from: localHashes, to: gadgetHashes });
assertAllGadgetFiles({ gadgetChanges });
}
const conflicts = getConflicts({ localChanges, gadgetChanges });
if (conflicts.size > 0) {
this.log.debug("conflicts detected", { conflicts });
printConflicts({
message: sprint`{bold You have conflicting changes with Gadget}`,
conflicts,
});
const preference = await select({
message: "How would you like to resolve these conflicts?",
choices: Object.values(ConflictPreference),
});
switch (preference) {
case ConflictPreference.CANCEL: {
process.exit(0);
break;
}
case ConflictPreference.LOCAL: {
gadgetChanges = withoutConflictingChanges({ conflicts, changes: gadgetChanges });
break;
}
case ConflictPreference.GADGET: {
localChanges = withoutConflictingChanges({ conflicts, changes: localChanges });
break;
}
}
}
assert(localChanges.size > 0 || gadgetChanges.size > 0, "there must be changes if hashes don't match");
if (gadgetChanges.size > 0) {
await this._getChangesFromGadget({ changes: gadgetChanges, filesVersion: gadgetFilesVersion });
}
if (localChanges.size > 0) {
await this._sendChangesToGadget({ changes: localChanges, expectedFilesVersion: gadgetFilesVersion });
}
}
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enh I think its fine as is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me.
Do we want to do any more in house testing before this becomes the default?
I feel confident enough to merge this into main, but I won't ship this until I've done another round of testing. I'll ship another experimental version and test that one while I wait for docs to finish. |
This improves how we detect and resolve file changes when
ggt sync
starts.I've split this PR into 2 commits. I recommend reading them individually so the changes are easier to follow.
Before this PR
To detect local changes, we compared the largest mtime on the local filesystem to the largest mtime we saw when
ggt sync
stopped. If the largest mtime on the local filesystem was greater than the largest mtime we last saw, we assumed local files had changed.To detect gadget (remote) changes, we compared the latest files version to the last files version we saw when
ggt sync
stopped. If the latest files version was greater than our local files version, we assumed gadget (remote) files had changed.If local changes were detected, we prompted the user to either reset/undo all their local changes or keep their local changes and merge them with any gadget (remote) changes.
After this PR
First, we fetch the hashes of the files version we last saw when
ggt sync
stopped.To detect local changes, we compare the files version hashes to the hashes on the local filesystem. If the hashes are different, we know local files have changed.
To detect gadget changes, we do the same thing and compare the files version hashes to the hashes of the latest files version. If the hashes are different, we know gadget files have changed.
If both the local filesystem and gadget changed the same file in conflicting ways, i.e.:
Then we prompt the user to either keep their local conflicting changes or keep gadgets conflicting changes. All non-conflicting changes are always merged.