ListMonitor strange behaviour #305

Closed
opened 2025-12-29 15:28:31 +01:00 by adam · 6 comments
Owner

Originally created by @spacedema on GitHub (Jan 27, 2020).

Hi!

Trying to use ListMonitor for triggering attachments delete from db to delete blobs locally:

class AttachesDbListener {
    typealias ListEntityType = Attachments

    private let monitor: ListMonitor<Attachments>

    private let dispatchQueue = DispatchQueue(label: "com.test.queue")
    
    init(dataStack: DataStack) {
        monitor = dataStack.monitorList(
            From<Attachments>()
                .orderBy(.ascending(\.local_ref))
        )
        monitor.addObserver(self)
    }
}
extension AttachesDbListener: ListObjectObserver {
    func listMonitorDidChange(_ monitor: ListMonitor<Attachments>) {
    }

    func listMonitorDidRefetch(_ monitor: ListMonitor<Attachments>) {
    }

    func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) {
        log.debug("Will delete attachment with local url \(String(describing: object.local_ref))")

        if let path = object.local_ref, let foundPath = URL.findCurrentPath(forPath: path) {
            dispatchQueue.async {
                do {
                    try FileManager.default.removeItem(atPath: foundPath)
                    log.debug("Did delete attachment with local url \(String(describing: object.local_ref))")
                } catch let error {
                    log.error(error.localizedDescription)
                }
            }
        }
    }
}

But object.local_ref is nil in func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath)

If I add

func listMonitorWillChange(_ monitor: ListMonitor<ListEntityType>) {
        let attaches = monitor.objectsInAllSections()
        for attach in attaches {
            log.debug("List Monitor Will Change for attachment with local url \(String(describing: attach.local_ref))")
        }
    }

object.local_re_ is NOT nil in func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath_ and everything works as expected

Has anyone encountered this behaviour?
Thanks

Originally created by @spacedema on GitHub (Jan 27, 2020). Hi! Trying to use ListMonitor for triggering attachments delete from db to delete blobs locally: ``` class AttachesDbListener { typealias ListEntityType = Attachments private let monitor: ListMonitor<Attachments> private let dispatchQueue = DispatchQueue(label: "com.test.queue") init(dataStack: DataStack) { monitor = dataStack.monitorList( From<Attachments>() .orderBy(.ascending(\.local_ref)) ) monitor.addObserver(self) } } extension AttachesDbListener: ListObjectObserver { func listMonitorDidChange(_ monitor: ListMonitor<Attachments>) { } func listMonitorDidRefetch(_ monitor: ListMonitor<Attachments>) { } func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) { log.debug("Will delete attachment with local url \(String(describing: object.local_ref))") if let path = object.local_ref, let foundPath = URL.findCurrentPath(forPath: path) { dispatchQueue.async { do { try FileManager.default.removeItem(atPath: foundPath) log.debug("Did delete attachment with local url \(String(describing: object.local_ref))") } catch let error { log.error(error.localizedDescription) } } } } } ``` But **object.local_ref is nil** in func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) If I add ``` func listMonitorWillChange(_ monitor: ListMonitor<ListEntityType>) { let attaches = monitor.objectsInAllSections() for attach in attaches { log.debug("List Monitor Will Change for attachment with local url \(String(describing: attach.local_ref))") } } ``` **object.local_re_ is NOT ni**l in func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath_ and everything works as expected Has anyone encountered this behaviour? Thanks
adam closed this issue 2025-12-29 15:28:31 +01:00
Author
Owner

@JohnEstropia commented on GitHub (Jan 28, 2020):

@spacedema Which base are you using, NSManageObject or CoreStoreObject?

@JohnEstropia commented on GitHub (Jan 28, 2020): @spacedema Which base are you using, `NSManageObject` or `CoreStoreObject`?
Author
Owner

@spacedema commented on GitHub (Jan 28, 2020):

@JohnEstropia nsmanagedobject

@spacedema commented on GitHub (Jan 28, 2020): @JohnEstropia nsmanagedobject
Author
Owner

