Skip to content
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

SQLite Error 5: 'database is locked' during writing from two DB connections #29514

Open
MaximMikhisor opened this issue Nov 9, 2022 · 11 comments

Comments

@MaximMikhisor
Copy link

Model

public class RootEntity
{
    public Int32 Id { get; set; }

    public ChildEntity? Child { get; set; }
}

public class ChildEntity
{
    public Int32 Id { get; set; }

    public String? AProperty { get; set; }
}

Code

// Comment this whole task and application will run without any errors.
Task.Run(() =>
{
    using (var db1 = new ExampleContext())
    {
        db1.RootEntities.First().Child = new ChildEntity();

        db1.SaveChanges();
    }
});

using (var db2 = new ExampleContext())
{
    var rootEntities = db2.RootEntities.OrderBy(i => i.Id);

    foreach (var rootEntity in rootEntities)
    {        
        // Wait a bit until db1.SaveChanges() above will finish execution.
        Thread.Sleep(2000);

        rootEntity.Child = new ChildEntity();                 

        // Here will be an error Error: SQLite Error 5: 'database is locked'
        db2.SaveChanges();
    }
}

Project example

https://github.com/LineSmarts/SqliteBusyError

Steps to reproduce bug

  1. Open two connections to Sqlite database.
  2. Write data in first connection, and begin to read data in second connection (by IQueryable).
  3. Wait until first connection will be closed.
  4. Attemp to write data in second connection.
  5. SQLite Error 5: 'database is locked'

Expected behaviour

Unfortunately did not find proper description of busy_timeout logic on sqlite.org.
Found below text on this site r-bloggers.com

If this timeout is set to a non-zero value, then the second connection will re-try the write operation several times, until it succeeds or the timeout expires.

Based on this description I expect that code above should not get any error during db2.SaveChanges(); execution because:

  1. There is no any other connection which could lock database at that moment
  2. busy_timeout is set to 30 second by default, this connection should re-try the write operation several times, and should succeed.

Include version information

Microsoft.Data.Sqlite version: 7.0.0
Target framework: (e.g. .NET 6.0)
Operating system: Windows 10

@ajcvickers
Copy link
Contributor

/cc @bricelam

@ajcvickers
Copy link
Contributor

Notes for triage:

  • Also fails with EF Core 6.0.11/M.D.Sqlite 6.
  • For the failure to happen, the query on the second context must run before SaveChanges is called on the first context.
  • Turning off connection pooling doesn't help.

Logs:

dbug: 11/22/2022 10:56:22.446 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteNonQuery' (2ms).
dbug: 11/22/2022 10:56:22.455 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      PRAGMA journal_mode = 'wal';
info: 11/22/2022 10:56:22.474 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (20ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      PRAGMA journal_mode = 'wal';
dbug: 11/22/2022 10:56:22.584 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'Serializable'.
dbug: 11/22/2022 10:56:22.585 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 11/22/2022 10:56:22.585 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "ChildEntity" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_ChildEntity" PRIMARY KEY AUTOINCREMENT,
          "AProperty" TEXT NULL
      );
info: 11/22/2022 10:56:22.585 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "ChildEntity" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_ChildEntity" PRIMARY KEY AUTOINCREMENT,
          "AProperty" TEXT NULL
      );
dbug: 11/22/2022 10:56:22.586 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 11/22/2022 10:56:22.586 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RootEntities" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RootEntities" PRIMARY KEY AUTOINCREMENT,
          "ChildId" INTEGER NULL,
          CONSTRAINT "FK_RootEntities_ChildEntity_ChildId" FOREIGN KEY ("ChildId") REFERENCES "ChildEntity" ("Id")
      );
info: 11/22/2022 10:56:22.586 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RootEntities" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RootEntities" PRIMARY KEY AUTOINCREMENT,
          "ChildId" INTEGER NULL,
          CONSTRAINT "FK_RootEntities_ChildEntity_ChildId" FOREIGN KEY ("ChildId") REFERENCES "ChildEntity" ("Id")
      );
