-
-
Notifications
You must be signed in to change notification settings - Fork 45
Merge Form Provider and Insance Provider to App DB #1940
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
Conversation
…new Form and Instance tables in app DB
|
@shubham1g5 Hey, Shubham. Maybe easiest for us to chat over voice, I think my main point of confusion is that it's still not obvious to me why we need to maintain FormRecord and InstanceRecord as different models, rather than unifying them. Can you clarify that specific point at a high level? |
|
|
||
| public static final String STORAGE_KEY = "form_def"; | ||
|
|
||
| public static final String META_DISPLAY_NAME = "displayName"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we audit whether we need to retain all of these? I think many/most of these properties can actually be eliminated, but i could be wrong about that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, Some of these are actually not getting used right now. I think I was being overly cautious to not create unnecessary changes as part of this migration. Can remove these now though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ctsims Here is a list of all fields that are not getting used currently -
Instance Provider/ Instance Record
- submissionUri
- date (last status change date)
- displaySubtext
Form Def
- description
- submissionUri
- displaySubtext
- md5Hash
- date
- jrcacheFilePath
- language
Just want to make sure we are fine with removing all of these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised to hear that we aren't using "last status change date" anywhere, is that still true after Aliza's recent fix for form record ordering?
I don't believe we use any of the formdef fields, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The private key is also unused, right? that's some weird odk encryption stuff (we use our Symetric key, but not their public/private keypairs I thought)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reg: "last status change date": You are right. We had 2 date fields one in FormRecord and other in InstacneRecord. Now in the merged FormRecord I have retained the original FormRecord field which also gets used for form ordering.
Reg: "private key" : There is no private key currently in FormDefRecord. So guessing we already removed it earlier. Though we do use the public key here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am 99% that EncryptedFormInformation stuff is only used by ODK. That class is only called while reading forms currently (and we've never written any that way), if you trace the code back, so I think we can remove it without concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. Going to remove the EncryptionUtils.getEncryptedFormInformation as well since that will always return null in case of no public key
| import org.commcare.provider.FormsProviderAPI; | ||
| import org.commcare.provider.InstanceProviderAPI; | ||
| import org.commcare.resources.model.Resource; | ||
| import org.javarosa.core.util.externalizable.PrototypeFactory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this file needs to have all of the upgradeXtoY methods from 1 to 8 audited to make sure their records use the old version of the classes. We need to load old Resources with the V1 installer for XForms, for example, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point though none of these methods seem to be loading Resources i.e. performing a read on Resource tables!
| // Migrate records form FormProvider and InstanceProvider to new FormDefRecord and InstanceRecord respectively | ||
| private boolean upgradeEightNine(SQLiteDatabase db) { | ||
| db.beginTransaction(); | ||
| android.database.Cursor cursor = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking no lines of code should occur after db.begin until the try starts.
| return sInstanceRecordStorage; | ||
| } | ||
|
|
||
| public static void setinstanceRecordStorage(SqlStorage<InstanceRecord> instanceRecordStorage) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not OK with managing this state statically. I know it's annoying, but I think we need to refactor to move all of the static functionality in this class to being called through some other pattern.
It would be OK in my mind for it to be linked through the platform through some context object, but it has only ever caused a lot of headache/tech debt to allow static maintenance of the storage sandboxes, so we should figure out how to make this something that comes from the platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do, though I would be curious to know what kind of problems have we seen in past arising from storage sandboxes being singletons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two big things that have come up a lot:
-
We already have a lot of lifecycle objects around the code (java lifecycle, app lifecycle, user lifecycle) and it's a lot of overhead to maintain registering and unregistering data from them.
-
In many cases we are on a track to being able to decouple static state from apps entirely, which would give us pretty incredible capabilities like the ability to update apps in the background without interfering with seated applications. Adding new statically managed holders (rather than coupling them into another lifecycle object) significantly complicates those efforts by making a full refactor necessary and requiring us to inject the new lifecycle object in lots of places (this came up a ton with things like ReferenceManager, and it's a pattern we want to move away from)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Currently we have different versions of getUserStorage and getAppStorage in Application class here. So If I just use these whenever I need instances of FormDefRecord and InstanceRecord instead of using the static instances, it should be fine right ? Or are we looking for something more here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That should be fine. Passing the storage as a constructor to this (and/or wrapping that operation in a static getter from the Application) is a fine solution, since that puts us in the same state.
As a best-practice pattern I try to only call the singleton application methods from lifecycle objects (like the Activity classes), because it makes it easier to manage the outcome if the lifecycle is in a bad state (say, the user has been logged out).
|
@ctsims I did not think that we would like to merge the two tables. I was confused myself in utility of having them as separate tables though assumed that we might want to keep the separation as it is even in the new world. Will work on merging them now! |
|
@shubham1g5 The reason they were split was because for a while we were trying to assume we'd treat ODK as a separate service on the phone (when android looked like it was going to be able to work that way), so we had to add our metadata somewhere else (which is where we made FormRecord). Now that we're scrapping the form entry modularity I think it just adds overhead. Thanks for the merge! |
|
@ctsims Is there a philosophy behind what things go to UserDb vs what goes to AppDb. When I created InstanceRecord and FormDefRecord, they both seemed app specific to me and so I added them to AppDB but now when I see the FormRecord it's part of UserDb. So now I am confused where should I put the merged version of FormRecord and InstanceRecord! |
|
@shubham1g5 FormRecords (and InstanceRecords) are UserDB Objects. They are encrypted by the user's keys and are protected assets for the user storage sandbox. FormDefRecords are part of the App sandbox because they are a shared resource that is accessed by any user who is logged into the app and stored unencrypted. |
|
@ctsims Thanks. Makes sense now! |
|
@ctsims Ready to review again |
ctsims
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for the continued hard work on this!
This looks super super close to me. I made 2-3 types of comments.
-
Most critical - This code breaks legacy installs from the oldest version of our code. It may be that retaining that legacy code requires keeping around lots of the content provider code, which might not be worth it. We may need to discuss as a team
-
Transaction Semantics - Failures in certain parts of the code should I think be permitted given two bad alternatives which could result in busted states (one inevitable, one hypothetical)
-
General and minor error messaging and small feedback.
| instanceState.setFormDefPath(formDefRecord.getFilePath()); | ||
| return new Pair<>(formDefRecord.getID(), isInstanceReadOnly); | ||
| } else if (formDefRecords.size() < 1) { | ||
| throw new FormEntryActivity.FormQueryException("Parent form does not exist"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "Parent" here is going to be a confusing word to whoever is reading this error.
It would be more helpful to describe "There was no XForm definition defined" and probably to also list the XMLNS of the form (although that may be more useful in a notification since it will be technobabble to most people)
| } else if (formDefRecords.size() < 1) { | ||
| throw new FormEntryActivity.FormQueryException("Parent form does not exist"); | ||
| } else { | ||
| throw new FormEntryActivity.FormQueryException("More than one possible parent form"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this error and the error above, we should also be soft-asserting (either here or where this is caught) very explicitly that this happened, along with the XMLNS
|
|
||
| public static final String STORAGE_KEY = "form_def"; | ||
|
|
||
| public static final String META_DISPLAY_NAME = "displayName"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised to hear that we aren't using "last status change date" anywhere, is that still true after Aliza's recent fix for form record ordering?
I don't believe we use any of the formdef fields, though.
|
|
||
| public static final String STORAGE_KEY = "form_def"; | ||
|
|
||
| public static final String META_DISPLAY_NAME = "displayName"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The private key is also unused, right? that's some weird odk encryption stuff (we use our Symetric key, but not their public/private keypairs I thought)
| public int save(SqlStorage<FormDefRecord> formDefRecordStorage) { | ||
| // if we don't have a path to the file, the rest are irrelevant. | ||
| // it should fail anyway because you can't have a null file path. | ||
| if (StringUtils.isEmpty(mFormFilePath)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a bit of an odd time to fail on this to me. Can't we validate that this path isn't empty when it is being set, rather than when it is being used?
| public static final String STATUS_JUST_DELETED = "just-deleted"; | ||
| private static final String TAG = FormRecord.class.getName(); | ||
|
|
||
| @StringDef({STATUS_UNSTARTED, STATUS_INCOMPLETE, STATUS_COMPLETE, STATUS_UNSENT, STATUS_SAVED, STATUS_QUARANTINED, STATUS_UNINDEXED, STATUS_JUST_DELETED}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really cool pattern, thanks for sharing it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. It also saves memory overhead caused by Enums as these annotations are compile time only.
| } | ||
|
|
||
| // Delete migrated entries | ||
| context.getContentResolver().delete(FormsProviderAPI.FormsColumns.CONTENT_URI, null, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So one quick point:
Errors in this operation can't be rolled back, and also aren't material to the future functionality of the app, since this CR will be ignored moving forward.
I think for those reasons that moving this op outside of the v8->v9 Migrater makes a bit more sense.
Right now, it's:
transaction[
1) add table
2) move data
3) delete old data
]
and if "delete old data" deletes half of the data before failing, the first two ops will be undone, but the delete old data op won't be, which will put the phone in an irrecoverable state. If instead #3 was treated as a "best effort" op
transaction[
1) add table
2) move data
]
try{
3) delete old data
} catch(){
//oops... do nothing.
}
"Half of a failure" in step three doesn't result in an irrecoverable app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh never realised deletes are not atomic. I think one other way to do this is wrap the delete call in Conent Provider here in a transaction. What do you think ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would make everything here work reliably, as long as the transaction for this code is halted immediately after the delete succeeds. I'm still a bit hesitant to introduce semantics where the ContentProvider.delete() call can succeed (in whole or in part) while the migrate transaction is still open, just as a matter of most-importance.
Like, if the line of code that says "commit the ContentProvider DB delete transaction" executes and then the phone battery dies, the next time the app spins up, our DB will roll back the migrate transaction and the content provider data will be gone.
If we are super concerned with the ContentProvider delete occurring, I'd rather we separate those transactional semantics from the semantics of getting the data into this db. It's ok for the app to crash if the delete is failing (if that's a real bug that we should be catching and fixing), but I want to separate that condition from this transaction, since there's no way to use SQL transactions in a way which will Atomically link loading the new DB with the ContentProvider data, and deleting the data in the content provider DB. We'd need that to come from a separate source.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I see the concerns. Will make the changes accordingly.
| // Merge Record's instance | ||
| String instanceUri = oldRecord.getInstanceURIString(); | ||
| Cursor cursor = c.getContentResolver().query(Uri.parse(instanceUri), null, null, null, null); | ||
| if (cursor != null && cursor.getCount() > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quick note that technically cursor operations should be wrapped in try{}closes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what will that achieve us here since 1. This whole function is wrapped in try{} 2. We want to abort the whole thing if an error occurs in cursor operations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to catch any exceptions, we just need a try{}finally{} block where the cursor gets closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha!
| } | ||
|
|
||
| // delete all instance provider entries | ||
| c.getContentResolver().delete(InstanceProviderAPI.InstanceColumns.CONTENT_URI, null, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same note as in the other migration that I think it's a better practice for us to track "success" for the transaction of this operation before this delete and not after, since it inherently can't be a part of the transaction
|
|
||
| //We also need to tell the XForm Provider that any/all of its forms have been moved | ||
|
|
||
| ArrayList<Pair<Uri, String>> toReplace = new ArrayList<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important Note: -This code will no longer work after this change. This legacy installer should continue assuming that a form provider exists (using hard-coded strings or whatever else is retained) or we should kill the legacy updater code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Didn't realize this block of code runs before DB update. We can defnitely keep the Content Provider code if it's worth it. What all versions of CommCare does legacy actually represent ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed over slack, but the Legacy db represents CommCare 2.3.0 and below, so we can move forward with the assumption that we are removing legacy migration code.
…of the transaction
…in case of multiple apps installed on same device
|
@damagatchi retest this please |
|
@ctsims This should be now ready for another pass. |
|
@shubham1g5 I think this covers everything I was concerned about other than one lingering comment above ("This seems like a bit of an odd time to fail on this to me. Can't we validate that this path isn't empty when it is being set, rather than when it is being used?"). |
|
@ctsims I made a mistake in incuding that change as part of a merge commit which I am guessing is why that change is not getting reflected here. But if you go to this commit and search for |
|
Great, I'm all set, then. Thanks for going through the 3-4 rounds of random testing for multiple apps, multiple users, etc! Ready to merge once we've cut 2.33 |
|
Merging this now in anticipation to find early bugs! |
|
🎉 🎉 🎉 Congrats @shubham1g5 ! Looked like a slog |
|
Yeah. I finally understood |
|
To destroy your enemy you must know your enemy |
Summary:
FormProviderwithFormDefRecordInstanceProviderintoFormRecord