Repository navigation
fix: save the profile without waiting for an unchanged copy - #1865
Conversation
A save with no tag change writes pubky.app and lets the Paykit copy write follow in the background, so Edit Profile Save takes one write again.
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f114a8ee68
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
[Medium impact] The change affects profile saves and their follow-up writes. The copy writes need ordering protection before merging because an earlier save can overwrite tags from a later save. Findings
SummarySaves with unchanged tags now return before the Paykit copy finishes. Saves with changed tags still wait for that write.
Reviews (1) · Last reviewed commit: "fix: save the profile without waiting fo..." · Reviewed by Greptile |
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. A save with no tag change now returns after the pubky.app write and launches the Paykit copy write on the repo scope; a tag change still waits for it. Reviewed f114a8e, full tier, static: Bitkit Android branches are not built here, so this is reasoned from the code and CI.
What I checked, and 4 candidates I ruled out
Read in full: saveProfile, writeProfile, profileWrite, deleteProfile, loadOwnProfile, publishPaykitProfileCopy, storedCopy in PubkyRepo.kt, and the new test
CI: unit tests, detekt, lint, build and UI tests green on f114a8e; two e2e jobs still pending when I read it
Open thread that stands
- @ben-kaufman's point on the launched copy clearing a later load's
tagsUnavailable: traced it.loadOwnProfilereads the copy outside the write lock and setstagsUnavailable = truewith empty tags on a failed read; the backgroundwriteCopythen runs.onSuccess { tagsUnavailable = false }; the nextprofileEditBase()handstagsUnread = false, sostoredCopyreturnstags = nulland the save writes the form's empty list as the copy. Before this change the copy write finished inside the save, so a load could not slot in between on the normal edit flow.
Ruled out
- An earlier copy overwriting a later save's tags (the Greptile and Codex threads):
profileWriteMutexis a FIFOMutex. The next save's pubky.app write takes the lock before its copy write queues, so the stale copy lands after the newer one only if the IO dispatcher has not started the launched coroutine through the user reopening the editor and a whole pubky.app round trip. - A copy written after a deletion:
deleteProfilebumpsprofileDeletionGenerationunder the same lock, so the launched write either holds the lock first or fails the generation check. - A failed background copy leaving the stored copy stale: the next
loadOwnProfileseespaykitData != profile.toProfileData()and republishes it throughpublishPaykitProfileCopy. - The new test leaking a suspended coroutine:
appScoperuns on the test'sUnconfinedTestDispatcher, andfinishCopyis completed before the test ends.
Merge confidence: 3/5, the open tagsUnavailable thread is a real tag loss on a reopen with a failed copy read.
A save waits for a copy write an earlier save left running before writing its own, and the background write no longer clears the unread flag a load may have set.
There was a problem hiding this comment.
Code scan at cd6aa23fe: no findings. This is the Android port of the iOS fix in synonymdev/bitkit-ios#1235 (718b20221).
- Unchanged tags: a save with unchanged tags launches the copy write in
scopeand returns after the pubky.app write.copyWriteOrderserializespendingCopyWrite, and each save joins the earlier write before starting its own, so an older copy never lands over a newer one. - Deletion: the background write still goes through
profileWrite(generation), so a deletion that bumped the generation refuses it. - Tag changes still wait for the copy, and only that path clears
tagsUnavailable, so a failed tag write is still reported. - A background write that fails is logged, and the next load refreshes the copy, as on iOS.
perf: relevant. This is the fix for the Edit Save slowdown measured on #1801 (median 13.2 s vs 8.8 s on master, under load). On iOS the same change brought a name-only save to 4.3–5.4 s.
Device gate (partial): cd6aa23fe — not run yet. Timing a name-only Save against master on the emulator, and manual tests 1–3, are next.
Reviewer model: Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
coreyphillips
left a comment
There was a problem hiding this comment.
Two independent reviews, nothing blocking a merge.
worth doing, does not block
- Background-path save keeps a stale unread flag it just resolved (app/src/main/java/to/bitkit/repositories/PubkyRepo.kt:1350-1356)
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (9f3ecc7). 2 points still block a merge.
addressed
- Background-path save keeps a stale unread flag it just resolved (app/src/main/java/to/bitkit/repositories/PubkyRepo.kt:1350-1356). Fixed:
writeProfileOncecapturesreadEpochat PubkyRepo.kt:1293.storedCopyclearstagsUnavailableafter a successful read when no load has bumpedtagsReadEpoch(1382), and both loads bump it (954, 1405). I traced the scenario: the edit opens with[a, b], a load fails, the user removesb, and the save takes the background path. The flag now ends false, so the next edit writes[a], which is what the new testa save that reads the copy itself lets the next edit remove a tagasserts. Not run locally. The same clear causes a new problem on the synchronous path, filed separately.
new since the last review
needs changing before merge
- Unread flag is cleared before the copy write, so a failed write loses tags (app/src/main/java/to/bitkit/repositories/PubkyRepo.kt:1382)
- A failed copy write lets the next save erase unread tags (app/src/main/java/to/bitkit/repositories/PubkyRepo.kt:1382)
jvsena42
left a comment
There was a problem hiding this comment.
Device gate on cd6aa23fe; the head is now 9f3ecc7b6, which I reviewed and is clean. Approving.
9f3ecc7b6 clears the unread-tags flag after a save that read the copy itself, but only when tagsReadEpoch hasn't moved since the save began, so a load that found the copy unreadable meanwhile wins. It doesn't touch the name-only save path timed below. It matches iOS 49f305a3f on synonymdev/bitkit-ios#1235.
Name-only Save → "Profile saved" (Gate_Throwaway emulator, staging; builds alternated; host load 5.7–15 at each tap):
| run | this PR | master d642de94b |
|---|---|---|
| 1 | 5.45 s | 16.70 s |
| 2 | 5.47 s | 17.92 s |
| 3 | 9.01 s | 21.41 s |
On master the toast follows the Paykit copy write. Here it comes first, and the copy follows 3–8 s later with the new name.
| Case | Result |
|---|---|
| 1. Name-only save | Pass: pubky.app has the new name before the toast, and the copy follows. |
| 2. Tag-change save | Pass: the toast comes only once the copy holds the tag (13.5 s, 9.7 s). |
| 3. Save, then Delete at once | Pass. Delete was confirmed while the background copy write was still pending; the write landed first, then both profile.json files returned 404 and stayed 404 for about 110 s. |
| Relaunch | Pass: name and tags kept. |
Device gate: cd6aa23fe (head 9f3ecc7b6, a flag-only change, reviewed) — manual: 3 passed plus relaunch; perf: name-only Save 5.5–9.0 s (master 16.7–21.4 s).
Reviewer model: Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (e571a6a). Nothing blocking remains.
addressed
- Unread flag is cleared before the copy write, so a failed write loses tags (app/src/main/java/to/bitkit/repositories/PubkyRepo.kt:1382). Fixed:
storedCopy(PubkyRepo.kt:1379-1384) no longer touchestagsUnavailable. The synchronous branch clears it only inonSuccess(1362), andmarkTagsReadruns only on the background branch (1359). In your Profile-screen sequence the failed write in step 2 now leaves the flag true, so step 3'sprofileEditBase()hastagsUnread = true, re-reads the copy and writes[friend, work, vip]. I traced this, and the new testa tag save after a failed read and write keeps the stored tags without the error's basecovers it without the error's base. I could not run it because the bitkit-core and paykit artifacts are missing from the local Gradle cache. - A failed copy write lets the next save erase unread tags (app/src/main/java/to/bitkit/repositories/PubkyRepo.kt:1382). Fixed: For the name-only save,
keptTagsis[friend, work]andbaseTagsis[], so the save takes the synchronous branch (1362). The failed write leaves the flag set becausestoredCopyno longer clears it. The next save re-reads the copy and writes[friend, work]. Clearing on the background branch (1359) is safe: therekeptTags == baseTagsand the stored profile carries every tag the copy holds, even if that write later fails. I confirmed this by tracing and by readinga name save after a failed read and write keeps the stored tags. I did not run the tests.
jvsena42
left a comment
There was a problem hiding this comment.
Re-review of e571a6a77: no findings. Approving. markTagsRead(stored, readEpoch) now runs when the save that read the copy itself launches its background write, still guarded by tagsReadEpoch, and the write's outcome leaves the flag alone. It doesn't touch the name-only save path timed at cd6aa23fe, so those timings stand.
Reviewer model: Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
|
QA for 9f3ecc7: Tested on a Pixel emulator (Android 16): dev debug build of 9f3ecc7 against staging, with a log-only timing hook that is not part of this PR. The stored profiles were read from the homeserver with the wallet's own key. e571a6a, the head since, only changes when a save clears the unread-tags flag after a failed copy write. The UI can't stage a failed write, so its two new unit tests cover it. 🟢 1. Edit only the nameWith the tag family on the profile, renaming it to Charlie shows "Profile saved" 1.3 s before the copy write ends (app log). pubky.app then has the name Charlie, and the Paykit copy has Charlie and still the tag family. After a relaunch, both stored profiles are unchanged and the name and tag show.
🟢 2. Edit the name and the tagsRenaming to Bravo and adding a second tag, family, shows "Profile saved" only once the copy write has ended. pubky.app has the name Bravo, and the copy has Bravo with friends and family. Removing friends then leaves only family in the copy and on screen, and pubky.app is unchanged.
🟢 3. Save, then delete at onceRenaming to Lima and saving, then going straight to Edit Profile → Delete Profile → Yes, Delete leaves both `profile.json` files at 404, still 404 ten seconds later, and the app ends on onboarding. Under this host's load the copy write ended before the delete. @jvsena42's run covered the delete landing while the copy write was still pending.
🟢 Save time, name onlyMeasured from the start of the save to "Profile saved", with the app log's write times. The host's load average swung from about 120 to 540 during these runs.
Here a save takes as long as the pubky.app write; on master, as long as both writes. Three runs per build under this load don't give a reliable median.
One open observation: in an earlier run of case 1, at load 184, the stored profiles were right and the app logged the toast, but it never appeared on screen. Every other save showed it, including master's under higher load, so I can't tell whether it was the load or this PR. |
|
Merging: approved by @coreyphillips and @jvsena42 on e571a6a, CI is green, the QA passed, and every thread is answered. It closes #1866 and ships with the pubky.app profile work in 3.1.1. |
Closes #1866
Refs: #1801
Description
Design
N/A — no UI changes.
Preview
N/A — no user-visible changes beyond the save being faster.
QA Notes
Manual Tests
/pub/paykit/profile.json) has the new name.Automated Checks
saveProfile without a tag change does not wait for the profile copy,a background copy write keeps a later load's unread tags,a save that reads the copy itself lets the next edit remove a tag,a tag save after a failed read and write keeps the stored tags without the error's baseanda name save after a failed read and write keeps the stored tagsfail without their change and pass with it.Evidence