Skip to content

Silent data loss when adding objects to an sqlite DB with PRAGMA journal_mode=DELETE in a multi-threaded environment. #30851

@EdwardNickson

Description

@EdwardNickson

Hi,

I'm experiencing a data loss issue when using EF Core with sqlite in a multi threaded scenario. This bug seems to only happen if PRAGMA journal_mode=DELETE. I can't replicate it in WAL mode.
What's happening is that sometimes when a new object is added and SaveChanges is called on a context, the database insert appears to run successfully, and DatabaseGenerated primary keys are created by the DB and set against the C# object, but when the database is later queried for that primary key the record doesn't exist. What's more, the next time an object is created and saved to the database it gets assigned the same primary key as the previous object. It's almost like it rolled-back the first transaction without throwing an exception.

This issue does not exist in EF Core 6, it started in EF Core 7. We have found that adding
ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>() to our options builder resolves this issue, so it seems to be a bug in the new SqliteUpdateSqlGenerator.

Sample Program

This program sets up a database with PRAGMA journal_mode=DELETE, creates two dBContexts for one sqlite database and then adds objects to the contexts in one-off tasks which run in parallel. After each addition it checks to ensure the primary keys set in each context's call to SaveChanges is unique. If the keys are not unique it throws an exception.

What I expect to happen:
The program runs to completion with no exceptions.

What actually happens:
The program throws an exception because the objects added to the two different contexts ended up with the same primary key.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
  </ItemGroup>

</Project>
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Sqlite.Update.Internal;
using Microsoft.EntityFrameworkCore.Update;

namespace collisionTest;

public class LogEvent
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public required string Message { get; set; }
}

public class LogDbContext : DbContext
{
    public DbSet<LogEvent> LogEvents => Set<LogEvent>();
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data source=testDb.db");

        // Bug can be worked around by using legacy update SQL generator
        // .ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();
    }
}

public class Program
{
    static async Task Main(string[] _)
    {
        using (var contextToCreateDb = new LogDbContext())
        {
            contextToCreateDb.Database.EnsureDeleted();
            contextToCreateDb.Database.EnsureCreated();
            using var connection = contextToCreateDb.Database.GetDbConnection();
            connection.Open();
            using var command = connection.CreateCommand();
            command.CommandText = "PRAGMA journal_mode=DELETE;";
            command.ExecuteNonQuery();
        }

        using var contextOne = new LogDbContext();
        using var contextTwo = new LogDbContext();
        for (int i = 0; i < 500; i++)
        {
            var taskOne = Task.Run(() =>
            {
                var logEvent = new LogEvent(){ Message = "Log Message" };
                contextOne.Add(logEvent);
                contextOne.SaveChanges();
                return logEvent.Id;
            });
            var taskTwo = Task.Run(() =>
            {
                var logEvent = new LogEvent(){ Message = "Log Message" };
                contextTwo.Add(logEvent);
                contextTwo.SaveChanges();
                return logEvent.Id;
            });
            await taskOne;
            await taskTwo;
            if (taskOne.Result == taskTwo.Result)
            {
                throw new Exception($"Duplicate IDs Returned: {taskOne.Result}");
            }
        }
    }
}

Include provider and version information

EF Core version: 7.0.5
Database provider: Microsoft.EntityFrameworkCore.Sqlite
Target framework: .NET 7.0
Operating system: Ubuntu 18.04 & Ubuntu 20.04
IDE: VS Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions