Status Update
Comments
da...@google.com <da...@google.com> #2
Hi, thanks for reaching out to us and reporting this issue! Will share this to the internal teams and open a report on your behalf. Will reach back with updates as soon as I have one.
Thanks!
yb...@google.com <yb...@google.com> #3
da...@google.com <da...@google.com> #4
The problem I see with a suspend refreshVersions() that simply notifies observers as usual is that it will cause a database transaction. Essentially on every page load all DB operation will get blocked, then I imagine a user flinging through a list, starving DB operations because on every load we need to force invalidation refresh. Secondly, imagine a long-transaction running for some other reason, an unrelated write, or a complicated read, it will delay the loading of the page because we are waiting to start a transaction to check for invalidation.
I also don't see how a suspend version is better than the sync, the load is already in an IO Dispatcher in some random IO thread from Kotlin, so its not really blocking Paging's ability to discard the source due to invalidation. There is little to no benefit to switching to a Room WRITE thread, other than serialization of transactions, which will just further delay things. Note that I am super-assuming here that Paging behaves well and discards a returned page if invalidate()
has been called before load()
is complete, which is what will happen with refreshVersionsSync()
since by the time the method returns the observer in the PagingSource has been called.
Numero 3 is basically just:
fun isTableInvalidated(name: String): Boolean {
val tableId = tableIdLookup[name] ?: false
val stmnt = db.compileStatement("SELECT invalidate FROM room_table_modification_log WHERE table_id = ?)
smnt.bindLong(0, tableId)
return smnt.simpleQueryForLong() == 1
}
That is it, no transaction. Then the paging source can delay as much as they want or call invalidate()
itself right there after the check, assuming that is allowed.
cl...@google.com <cl...@google.com> #5
I'm currently working on adding an InvalidDataException that can be thrown from load() for reasons such as invalid data. This exception can be thrown at any point within load (i.e. when db query is complete) and paging will handle it by closing the page event flow + collection flow so that exception doesn't bubble up. load() should never complete in this case and data will be discarded. Then paging will simply wait for the invalidation signal to arrive and then start new generation of paging source etc.
Hope i'm making sense!
yb...@google.com <yb...@google.com> #6
The part i don't fully understand is that refreshVersionsAsync wont obtain a transaction unless it has to:
public void refreshVersionsSync() {
if (mAutoCloser != null) {
// This increment call must be matched with a corresponding call in mRefreshRunnable.
mAutoCloser.incrementCountAndEnsureDbIsOpen();
}
syncTriggers();
mRefreshRunnable.run();
}
void syncTriggers(SupportSQLiteDatabase database) {
if (database.inTransaction()) {
return;
}
try {
while (true) {
Lock closeLock = mDatabase.getCloseLock();
closeLock.lock();
try {
final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
if (tablesToSync == null) {
return;
}
@VisibleForTesting
Runnable mRefreshRunnable = new Runnable() {
@Override
public void run() {
final Lock closeLock = mDatabase.getCloseLock();
Set<Integer> invalidatedTableIds = null;
closeLock.lock();
try {
if (!ensureInitialization()) {
return;
}
if (!mPendingRefresh.compareAndSet(true, false)) {
// no pending refresh
return;
So if no endTransaction
calls happened, as far as i can see, this won't create a transaction but will obtain a bunch of locks.
But obvioulsy, there are cases where transactions are used for reads and if paging does it itself (e.g. paging relational data), it might get into a transaction in every case but i don't know how we can even avoid it.
da...@google.com <da...@google.com> #7
True that refreshVersionsSync()
might not even check the DB or start a transaction if there are no pending refresh, but we have to solve for the worst case too right? It starts a transaction after any transaction is done, worst yet any WRITE operation (including non-observing tables) will cause refresh to do a transaction, so I really think it is just best to avoid going through path and having a more direct check for Paging, even if it has some false-positives.
An example of a false positive, would be: Do the LIMIT / OFFSET query where data is valid, then a WRITE happens before we check for invalid. This will mean we have a valid page to return but opted for invalidating the paging source (throwing InvalidDataException
), this is OK.
yb...@google.com <yb...@google.com> #8
yea false positives is fine. i'm not fully convinced that a new API is necessary just to check invalidation without transaction to be honest, but keeping an open mind about it.
We should write a design doc for this.
ap...@google.com <ap...@google.com> #9
Branch: androidx-main
commit 3882ac8364fd905c9935d38c93f84599009226d6
Author: Clara Fok <clarafok@google.com>
Date: Fri Jul 09 14:17:48 2021
Implement LoadResult.Invalid to handle race
For non-initial loads in LimitOffsetPagingSource, loads will run Room
InvalidationTracker's refreshVersionSync to synchronously check if
tables have been invalidated and notify observers. If invalidated,
load will return LoadResult.Invalid.
Paging3 will handle Invalid return type by invalidating the paging
source and closing the page event flow, such that no more loads will be
attempted on this paging source.
Note that refreshVersionSync is a temporary solution for
LimitOffsetPagingSource to check table invalidation. Another solution
that does not require a transaction is in development. Related
Bug: 191806126
Test: ./gradlew :room:integration-tests:room-testapp-kotlin:cC
Test: ./gradlew room:room-paging:cC
Change-Id: I48634ab95949bae608374f5156a4edd0500a74a3
M room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
M room/room-paging/build.gradle
M room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
M room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
my...@gmail.com <my...@gmail.com> #10
Hello, i’m facing a problem on some devices where i have a paging source with multiple tables query (relationships join) when the user clicks on an item from the list and navigates to the details page the details page refreshes the “progress” for the entity which is one of the paging source relationships which in turn invalidates the paging source, the details page have a list of items that also fetches a progress, when the user navigates back the list which is a paging source takes seconds to display the pages I noticed two things:
-
when the loaded pages are less the fastest the pages display
-
when i remove the “progress” relation from the paging source query the issue is completely gone and the list displayed quickly
I hope that this issue gets resolved soon, Thanks for your efforts.
my...@gmail.com <my...@gmail.com> #11
Hello please find the attached sample project
Description
LimitOffsetPagingSource requires a way to validate db status so that it can eagerly call invalidation() on itself rather than waiting for signal propagation from InvalidationTracker.
One existing solution is to leverage InvalidationTracker's refreshVersionsSync() to synchronously check for db invalidation and notify observers. However, this is not ideal because it is executed in transaction and blocks LimitOffsetPagingSource's load().
The ideal solution would perform invalidation check without blocking load().