Avoid deadlock on Firebird

417 Views Asked by At

I would like to avoid DeadLock issue in code. I read the way is to use FOR UPDATE WITH LOCK also FbTransactionBehavior.Wait. But my second transaction, despite it is waiting for the first one to finish, always dies by DeadLock.

I wrote a very simple test to illustrate my issue, I pasted the test here below, easier than trying to explain with words (I guess)

The goal is to complete both transactions. The test result is a bit bizarre, the second transaction waits until the first one finishes but also dies. I expect both transactions completes (if it is possible)

tx1 tx2 Comments
tx starts
tx starts
Read id=6
Update id=6
Read id=6 <- tx2 Locked, it's ok, waiting for commit
Commit <- tx2 unlocked
<- tx2 deadlock :(
using FirebirdSql.Data.FirebirdClient;

namespace fireb;

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        // tx 1 starts
        using var connection1 = new FbConnection("database=localhost:demo.fdb;user=sysdba;password=masterkey");
        connection1.Open();
        using var transaction1 = connection1.BeginTransaction(
            new FbTransactionOptions() { TransactionBehavior = FbTransactionBehavior.Wait, WaitTimeout = TimeSpan.FromSeconds(5) });

        // tx 2 starts
        using var connection2 = new FbConnection("database=localhost:demo.fdb;user=sysdba;password=masterkey");
        connection2.Open();
        using var transaction2 = connection2.BeginTransaction(
            new FbTransactionOptions() { TransactionBehavior = FbTransactionBehavior.Wait, WaitTimeout = TimeSpan.FromSeconds(30) });

        // updating on tx1
        using var command_pre1 = new FbCommand("select * from demo where id = 6 FOR UPDATE WITH LOCK;", connection1, transaction1);
        command_pre1.ExecuteReader().Read();
        using var command1 = new FbCommand("update demo set foobar = 'pp1' where id = 6;", connection1, transaction1);
        command1.ExecuteNonQuery();

        // commit in 10''
        Action myCommit = () => {
            transaction1.Commit();
            transaction1.Dispose();
            command_pre1.Dispose();
            command1.Dispose();
            connection1.Close();
            connection1.Dispose();
        };

        var aTimer = new System.Timers.Timer(10000);
        aTimer.Elapsed += (a,b) => myCommit();
        aTimer.AutoReset = false;
        aTimer.Enabled = true;        

        // updating tx2
        using var command_pre2 = new FbCommand("select * from demo where id = 6 FOR UPDATE WITH LOCK;", connection2, transaction2);
        command_pre2.ExecuteReader().Read();  // <--- here dies. How to avoid? 
        using var command2 = new FbCommand("update demo set foobar = 'pp2' where id = 6;", connection2, transaction2);
        command2.ExecuteNonQuery();

        // Commit tx 2
        transaction2.Commit();

    }

This is the error:

FirebirdSql.Data.FirebirdClient.FbException : deadlock update conflicts with concurrent update concurrent transaction number is 67 ---- FirebirdSql.Data.Common.IscException : deadlock update conflicts with concurrent update

End of question

If you want to reproduce:

To create the table ...

    // from here just arranging database and data

    public UnitTest1()
    {
        // Just to create database, table and row
        // CreaBd();
        // CreaTaula();
        // InsertData();
    }

    private static void CreaTaula()
    {
        using var connection = new FbConnection("database=localhost:demo.fdb;user=sysdba;password=masterkey");
        connection.Open();
        using var transaction = connection.BeginTransaction(System.Data.IsolationLevel.RepeatableRead);
        using var command = new FbCommand(
            "create table demo (id int primary key, foobar varchar(20) character set utf8); ", 
            connection, transaction);
        command.ExecuteNonQuery();
        transaction.Commit();
    }

    private static void InsertData()
    {
        using var connection = new FbConnection("database=localhost:demo.fdb;user=sysdba;password=masterkey");
        connection.Open();
        using var transaction = connection.BeginTransaction(System.Data.IsolationLevel.RepeatableRead);
        using var command = new FbCommand(
            "insert into demo values (6, 'FooBar');", 
            connection, transaction);
        command.ExecuteNonQuery();
        transaction.Commit();
    }

    private static void CreaBd()
    {
        FbConnectionStringBuilder builder = new FbConnectionStringBuilder();
        builder.DataSource = "localhost";
        builder.UserID = "sysdba";
        builder.Password = "masterkey";
        builder.Database = "demo.fdb";
        builder.ServerType = FbServerType.Default;

        FbConnection.CreateDatabase(builder.ConnectionString);
    }
}

If you want to run firebird in docker

docker run -d \ 
    --name firebird \
    -p 3050:3050 \
    -e "ISC_PASSWORD=masterkey" \
    -v /Users/dani/tmp/fireb/dockerdb:/databases \
    jacobalberty/firebird    
0

There are 0 best solutions below