NSPersistentStoreRemoteChangeNotification not getting fired - core-data

I am trying to perform history tracking in my CoreData+CloudKit project which uses NSPersistentCloudKitContainer. I have been following along with Apple's sample project
I want to perform certain task when the remote store has been updated. For this apple recommends enabling remote notification in the Signing & capabilities's Background Mode section of the app.
I have enabled History Tracking for my project as shown in Apple's sample project.
// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber,
forKey: NSPersistentHistoryTrackingKey)
// ...
Also I have registered my store to listen for store changes.
// turn on remote change notifications
let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
description?.setOption(true as NSNumber,
forKey: remoteChangeKey)
// ...
Observer is also added to listen for NSPersistentStoreRemoteChangeNotification.
However there is no NSPersistentStoreRemoteChangeNotification being fired. To make sure there is no mistake in my implementation, I am have simply put breakpoints in #objc func storeRemoteChange(_ notification: Notification) the Apple's provided sample code but still I can not see any notification being fired and no breakpoints are activated.
I have understood the deduplication of the Tags done in the sample project and also tried testing it but without any success. Is it a bug in the Apple's implementation or am I missing any setup which is required?

My guess is you are observing the container instead of the store coordinator, add your observer like this:
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
Note the last param container.persistentStoreCoordinator
And a warning, this notification comes in on all different threads so you be careful with concurrency. Just put a 5 second sleep in the method and you'll see on app launch 3 different threads call it. This is likely why in the example there is a historyQueue with maxOperationCount 1 to handle it.
Some notifications have NSPersistentHistoryTokenKey in the userInfo not sure why.

Debugging the sample app mentioned by the OP, I observed the following:
As of XCode Version 11.3 (11C29), there are SDK constants both for the option key (NSPersistentStoreRemoteChangeNotificationPostOptionKey) and for the notification name (.NSPersistentStoreRemoteChange), and these are reflected in the latest download of the sample code.
The sample app registers for the remote change notifications on the wrong object, so it never receives any. Changing the sender as per the accepted answer fixes this.
The app UI always updates to reflect changes received from the cloud, but those updates are prompted not by remote change notifications but by the app's NSFetchedResultsController delegate using the controllerDidChangeContent callback to refresh the UI.
The standard NSPersistentCloudKitContainer used by the sample app is doing automatic imports into the local persistent store of all the cloud-sent updates and, because the persistentStore is set up for history tracking and the viewContext is set up to auto-update to the latest generation of data, each import triggers a UI update.
Based on these observations, I wrote a small app from scratch based on the XCode template you get by specifying use of CoreData, CloudKit, and SwiftUI. I set up its persistent container and view context the same way they are set up in the sample app, and used SwiftUI's #FetchRequest wrapper to obtain the data in the master view display. Sure enough, I saw the exact same remote import behavior without using any remote change notifications, and the UI updated after each import.
I then confirmed that, as per the accepted answer, if I registered for remote change notifications correctly, they would be received. They seem to be sent after each receive and import operation in the NSPersistentCloudKit completes. Observing them is not needed to get notifications of the local data changes initiated by those imports.

I was able to reliably echo Core Data changes via iCloud between two devices in my project. But I reached a point where I needed access to the change history. Apple has nicely described the steps to set it up in Consuming Relevant Store Changes
I followed along and happily copy and pasted the relevant code into my app. But the NSPersistentStoreRemoteChange notification was not coming through. As in comedy, timing is everything. Per the documentation for persistentStoreDescriptions I
If you will be configuring custom persistent store descriptions, you
must set this property before calling
loadPersistentStores(completionHandler:)
I was configuring persistentStoreDescriptions inside of loadPersistentStores(completionHandler:) So the painfully obvious way to do it is setup the following code in the AppDelegate.
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentCloudKitContainer(name: "yourProjectNameGoesHere")
// turn on persistent history tracking
// https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber,
forKey: NSPersistentHistoryTrackingKey)
// turn on remote change notifications
let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
description?.setOption(true as NSNumber,
forKey: remoteChangeKey)
// this will make background updates from iCloud available to the context.
container.viewContext.automaticallyMergesChangesFromParent = true
// call this LAST, after the persistentStoreDescriptions configuration.
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
Catch the notification from your view controller or model.
init() {
NotificationCenter.default.addObserver(self,
selector: #selector(fetchChanges),
name: .NSPersistentStoreRemoteChange,
object: pc.persistentStoreCoordinator)
}
#objc func fetchChanges(note: Notification) {
print("Just received a NSPersistentStoreRemoteChange notification")
}