dbug: 11/22/2022 10:56:22.587 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 11/22/2022 10:56:22.587 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_RootEntities_ChildId" ON "RootEntities" ("ChildId");
info: 11/22/2022 10:56:22.587 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_RootEntities_ChildId" ON "RootEntities" ("ChildId");
dbug: 11/22/2022 10:56:22.592 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.
dbug: 11/22/2022 10:56:22.594 RelationalEventId.TransactionDisposed[20204] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Disposing transaction.
dbug: 11/22/2022 10:56:22.743 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'Serializable'.
dbug: 11/22/2022 10:56:22.745 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.749 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 11/22/2022 10:56:22.752 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 11/22/2022 10:56:22.768 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.768 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 11/22/2022 10:56:22.769 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 11/22/2022 10:56:22.769 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.769 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 11/22/2022 10:56:22.770 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 11/22/2022 10:56:22.770 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.770 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 11/22/2022 10:56:22.771 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 11/22/2022 10:56:22.771 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.771 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 11/22/2022 10:56:22.772 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 11/22/2022 10:56:22.774 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.
dbug: 11/22/2022 10:56:22.775 RelationalEventId.TransactionDisposed[20204] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Disposing transaction.
dbug: 11/22/2022 10:56:22.993 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.993 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:22.994 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      LIMIT 1
dbug: 11/22/2022 10:56:22.994 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      ORDER BY "r"."Id"
info: 11/22/2022 10:56:22.996 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      LIMIT 1
info: 11/22/2022 10:56:23.153 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (159ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      ORDER BY "r"."Id"
Waiting on 2
dbug: 11/22/2022 10:56:23.534 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'Serializable'.
dbug: 11/22/2022 10:56:23.534 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:23.534 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p0=NULL], CommandType='Text', CommandTimeout='30']
      INSERT INTO "ChildEntity" ("AProperty")
      VALUES (@p0)
      RETURNING "Id";
info: 11/22/2022 10:56:23.534 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0=NULL], CommandType='Text', CommandTimeout='30']
      INSERT INTO "ChildEntity" ("AProperty")
      VALUES (@p0)
      RETURNING "Id";
dbug: 11/22/2022 10:56:23.538 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command)
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 11/22/2022 10:56:23.538 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing DbCommand [Parameters=[@p2='1', @p1='1' (Nullable = true)], CommandType='Text', CommandTimeout='30']
      UPDATE "RootEntities" SET "ChildId" = @p1
      WHERE "Id" = @p2
      RETURNING 1;
info: 11/22/2022 10:56:23.539 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@p2='1', @p1='1' (Nullable = true)], CommandType='Text', CommandTimeout='30']
      UPDATE "RootEntities" SET "ChildId" = @p1
      WHERE "Id" = @p2
      RETURNING 1;
