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

Fix IndexOutOfBounds after getRefreshKey #2477

Closed

Conversation

kevincianfarini
Copy link
Collaborator

It's possible that on a paging source that's not in the initial
generation, we fail with an IndexOutOfBoundsException. This
is caused when the underlying data source undergoes a delete, which
then invalidates the paging source, which then calls
getRefreshKey. In the event that the the refresh key is the
very last index of the previous paging source, we would fail
with the above exception because that index does not exist
in the next generation paging source.

Closes #2434.

@kevincianfarini
Copy link
Collaborator Author

kevincianfarini commented Jul 15, 2021

I'm not a member of this repo so I can't do it, but could @AlecStrong please request @dlam as a reviewer? I'm curios to see if they ran into similar problems when implementing paging3 with Room.

@JakeWharton
Copy link
Member

We can't add non-member reviewers, but he'll see it from the @-mention and can always review.

@@ -34,9 +34,13 @@ internal class OffsetQueryPagingSource<RowType : Any>(
params: LoadParams<Long>
): LoadResult<Long, RowType> = withContext(dispatcher) {
try {
val key = params.key ?: 0L
transacter.transactionWithResult {
val count = countQuery.executeAsOne()
Copy link

Choose a reason for hiding this comment

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

Btw, it is expensive to use a transaction on every load because in sqlite (I assume the default for main use-case for sqldelight) it blocks all read/write to any table, so what we do in Room is cache this count and only get it for initial load. Any write into the DB should invalidate anyway so this count should never change while this PagingSource is able to return valid results.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea.

Copy link

Choose a reason for hiding this comment

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

On the topic of invalidation, although you use a transaction here, it is possible for a write to happen after PagingSource.load is called, but before you begin your transaction, and in the scenario where you return LoadResult.Page here before invalidation is able to propagate cancellation on this generation, it is possible you will return inconsistent results. Starting from the next Paging release (3.1.0-alpha03), we'll be adding a LoadResult.Invalid return type you can use to get Paging to discard your LoadResult. You will want to check if you are about to be invalidated after you finish loading from DB but before you return and return LoadResult.Invalid instead.

Copy link

Choose a reason for hiding this comment

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

This is very complicated so here is a more concrete example:

  1. DB has items [1, 2, 3, 4]
  2. PagingSource_1 loads [1, 2] and Paging displays those two items
  3. A write to DB is kicked off to insert item 0
  4. Paging library asks PagingSource_1 for an APPEND
  5. Your load starts a transaction for the APPEND which needs to wait for the write to finish
  6. Write finishes, DB now has items [0, 1, 2, 3, 4]
  7. PagingSource_1 loads with params.key == 2, this results in LoadResult.Page(data = [2, 3])
  8. Paging presents items [1, 2, 2, 3], duplicating one of the rows.
  9. Invalidation callback finally triggers cancellation and a new generation.
  10. Now PagingSource_2 is created to load data from scratch.

While you wait for PagingSource_2 to finish loading and finally send to RV to get presented, you have the duplicate row shown to UI incorrectly.

Note that it doesn't matter when you trigger invalidation, even if you are able to synchronously do it, I don't think it's possible to guarantee that cancellation successfully propagates before load completes and sends it off to presenter to show the accidentally duplicated row.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I factored out the transaction like you mentioned. I think we'll wait until the new API in paging 3.1 is released before we worry about the complicated invalidation.

@JakeWharton
Copy link
Member

/rebase

@kevincianfarini
Copy link
Collaborator Author

Thanks for the rebase. Please don't merge anything yet. I'm unable to mark this as a WIP for some reason, but I still have some work to do.

@JakeWharton JakeWharton marked this pull request as draft July 19, 2021 15:01
@kevincianfarini kevincianfarini force-pushed the fix-out-of-bounds branch 3 times, most recently from acb2368 to 56a3545 Compare July 28, 2021 21:32
@dlam
Copy link

dlam commented Jul 28, 2021

FYI: LoadResult.Invalid which allows you to tell Paging to stop loading from a PagingSource in cases where invalidation races with cancellation of PagingSource.load() is now released in paging-3.1.0-alpha03

@kevincianfarini
Copy link
Collaborator Author

@dlam I don't believe we'd be able to release a paging extension based on an alpha paging library. @JakeWharton or @AlecStrong please correct me if I'm wrong, and I'll update this PR.

@JakeWharton
Copy link
Member

Yep, we'll have to wait for it to go stable.

@JakeWharton
Copy link
Member

I mean you can update the PR to the alpha and confirm tests pass and stuff, but I wouldn't merge until stable so that if we do a release we don't accidentally release an unstable API surface.

@nabrozidhs
Copy link

paging 3.1.0 is released!

@AlecKazakova
Copy link
Collaborator

/rebase

It's possible that on a paging source that's not in the initial
generation, we fail with an `IndexOutOfBoundsException`. This
is caused when the underlying data source undergoes a delete, which
then invalidates the paging source, which then calls
`getRefreshKey`. In the event that the the refresh key is the
very last index of the previous paging source, we would fail
with the above exception because that index does not exist
in the next generation paging source.

Closes cashapp#2434.
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.

IndexOutOfBoundsException In OffsetQueryPagingSource
5 participants