Skip to content

Conversation

@darinkrauss
Copy link

  • https://tidepool.atlassian.net/browse/LOOP-1201
  • Add critical event log export to several Core Data stores
  • Add Encodable to various Core Data models
  • Add modificationCounter to DeviceLogEntry
  • Add JSONStreamEncoder
  • Replace StoredSettings.InsulinModel with StoredInsulinModel
  • Fix PumpManagerStatus Codable for optional pumpStatusHighlight and pumpLifecycleProgress

Note: This will require @ps2 and one other reviewer. Also, I looked into breaking this into smaller PRs, but could not come up with a way to do so that made any sense. (I could have broken out the Codable implementations, but that doesn't make this PR any easier, just a bit smaller, and is not worth the work.)

Note: All of the store implementations of CriticalEventLog are very similar (with only a few differences unique to each store). Once you've understood one, you've understood 95% of the rest).

- https://tidepool.atlassian.net/browse/LOOP-1201
- Add critical event log export to several Core Data stores
- Add Encodable to various Core Data models
- Add modificationCounter to DeviceLogEntry
- Add JSONStreamEncoder
- Replace StoredSettings.InsulinModel with StoredInsulinModel
- Fix PumpManagerStatus Codable for optional pumpStatusHighlight and pumpLifecycleProgress
@darinkrauss
Copy link
Author

Related LoopWorkspace PR for demonstration of build: https://github.com/tidepool-org/LoopWorkspace/pull/250

Copy link

@ps2 ps2 left a comment

Choose a reason for hiding this comment

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

LGTM. If these operations are really background operations, and we no longer need progress estimation; I'd recommend that be cleaned up; there is a lot of code around progress, and possibly some significant io due to the count(*) operations that I think are only necessary for progress reporting.


// MARK: - Critical Event Log Export