dbug: 11/22/2022 10:56:23.542 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.
dbug: 11/22/2022 10:56:23.542 RelationalEventId.TransactionDisposed[20204] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Disposing transaction.
Done 1
Starting 2
Unhandled exception. Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 5: 'database is locked'.
   at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
   at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteNonQuery()
   at Microsoft.Data.Sqlite.SqliteConnectionExtensions.ExecuteNonQuery(SqliteConnection connection, String commandText, SqliteParameter[] parameters)
   at Microsoft.Data.Sqlite.SqliteTransaction..ctor(SqliteConnection connection, IsolationLevel isolationLevel, Boolean deferred)
   at Microsoft.Data.Sqlite.SqliteConnection.BeginTransaction(IsolationLevel isolationLevel, Boolean deferred)
   at Microsoft.Data.Sqlite.SqliteConnection.BeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.Data.Sqlite.SqliteConnection.BeginDbTransaction(IsolationLevel isolationLevel)
   at System.Data.Common.DbConnection.BeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.ConnectionBeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransaction()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__107_0(DbContext _, ValueTuple`2 t)
   at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
   at EfCoreNotNullNestedOwned.Program.Main(String[] args) in C:\local\code\repros\SqliteBusyError-master\SqliteBusyError-master\Program.cs:line 56

Process finished with exit code -532,462,766.

Updated code:

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace EfCoreNotNullNestedOwned
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new ExampleContext())
            {
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();

                for (int i = 0; i < 5; i++)
                    db.Add(new RootEntity());

                db.SaveChanges();
            }

            // Comment this whole task and application will run without any errors.
            Task.Run(() =>
            {
                using (var db1 = new ExampleContext())
                {
                    db1.RootEntities.First().Child = new ChildEntity();

                    Thread.Sleep(500);

                    db1.SaveChanges();
                }

                Console.WriteLine("Done 1");
            });         

            using (var db2 = new ExampleContext())
            {
                var rootEntities = db2.RootEntities.OrderBy(i => i.Id);

                foreach (var rootEntity in rootEntities)
                {
                    Console.WriteLine("Waiting on 2");
                    // Wait a bit until db1.SaveChanges() above will finish execution.
                    Thread.Sleep(2000);
                    Console.WriteLine("Starting 2");

                    rootEntity.Child = new ChildEntity();                 

                    // Here will be an error Error: SQLite Error 5: 'database is locked'
                    db2.SaveChanges();
                    Console.WriteLine("Done 2");
                }
            }
        }
    }

    public class ExampleContext : DbContext
    {
        public DbSet<RootEntity> RootEntities { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options
                .LogTo(Console.WriteLine, new[]
                {
                    RelationalEventId.TransactionStarted,
                    RelationalEventId.CommandCreated,
                    RelationalEventId.CommandExecuting,
                    RelationalEventId.CommandExecuted,
                    RelationalEventId.TransactionCommitted,
                    RelationalEventId.TransactionRolledBack,
                    RelationalEventId.TransactionDisposed,
                })
                .EnableSensitiveDataLogging()
                .UseSqlite($"Pooling=False;Data Source=blogging.db");
    }

    public class RootEntity
    {
        public Int32 Id { get; set; }

        public ChildEntity? Child { get; set; }
    }

    public class ChildEntity
    {
        public Int32 Id { get; set; }

        public String? AProperty { get; set; }
    }
}

@Gibrid89
Copy link

Gibrid89 commented Mar 20, 2023

I am using this extension to fix this error. It is a sad that there is no normal solution.

public static class SQLiteThreadSaveExtension
  {
      public static ReaderWriterLockSlim DBReaderWriterLock { get; private set; } = new ReaderWriterLockSlim();

      public static void SaveChangesConcurrent(this MyDBContext context)
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              context.SaveChanges();
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static async Task SaveChangesConcurrentAsync(this MyDBContext context)
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              await context.SaveChangesAsync();
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static void BulkInsertConcurrent<T>(this MyDBContext context, IList<T> entities, BulkConfig? bulkConfig = null, Action<decimal>? progress = null, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              context.BulkInsert(entities, bulkConfig, progress, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static async Task BulkInsertConcurrentAsync<T>(this MyDBContext context, IList<T> entities, BulkConfig? bulkConfig = null, Action<decimal>? progress = null, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              await context.BulkInsertAsync(entities, bulkConfig, progress, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static void BulkUpdateConcurrent<T>(this MyDBContext context, IList<T> entities, BulkConfig? bulkConfig = null, Action<decimal>? progress = null, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              context.BulkUpdate(entities, bulkConfig, progress, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static async Task BulkUpdateConcurrentAsync<T>(this MyDBContext context, IList<T> entities, BulkConfig? bulkConfig = null, Action<decimal>? progress = null, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              await context.BulkUpdateAsync(entities, bulkConfig, progress, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static void BulkDeleteConcurrent<T>(this MyDBContext context, IList<T> entities, BulkConfig? bulkConfig = null, Action<decimal>? progress = null, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              context.BulkDelete(entities, bulkConfig, progress, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static async Task BulkDeleteConcurrentAsync<T>(this MyDBContext context, IList<T> entities, BulkConfig? bulkConfig = null, Action<decimal>? progress = null, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              await context.BulkDeleteAsync(entities, bulkConfig, progress, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static int BatchUpdateConcurrent<T>(this IQueryable<T> query, Expression<Func<T, T>> updateExpression, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              return query.BatchUpdate(updateExpression, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static int BatchDeleteConcurrent<T>(this IQueryable<T> query) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              return query.BatchDelete();
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }


      public static async Task<int> BatchUpdateConcurrentAsync<T>(this IQueryable<T> query, Expression<Func<T, T>> updateExpression, Type? type = null) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              return await query.BatchUpdateAsync(updateExpression, type);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }

      public static async Task<int> BatchDeleteConcurrentAsync<T>(this IQueryable<T> query) where T : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              return await query.BatchDeleteAsync();
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }



      public static IDbContextTransaction BeginTransactionConcurrent(this DatabaseFacade database) 
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              return database.BeginTransaction();
          }
          catch
          {
              DBReaderWriterLock.ExitWriteLock();
              throw;
          }
      }
      public static void CommitConcurrent(this IDbContextTransaction transaction)
      {
          try
          {
              transaction.Commit();
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }


      public static void TruncateConcurrent<TEntity>(this DbSet<TEntity> dbSet, MyDBContext context) where TEntity : class
      {
          try
          {
              DBReaderWriterLock.EnterWriteLock();
              dbSet.Truncate(context);
          }
          finally
          {
              DBReaderWriterLock.ExitWriteLock();
          }
      }


      public static void Truncate<TEntity>(this DbSet<TEntity> dbSet, MyDBContext context) where TEntity : class
      {
          if (dbSet is null)
              throw new ArgumentNullException(nameof(dbSet));

          var tableName = context.GetTableName(typeof(TEntity));
          context.Database.ExecuteSqlRaw($"DELETE FROM \"{tableName}\"");
      }
  }

@albyrock87
Copy link

albyrock87 commented Apr 16, 2024

Have anyone managed to workaround this? I think it's really critical.

I also tried to use ReaderWriterLockSlim as follows, but even if calls are serialized in the right way, I still get the database locked issue.

It doesn't seem a parallelism problem, it seems to me that something has not been released on the database after writing.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Internal;

namespace EfCoreNotNullNestedOwned
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new ExampleContext())
            {
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();

                for (int i = 0; i < 5; i++)
                    db.Add(new RootEntity());

                db.SaveChanges();
            }

            // Comment this whole task and application will run without any errors.
            var task = Task.Run(() =>
            {
                using var db1 = new ExampleContext();
                db1.RootEntities.First().Child = new ChildEntity();
                db1.SaveChanges();
                Console.WriteLine("Done 1");
            });
            
            using (var db2 = new ExampleContext())
            {
                var rootEntities = db2.RootEntities.OrderBy(i => i.Id);
                foreach (var rootEntity in rootEntities)
                {
                    Console.WriteLine("Waiting on 2");
                    // Wait a bit until db1.SaveChanges() above will finish execution.
                    task.Wait();
                    
                    Console.WriteLine("Starting 2");

                    rootEntity.Child = new ChildEntity();                 

                    // Here will be an error Error: SQLite Error 5: 'database is locked'
                    db2.SaveChanges();
                    
                    Console.WriteLine("Done 2");
                }
            }
        }
    }

    public class SynchronizedEntityQueryProvider(IQueryCompiler queryCompiler) : EntityQueryProvider(queryCompiler)
    {
        public override object Execute(Expression expression)
        {
            ExampleContext.Lock.EnterReadLock();
            try
            {
                return base.Execute(expression);
            }
            finally
            {
                ExampleContext.Lock.ExitReadLock();
            }
        }

        public override TResult Execute<TResult>(Expression expression)
        {
            ExampleContext.Lock.EnterReadLock();
            try
            {
                return base.Execute<TResult>(expression);
            }
            finally
            {
                ExampleContext.Lock.ExitReadLock();
            }
        }

        public override TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = new CancellationToken())
        {
            ExampleContext.Lock.EnterReadLock();
            try
            {
                return base.ExecuteAsync<TResult>(expression, cancellationToken);
            }
            finally
            {
                ExampleContext.Lock.ExitReadLock();
            }
        }
    }

    public class ExampleContext : DbContext
    {
        public static readonly ReaderWriterLockSlim Lock = new();
        public DbSet<RootEntity> RootEntities { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options
                .UseSqlite($"Data Source=blogging.db")
                .LogTo(Console.WriteLine, new[]
                {
                    RelationalEventId.TransactionStarted,
                    RelationalEventId.CommandCreated,
                    RelationalEventId.CommandExecuting,
                    RelationalEventId.CommandExecuted,
                    RelationalEventId.TransactionCommitted,
                    RelationalEventId.TransactionRolledBack,
                    RelationalEventId.TransactionDisposed,
                })
                .EnableSensitiveDataLogging()
                .ReplaceService<IAsyncQueryProvider, SynchronizedEntityQueryProvider>();
        }

        public override int SaveChanges()
        {
            Lock.EnterWriteLock();
            try
            {
                return base.SaveChanges();
            }
            finally
            {
                Lock.ExitWriteLock();
            }
        }

        public override void Dispose()
        {
            Database.CloseConnection();
            base.Dispose();
        }
    }

    public class RootEntity
    {
        public Int32 Id { get; set; }

        public ChildEntity? Child { get; set; }
    }

    public class ChildEntity
    {
        public Int32 Id { get; set; }

        public String? AProperty { get; set; }
    }
}

Logs:

dbug: 16/04/2024 10:34:21.720 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 16/04/2024 10:34:21.724 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      PRAGMA journal_mode = 'wal';
info: 16/04/2024 10:34:21.730 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      PRAGMA journal_mode = 'wal';
dbug: 16/04/2024 10:34:21.756 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Began transaction with isolation level 'Serializable'.
dbug: 16/04/2024 10:34:21.756 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 16/04/2024 10:34:21.756 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "ChildEntity" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_ChildEntity" PRIMARY KEY AUTOINCREMENT,
          "AProperty" TEXT NULL
      );
info: 16/04/2024 10:34:21.757 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "ChildEntity" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_ChildEntity" PRIMARY KEY AUTOINCREMENT,
          "AProperty" TEXT NULL
      );
dbug: 16/04/2024 10:34:21.757 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 16/04/2024 10:34:21.757 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RootEntities" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RootEntities" PRIMARY KEY AUTOINCREMENT,
          "ChildId" INTEGER NULL,
          CONSTRAINT "FK_RootEntities_ChildEntity_ChildId" FOREIGN KEY ("ChildId") REFERENCES "ChildEntity" ("Id")
      );
info: 16/04/2024 10:34:21.757 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "RootEntities" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_RootEntities" PRIMARY KEY AUTOINCREMENT,
          "ChildId" INTEGER NULL,
          CONSTRAINT "FK_RootEntities_ChildEntity_ChildId" FOREIGN KEY ("ChildId") REFERENCES "ChildEntity" ("Id")
      );
dbug: 16/04/2024 10:34:21.757 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteNonQuery' (0ms).
dbug: 16/04/2024 10:34:21.757 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_RootEntities_ChildId" ON "RootEntities" ("ChildId");
info: 16/04/2024 10:34:21.757 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE INDEX "IX_RootEntities_ChildId" ON "RootEntities" ("ChildId");
dbug: 16/04/2024 10:34:21.758 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committed transaction.
dbug: 16/04/2024 10:34:21.758 RelationalEventId.TransactionDisposed[20204] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Disposing transaction.
dbug: 16/04/2024 10:34:21.797 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Began transaction with isolation level 'Serializable'.
dbug: 16/04/2024 10:34:21.798 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.799 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 16/04/2024 10:34:21.800 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (1ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
info: 16/04/2024 10:34:21.805 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[@p0=NULL (DbType = Int32)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "RootEntities" ("ChildId")
      VALUES (@p0)
      RETURNING "Id";
dbug: 16/04/2024 10:34:21.805 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committed transaction.
dbug: 16/04/2024 10:34:21.805 RelationalEventId.TransactionDisposed[20204] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Disposing transaction.
dbug: 16/04/2024 10:34:21.880 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.880 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      ORDER BY "r"."Id"
info: 16/04/2024 10:34:21.880 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      ORDER BY "r"."Id"
dbug: 16/04/2024 10:34:21.880 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.880 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      LIMIT 1
info: 16/04/2024 10:34:21.880 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "r"."Id", "r"."ChildId"
      FROM "RootEntities" AS "r"
      LIMIT 1
Waiting on 2
dbug: 16/04/2024 10:34:21.888 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Began transaction with isolation level 'Serializable'.
dbug: 16/04/2024 10:34:21.888 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.888 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p0=NULL], CommandType='Text', CommandTimeout='30']
      INSERT INTO "ChildEntity" ("AProperty")
      VALUES (@p0)
      RETURNING "Id";
info: 16/04/2024 10:34:21.888 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[@p0=NULL], CommandType='Text', CommandTimeout='30']
      INSERT INTO "ChildEntity" ("AProperty")
      VALUES (@p0)
      RETURNING "Id";
dbug: 16/04/2024 10:34:21.889 RelationalEventId.CommandCreated[20104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Created DbCommand for 'ExecuteReader' (0ms).
dbug: 16/04/2024 10:34:21.889 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing DbCommand [Parameters=[@p2='1', @p1='1' (Nullable = true)], CommandType='Text', CommandTimeout='30']
      UPDATE "RootEntities" SET "ChildId" = @p1
      WHERE "Id" = @p2
      RETURNING 1;
info: 16/04/2024 10:34:21.889 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (0ms) [Parameters=[@p2='1', @p1='1' (Nullable = true)], CommandType='Text', CommandTimeout='30']
      UPDATE "RootEntities" SET "ChildId" = @p1
      WHERE "Id" = @p2
      RETURNING 1;
dbug: 16/04/2024 10:34:21.890 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Committed transaction.
dbug: 16/04/2024 10:34:21.890 RelationalEventId.TransactionDisposed[20204] (Microsoft.EntityFrameworkCore.Database.Transaction) 
      Disposing transaction.
Done 1
Starting 2
Unhandled exception. Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 5: 'database is locked'.
   at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db)
   at Microsoft.Data.Sqlite.SqliteDataReader.NextResult()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader()
   at Microsoft.Data.Sqlite.SqliteCommand.ExecuteNonQuery()
   at Microsoft.Data.Sqlite.SqliteConnectionExtensions.ExecuteNonQuery(SqliteConnection connection, String commandText, SqliteParameter[] parameters)
   at Microsoft.Data.Sqlite.SqliteTransaction..ctor(SqliteConnection connection, IsolationLevel isolationLevel, Boolean deferred)
   at Microsoft.Data.Sqlite.SqliteConnection.BeginTransaction(IsolationLevel isolationLevel, Boolean deferred)
   at Microsoft.Data.Sqlite.SqliteConnection.BeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.Data.Sqlite.SqliteConnection.BeginDbTransaction(IsolationLevel isolationLevel)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.ConnectionBeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransaction(IsolationLevel isolationLevel)
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransaction()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IList`1 entries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(StateManager stateManager, Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<>c.<SaveChanges>b__112_0(DbContext _, ValueTuple`2 t)
   at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()

@albyrock87
Copy link

albyrock87 commented Apr 16, 2024

@ajcvickers @MaximMikhisor I partially understood what's happening here.

  • main thread enters foreach section, opens a connection C1 which executes the OrderBy command and starts enumerating results
    • the underneath data reader is open at this point, and it is reading through the RootEntity table
    • C1 can read and write from the table now
  • now the second thread opens a connection C2 and updates one row
    • at this point C1 is still reading the table and it has to finish reading the data before being able to write
    • if you move db2.SaveChanges(); outside of the foreach the issue is now fixed

Now, I think this is a matter of "Isolation Level".
If C1 is reading and writing from the table (and it is the only one doing that) the connection can access the latest state, and therefore can always perform an update.
If someone acts in the middle C2, now C1 must complete what it's doing in order to "access" the updated state and ensure consistent writes.

I think this narrows down to how WAL works by using these checkpoints https://www.sqlite.org/wal.html

So my suggestion, in general, is to never write while reading from the same db context, and maybe avoid lazy evaluation of enumerables (simply use ToList).

@MaximMikhisor
Copy link
Author

MaximMikhisor commented Apr 24, 2024

@albyrock87

Inside loop we have "Thread.Sleep(2000)"
It means that when C1 is trying to write to DB, C2 already closed and should not influence on C1.
But instead we have an exception.
Are you agree?

at this point C1 is still reading the table and it has to finish reading the data before being able to write

Who "being able to write"? C1 or C2?

@albyrock87
Copy link

@MaximMikhisor I'm saying C1 is "auto blocking itself" due to the fact it is still reading (foreach on IEnumerable) from an old checkpoint, while writing requires access to the latest checkpoint.

@MaximMikhisor
Copy link
Author

If you comment or delete task, i.e. if only C2 will work, you will see that C2 is working ok.
Even with db2.SaveChanges(); inside of the foreach.

@albyrock87
Copy link

albyrock87 commented Apr 24, 2024

@MaximMikhisor exactly, that's because no one changed the checkpoint state in the mean time, so it can always write and read from the latest checkpoint.

At that poi C1 is the only one doing writes and updating the checkpoint, so he knows where to write.

This is the only explanation I could come up with.

@MaximMikhisor
Copy link
Author

You a bit confusing "checkpoint" word.
From documentation: Moving the WAL file transactions back into the database is called a "checkpoint".

Checkpoint is not source of problem.

I guess I found what is a source of "SQLite Error 5: 'database is locked'" error: SQLITE_BUSY_SNAPSHOT

The SQLITE_BUSY_SNAPSHOT error code is an extended error code for SQLITE_BUSY that occurs on WAL mode databases when a database connection tries to promote a read transaction into a write transaction but finds that another database connection has already written to the database and thus invalidated prior reads.

But for some reason SQLite return "SQLite Error 5: 'database is locked'" instead of "(517)SQLITE_BUSY_SNAPSHOT" error.

I think ticket could be closed.
Because it is not a bug of EFCore, but it is expected behavior of SQLite.

@albyrock87
Copy link

@MaximMikhisor you're right I switched words between "checkpoint" and "snapshot", but still that's the concept I wanted to explain: if someone writes while a connection is actively reading a snapshot (which I called checkpoint above) then that connection cannot write until it completes the read, because otherwise it would try to write to an old snapshot.

Anyway, as we both mentioned this is a SQLite expected behavior, so we cannot do anything about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants