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: add "on backgrounded" function #611

Merged
merged 29 commits into from
May 27, 2024
Merged

feat: add "on backgrounded" function #611

merged 29 commits into from
May 27, 2024

Conversation

EvanHahn
Copy link
Contributor

@EvanHahn EvanHahn commented May 6, 2024

MapeoManager#onBackgrounded() should be called when the app goes into the background. It will do its best to gracefully shut down sync.

Closes #576.

`MapeoManager#onBackgrounded()` should be called when the app goes into
the background. It will do its best to gracefully shut down sync.

Closes [#576].

[#576]: #576
@EvanHahn EvanHahn requested a review from gmaclennan May 6, 2024 21:58
@EvanHahn EvanHahn marked this pull request as draft May 9, 2024 14:15
src/sync/peer-sync-controller.js Show resolved Hide resolved
src/sync/autostopper.js Outdated Show resolved Hide resolved
src/sync/sync-api.js Outdated Show resolved Hide resolved
@EvanHahn EvanHahn marked this pull request as ready for review May 15, 2024 18:49
@EvanHahn EvanHahn requested a review from gmaclennan May 15, 2024 19:07
Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

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

I find it quite hard to follow the logic here, and the overall approach doesn't "feel" quite right (sorry to be vague, maybe talking it through will help me get clear on specifics). I think what autostop is doing is stopping sync after a fixed period of time, rather than unreplicating cores when sync has finished, but I'm not sure because I don't completely understand the logic. I think it would be worth co-working on this a bit more.

src/sync/peer-sync-controller.js Outdated Show resolved Hide resolved
src/sync/peer-sync-controller.js Show resolved Hide resolved
@EvanHahn
Copy link
Contributor Author

Simplified this a bunch (I think).

When the app is in the background and sync is complete, cores will be un-replicated. There is no longer an auto-stop timeout, but it should be easy to add in the future.

There are now two booleans: wantsToSyncData and isBackgrounded.

  • start() enables wantsToSyncData.
  • stop() disables wantsToSyncData.
  • background() enables isBackgrounded.
  • foreground() disables isBackgrounded.

After these operations and when the state is updated, #updateState() is called. This figures out the desired SyncEnabledState and applies it.

@EvanHahn EvanHahn requested a review from gmaclennan May 16, 2024 20:32
Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

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

Good work on this, the logic looks correct to me, but there is one place in particular where it seems very fragile around management of the syncEnabledState - it should work as-is, but only because of nuances of Map iteration order and how we read it. I think we should fix this before merging.

Summary of other comments:

  • We need to define the state.initial.syncing now, because currently it defaults to true always.
  • There are a few naming things that might help future maintainers understand things.
  • Finally I think it's not great to have duplicate logic for deriving syncEnabledState in both the #getState and #updateState methods.

src/sync/peer-sync-controller.js Show resolved Hide resolved
Comment on lines 133 to 137
state.data.syncing =
this.#wantsToSyncData &&
(!this.#isBackgrounded ||
!isSynced(namespaceSyncState, 'full', this.#peerSyncControllers))

Copy link
Member

Choose a reason for hiding this comment

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

It probably makes sense to rename state['data' | 'initial'].syncing to state['data' | 'initial'].syncEnabled to be clear about what this means.

Copy link
Member

Choose a reason for hiding this comment

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

This is essentially calculating the value of syncEnabledState based on the same logic that is in #updateState(), and we need to remember to keep those two pieces of logic in sync. Hoisting syncEnabledState to somewhere that can be read here would avoid having this duplication.

For completeness we should also set state.initial.syncing here, because currently it just defaults to always be true, but now it can be false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored in 8fe34aa.

@@ -50,7 +56,8 @@ export class SyncApi extends TypedEmitter {
#pscByPeerId = new Map()
/** @type {Set<string>} */
#peerIds = new Set()
#isSyncing = false
#wantsToSyncData = false
#isBackgrounded = false
Copy link
Member

Choose a reason for hiding this comment

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

I think this naming (isBackgrounded) is confusing, because it doesn't describe what this does, it describes a state that only really makes sense on mobile, and it's behaviour is determined by platform specifics of how we want sync to behave when in the background. Understanding what it does requires reading the code, or understanding that we want sync to gracefully stop when complete when the app is in the background. I'm thinking ahead to when we integrate this into Desktop and Mapeo Cloud server. This is why in my hacking around yesterday I was moving towards the concept of starting and stopping both sync types (initial & data), and being more explicit about auto-starting initial sync on initialization (e.g. starting initial sync in the constructor).

I'm not great at names, but perhaps we can call this either wantsToStopAllSync or the inverse wantsToSyncInitial or wantsToSyncPresync. Then perhaps the methods can be kStopAllSyncGracefully and kResumeSync?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed in 4a641e7.

Comment on lines 147 to 150
for (const peerSyncController of this.#peerSyncControllers.values()) {
isStopped = peerSyncController.syncEnabledState === 'none'
break
}
Copy link
Member

Choose a reason for hiding this comment

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

I think it starts to get quite confusing storing this state on the peer sync controllers, since a future maintainer could without understanding set a peer controller sync enabled state somewhere, so that this state is different for some PSCs, which would break this. There is also kind of vague behaviour around when PSC instances are created with syncEnabledState === 'none', before it is set from this class, because this class reads that state as the source of truth while also setting that state. It's kind of circular...

I suggest that the syncEnabled state is hoisted, either to this class or to the SyncState class, which is already passed to the PSC instances, so they could read syncState.syncEnabledState from there. That would also avoid the not-quite-determined behaviour when PSC instances are created, because they will read the shared value from SyncState, rather than having their own state until it starts being controlled by this class.

Copy link
Member

Choose a reason for hiding this comment

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

What I'm wanting to avoid here is implicit context knowledge in the codebase, e.g. "you must set the syncEnabledState of all PSCs at the same time, but you can't entirely rely on any one PSC's syncEnabledState to be accurate, so we should re-set syncEnabledState each time we update".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ba539cf. (I should've left a PR comment saying that I didn't like the way I addressed this.)

EvanHahn and others added 6 commits May 20, 2024 16:07
Co-authored-by: Gregor MacLennan <gmaclennan@digital-democracy.org>
_I recommend reviewing this with whitespace changes disabled._

This test-only change drops Brittle from our FastifyController tests and
replaces them with `node:test` and `node:assert`.
This test-only change drops Brittle from our basic manager tests and
replaces them with `node:test` and `node:assert`.

Two notable things about this change:

1. There was a test that was commented out because of a Brittle bug.
   This uncomments it and sets the `skip: true` option.
2. Brittle has an ([undocumented][0]) `t.snapshot()` method which we
   can't use. I replaced it with a custom snapshot assertion.

The rest was just mechanical changes.

[0]: holepunchto/brittle#45
This test-only change drops Brittle from our Fastify server tests and
replaces them with `node:test` and `node:assert`.
This test-only change drops Brittle from our project settings tests and
replaces them with `node:test` and `node:assert`.
Co-authored-by: Evan Hahn <me@evanhahn.com>
EvanHahn and others added 6 commits May 20, 2024 21:07
We plan to replace Brittle with `node:test` which doesn't have an
equivalent for `t.plan`. Remove it to make it easier to migrate.
When you leave a project, we now delete all indexed data outside of the
`auth` namespace. Fixes [#427].

[#427]: #427

Co-Authored-By: Andrew Chou <andrewchou@fastmail.com>
@EvanHahn
Copy link
Contributor Author

Thanks for all the review here. Hopefully this is the last round!

@EvanHahn EvanHahn requested a review from gmaclennan May 20, 2024 22:23
return state
}

#updateState = () => {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add namespace sync state as an optional parameter, to save recalculating it on state events. It's expensive to calculate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in db4ff6c.

transform.ts Outdated Show resolved Hide resolved
@EvanHahn EvanHahn merged commit bb9b16c into main May 27, 2024
7 checks passed
@EvanHahn EvanHahn deleted the evanhahn/576 branch May 27, 2024 22:02
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.

Expose API to stop syncing when app goes into background
3 participants