0

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    
dani herrera
  • 48,760
  • 8
  • 117
  • 177
  • 1
    Although this error is called deadlock, it is not in fact a deadlock (in the way it is used in normal computer science terminology). It's an update conflict, because the record was modified by a transaction which was committed after your transaction started. The appropriate way to solve it is by rolling back your current transaction, and restarting your unit of work. Alternatively, try switching to ReadCommitted, the new READ COMMITTED READ CONSISTENCY mode introduced in Firebird 4 should allow this to succeed, IIRC. – Mark Rotteveel Dec 13 '22 at 17:37
  • Hi @MarkRotteveel, thanks about your fast reply. I changed to `TransactionBehavior = FbTransactionBehavior.Wait | FbTransactionBehavior.ReadConsistency` (also tried with `TransactionBehavior = FbTransactionBehavior.ReadConsistency` ) and the error persists . What I'm doing wrong? Thanks again. – dani herrera Dec 13 '22 at 17:46
  • ReadConsistency only works for isolation level read committed (and with default Firebird 4 settings, it will be applied irrespective of the transaction config) – Mark Rotteveel Dec 13 '22 at 17:52
  • And otherwise I'll need to reproduce and test this, but I probably won't have time for that until the weekend. – Mark Rotteveel Dec 13 '22 at 17:53
  • @MarkRotteveel, awesome. I posted ready copy-paste code also with Firebird (v4) docker environment. See you on Monday :) – dani herrera Dec 13 '22 at 17:56
  • If your target is to always see the latest data, you'd better use some transaction-less NoSQL. – user13964273 Dec 15 '22 at 12:00
  • @user13964273 I want serializable isolation level with pessimistic concurrency (lock read data) – dani herrera Dec 15 '22 at 13:47
  • And that's what transaction-less storages are for. Firebird is aimed on contrary: optimistic concurrency with minimal locking and you cannot get what you want without, as Mark said, rolling back transaction on error and repeat it. – user13964273 Dec 16 '22 at 11:42
  • @user13964273, thanks about your comment. For me, it's valuable information. Can you share with me some link to "official" documentation? – dani herrera Dec 16 '22 at 12:16
  • https://firebirdsql.org/file/documentation/chunk/en/refdocs/fblangref40/fblangref40-dml.html#fblangref40-dml-with-lock pay attention to the table describing locks communication for different TILs. What may be missed (implied) there is requirement for both transactions to use SELECT WITH LOCK because interaction of SELECT WITH LOCK with plain UPDATE is even more tricky. – user13964273 Dec 16 '22 at 16:17
  • @user13964273 obviously my approach to solve concurrency is bad. Reading docs it looks it is much better a retry approach. Are you so kind to write an answer suggesting this way? I will check as solution. – dani herrera Dec 16 '22 at 16:43
  • No because I think that both approaches are bad and should not be used in any real-life application. Right solution would be to decide concurrency rules at business logic level IMHO. – user13964273 Dec 16 '22 at 22:39
  • @user13964273 can you elaborate? – dani herrera Dec 17 '22 at 07:40
  • Quite a lot of real-life activity physically cannot be done concurrently. You cannot put single bank card into two ATMs at the same time, you cannot sell the same piece of meat twice and so on. For me it is very hard to imagine a business process that would cause concurrent access to the same field of the same record. Because of that when you said "I want", for me it sounded as you somehow screwed system analyze and your (or most likely customer's) requirements are unrealistic. That's why I don't want to write an answer for a wrong question. https://en.wikipedia.org/wiki/XY_problem you know... – user13964273 Dec 17 '22 at 12:27
  • @user13964273 really do you think concurrency is not a thing in databases? O_O – dani herrera Dec 17 '22 at 13:20

0 Answers0