I don't know whether it's a bug. Simply downloading and running the Apple's Sample Project but the NSPersistentStoreRemoteChangeNotification is never fired.
I added one more observer for the same NSPersistentStoreRemoteChangeNotification in my AppDelegate and it is firing.
I added notification observer in AppDelegate and then simply call the StoreRemoteChange(_:) of the CoreDataStack. Also, Tag deduplication logic works properly.
Here is the code which I added in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// The view controller hierarchy is defined in the main storyboard.
guard let splitViewController = window?.rootViewController as? UISplitViewController,
let navController = splitViewController.viewControllers[splitViewController.viewControllers.count - 1] as? UINavigationController,
let topViewController = navController.topViewController else {
return false
}
// Configure the splitViewController.
topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
splitViewController.delegate = self
splitViewController.preferredDisplayMode = .allVisible
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: nil)
return true
}
#objc
func storeRemoteChange(_ notification: Notification) {
coreDataStack.storeRemoteChange(notification)
}

SwiftUI
Here's a way to be notified of CloudKit remote changes in a SwiftUI view, and, say, update the contents of a List that would depend on a #FetchRequest--not shown in the code for simplicity:
struct MyView: View {
#State var refresh = UUID()
var didRemoteChange = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: RunLoop.main)
var body: some View {
List {
// ...
}
.id(refresh)
.onReceive(self.didRemoteChange) { _ in
self.refresh = UUID()
}
}
}
Note: .receive(on: RunLoop.main) is necessary in order to avoid modifying the UI from a background thread, as the remote event could (and will) otherwise fire from a background thread. Alternatively, .receive(on: DispatchQueue.main) can also be used.
For that to work, the NSPersistentCloudKitContainer needs to be set up to fire events when remote changes occur:
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "YourApp")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
//
// Generate notifications upon remote changes
//
container.persistentStoreDescriptions.forEach {
$0.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}

Related

SwiftUI: How to add CoreData record from Siri Intent

