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

Updated PerResourcePatchGenerator to return ordered PatchMapping to avoid referential integrity issues. #2442

Merged
merged 14 commits into from
Mar 11, 2024

Conversation

aditya-07
Copy link
Collaborator

@aditya-07 aditya-07 commented Feb 15, 2024

IMPORTANT: All PRs must be linked to an issue (except for extremely trivial and straightforward changes).

Fixes #[issue number]

Description

  1. Create a Directed Graph based on all the outgoing references for each Patch. Since the referential integrity comes to picture if the resource is not created yet, the edges of the graph are only the outgoing references to resources that are getting created during this upload. This may helps avoid some cycles as well.
  2. The references are taken from LocalChangeResourceReferenceEntity which stores each outgoing references associated with a particular LocalChange as new entry.
  3. Order the Patches so that if the resource A has outgoing references {B,C} (CREATE) and {D} (UPDATE), then B,C needs to go before the resource A so that referential integrity is retained. Order of D shouldn't matter for the purpose of referential integrity.

Alternative(s) considered

  1. Splitting changes to CREATE first and subsequent UPDATE, but this would also require ordering the the create list incase it can overflow to multiple Bundles. This would be similar to the current approach but, would require multiple PUSH for the same resource incase the resource was Created and then Updated on the device before being pushed to the server.

Type
Choose one: Bug fix

Screenshots (if applicable)

Checklist

  • I have read and acknowledged the Code of conduct.
  • I have read the Contributing page.
  • I have signed the Google Individual CLA, or I am covered by my company's Corporate CLA.
  • I have discussed my proposed solution with code owners in the linked issue(s) and we have agreed upon the general approach.
  • I have run ./gradlew spotlessApply and ./gradlew spotlessCheck to check my code follows the style guide of this project.
  • I have run ./gradlew check and ./gradlew connectedCheck to test my changes locally.
  • I have built and run the demo app(s) to verify my change fixes the issue and/or does not break the demo app(s).

Copy link
Collaborator

@MJ1998 MJ1998 left a comment

Choose a reason for hiding this comment

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

I feel we can improve complexity of this solution by using LocalChangeReferenceEntity table to fetch all the outgoing references.

@aditya-07 aditya-07 marked this pull request as ready for review February 22, 2024 18:02
Copy link
Collaborator

@MJ1998 MJ1998 left a comment

Choose a reason for hiding this comment

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

Nice job stitching the LocalChangeResourceReferenceEntity here.

Although I dont like PatchGenerator having access to database which is from another package. I think we should abstract this out in a separate interface which is parent of the two packages: db and sync.
Maybe FhirSyncDbInteractor works well in this case as well. Will raise a PR soon so that this can be imagined better.

@MJ1998
Copy link
Collaborator

MJ1998 commented Feb 28, 2024

I feel this approach cannot solve for SingResource uploading strategy. Issue #2454.

But I also believe we should not discard this strategy as some fhir servers may not support bundle transaction and relying on SingleChange strategies can become expensive.

Currently for SingleResource upload strategy we fetch relevant LocalChanges from db, generate a patch and upload it.
What if we fetch all LocalChanges irrespective of the strategy, generate patches, order the patches according to this PR and then based on the upload strategy create upload requests ?!

@jingtang10 @aditya-07

Copy link
Collaborator

@jingtang10 jingtang10 left a comment

Choose a reason for hiding this comment

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

thanks aditya! great job!

Copy link
Collaborator

@MJ1998 MJ1998 left a comment

Choose a reason for hiding this comment

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

I think we can have a better design:-
Crux: Instead of fetching LocalChanges we will fetch LocalChange.id as we don't need full LocalChange objects to create a toposort of Patches.

Steps:-

  1. fetchLocalChangeIds(): List<LocalChangeId>
  2. orderByResourceTypeAndId(): Map<ResourceTypeWithId, List<LocalChangeId>>
    [ referencing ResourceTypeWithId as RTWI, and List<LocalChangeId> as IDPatch ]
    So orderByResourceTypeAndId(): Map<RTWI, IDPatch>
  3. buildTopoSort(): List<Pair<RTWI, IDPatch>> (use database.getLocalChangeResourceReferenceIds)
  4. Process this list based on UploadStrategy. For "single-resource" strategy process one by one, for "all-local-changes" process all of them at once. process = fetch whole of local changes: Flow<List<LocalChange>>
  5. patchGenerator(List<LocalChange>>): List<Patch>
  6. uploadRequestGenerator(List<Patch>): List<UploadRequest>

Pros:-
This will significantly reduce the memory consumed by delaying the fetching of full LocalChange objects.
Supports both upload strategies.

Con:-
One caveat could be double database fetching. But fetching just the LocalChangeIds should not take much time as its LocalChangeEntity's PrimaryKey.

I think given we want to support offline, memory is a more concerning issue than time complexity to sync here. But without profiling the two designs agains a db it's difficult to say.

Also, maybe we can take this up as a new issue and a new PR.

@jingtang10
Copy link
Collaborator

I think we can have a better design:- Crux: Instead of fetching LocalChanges we will fetch LocalChange.id as we don't need full LocalChange objects to create a toposort of Patches.

Steps:-

  1. fetchLocalChangeIds(): List
  2. orderByResourceTypeAndId(): Map<ResourceTypeWithId, List>
    set alias RTWI = ResourceTypeWithId
    set alias IDPatch = List
    So orderByResourceTypeAndId(): Map<RTWI, IDPatch>
  3. buildTopoSort(): List<Pair<RTWI, IDPatch>> (use database.getLocalChangeResourceReferenceIds)
  4. Process this list based on UploadStrategy. For "single-resource" strategy process one by one, for "all-local-changes" process all of them at once. process = fetch whole of local changes: Flow<List>
    Now our old approach
  5. patchGenerator(List>): List
  6. uploadRequestGenerator(List): List

Pros:- This will significantly reduce the memory consumed by delaying the fetching of full LocalChange objects. Supports both upload strategies.

Con:- One caveat could be double database fetching. But fetching just the LocalChangeIds should not take much time as its LocalChangeEntity's PrimaryKey.

I think given we want to support offline, memory is a more concerning issue than time complexity to sync here. But without profiling the two designs agains a db it's difficult to say.

Also, maybe we can take this up as a new issue and a new PR.

thanks for this suggestion jajoo! i'd like to understand a bit better the exact savings of this approach. but yes we should do this in a separate issue.

@jingtang10 jingtang10 enabled auto-merge (squash) March 8, 2024 17:31
@jingtang10 jingtang10 requested a review from MJ1998 March 8, 2024 17:32
@jingtang10
Copy link
Collaborator

looks good - if jajoo is ok with this you can merge this aditya!

thanks!

Copy link
Collaborator

@MJ1998 MJ1998 left a comment

Choose a reason for hiding this comment

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

LGTM

@jingtang10 jingtang10 merged commit 9697a8a into google:master Mar 11, 2024
4 checks passed
Comment on lines +351 to +361
@Query(
"""
SELECT *
FROM LocalChangeResourceReferenceEntity
WHERE localChangeId = (:localChangeId)
""",
)
abstract suspend fun getReferencesForLocalChanges(
localChangeId: List<Long>,
): List<LocalChangeResourceReferenceEntity>

Copy link
Collaborator

Choose a reason for hiding this comment

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

Upload on sync fails here with

(1) row value misused in "SELECT *
        FROM LocalChangeResourceReferenceEntity
        WHERE localChangeId = (?,?,?,?,?,?,?,?,?,?,?)"
        
java.util.concurrent.ExecutionException: android.database.sqlite.SQLiteException: row value misused (code 1 SQLITE_ERROR): , while compiling: SELECT *
        FROM LocalChangeResourceReferenceEntity
        WHERE localChangeId = (?,?,?,?,?,?,?,?,?,?,?)
	at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:515)
	at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:474)
	at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:316)
	at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
	at java.lang.Thread.run(Thread.java:1012)
Caused by: android.database.sqlite.SQLiteException: row value misused (code 1 SQLITE_ERROR): , while compiling: SELECT *
        FROM LocalChangeResourceReferenceEntity
        WHERE localChangeId = (?,?,?,?,?,?,?,?,?,?,?)
	at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
	at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1069)
	at android.database.sqlite.SQLiteConnection.prepare(SQLiteConnection.java:673)
	at android.database.sqlite.SQLiteSession.prepare(SQLiteSession.java:590)
	at android.database.sqlite.SQLiteProgram.<init>(SQLiteProgram.java:62)
	at android.database.sqlite.SQLiteQuery.<init>(SQLiteQuery.java:37)
	at android.database.sqlite.SQLiteDirectCursorDriver.query(SQLiteDirectCursorDriver.java:46)
	at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1714)
	at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1689)
	at androidx.sqlite.db.framework.FrameworkSQLiteDatabase.query(FrameworkSQLiteDatabase.kt:156)
	at androidx.room.RoomDatabase.query(RoomDatabase.kt:490)
	at androidx.room.util.DBUtil.query(DBUtil.kt:75)
	at com.google.android.fhir.db.impl.dao.LocalChangeDao_Impl$20.call(LocalChangeDao_Impl.java:927)
	at com.google.android.fhir.db.impl.dao.LocalChangeDao_Impl$20.call(LocalChangeDao_Impl.java:924)
	at androidx.room.CoroutinesRoom$Companion$execute$4$job$1.invokeSuspend(CoroutinesRoom.kt:88)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	... 3 more

Guessing the query should probably use an IN instead of =

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

the temporary change below worked for me

SELECT *
        FROM LocalChangeResourceReferenceEntity
        WHERE localChangeId IN (:localChangeId)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes it should be IN. Strange the test case didn't track this.
Probably the function is being called with one LocalChangeId in the list.

@aditya-07 might wanna add / modify the test case to cover this scenario ?!

ndegwamartin added a commit to opensrp/android-fhir that referenced this pull request Apr 12, 2024
…ing to avoid referential integrity issues. (google#2442)"

This reverts commit 9697a8a.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Complete
Development

Successfully merging this pull request may close these issues.

None yet

6 participants