extension CarbStore: CriticalEventLog {
Copy link

Choose a reason for hiding this comment

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

Agree with Rick; if this work is really isolated to an extension, putting it into a separate file might make this a little more navigable.

Copy link
Author

Choose a reason for hiding this comment

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

Okay, I can do this, but if we are going this way, we should make this a general, well-known policy. We haven't been doing this before.

Choose a reason for hiding this comment

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

I don't feel that strongly, though, FWIW

Copy link

Choose a reason for hiding this comment

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

Yeah, it's not a big issue for me either; just seems a little more navigable.

Copy link

Choose a reason for hiding this comment

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

I'd be fine if we don't do this here.

Copy link
Author

Choose a reason for hiding this comment

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

The code in this extension uses the managed object context, which is a private property. To move this extension out of the main file would require changing the property access from private to internal. I believe keeping the property private is more important than moving the extension to a separate file. Going to leave as-is unless anyone objects.

Copy link
Author

Choose a reason for hiding this comment

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


import Foundation

public protocol EstimatedDurationProgressor {
Copy link

Choose a reason for hiding this comment

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

The idea of cancelable progress and rolled up tasks seems really similar to the built in Progress class: https://developer.apple.com/documentation/foundation/progress

Copy link

Choose a reason for hiding this comment

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

On a larger scale; if this is happening in the background, do we need progress reporting?

Copy link
Author

Choose a reason for hiding this comment

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

We need progress for the full export in case the nightly exports have (for whatever reason) not completed. It is an edge case, but it definitely needs progress. (Also, with my current work looking into a different zip implementation, it looks like it may take longer than 3-5 seconds for the full export to complete so we at least need a progress bar.)

I did look into Progress and at the time did not think it appropriate. I will revisit.

Copy link
Author

Choose a reason for hiding this comment

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

Updated to use Progress. Required a number of downstream changes, but should be good now. ✅

let request: NSFetchRequest<DeviceLogEntry> = DeviceLogEntry.fetchRequest()
request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)

let objectCount = try self.managedObjectContext.count(for: request)
Copy link

Choose a reason for hiding this comment

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

count(*) operations often require a full table scan (and thus a lot of io). I'm not sure that's the case here or how heavily you're relying on this being performant, but thought I'd mention it. Count can be faster than an actual fetch because you save on hydration; but since IO is usually the slowest part of the query, the count operation might be nearly as slow an actual fetch of all the objects involved.

Copy link
Author

Choose a reason for hiding this comment

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

We need progress (as discussed above) and progress highly depends upon the amount of data, thus the need for the count. The count has only a little effect on overall duration. (It was necessary to add indexes on dosing decisions and device comms table for exporting in general, not specific to progress, so this solves most of the issue).

Copy link
Author

Choose a reason for hiding this comment

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

Copy link

@rickpasetto rickpasetto left a comment

Choose a reason for hiding this comment

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

Wow, this is a lot of work! Great job, Darin!
Sorry, but I have a few questions that I need answered before I can approve. Happy to hop on a call to discuss.


// MARK: - Critical Event Log Export

extension CarbStore: CriticalEventLog {

Choose a reason for hiding this comment

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

I don't feel that strongly, though, FWIW

/// - stream: The output stream to write the critical event log to. Typically writes JSON UTF-8 text.
/// - progressor: The estimated duration progress to use to check if cancelled and report progress.
/// - Returns: Any error that occurs during the export, or nil if successful.
func export(startDate: Date, endDate: Date, to stream: OutputStream, progressor: EstimatedDurationProgressor) -> Error?

Choose a reason for hiding this comment

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

[nit] Consider returning Result<Void, Error> or, if more appropriate, having the function throw

Copy link
Author

Choose a reason for hiding this comment

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

Per discussion, going to leave as-is.

Copy link
Author

Choose a reason for hiding this comment

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


import Foundation

extension OutputStream {

Choose a reason for hiding this comment

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

[nit] unit test for this?

Copy link
Author

Choose a reason for hiding this comment

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

Good catch, done. ✅

return result!
}

public func export(startDate: Date, endDate: Date, to stream: OutputStream, progressor: EstimatedDurationProgressor) -> Error? {

Choose a reason for hiding this comment

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

These export(...) functions on the Stores are so similar. Is there a way to factor them into common code?

Copy link
Author

@darinkrauss darinkrauss Sep 18, 2020

Choose a reason for hiding this comment

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

I gave it a try and found there are too many differences between the different stores for it to be straightforward. It is possible, of course, but there would have to be at least on "customizer" function to account for the store differences and the final code would be less understandable.

For example, each store counts each object as a different amount towards overall progress (which is because each object takes a different amount of time to query, serialize, and write - there is a very large difference in object size between, say, alerts and dosing decisions). So each store would have to customize this cost while the common code is iterating over and exporting each object.

Also, while most stores use a modificationCounter property to sort and query on, the CarbStore uses an anchorKey property. These properties are used to provide a stable sort for paging data during an export.

These differences and others would make the code much less understandable if we tried to make a common "export" function.

Long term we could work on making the stores more similar in functionality (and property names) and this might be easier.

self.syncIdentifier = syncIdentifier
}

public struct InsulinModel: Codable, Equatable {

Choose a reason for hiding this comment

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

Hooray!! 💯

Copy link
Author

Choose a reason for hiding this comment

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


private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
var addedDatePredicate = NSPredicate(format: "addedDate >= %@", startDate as NSDate)
var supercededDatePredicate = NSPredicate(format: "supercededDate >= %@", startDate as NSDate)

Choose a reason for hiding this comment

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

I have a roughly similar question about this that I had with AlertStore. If you're looking for something "after" startDate, and supercededDate is never before addedDate, then why do you have to include it in the predicate?

Similarly for endDate: what if the record is modified with supercededDate that is later than an addedDate but endDate is in between them? i.e.: addedDate < endDate < supercededDate?

Copy link
Author

Choose a reason for hiding this comment

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

Since historical exports are captured on a daily basis it is necessary to cover the use case of the carb object added during one daily period and the carb object superseded in a subsequent daily period. If endDate is nil, then the final predicate can simplified slightly at the expense of slightly more complicated code, but that is not the common use case. (Most exports are historical and those will have an endDate specified.) Going to keep as-is.

Copy link
Author

Choose a reason for hiding this comment

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

Copy link

@rickpasetto rickpasetto left a comment

Choose a reason for hiding this comment

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

LGTM! 🥍

@darinkrauss darinkrauss merged commit d3851ce into dev Sep 19, 2020
@darinkrauss darinkrauss deleted the darinkrauss/LOOP-1201-export-critical-event-logs branch September 19, 2020 04:11
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.

4 participants