Skip to content

Conversation

@shubham1g5
Copy link
Contributor

@shubham1g5 shubham1g5 commented Jan 31, 2018

Summary:

  • Replaces FormProvider with FormDefRecord
  • Merges InstanceProvider into FormRecord
  • Refactors to replace all URIs dependent code to work on FormDefRecord sql ids and in process creates new versions for XFormAndroidInstaller and FormRecord
  • Takes care of migration from old user/app DBs to their newer versions

@ctsims
Copy link
Member

ctsims commented Feb 22, 2018

@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";
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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)

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

@shubham1g5 shubham1g5 Apr 11, 2018

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;
Copy link
Member

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?

Copy link
Contributor Author

@shubham1g5 shubham1g5 Mar 15, 2018

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;
Copy link
Member

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) {
Copy link
Member

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.

Copy link
Contributor Author

@shubham1g5 shubham1g5 Feb 22, 2018

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.

Copy link
Member

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:

  1. 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.

  2. 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)

Copy link
Contributor Author

@shubham1g5 shubham1g5 Feb 26, 2018

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.

Copy link
Member

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).

@shubham1g5
Copy link
Contributor Author

@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!

@ctsims
Copy link
Member

ctsims commented Feb 22, 2018

@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!

@shubham1g5
Copy link
Contributor Author

@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!

@ctsims
Copy link
Member

ctsims commented Feb 26, 2018

@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.

@shubham1g5
Copy link
Contributor Author

@ctsims Thanks. Makes sense now!

@shubham1g5
Copy link
Contributor Author

@ctsims Ready to review again

Copy link
Member

@ctsims ctsims left a 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.

  1. 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

  2. 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)

  3. 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");
Copy link
Member

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");
Copy link
Member

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";
Copy link
Member

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";
Copy link
Member

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)) {
Copy link
Member

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})
Copy link
Member

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!

Copy link
Contributor Author

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);
Copy link
Member

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.

Copy link
Contributor Author

@shubham1g5 shubham1g5 Apr 10, 2018

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 ?

Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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<>();
Copy link
Member

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.

Copy link
Contributor Author

@shubham1g5 shubham1g5 Apr 10, 2018

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 ?

Copy link
Member

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.

@dimagi dimagi deleted a comment from shubham1g5 Apr 10, 2018
@amstone326
Copy link
Contributor

@damagatchi retest this please

@shubham1g5
Copy link
Contributor Author

@ctsims This should be now ready for another pass.

@ctsims
Copy link
Member

ctsims commented Apr 16, 2018

@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?").

@shubham1g5
Copy link
Contributor Author

@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 FormDefRecord.java you will be able to see the required changes for pre-validating filePath.

@ctsims
Copy link
Member

ctsims commented Apr 17, 2018

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

@amstone326 amstone326 added this to the commcare_2.44 milestone Apr 18, 2018
@shubham1g5
Copy link
Contributor Author

Merging this now in anticipation to find early bugs!

@shubham1g5 shubham1g5 merged commit 13a36fb into master Apr 20, 2018
@shubham1g5 shubham1g5 deleted the mergeFormInsanceProviders branch April 20, 2018 09:56
@wpride
Copy link

wpride commented Apr 20, 2018

🎉 🎉 🎉 Congrats @shubham1g5 ! Looked like a slog

@shubham1g5
Copy link
Contributor Author

shubham1g5 commented Apr 20, 2018

Yeah. I finally understood InstanceRecord before killing it in favour of FormRecord :D

@wpride
Copy link

wpride commented Apr 20, 2018

To destroy your enemy you must know your enemy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants