sqlite-net icon indicating copy to clipboard operation
sqlite-net copied to clipboard

SQLite.SQLiteException: (Busy) (database is locked)

Open aherrick opened this issue 6 years ago • 40 comments

I have a Xamarin Forms app using sqlite-net.

In iOS, I sometimes see the following exceptions which are crashing the App.

DB.InsertOrReplace[T] (T item)
SQLite.SQLiteException: Busy
DB.ResetTable[T] ()
SQLite.SQLiteException: database is locked

In my .NET Standard 2.0 project I have a simple DB Class that looks like the following:

	public class DB
	{
		private string dbPath = null;

		public DB(string dbPath)
		{
			this.dbPath = dbPath;

			using (var database = new SQLiteConnection(dbPath))
			{
				database.CreateTable<GenericObjectCacheDB>();
			}
		}

		#region Generic Table Methods

		public void ResetTable<T>()
		{
			using (var database = new SQLiteConnection(dbPath))
			{
				database.DropTable<T>();
				database.CreateTable<T>();
			}
		}

		public void InsertOrReplace<T>(T item)
		{
			using (var database = new SQLiteConnection(dbPath))
			{
				database.InsertOrReplace(item, typeof(T));
			}
		}

		#endregion Generic Table Methods
	}

I spin up the DB Class on App Load one time and use throughout the App. Sometimes it crashes with the above exception.

        public static DB Database;

        public App()
        {
            InitializeComponent();

            Database = new DB(DependencyService.Get<IFileHelper>().GetLocalFilePath("DBSQLite.db3"));

            MainPage = new NavPage(new Page());
        }

Am I not correctly creating the DB or handling/disposing the instance properly?

Is there some other way I need to handle this for Xamarin Forms?

aherrick avatar Mar 08 '18 16:03 aherrick

I would advise you to keep a single SQLiteConnection for your app and cache it to take advantage of the type mapping caching strategy. Opening it with the Create | ReadWrite | FullMutex flags will ensure all operations are multithread-wise serialized. Don't forget to Dispose the connection whenever your app closes

Weldryn avatar Mar 29 '18 10:03 Weldryn

@Weldryn are there any examples showing this?

aherrick avatar Mar 30 '18 15:03 aherrick

Have a look at the issues, SO

Weldryn avatar Apr 04 '18 10:04 Weldryn

@Weldryn can you elaborate?

aherrick avatar Apr 04 '18 14:04 aherrick

@aherrick I've used something similar to this in the past:

public class ProgressTracker : IDisposable
{
        private readonly SQLiteConnection _connection;

        public ProgressTracker()
        {
            var dbPath = Path.Combine(Environment.CurrentDirectory, "Progress.db");
            _connection = new SQLiteConnection(dbPath, SQLiteOpenFlags.Create | SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.FullMutex);
            _connection.CreateTable<Progress>();
        }

        public void Dispose()
        {
            _connection.Close();
        }
}

wub avatar May 11 '18 00:05 wub

I ended up using lock which seems to work...

		private static object collisionLock = new object();

		public void Insert<T>(T item)
		{
			lock (collisionLock)
			{
				using (var database = new SQLiteConnection(dbPath))
				{
					database.Insert(item, typeof(T));
				}
			}
		}

aherrick avatar May 21 '18 18:05 aherrick

@praeclarum Do you have any insights on the correct solution to this? Seeing as SQLite comes with the specific flags necessary to solve this (FullMutex), it seems strange to "recreate" this with lock.

wub avatar May 22 '18 01:05 wub

@aherrick You are still using the SQLiteConnection in a using block. As I said, just don't. It's very costly. You are adding pressure on the CPU and GC and avoiding optimisation by doing that. As a good rule of thumb, cache it at the application level and use the same instance every time. Don't dispose it unless the app shuts down.

Weldryn avatar May 22 '18 11:05 Weldryn

@Weldryn it's the only way I can prevent the iOS App from completely crashing when trying to access data.

Adding lock fixed the crashes. If there are other examples without using lock, I'd be happy to give it a go.

aherrick avatar May 22 '18 13:05 aherrick

The example is: take your code as it is, remove the locks and the using blocks everywhere, remove the lines where you Dispose() the connection, create a single SQLiteConnection property in your App class to which you assign a new connection when the app starts, finally use this property everywhere. Have a look at #695 to know when to dispose the connection. Let me know if it helped

Weldryn avatar May 22 '18 15:05 Weldryn

Originally I had it setup without using or lock and it was still crashing on iOS.

Would love to see a basic Xamarin Forms sample app with all these best practices setup.

aherrick avatar May 22 '18 18:05 aherrick

@aherrick did you found a Xamarin Forms sample ? Can you share the code ?

leowsouza avatar Oct 16 '18 16:10 leowsouza

Never saw an official example but something like this working in my App:

	public class DB
	{
		private string dbPath = null;
		private static object collisionLock = new object();

		public DB(string dbPath)
		{
			this.dbPath = dbPath;

			CreateTable<MyTable>();
		}

		public MyTable GetMyTable(long id)
		{
			lock (collisionLock)
			{
				using (var database = new SQLiteConnection(dbPath))
				{
					return database.Table<MyTable>().Where(x => x.Id == id).SingleOrDefault();
				}
			}
		}

		private void CreateTable<T>()
		{
			lock (collisionLock)
			{
				using (var database = new SQLiteConnection(dbPath))
				{
					database.CreateTable<T>();
				}
			}
		}
	}

aherrick avatar Oct 16 '18 19:10 aherrick

Note that SQLite on iOS is not thread-safe on a single connection. That's why you're getting the crashes without the locks. I would consider using the async API as that implements a connection pool in the code-behind.

duraz0rz avatar Mar 13 '19 14:03 duraz0rz

The connection pool only implements 1 connection ad far as I can remember so there must be another mechanism that ensures thread safety.

On Wed, 13 Mar 2019, 14:35 Marc Vertido, [email protected] wrote:

Note that SQLite on iOS is not thread-safe on a single connection. That's why you're getting the crashes without the locks. I would consider using the async API as that implements a connection pool in the code-behind.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/praeclarum/sqlite-net/issues/700#issuecomment-472447205, or mute the thread https://github.com/notifications/unsubscribe-auth/AcWbQhWQ2mrmcAM7sblnZapvAj7cTSlTks5vWQydgaJpZM4SjAjD .

Weldryn avatar Mar 14 '19 07:03 Weldryn

Running into the same issue. Is making use of locks the proper way of handling that for the sync API?

raquelsa avatar May 31 '19 21:05 raquelsa

Is there any update on this, or the "correct" way to avoid this issue? Running into now myself.

NickA55 avatar Jul 24 '19 17:07 NickA55

So, I'm running into this now as well. Guessing despite using the SQLiteAsyncConnection variant, we still need to ensure only one instance is ever created of said object. It can perform proper serialization via locks under the hood, but they won't mean a thing if there's 2 instances trying to hit the same DB at the same time, right? I must have 2 connections attempting to hit the DB somewhere... ugh...

anthcool avatar Sep 25 '19 20:09 anthcool

Note that SQLite on iOS is not thread-safe on a single connection. That's why you're getting the crashes without the locks. I would consider using the async API as that implements a connection pool in the code-behind.

I thought that the FullMutex flag allowed multiple connections? Is this not true for iOS?

I am only seeing this database locked error on iOS, not Android or UWP. Where did you find the information that says that SQLite on iOS is not thread-safe? and what is the propsed solution?

My application is multi-threaded using the fullmutex flag so does this mean I need to provide different implementations for iOS (using the lock technique) to android and uwp?

JKennedy24 avatar Nov 04 '19 09:11 JKennedy24

Note that SQLite on iOS is not thread-safe on a single connection. That's why you're getting the crashes without the locks. I would consider using the async API as that implements a connection pool in the code-behind.

I thought that the FullMutex flag allowed multiple connections? Is this not true for iOS?

I am only seeing this database locked error on iOS, not Android or UWP. Where did you find the information that says that SQLite on iOS is not thread-safe? and what is the propsed solution?

My application is multi-threaded using the fullmutex flag so does this mean I need to provide different implementations for iOS (using the lock technique) to android and uwp?

SQLite does allow multiple connections for readers in FullMutex, but it only allows one connection to write at any given time. See a relevant FAQ question. Furthermore, FullMutex isn't supported by the underlying SQLite library in iOS, which SQLite.NET uses, so even if you open a DB in FullMutex, it won't work the way you expect it to (where the connection waits gracefully instead of throwing an exception). We've been bit by this on our Xamarin app.

duraz0rz avatar Nov 05 '19 16:11 duraz0rz

@duraz0rz thanks for the explanation. That really helped.

What was your Xamarin solution in the end? The only solution I can see is to provide a C# lock around the writes.

Did you also encounter this database is locked problem on Android and UWP?

I am in the same situation, writing a Xamarin app for Android, iOS and UWP and have only experienced this on iOS

JKennedy24 avatar Nov 05 '19 16:11 JKennedy24

Given what @duraz0rz said:

Note that SQLite on iOS is not thread-safe on a single connection.

Note that the main sqlite-net nuget package takes its dependency on SQLitePCLRaw.bundle_green, which uses my e_sqlite3 builds on every platform except iOS. I tend to discourage the use of bundle_green for cross-platform projects, because it means that one of the your platforms is using a SQLite which was built differently from the others.

So one possible fix here would be to use SQLitePCLRaw.bundle_e_sqlite3 instead.

Easiest way to do this would be to use sqlite-net-base instead of sqlite-net, and then add SQLitePCLRaw.bundle_e_sqlite3 to your app project(s).

ericsink avatar Nov 05 '19 17:11 ericsink

Easiest way to do this would be to use sqlite-net-base instead of sqlite-net, and then add SQLitePCLRaw.bundle_e_sqlite3 to your app project(s).

That makes sense.

If I'm using SqlCipher would I use SQLitePCLRaw.Bundle_e_SqlCipher instead of SQLitePCLraw.Bundle_SqlCipher

I'm guessing SQLitePCLraw.Bundle_SqlCipher is obsolete although I can't find any information on it

JKennedy24 avatar Nov 05 '19 17:11 JKennedy24

bundle_sqlcipher is for SQLitePCLRaw 1.1.x

With my 2.0 release, I changed the name of my unofficial SQLCipher builds to include e_ prefix, to more clearly distinguish them from official builds available from the SQLCipher developers, so that portion of the package ID changed to bundle_e_sqlcipher.

But SQLitePCLRaw 2.0 contained breaking changes, so you have to use the right one for the sqlite-net version.

In order to use SQLitePCLRaw 2.x stuff, you have to use sqlite-net 1.7, which is still marked as beta.

ericsink avatar Nov 05 '19 17:11 ericsink

Given what @duraz0rz said:

Note that SQLite on iOS is not thread-safe on a single connection.

Note that the main sqlite-net nuget package takes its dependency on SQLitePCLRaw.bundle_green, which uses my e_sqlite3 builds on every platform except iOS. I tend to discourage the use of bundle_green for cross-platform projects, because it means that one of the your platforms is using a SQLite which was built differently from the others.

So one possible fix here would be to use SQLitePCLRaw.bundle_e_sqlite3 instead.

Easiest way to do this would be to use sqlite-net-base instead of sqlite-net, and then add SQLitePCLRaw.bundle_e_sqlite3 to your app project(s).

In my project SQLitePCLRaw.bundle_e_sqlite3 was causing problems and the bundle_green version worked perfectly. This is iOS 13.2, many threads but semaphore around writes.

philipag avatar Nov 28 '19 12:11 philipag

Any updates on this? We are facing the same issue with the latest nuget package. A lot of users get SQLite.SQLiteException: database is locked

stefan89 avatar Dec 09 '19 13:12 stefan89

@stefan89

Use the solution in this thread:

https://github.com/praeclarum/sqlite-net/issues/881#issuecomment-552865565

This is the only solution for now

JKennedy24 avatar Dec 09 '19 14:12 JKennedy24

@stefan89 Brandon Minnick (@brimmick) did a great writeup on this. of particular importance is part 6, Attempt and Retry

He suggests using Polly, a library which catches exceptions, and then attempts to retry it asynchronously with an exponentially increasing backoff.

https://codetraveler.io/2019/11/26/efficiently-initializing-sqlite-database/

Polly has an initial learning curve, however the repo Brandon provides is super helpful in working it out. https://github.com/brminnick/GitTrends

LuckyDucko avatar Dec 10 '19 21:12 LuckyDucko

Rather than all these complex workarounds, has anyone tried to simply set a busy_timeout for SQLite, e.g. PRAGMA busy_timeout = 2000;?

sjlombardo avatar Dec 10 '19 21:12 sjlombardo

Rather than all these complex workarounds, has anyone tried to simply set a busy_timeout for SQLite, e.g. PRAGMA busy_timeout = 2000;?

As suggested in this answer, I tried to define SetBusyTimeoutAsync to 2000 Milliseconds without success

cbs-cbt avatar Dec 18 '19 11:12 cbs-cbt