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

feat!: cross-save resistant configFile #959

Merged
merged 80 commits into from
Nov 14, 2023
Merged

feat!: cross-save resistant configFile #959

merged 80 commits into from
Nov 14, 2023

Conversation

mshanemc
Copy link
Contributor

@mshanemc mshanemc commented Oct 13, 2023

requires salesforcecli/plugin-data#721

Primary: make configFile save from cross-saving

  • every property in a file is now a Last-Write-Wins Register on a Conflict Free Replicated Data Type, and BaseConfigStore now holds the CRDT
    • they record timestamps, and the more recent timestamp wins
  • when a ConfigFile is read from fs, the fs modstamp is used as the timestamp for every property in the CRDT
  • deletes (removed properties) are handled with a special tombstone in their state
  • lockfiles (same pattern uses to fix the alias cross-save problem) are used in the write/writeSync methods

BREAKING CHANGE: AuthInfo.getFields now returns a read-only object. Use AuthInfo.update to change values in the fields.

BREAKING CHANGE: setContents method is no longer available in the ConfigFile stack.
BREAKING CHANGE:awaitEach is removed from ConfigFile stack
BREAKING CHANGE: write(sync) method no longer accepts a param. Use other methods (set, unset) to make modifications, then call write()/writeSync() to do the write.
BREAKING CHANGE: the use of lodash-style get/set (ex: set('foo.bar.baz[0]', 3) no longer works.
BREAKING CHANGE: You can no longer override the setMethod and getMethod when extending classes built on ConfigFile. Technically you could override get/set, but DON'T!
BREAKING CHANGE: everything related to tokens/tokenConfig is gone.

Other changes

  • BREAKING CHANGE: node18+ only, compiles to es2022
  • move uniqid to a shared function, outside of testSetup

Remove previously deprecated stuff since this is a major release

  • BREAKING CHANGE: removed sfdc.isInternalUrl. Use new SfdcUrl(url).isInternalUrl()
  • BREAKING CHANGE: removed sfdc.findUppercaseKeys. There is no replacement.
  • BREAKING CHANGE: removed SchemaPrinter. There is no replacement.

also ttypescript => ts-patch @W-14388558@

TODO:

  • make sure this publishes a major version
  • setContents, especially with sfdxConfig.merge
  • plugin-data adjustments
  • what to do about getMethod/setMethod
  • can we do without the CRDT peer/id stuff?
  • local NUTs for massive parallelization of stuff hitting the same file
  • migration docs
  • check command perf in plugin-settings

What issues does this PR fix or reference?

forcedotcom/cli#2423
forcedotcom/cli#2528
@W-14085765@

Release notes (CLI)

Sometimes the CLI would try to save 2 copies of the same file concurrently, resulting in either an invalid file (busted JSON) or one of the "savers" losing their changes. We've redesigned how files are saved to try to be safer about concurrency.

QA highlights

The code for sfdxConfig interop changed quite a bit. Make sure it's still reading/writing that file correctly.
Compare perf in things like plugin-settings and plugin-auth where we're writing to these files. the lock stuff could introduce some extra latency.

Check cases (ex: no file exists)

Things outside of core that use ConfigFile to test with

  • deployCache and manifestCache in PDR
  • scratch and sandbox caches in plugin-org
  • something in plugin-data for supporting --use-most-recent
  • remoteSourceTrackingService in STL
  • most of plugin-settings (config and alias)

@mshanemc mshanemc changed the title chore: bump jsforce deps feat!: cross-save resistant configFile Oct 13, 2023
@mshanemc mshanemc requested a review from shetzel October 27, 2023 22:09
MIGRATING_V5-V6.md Outdated Show resolved Hide resolved

if (value == null) {
delete content[propertyName];
if (value == null || value === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since value == null uses the double equals, adding || value === undefined is redundant.

src/config/config.ts Outdated Show resolved Hide resolved
await fs.promises.mkdir(pathDirname(path), { recursive: true });
await fs.promises.writeFile(path, JSON.stringify(translated, null, 2));
} catch (error) {
/* Do nothing */
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you 100% positive that swallowing this error will not cause future headaches? Should the error be logged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

src/config/config.ts Show resolved Hide resolved

// unlock the file
if (typeof unlockFn !== 'undefined') {
await unlockFn();
Copy link
Contributor

Choose a reason for hiding this comment

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

should unlocking be done in a finally where the lock occurs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I put the writeFile in a try/finally I think that takes care of the only other place it could fail. See change.

[Property in keyof P]: LWWRegister<P[Property] | typeof SYMBOL_FOR_DELETED>['state'];
};

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

sfdx-core has generated docs from code so I think it should be added here.

@mdonnalley
Copy link
Contributor

QA

Using https://github.com/mdonnalley/crdt-config-test (and yarn link @salesforce/core)

single process

bin/dev.js crdt --operation set --key foo --value bar
🟢 Writes new value to config

bin/dev.js crdt --operation update --key foo --value baz
🟢 Updates value in config

bin/dev.js crdt --operation unset --key foo
🟢 Removes value from config

two processes

🟢 process 1 sets new key last

bin/dev.js crdt --operation set --key foo --value process1 --pre-change-wait 1000 & bin/dev.js crdt --operation set --key foo --value process2 & wait && jq .foo /Users/mdonnalley/.sfdx/crdt.json
[1] 25373
[2] 25374
[25374] Reading config file after 0ms...
[25373] Reading config file after 0ms...
[25374] config file: /Users/mdonnalley/.sfdx/crdt.json
[25373] config file: /Users/mdonnalley/.sfdx/crdt.json
[25374] Changing config file after 0ms...
[25374] SET: foo=process2
[25374] Writing config file after 0ms...
[25374] Done!
[2]  + 25374 done       bin/dev.js crdt --operation set --key foo --value process2
[25373] Changing config file after 1000ms...
[25373] SET: foo=process1
[25373] Writing config file after 0ms...
[25373] Done!
[1]  + 25373 done       bin/dev.js crdt --operation set --key foo --value process1 --pre-change-wait
"process1"

🟢 process 2 sets new key last

bin/dev.js crdt --operation set --key foo --value process1  & bin/dev.js crdt --operation set --key foo --value process2 --pre-change-wait 1000 & wait && jq .foo /Users/mdonnalley/.sfdx/crdt.json
[1] 25434
[2] 25435
[25435] Reading config file after 0ms...
[25435] config file: /Users/mdonnalley/.sfdx/crdt.json
[25434] Reading config file after 0ms...
[25434] config file: /Users/mdonnalley/.sfdx/crdt.json
[25434] Changing config file after 0ms...
[25434] SET: foo=process1
[25434] Writing config file after 0ms...
[25434] Done!
[1]  - 25434 done       bin/dev.js crdt --operation set --key foo --value process1
[25435] Changing config file after 1000ms...
[25435] SET: foo=process2
[25435] Writing config file after 0ms...
[25435] Done!
[2]  + 25435 done       bin/dev.js crdt --operation set --key foo --value process2 --pre-change-wait
"process2"

🟢 process 1 sets new key last, process 2 does the last config.write

bin/dev.js crdt --operation set --key foo --value process1 --pre-change-wait 1000 & bin/dev.js crdt --operation set --key foo --value process2 --pre-write-wait 5000 & wait && jq .foo /Users/mdonnalley/.sfdx/crdt.json
[1] 25803
[2] 25804
[25803] Reading config file after 0ms...
[25804] Reading config file after 0ms...
[25803] config file: /Users/mdonnalley/.sfdx/crdt.json
[25804] config file: /Users/mdonnalley/.sfdx/crdt.json
[25804] Changing config file after 0ms...
[25804] SET: foo=process2
[25803] Changing config file after 1000ms...
[25803] SET: foo=process1
[25803] Writing config file after 0ms...
[25803] Done!
[1]  - 25803 done       bin/dev.js crdt --operation set --key foo --value process1 --pre-change-wait
[25804] Writing config file after 5000ms...
[25804] Done!
[2]  + 25804 done       bin/dev.js crdt --operation set --key foo --value process2 --pre-write-wait
"process1"

🟢 process 2 sets new key last, process 1 does the last config.write

bin/dev.js crdt --operation set --key foo --value process1 --pre-write-wait 5000 & bin/dev.js crdt --operation set --key foo --value process2 --pre-change-wait 1000 & wait && jq .foo /Users/mdonnalley/.sfdx/crdt.json
[1] 26063
[2] 26064
[26064] Reading config file after 0ms...
[26064] config file: /Users/mdonnalley/.sfdx/crdt.json
[26063] Reading config file after 0ms...
[26063] config file: /Users/mdonnalley/.sfdx/crdt.json
[26063] Changing config file after 0ms...
[26063] SET: foo=process1
[26064] Changing config file after 1000ms...
[26064] SET: foo=process2
[26064] Writing config file after 0ms...
[26064] Done!
[2]  + 26064 done       bin/dev.js crdt --operation set --key foo --value process2 --pre-change-wait
[26063] Writing config file after 5000ms...
[26063] Done!
[1]  + 26063 done       bin/dev.js crdt --operation set --key foo --value process1 --pre-write-wait
"process2"

🔴 process 1 sets new key then process 2 deletes it, process 1 does final config.write

bin/dev.js crdt --operation set --key foo --value process1 --pre-write-wait 5000 & bin/dev.js crdt --operation unset --key foo --pre-change-wait 1000 & wait && jq .foo /Users/mdonnalley/.sfdx/crdt.json
[1] 26969
[2] 26970
[26969] Reading config file after 0ms...
[26970] Reading config file after 0ms...
[26969] config file: /Users/mdonnalley/.sfdx/crdt.json
[26970] config file: /Users/mdonnalley/.sfdx/crdt.json
[26969] Changing config file after 0ms...
[26969] SET: foo=process1
[26970] Changing config file after 1000ms...
[26970] UNSET: foo
[26970] Writing config file after 0ms...
[26970] Done!
[2]  + 26970 done       bin/dev.js crdt --operation unset --key foo --pre-change-wait 1000
[26969] Writing config file after 5000ms...
[26969] Done!
[1]  + 26969 done       bin/dev.js crdt --operation set --key foo --value process1 --pre-write-wait
"process1"

It should have been null since process 2 had the later change

🟢 process 1 sets new key then process 2 deletes it, process 2 does final config.write

bin/dev.js crdt --operation set --key foo --value process1 & bin/dev.js crdt --operation unset --key foo --pre-change-wait 1000 --pre-write-wait 5000 & wait && jq .foo /Users/mdonnalley/.sfdx/crdt.json
[1] 27390
[2] 27391
[27391] Reading config file after 0ms...
[27390] Reading config file after 0ms...
[27391] config file: /Users/mdonnalley/.sfdx/crdt.json
[27390] config file: /Users/mdonnalley/.sfdx/crdt.json
[27390] Changing config file after 0ms...
[27390] SET: foo=process1
[27390] Writing config file after 0ms...
[27390] Done!
[1]  - 27390 done       bin/dev.js crdt --operation set --key foo --value process1
[27391] Changing config file after 1000ms...
[27391] UNSET: foo
[27391] Writing config file after 5000ms...
[27391] Done!
[2]  + 27391 done       bin/dev.js crdt --operation unset --key foo --pre-change-wait 1000  5000
null

shetzel
shetzel previously approved these changes Oct 31, 2023
@mdonnalley
Copy link
Contributor

QA (sf specific)

config/alias

yarn link @salesforce/core && sf plugins link --no-install in plugin-settings

in dreamhouse-lwc
🟢 sf config list
🟢 sf config set org-metadata-rest-deploy=true --global
🟢 sf config set org-metadata-rest-deploy=true
🟡 sf config set org-metadata-rest-deploy=true & sf config set org-metadata-rest-deploy=false & wait (non-determinant which one wins but that's just because we can't predict which process does the final change)
🟢 sf config unset org-metadata-rest-deploy
🟢 sf config unset org-metadata-rest-deploy --global
🟢 sf alias list
🟢 sf alias set foo=bar
🟢 sf alias unset foo

org create

yarn link @salesforce/core && sf plugins link --no-install in plugin-org

in dreamhouse-lwc
🟢 sf org create scratch --edition developer --alias qa -f config/project-scratch-def.json --set-default

deploy/retrieve

yarn link @salesforce/core && sf plugins link --no-install in plugin-deploy-retrieve

in dreamhouse-lwc
🟢 sf project deploy start --source-dir force-app
🟢 sf project deploy resume --use-most-recent
🟢 sf project deploy start --source-dir force-app --async
🟢 sf project deploy report --use-most-recent
cleared source tracking using sf project tracking delete
🟢 sf project deploy start --ignore-conflicts
🟢 sf project deploy resume --use-most-recent
created new custom object in org
🟢 sf project retrieve start

mdonnalley
mdonnalley previously approved these changes Oct 31, 2023
@mshanemc mshanemc dismissed stale reviews from mdonnalley and shetzel via 5562a3d November 9, 2023 17:52
mdonnalley
mdonnalley previously approved these changes Nov 13, 2023
@mshanemc mshanemc marked this pull request as ready for review November 13, 2023 19:46
@mshanemc mshanemc merged commit 6836868 into main Nov 14, 2023
39 of 67 checks passed
@mshanemc mshanemc deleted the sm/crdt-config branch November 14, 2023 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants