-
-
Notifications
You must be signed in to change notification settings - Fork 727
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
Migrating existing DB to an AppGroup #643
Comments
Hello @foxware00, My advice is to use the backup API. This sounds to me like the safest way to migrate your app in a context where several processes are attempting at using it. I hope that SQLite is able to deal with on-progress backups correctly. This is not something I have personally tested. Now, I suggest that you:
If you have an interesting story to tell after this, please come back and share your experience! I'll answer other questions if you have any. But will have to grasp a few things about SQLite: click on the links I gave you. |
@groue Thanks as always for a prompt and detailed response. The backup API looks ideal, I will do some investigation. I didn't want to manually copy files if there was a better way to achieve it. It looks like a simple way to easily transfer the data. I'll be sure to open the existing connection I want to migrate in read-only mode to prevent data changing before the backup has finished. Given it's a blocking call, I'm hoping it'll be a simple as follows.
I'll give those links a good read over. My current thought was to use the app extensions mainly to just query data the main app has. Given the easiest way to achieve this is by sharing my base target framework (Containing my data layer), I'm getting a lot of the data I need for free without having to re-write the queries and syncing logic. I'm going to have a proper play at sharing the connection, but given all writes are transactional it'll be interesting to see if there are any issues. I'm currently using a database pool in WAL mode. I might consider also opening the extension's connection in readOnly mode to prevent multiple writes to the same SQL file. From the looks of it, WAL mode is designed for this purpose, with the small unknown of how IPC and shared memory working with App Extensions and the host app on iOS. So off for some in-depth testing of how it all works. From 2.2 in this link "Readers can exist in separate processes" Is there something different in GRDB from raw SQLite that is abstracting some of the normal connection locks and transactions away from native SQLite? I guess my point is, isn't SQLite designed for this scenario when using transactions and WAL mode? Or does each process get a different WAL? Anyway sorry for my rambling. Feel free to close the issue, and thank you for taking the time to give me a detailed response! |
GRDB is indeed a very thin layer over SQLite. As you say, one writer and several readers are totally OK for a WAL database. A read-only pool or queue in the extension should not impact the read-write pool in the main app at all. But we also have to deal with iOS. This and this tell us that... your rambling is normal, and I don't have a definitive answer because I haven't experienced this setup yet. |
Fortunately, the Database is not encrypted nor do I target iOS 8 and below. I'm going to have a good play today and test a few things. Many thanks for all the help. |
Yes, I didn't want to sound alarming, sorry :-) I just wanted to say that iOS has its word, so... yeah, test your stuff. I'm closing this issue now. Please open a new one if you have any further question! |
@foxware00 I would really be interested in your long time experience and if you find some edge cases that have to be taken into account. Even a thumbs up or down would be great :-) I'm facing a similar issue and right now I'm using GRDB only in the main app. The share extension and the main app communicate with one-way-only JSON files (e.g. every shared item creates a small unique file that is integrated in the database as soon as the main app gets a chance to do so). |
@klaas I've chosen a slightly different route from the one you mention. I make a simple check during the startup of the Share extension to check whether the migration has happened. Directing them to the main app first so I can perform the migration. I can also use the migrated data to validate a user being logged in being able to perform functions. This was I can write straight to the database from the extension. As mentioned higher up in this thread, I'm using a WAL file (database pools) to prevent read's and writes being blocked by either app. I'm keeping the logic and writing to the DB to a minimum, simply writing the state of the shared item into a pending state so when the app opens I can show the progress of the upload awaiting the real post from the server. I've also had to migrate my keychain and user defaults. Shout if you have any more questions or would like me to explain something in more detail. |
I'd like to distinguish a few cases:
|
@groue An update on this. I've done a bunch of testing and on the surface, everything is working perfectly. I'm able to migrate the DB, clean up the old one and move users onto the new db version. However, I've been scratching my head for quite a while. I'm getting a bunch of crashes from our internal testers on testflight. The stack traces don't shed any light, are often random, and often the only code that crashed is "main". However after analysing a large number of crash reports. I've noticed almost always there is another thread in the background running a transaction. (I pre-fetch data, and sync small bits of data off the back of an APNS notification). This is where it gets difficult. It's only happening on release builds as far as I can tell and only after a couple of hours. No reproduction steps, no explicit code has crashed according to the crashes being uploaded. It seems almost all of the crashes have a background thread that begun a transaction but never finished it. I don't see the line for For additional context, the App Extension isn't actually being run. It's just the "app group" database that's being accessed. Is it all just a coincidence that that background thread is as such. Crashes are always happening in the background, never when a user is using that app. And the only reason we'd be running in the background is to perform the sync so maybe it is just that. I wonder if in your little reading around the subject you know of anything that might explain this?
My database config
|
@foxware00, do you have any information about the crash? Could it be related to Data Protection? iOS won't let the app access the database file if it is locked according to its FileProtectionType. |
Not really, it gets a bunch of stack traces which have no reference to my code other than `main`
I don't even have an error code or reason as these aren't visible in the organiser. I can share some symbolicated crashes, but no actual code is being marked as an error only 'main' or apple internal APIs leading to more confusion.
I have thought it was something related to that but as far as I can tell I have the correct access. It is accessible whilst locked in the background just sometimes it crashes. I'm getting the URL via https://developer.apple.com/documentation/foundation/filemanager/1412643-containerurl
|
All right @foxware00, thanks for the report. I can only hope you can eventually gather or Google more information. |
@groue I've found some more information by inspecting the crash reports manually. It seems the root cause is
Which according to Apple
|
Thanks @foxware00. Could it help if GRDB would wrap its transactions around a request for additional background execution? There is a related comment in the FMDB repo, even if they were not chasing after the same crash: ccgus/fmdb#754 (comment). |
Possibly. However, it looks like you can't request it in the same way from an app extension. I wonder if there is much of an overhead to doing what you suggest.
|
Sure. Please tell us if this fixes the crash! This will be a very precious information if someone someday decides to wrap all of this is an easy-to-grasp high-level API. |
Absolutely. Currently, I have a timeout of 29 seconds on the background task, which may be a little close to comfort to the supposed 30 seconds. I'm going to try lowering this to 20 seconds so I can cancel any pre-fetches that might be close to being overrun. My theory is that a request is in flight, and Apple kills the process just before I timeout and callback. At which point my URLSession returns and tries to hold the transaction open to write the response. If this lowers the crashes substantially. I'm going to additionally try wrapping the call in a background execution request and hopefully, the crashes will subside completely. Stay tuned! |
I surely stay tuned. I was thinking several things, like:
But there is no rush: we need your feedback first ;-) |
They both sound like great additions. Dealing with extensions is always tricky as you don't have access to I'm pushing a build to my internal guinea pigs so I can collect some crash statistics on the two options I've implemented. I really like the idea of a generic solution within GRDB though. Also an interesting tidbit. Logging out |
Seems both of my approaches didn't fully fix the issue. Crashes are still occurring with the same error. I found this link giving an example to a similar issue to the one you linked around SQLCipher a while back. Given I'm not encrypting the DB, is there something we're missing? I'm now trying moving back to a DatabaseQueue rather than a pool. This obviously won't settle for release but might see if the issue lies somewhere around that logic. I'm thinking to have something wrapping around the transaction itself might be more bulletproof. Back to the drawing board, for now... |
Maybe it just happens that our friends work with SQLCipher. This does not mean that the issue does not happen to regular unencrypted SQLite databases. May I ask @charlesmchen-signal and @michaelkirk-signal if you were able to overcome this trouble? |
I see they've forked GRDB with fixes for the state of the DB when SQLite cipher is used. They do allude to it being a problem for only encrypted databases, but it could seem there is an issue with iOS recognizing the state of the files during a suspended state. As I've been experiencing though, debug builds and simulators don't exhibit the issue which is making it all the more difficult to fix.
|
Yes, |
More woes, it looks as though the closing the connection could be a candidate. https://twitter.com/BigZaphod/status/1164924859954159618 https://bugzilla.mozilla.org/show_bug.cgi?id=1307822 https://bugzilla.mozilla.org/show_bug.cgi?id=1291304 This has a lot of interesting stuff in it. Not sure of any definitive solution that applies to GRDB https://bugzilla.mozilla.org/show_bug.cgi?id=1317324#c10 Anyway, DatabaseQueue is uploading, so I'll see if there is something different when using WAL. |
Sorry for bombarding this thread. More a place for a record than pestering you for a response. My train of thought is currently in two places. Another thing, not sure how much of an issue it is. But I don't delete the old WAL file during migration. I never open a connection to it, but maybe system is looking at the app's directory finding the old (closed) wal and seeing it closed whilst the open connection is still valid. UPDATE. For opening a connection inside app group it looks like it must be a WAL db. Not sure why mine isn't working. But will try out removing the WAL file as well to ensure no files are hanging around |
👋
We are not currently using a forked version of GRDB. In the past we've briefly used forked versions as we wait for changes to get upstreamed. So far we've either been able to get our changes upstreamed or worked with @groue to find some better solution (thanks @groue!). In this particular case, rather than the specific
But, back to the 10deadcc's, just to be clear about what I think we're talking about: From https://developer.apple.com/library/archive/technotes/tn2408/_index.html
So yes, this will affect encrypted and unencrypted db's alike. Without the cipher_plaintext_header_size configuration it will affect encrypted db's much more. The short version is we've never completely eliminated these kinds of crashes.
Additionally, in our codebase there is some amount of non-obvious async writes happening as side effects of other code. So we might be at second 29.5 of a background task when some async thing kicks off another write. We have had a long tail of tracking down this kind of code and, where possible, ensure it doesn't run in the share extension. But sometimes these writes are critical to the functioning of the share extension. e.g. in our case sending a message through the share extension on a shoddy network might fail several times in the background and be ready for another retry at second 29. We still want to try to get it out in that last second if we can, even though it risks running afoul of the locking rules. So to some degree we've adjusted our expectations to the rules of the platform and accepted that some level of crashing is worth it if it means we'll be more likely to get messages through. We've continued to see improvements here as we've optimized our writes to be faster. Another thing, since it sounds like you will be doing writes from the share extension, you probably want to look at: |
Thank you @michaelkirk-signal :-) I did setup a sample app which reproduces the crash on my device (iOS 13.2.3). A simple app, without any extension. When I open an transaction (immediate so that the lock is acquired) and send the app to background, here is what I witness in the debugger:
When the app runs without debugger, Xcode is able to import a crash log which mentions
What bugs me is that I could never have ProcessInfo.processInfo.performExpiringActivity tell that the process is about to be suspended. |
The experiments app: https://github.com/groue/10deadcc |
Thank you @michaelkirk-signal for your breakdown that makes a lot of sense. I think my train of thought was that a transaction on the database in WAL Journal mode should be allowed to complete in suspended scenarios, which isn't true 100% of the time, similar to what you mention. It looks like we've made some real headway in understanding the problem in more depth, so thank you for taking the time to explain.
I was thinking it was a performance consideration, a stricter, cancellation and interrupt policy which was only applying to app group containers. Your explanation clarifies it greatly.
This is fantastic! Thank you, is there anything I can help with? I've had a quick look through those two PR's and look great. I need to do some more bedtime reading on SQLite locking to understand the second one better and the problems you've mentioned. Happy to test anything with my internal testers. |
Thanks @foxware00. Yes, working on GRDB is a great opportunity to learn many things about SQLite. Like almost everybody, I used to know nothing about this database. The technique is somewhat simple: find a funny puzzle to solve, read a lot of documentation, perform experiments, discover... stuff, and eventually ship an API that is robust and makes sense for other developers ;-) |
Ah, that very well could be. All of our use cases and testing were against WAL mode. |
I completely understand ;-) Just in case you'd use a plain dbQueue one day ;-) |
Out of interest what did you have in mind for this? |
What I have in mind is:
First item is easy. Second item is not easy, or rather, full of shadows. For more detail: https://forums.developer.apple.com/thread/126438 |
Some interesting reading. Is this because you're trying to block further access to the database when a suspension is happening? It sounds like the API's are designed as wrappers around individual functionality rather than reporting the state of the system. For point 2, if we want to know when the app is allowed to authorize locks. Isn't on the successful completion of a transaction or when the background task expires? At this stage, we're being killed anyway and any other transactions wouldn't have been allowed due to the panic. And further ones would again wrap in the same background handlers. Is it this stage, the further transactions, immediately after a handler expiring that we're not notified? I might be missing the point, however, it's a fascinating topic and lesson in learning how these things are working. |
Welcome to the world of modern applications. After a background task has expired, the application is not suspended yet. Many things can happen until it gets suspended for good. Core Location can notify a location update. An application timer can fire. A network request can end. All of those events may trigger a database access. And if we would start a background task then, we would not be notified of its expiration, because it was started too late. Yes, that means |
Imagine what it would mean if your app had to deal with this, with your little hands. Your app would had to setup a background task. Wait for its expiration. At that moment, your app would have to notify all other places in your application that they must no longer access the database. Cancel timers. Cancel Core Location updates. Cancel network requests. Or let them all go, but inject somehow that database must no longer be used. ... and of course this state must be lifted eventually, because your app won't block access to its database forever, will it? Imagine the pollution in the code of your app. Imagine what it means if you forget to handle properly the background task expiration in one place (No |
That makes a lot of sense thank you for explaining.
This is the interesting bit, it seems like a bug on iOS's part not notifying you the second time. With Is this not what you're seeing? Or background tasks are behaving differently? |
It doesn't fit our need. But I'm sure Apple people will tell it behaves as expected. If you think about it, it is exactly what you intended as well with your sentence above:
The use case they want to support is exactly this one. You wrap tasks. What happens after task expiration is none of their business, and errs on the side of programmer error. What happens if an app starts a task too late is none of their business, and errs on the side of programmer error as well. Now we are after another use case, a use case that is explicitly not supported by Apple APIs. What do we know? GRDB is surely benevolent. But maybe Apple engineers did want to prevent aggressive applications from turning the feature we are after into real dark patterns. |
OK we have a clear answer now: https://forums.developer.apple.com/thread/126438. Fun fact, Core Data is not immune against I believe we can do better. |
The blog post above contains this interesting paragraph:
One could not have expressed it in a better way. Having GRDB users tell how they spent several days dealing with markers is exactly what I want to avoid. What a shit work. No. What I want is something like: do {
try databaseStuff()
} catch {
// Handle eventual error due to lock prevention.
// For example try again when database becomes available again.
} We see that we'll have to expose this notion of "database becomes available again". In spirit it should be very close to UIApplicationProtectedDataDidBecomeAvailable. In the same trend of thought, application developers will also have to be able to easily restart database observations that have failed because of lock prevention (and see how it fits with RxGRDB and GRDBCombine). There's still a lot of work before we can ship the "App Group edition" of GRDB. And I'm not talking about cross-process database observation (this will probably ship in the "App Group Gold edition"). |
This was exactly my path. Nice to know I wasn't alone.
Does this effectively notify us to unblock the DB?
It sounds like we might need to live with the occasional crash. Minimising the potential impact of such. It seems like a scenario Apple has just decided to accept the fact there they might kill you at any point. An interesting stance indeed. Interestingly in my scenario the second the app opens from I feel your pain and frustration and thank your efforts in rolling out a robust solution for the rest of us and to keep GRDB simple and effective API. |
The "final" PR is drafted: #663 |
Very exciting! I'll have a proper read through shortly |
#663 is able to prevent #663 is thus on hold, but I want to merge into master the preliminary work. This is #668, flagged experimental. |
If you know how to reproduce the |
@groue unfortunately I haven't experienced an extension causing |
There are some evidence that this crash exists in extensions, such as this tweet by @marcoarment. But it's difficult to grab any actionable information. If shipping a real app is a mandatory step, then we'll hardly see any advance on this topic in an open source project. |
Agreed, I saw this too. From my testing it's less predictable from an app extension. What have you tried so far? I can try test this later on today. |
I don't understand. What do you have in mind? |
Calling |
Yes. It could also be a side-effect of the debugger. But when I run the extension without any debugger attached, I don't see anything weird in Console.app. The Devices window of Xcode does not show any crash log. Of course, I may just have missed something. This is a
You mean the transaction held by the extension. Thanks for the tip, this may give something. Did you make an attempt? |
One moment, let me give it a go... Bingo. Is this what you're expecting?
Looks like it worked
To replicate this. I installed the application. Removed the debugger and opened console This is code that caused it. I added this to a button press within the ShareExtension. The
|
Thanks @foxware00. Meanwhile, I'll shortly merge and ship #668. It does not automatically suspend databases, but you can do it from your app and extension. You are warmly encouraged to perform your experiments and see if you can get rid of |
@groue Thank you I will shortly have a play. Thanks for all your hard work on the topic. Getting closer to a really fantastic and robust solution. |
Question
I'm trying to create a Share extension that has access to my host application's database. The problem is that users already have the SQLite file created and populated. I know the path of the existing database and where the new one is.
My question is there a clean and easy way to migrate the SQLite file to another path in a one-off operation. I'm guessing there are a few files that all need to be copied and duplicated cleanly.
My initial thoughts would be to open the current DB, create a new on in a new path and copy all the data over. This might take a few seconds the first run but seems like the cleanest way to do it. I'd delete the old DB, and switch over to the new DB once the migration has completed. Or would a quicker direct file copy make more sense?
I've found a CoreData method that migrates data from one URL to another, is this something that's easy to achieve in GRDB?
Environment
GRDB flavor(s): GRDB
GRDB version: 4.1.1
Installation method: CocoaPods
Xcode version: 11.1
Swift version: 5.1
Platform(s) running GRDB: iOS
macOS version running Xcode: 10.14.5
The text was updated successfully, but these errors were encountered: