Skip to content

Client-side Purging - #5075

Merged
garethbowen merged 27 commits into
masterfrom
5048_purge_client_side
Jan 8, 2019
Merged

Client-side Purging#5075
garethbowen merged 27 commits into
masterfrom
5048_purge_client_side

Conversation

@SCdF

@SCdF SCdF commented Dec 6, 2018

Copy link
Copy Markdown
Contributor

For your consideration.

I spent a couple of hours today knocking out the basics of purge on the phone:

  • Runs every N days on refresh (in my hard-coded configuration I have it at 0 so it runs on every refresh)
  • Runs over all reports for all contacts and executes the configured function
  • Converts the result (guarding against bad ids) into minimally deleted files and writes them into PouchDB
  • Doesn't sync those deletes upwards
  • Initiates a compaction if anything was purged Does nothing because we auto-compact

I have tested it and it seems to nominally work.

Still to do:

  • Write tests!
  • Actually pull config from config
  • Document that config's existence, write a TL / CHT guide on how to use the feature
  • Performance test to make sure running like this on startup doesn't destroy a typically full phone in an obviously bad way. If it does, consider pausing between sets for n seconds to reduce constant cpu usage
  • Consider exposing this on the about page so users can manually run purge.
  • Consider putting this behind a "persisted queue", so if a user gets halfway through and refreshes we pick up where we left off.
  • Consider view warming. Doesn't really seem worth it on PouchDB, idk. Which would you pick etc.

More notes:

  • If at some point we want purge rules to only apply to certain people we could allow for the function to return a map of id to role, e.g. ['id1', 'id2'] -> {id1: true, id2: ['role1', 'role2']}
  • If we decide that we don't want to ship without it running on the server, while it was good practice for thinking about the problem and not much time investment I'd probably have to throw the vast majority of it away.

Interested in your thoughts.

#5048

@SCdF
SCdF requested review from alxndrsn and garethbowen December 6, 2018 16:48
@SCdF

SCdF commented Dec 6, 2018

Copy link
Copy Markdown
Contributor Author

Oh yeah, please don't merge 🙃

@garethbowen

Copy link
Copy Markdown
Contributor

Looking good, nice work!

A few thoughts...

  1. I think it's critical we never purge anything that's not yet replicated. I'm not sure what the best way to achieve this is. You could check the changes feed for the last updated date for this doc and compare that to the last sync time. Alternatively you could listen for dbsync to complete and then prompt Purger to check.
  2. Seeing as you're doing this on refresh consider doing it in the bootstrap layer so the UI is blocked (nice spinner, "purging docs" message, etc). This would mean the whole process can run faster because angular wouldn't be constantly updating the UI as docs are purged.
  3. We set auto compaction on in PouchDB but it would be worth considering an explicit compaction after deletion. This is a slow process but IIRC until compaction you don't see improvements in performance or disk space and I don't completely trust auto compaction to keep working.
  4. If the doc is updated on the server it'll conflict on replication. I'm guessing that'll be silently ignored which is probably the right thing to do in this case, but we could consider bringing the doc back?

@SCdF

SCdF commented Dec 10, 2018

Copy link
Copy Markdown
Contributor Author

@garethbowen,

  1. This is pretty hard, and is made redundant by running it on the server, which is another great reason to migrate to that at some point.
  2. This is bad because it blocks, but good because it makes the flow cleaner and more obvious (things don't randomly delete below you), and also removes any stability concerns we have about our angular code handling deletes. This is great because it also half-solves replicating super large users: we'll still download all their data but we will never boot angular or even pouchdb views (I can rewrite to not use views) against the large data set.
  3. OK can look into that
  4. The result this is non-deterministic, but in the style of eventual consistency even if it appears again the next purge will remove it, so I figured this was OK.

So:

  • I think there are more pros than cons to moving this code to run between PouchDB bootstrap and angular bootstrap, so I'll move it there
  • I'll change it to not use views at all, and just use all docs, which sounds bad but really 99% of a user's data is going to be a contact or a report anyway, so there is little wastage
  • I will look into fixing 1.

Comment thread webapp/src/js/controllers/inbox.js Outdated

if (!Session.isOnlineOnly()) {
Purger();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this delays starting of the app UI, can we have a startup message for this stage?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It didn't (it spawns of a promise that we ignore), but in other discussions I think we've decided to move it to be an active part of pre-angular startup.

So yeah, it's going to be reworked into be an event emitter so we can follow progress etc

keys.includes('_rev') &&
keys.includes('_deleted')) {
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be simpler just to have a _medic_purged: true flag instead of checking for these specific things? Is this doing something clever by deliberately copying pouch/couch soft-delete functionality?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I feel more comfortable with this, because it increases the specificity of what we're trying to avoid replicating. It means we can also (for now) do things like block writes of that in validate_doc_update for the server if we're so inclined

Comment thread webapp/src/js/services/purger.js Outdated
return $q.all([
Search('reports', {}, options),
Search('contacts', {}, options)
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assuming Search() is slow and that almost all docs are either contacts or reports, would it be quicker to do allDocs() here and then filter manually? Or just hand the filtering off to the custom function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's basically pointless to use search here AFAICT yeah, it's just copying what nools bootstrap does.

Since I'm moving the code to be before angular even starts I am definitely just going to do an all docs in the future

@SCdF
SCdF force-pushed the 5048_purge_client_side branch from 6be639b to 39a5175 Compare December 11, 2018 12:20
@SCdF

SCdF commented Dec 12, 2018

Copy link
Copy Markdown
Contributor Author

@garethbowen / @alxndrsn I moved it to startup. I still need to hook up a progress bar etc so it looks nice.

I am also not protecting against documents being deleted that are not yet replicated.

It's still not clear to me a safe way of doing this, especially since we're now doing this before angular boots.

We could reverse engineer the _local document name, read that and only purge if the seq is the same as the db's current max seq. However, that's not an exposed API so we'd have to... copy it I guess? It doesn't seem safe.

I think if we really don't want to ship without this feature we are going to have to spend the effort to run the code on the server.

@garethbowen

Copy link
Copy Markdown
Contributor

Looking good! Let's chat sometime about the server/client tradeoffs so we can move forward here.

@alxndrsn

Copy link
Copy Markdown
Contributor

It's still not clear to me a safe way of doing this, especially since we're now doing this before angular boots.

Can you compare last-replication-time vs doc.reported_date?

@SCdF

SCdF commented Dec 13, 2018

Copy link
Copy Markdown
Contributor Author

@alxndrsn unless I've missed it we don't store last-replication-time anywhere, it's only available as a live piece of data while angular is running.

We could change that so it writes it somewhere (eg to a local doc we do have access to), and then we use that on boot, presuming if that doesn't exist then we are also allowed to purge because it means no real replication has happened yet.

However, it's another write, which we're performing on every replication, to solve a check that happens once a month that probably shouldn't need to happen given good rules, and would not be reverted (since we don't need it for anything else) once we moved to server-side.

IDK, if people think it's worth it then that's cool and can be done, I personally just didn't have strong interest in it when I was knocking this together.

@garethbowen

Copy link
Copy Markdown
Contributor

It's possible doc updates will still be purged before they're replicated because the code only checks the reported_date. Unfortunately we don't record when a doc was updated so it's not possible currently to check that.

One solution would be to move the code back into the angular app (sorry!) and trigger purging only once replication is completely caught up, but I think the faster execution in bootstrap still wins.

The other solution is running server side but we can do that in the next iteration.

This approach is good enough for now, do it!

@SCdF
SCdF force-pushed the 5048_purge_client_side branch from 12965cf to 1713e17 Compare December 18, 2018 09:57
@SCdF

SCdF commented Dec 18, 2018

Copy link
Copy Markdown
Contributor Author

Demo: https://www.youtube.com/watch?v=laDvd0ja2ac

@SCdF
SCdF force-pushed the 5048_purge_client_side branch from e7f42e6 to de9e4aa Compare December 20, 2018 16:56
@SCdF SCdF changed the title Client-side Purging [DRAFT] Client-side Purging Dec 20, 2018
@SCdF

SCdF commented Dec 20, 2018

Copy link
Copy Markdown
Contributor Author

@garethbowen THE TESTS PASS.

I am happy for this to be merged at this stage.

I'm currently writing documentation in medic-docs, which will be a separate PR in the next few hours. Feel free to wait for that or not, depending on your schedule.

@SCdF

SCdF commented Dec 20, 2018

Copy link
Copy Markdown
Contributor Author

NB: I got rid of blocking purges for documents younger than 60 days, because I managed to check for upward replication.

We could add this back though, if we want to be even more careful.

Doing it for the last 30 days might be a good idea actually:

  • Means (if people are using targets properly) that targets can't be broken
  • Stops overzealous configurers confusing users by purging stuff super fast

@SCdF

SCdF commented Dec 20, 2018

Copy link
Copy Markdown
Contributor Author

Documentation is here: medic/medic-docs#63

@garethbowen garethbowen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Awesome work!

I've put some suggestions inline but no blockers so feel free to do them or not. I've approved this PR so you can go an merge without having to come back for another round.

Comment thread webapp/src/js/bootstrapper/purger.js Outdated
Comment thread webapp/src/js/bootstrapper/purger.js Outdated
PURGE_INFO: function(progress) {
const n = progress.purged;
const percent = Math.floor((progress.processed / progress.total) * 100);
return `Cleaned ${n} documents (${percent}% complete)…`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To make translation easier, perhaps do the calculation first, and call the translator with count and percent. Otherwise each language will have to do this calculation...

const percent = Math.floor((progress.processed / progress.total) * 100);
return `Cleaned ${n} documents (${percent}% complete)…`;
},
PURGE_AFTER: 'Optimising…',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Obviously these two keys need to be translated into all the languages we support. It might be difficult to get this done on short notice so feel free to raise an issue blocking the 3.4.0 release to get the translations for each language.

Comment thread webapp/src/js/services/db-sync.js
Comment thread webapp/src/js/services/rules-engine.js Outdated
Comment thread webapp/tests/karma/unit/services/db-sync.js Outdated
Comment thread webapp/src/js/bootstrapper/index.js

@garethbowen garethbowen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On second thoughts, I think we may need to handle the failure case more gracefully... comment inline.

@SCdF
SCdF requested a review from garethbowen January 8, 2019 12:29
@SCdF

SCdF commented Jan 8, 2019

Copy link
Copy Markdown
Contributor Author

@garethbowen OK check out 3f7a58c

We no longer ever hard fail for anything.

Instead, if the purge function cannot be compiled, or a purge set fails for some reason, we generate feedback errors (up to 10 to avoid spam), which are then generated into real feedback documents on successful Angular boot.

Additionally, in testing I decided that it makes sense that purge should re-run if you change the purge function, so I added that as well. It's going to be an interesting decision when moving from client to server, but that's a conversation for another time.

@garethbowen garethbowen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Awesome!

@garethbowen
garethbowen merged commit 4a6972e into master Jan 8, 2019
@garethbowen
garethbowen deleted the 5048_purge_client_side branch January 8, 2019 22: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.

3 participants