@JohnEstropia commented on GitHub (Jan 28, 2020):

Ah, didDeleteObject is too late to access any values. At this point the cache dictionary is destroyed (all the properties will be nil, 0, or "" depending on the type). The only valid value you can reliably use here is objectID.

@JohnEstropia commented on GitHub (Jan 28, 2020): Ah, `didDeleteObject` is too late to access any values. At this point the cache dictionary is destroyed (all the properties will be `nil`, `0`, or `""` depending on the type). The only valid value you can reliably use here is `objectID`.
Author
Owner

@spacedema commented on GitHub (Jan 28, 2020):

But values not nil in didDeleteObject if I implement listMonitorWillChange as I wrote above.

@JohnEstropia, can you suggest me the right way to solve my task?

@spacedema commented on GitHub (Jan 28, 2020): But values not nil in didDeleteObject if I implement listMonitorWillChange as I wrote above. @JohnEstropia, can you suggest me the right way to solve my task?
Author
Owner

@JohnEstropia commented on GitHub (Jan 28, 2020):

That's because your log.debug() accesses all the local_ref, which fires the faults for all objects and caches them in memory. Otherwise the ListMonitor needs to query the database, which would not contain the value anymore since it was just deleted.

You'll need to manage your own cache that is guaranteed to not rely on Core Data's fault. Something like

var pendingObjects: [NSManagedObjectID: String] = [:]

// ...
func listMonitorWillChange(_ monitor: ListMonitor<ListEntityType>) {
    let attaches = monitor.objectsInAllSections()
    self.pendingObjects = attaches.reduce(into: [:]) {
        $0[$1.objectID] = $1. local_ref
    }
}

func listMonitorDidChange(_ monitor: ListMonitor<Attachments>) {
    self.pendingObjects = [:]
}

func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) {
    guard let path = self.pendingObjects[object.objectID] else {
        return
    }
    dispatchQueue.async {
        do {
            try FileManager.default.removeItem(atPath: foundPath)
            log.debug("Did delete attachment with local url \(path)")
        } 
        catch let error {
            log.error(error.localizedDescription)
        }
    }
}

In any case, you might want to run a regular cleanup anyway because removeItem may fail if that file's handle is still held by any process in the device. If you'll do that you'll need to query all saved local_refs, iterate all files in your blob folder, and delete all files not in the local_refs list. Of course this assumes you don't have other unrelated files mixed into that folder.

@JohnEstropia commented on GitHub (Jan 28, 2020): That's because your `log.debug()` accesses all the `local_ref`, which fires the faults for all objects and caches them in memory. Otherwise the `ListMonitor` needs to query the database, which would not contain the value anymore since it was just deleted. You'll need to manage your own cache that is guaranteed to not rely on Core Data's fault. Something like ```swift var pendingObjects: [NSManagedObjectID: String] = [:] // ... func listMonitorWillChange(_ monitor: ListMonitor<ListEntityType>) { let attaches = monitor.objectsInAllSections() self.pendingObjects = attaches.reduce(into: [:]) { $0[$1.objectID] = $1. local_ref } } func listMonitorDidChange(_ monitor: ListMonitor<Attachments>) { self.pendingObjects = [:] } func listMonitor(_ monitor: ListMonitor<ListEntityType>, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) { guard let path = self.pendingObjects[object.objectID] else { return } dispatchQueue.async { do { try FileManager.default.removeItem(atPath: foundPath) log.debug("Did delete attachment with local url \(path)") } catch let error { log.error(error.localizedDescription) } } } ``` In any case, you might want to run a regular cleanup anyway because `removeItem` may fail if that file's handle is still held by any process in the device. If you'll do that you'll need to query all saved `local_ref`s, iterate all files in your blob folder, and delete all files not in the `local_ref`s list. Of course this assumes you don't have other unrelated files mixed into that folder.
Author
Owner

@spacedema commented on GitHub (Jan 28, 2020):

Thanks for your help, @JohnEstropia !

@spacedema commented on GitHub (Jan 28, 2020): Thanks for your help, @JohnEstropia !
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#305