I am trying to create an Intent that saves a record to a CoreData database. The record will be created if I run the code from the main app, but not in the Intent.
Here is the code:
import Intents
import CoreData
import SwiftUI
let persistenceController = PersistenceController.shared
class IntentHandler: INExtension, DiaryIntentHandling
{
var moc = PersistenceController.shared.context
override func handler(for intent: INIntent) -> Any?
{
guard intent is DiaryIntent else
{
fatalError("Unknwonwn intent type: \(intent)")
}
return self
}
func handle(intent: DiaryIntent, completion: #escaping (DiaryIntentResponse) -> Void)
{
guard let message = message
else
{
completion(DiaryIntentResponse(code: .failure, userActivity: nil))
return
}
completion(DiaryIntentResponse.success(message: message))
let context = PersistenceController.shared.container.viewContext
let myRecord = MyRecord(context: context)
myRecord.timestamp = Date()
myRecord.message = message
do {
try context.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
func resolveMessage(for intent: DiaryIntent, with completion: #escaping (INStringResolutionResult) -> Void)
{
if let message = intent.message
{
completion(INStringResolutionResult.success(with: message))
}
else
{
completion(INStringResolutionResult.needsValue())
}
}
public func confirm(intent: DiaryIntent, completion: #escaping (DiaryIntentResponse) -> Void) {
completion(DiaryIntentResponse(code: .ready, userActivity: nil))
}
}
Do I need to share access to the CoreData database? How do I create the record?
App extensions work like separate apps, so you need to set up an app "group" to share data between them. It gives you a directory that's not part of your app's sandbox that your app and your app extensions can share. Using one requires some setup work:
Turn on app groups by adding the group entitlement. Apple has some documentation on this. I also have a somewhat old blog post that's still accurate as far as setting up the group.
Set up your persistent container use the group directory for Core Data. Normally it saves data in your app's sandbox, but you can tell it to use the app group directory. To do that,
Get a file URL for the directory using FileManager's function containerURL(forSecurityApplicationGroupIdentifier:). The argument is the same as your app group identifier.
Make sure this directory exists! It doesn't get created automatically. Use FileManager.default.fileExists(atPath:) to check if it exists, and if not, use FileManager.default.createDirectory(at:withIntermediateDirectories:attributes:) to create it.
Use a NSPersistentStoreDescription to tell your persistent container to use that URL for Core Data. That would be something like
let persistentContainer = NSPersistentContainer(name: containerName)
let persistentStoreDescription = NSPersistentStoreDescription(url: persistentStoreUrl)
persistentStoreDescription.type = NSSQLiteStoreType
persistentContainer.persistentStoreDescriptions = [ persistentStoreDescription ]
After the previous step, the persistent container won't be able to find any data that's currently in Core Data. So:
If your app has not already been released, delete it from your test devices and simulators and then rebuild. You'll get a new persistent store in the app group directory.
If your app has already been released, add code to copy the persistent store from the current location to the new location. Do this before loading the persistent store, and only do it if the copy in the app group doesn't already exist (so you don't re-copy old data). The best way to do that is with the migratePersistentStore(_:to:options:withType:) function from NSPersistentStoreCoordinator. Don't just copy your SQLite file over, because that won't include all the data.

callkit with pushkit in xamarin

I'm trying to integrate callkit with pushkit in one of my app in xamarin using Twilio voip. I was able to do so by defining required classes and delegates.
I can receive a call when my app is in foreground. but when my app is backgrounded or killed, its not received.
I have this method in my appdelegate:
[Export("pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:")]
[Preserve(Conditional = true)]
public void DidReceiveIncomingPush(PKPushRegistry registry, PKPushPayload payload, string type, Action completion)
{
try
{
Console.WriteLine("My push is coming (Inside Action method!");
var callerid = payload.DictionaryPayload["twi_from"].ToString();
Console.WriteLine($"from: {callerid}");
completion= delegate {
if (payload != null)
{
TwilioService.Setnotification(payload);
}
};
completion.Invoke();
// Tried using only this
// completion(); but it didn't work.
}catch(Exception ex)
{
Console.WriteLine(ex.message);
}
}
So question is how to bring Native dialer when call is arriving and app is in background or killed. I don't understand how to use "Action" parameter of above method.
I see this error in my device logs:
Info (114) / callservicesd: Application <private> will not be launched because it failed to report an incoming call too many times (or repeatedly crashed.)
Thanks.

Application Insights telemetry correlation using log4net

I'm looking to have our distributed event logging have proper correlation. For our Web Applications this seems to be automatic. Example of correlated logs from one of our App Services API:
However, for our other (non ASP, non WebApp) services were we use Log4Net and the App Insights appender our logs are not correlated. I tried following instructions here: https://learn.microsoft.com/en-us/azure/azure-monitor/app/correlation
Even after adding unique operation_Id attributes to each operation, we're not seeing log correlation (I also tried "Operation Id"). Example of none correlated log entry:
Any help on how to achieve this using log4net would be appreciated.
Cheers!
Across services correlation IDs are mostly propagated through headers. When AI is enabled for Web Application, it reads IDs/Context from the incoming headers and then updates outgoing headers with the appropriate IDs/Context. Within service, operation is tracked with Activity object and every telemetry emitted will be associated with this Activity, thus sharing necessary correlation IDs.
In case of Service Bus / Event Hub communication, the propagation is also supported in the recent versions (IDs/Context propagate as metadata).
If service is not web-based and AI automated correlation propagation is not working, you may need to manually get incoming ID information from some metadata if any exists, restore/initiate Activity, start AI operation with this Activity. When telemetry item is generated in scope of that Activity, it will get proper IDs and will be part of the overarching trace. With that, if telemetry is generated from Log4net trace that was executed in the scope of AI operation context then that telemetry should get right IDs.
Code sample to access correlation from headers:
public class ApplicationInsightsMiddleware : OwinMiddleware
{
// you may create a new TelemetryConfiguration instance, reuse one you already have
// or fetch the instance created by Application Insights SDK.
private readonly TelemetryConfiguration telemetryConfiguration = TelemetryConfiguration.CreateDefault();
private readonly TelemetryClient telemetryClient = new TelemetryClient(telemetryConfiguration);
public ApplicationInsightsMiddleware(OwinMiddleware next) : base(next) {}
public override async Task Invoke(IOwinContext context)
{
// Let's create and start RequestTelemetry.
var requestTelemetry = new RequestTelemetry
{
Name = $"{context.Request.Method} {context.Request.Uri.GetLeftPart(UriPartial.Path)}"
};
// If there is a Request-Id received from the upstream service, set the telemetry context accordingly.
if (context.Request.Headers.ContainsKey("Request-Id"))
{
var requestId = context.Request.Headers.Get("Request-Id");
// Get the operation ID from the Request-Id (if you follow the HTTP Protocol for Correlation).
requestTelemetry.Context.Operation.Id = GetOperationId(requestId);
requestTelemetry.Context.Operation.ParentId = requestId;
}
// StartOperation is a helper method that allows correlation of
// current operations with nested operations/telemetry
// and initializes start time and duration on telemetry items.
var operation = telemetryClient.StartOperation(requestTelemetry);
// Process the request.
try
{
await Next.Invoke(context);
}
catch (Exception e)
{
requestTelemetry.Success = false;
telemetryClient.TrackException(e);
throw;
}
finally
{
// Update status code and success as appropriate.
if (context.Response != null)
{
requestTelemetry.ResponseCode = context.Response.StatusCode.ToString();
requestTelemetry.Success = context.Response.StatusCode >= 200 && context.Response.StatusCode <= 299;
}
else
{
requestTelemetry.Success = false;
}
// Now it's time to stop the operation (and track telemetry).
telemetryClient.StopOperation(operation);
}
}
public static string GetOperationId(string id)
{
// Returns the root ID from the '|' to the first '.' if any.
int rootEnd = id.IndexOf('.');
if (rootEnd < 0)
rootEnd = id.Length;
int rootStart = id[0] == '|' ? 1 : 0;
return id.Substring(rootStart, rootEnd - rootStart);
}
}
Code sample for manual correlated operation tracking in isolation:
async Task BackgroundTask()
{
var operation = telemetryClient.StartOperation<DependencyTelemetry>(taskName);
operation.Telemetry.Type = "Background";
try
{
int progress = 0;
while (progress < 100)
{
// Process the task.
telemetryClient.TrackTrace($"done {progress++}%");
}
// Update status code and success as appropriate.
}
catch (Exception e)
{
telemetryClient.TrackException(e);
// Update status code and success as appropriate.
throw;
}
finally
{
telemetryClient.StopOperation(operation);
}
}
Please note that the most recent version of Application Insights SDK is switching to W3C correlation standard, so header names and expected format would be different as per W3C specification.

Handling Acumatica timeout on API Invoke action

I have code in a standalone application that invokes an Acumatica action to generate reports; I am running into timeouts on large documents while the action completes.
What is the best method to handle these timeouts? I need to wait for the action to complete in order to retrieve the files I've generated.
Standalone application code:
public SalesOrder GenerateAcumaticaLabels(string orderNbr, string reportType)
{
SalesOrder salesOrder = null;
using (ISoapClientProvider clientProvider = soapClientFactory.Create())
{
try
{
SalesOrder salesOrderToFind = new SalesOrder
{
OrderType = new StringSearch { Value = orderNbr.Split(OrderSeparator.SalesOrder).First() },
OrderNbr = new StringSearch { Value = orderNbr.Split(OrderSeparator.SalesOrder).Last() },
ReturnBehavior = ReturnBehavior.OnlySpecified,
};
salesOrder = clientProvider.Client.Get(salesOrderToFind) as SalesOrder;
InvokeResult invokeResult = new InvokeResult();
invokeResult = clientProvider.Client.Invoke(salesOrder, new exportSFPReport());
ProcessResult processResult = clientProvider.Client.GetProcessStatus(invokeResult);
//Wait for the update to complete before we attempt to retrieve the files
while (processResult.Status == ProcessStatus.InProcess)
{
Thread.Sleep(1000); //pause for 1 second
processResult = clientProvider.Client.GetProcessStatus(invokeResult);
}
}
And the action in Acumatica:
public PXAction<SOOrder> ExportSFPReport;
[PXButton]
[PXUIField(DisplayName = "Generate Robot SFP PDF")]
protected IEnumerable exportSFPReport(PXAdapter adapter)
{
//Report Paramenters
Dictionary<String, String> parameters = new Dictionary<String, String>();
parameters["SOOrder.OrderType"] = Base.Document.Current.OrderType;
parameters["SOOrder.OrderNbr"] = Base.Document.Current.OrderNbr;
IEnumerable reportFileInfo = ExportReport(adapter, "IN619217", parameters);
exportTrayLabelReport(adapter, "SFP");
return reportFileInfo;
}
The problem here is that your action is synchronous, so it is trying to complete within the Invoke call (which is not a good thing for long processes). You have to explicitly make your operation long-running by using PXLongOperation.StartOperation inside your handler, and then your client code should work properly, as it already handles the waiting and checking.
I believe the reason why you encounter time-out is because there is no TCP communication between the time you sent the request and receive the response. With TCP KeepAlive flag set to true, the client will periodically ping the server to reset the time-out period.
That would be the best way. However Acumatica connections are rather high level so I don't think you'll be able to easily access that flag. What I would try first in a scenario that doesn't involve external application is to wrap your action event-handler code in a PXLongOperation block which has to do something similar to keep connection alive under the hood:
PXLongOperation.StartOperation(this or Base, delegate
{
your code here
});
When I do encounter time-outs in Acumatica that can't be solved with PXLongOperation I go for the simplest method which is increasing IIS timeout in Web.Config file. I'm not sure if your use case with external application will go well with async PXLongOperation. The handler would return prematurely and the client could not be able to retrieve the async payload.
So you might have to increase time-out instead. As far as I know there's no real practical drawback to doing this unless your website is under threat of DOS attacks.
You can locate and edit the Web.Config file of your Acumatica instance using inetmgr program if you are self-hosting Acumatica. Otherwise talk to your SAAS contact to see if that's an option.
I'm pretty sure you are hitting IIS time-out. A tell-tale sign would be lost connection after exactly 5 minutes which is the default 300 seconds value. You can edit Web.Config file to increase executionTimeout value. It's not a bad idea to increase maxRequestLength too if you are requesting large amount of data from Acumatica API as this is also a common cause of failure that you miss in testing and occurs in real-life scenarios:
<httpRuntime executionTimeout="300" requestValidationMode="2.0" maxRequestLength="1048576" />

Unable to resolve sync conflict when using Azure Mobile Client - error keeps coming back

I'm using a Node.JS backend on Azure with Easy Tables. The table contains the required columns to support offline syncing.
While testing the sync process I noticed that conflicts keep coming back even though I'm resolving them.
My test:
Pull table content from Azure to iOS and Android device
Change a record on iOS but don't sync back to Azure
Change the same record on Android and sync
Now sync iOS
As expected, the conflict is detected correctly and I catch a MobileServicePushFailedException. I am then resolving the error by replacing the local item with the server item:
localItem.AzureVersion = serverItem.AzureVersion;
await result.UpdateOperationAsync(JObject.FromObject (localItem));
However, the next time I sync, the same item fails again with the same error.
The AzureVersion property is declared like this:
[Version]
public string AzureVersion { get; set; }
What exactly is result.UpdateOperationAsync() doing? Does it update my local database? Do I have to do it manually?
And also: am I supposed to trigger an explicit PushAsync() afterwards?
EDIT:
I changed the property from AzureVersion to Version and it works. I noticed that the serverItem's AzureVersion property was NULL even though the JSON contained it. Bug in Json.Net or in the Azure Mobile Client?
You should be using something like the following:
public async Task SyncAsync()
{
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
await this.client.SyncContext.PushAsync();
await this.todoTable.PullAsync(
//The first parameter is a query name that is used internally by the client SDK to implement incremental sync.
//Use a different query name for each unique query in your program
"allTodoItems",
this.todoTable.CreateQuery());
}
catch (MobileServicePushFailedException exc)
{
if (exc.PushResult != null)
{
syncErrors = exc.PushResult.Errors;
}
}
// Simple error/conflict handling. A real application would handle the various errors like network conditions,
// server conflicts and others via the IMobileServiceSyncHandler.
if (syncErrors != null)
{
foreach (var error in syncErrors)
{
if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null)
{
//Update failed, reverting to server's copy.
await error.CancelAndUpdateItemAsync(error.Result);
}
else
{
// Discard local change.
await error.CancelAndDiscardItemAsync();
}
Debug.WriteLine(#"Error executing sync operation. Item: {0} ({1}). Operation discarded.", error.TableName, error.Item["id"]);
}
}
}
Note the CancelAndUpdateItemAsync(), which updates the item to the server copy or CancelAndDiscardItemAsync(), which accepts the local item. These are the important things for you.
This code came from the official HOWTO docs here: https://azure.microsoft.com/en-us/documentation/articles/app-service-mobile-dotnet-how-to-use-client-library/##offlinesync

Resources