Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Node.js + SQLite async transactions

I am using node-sqlite3, but I am sure this problem appears in another database libraries too. I have discovered a bug in my code with mixing transactions and async code.

function insertData(arrayWithData, callback) {
    // start a transaction
    db.run("BEGIN", function() {
        // do multiple inserts
        slide.asyncMap(
            arrayWithData,
            function(cb) {
                db.run("INSERT ...", cb);
            },
            function() {
                // all done
                db.run("COMMIT");
            }
        );
    });
}

// some other insert
setInterval(
    function() { db.run("INSERT ...", cb); },
    100
);

You can also run the full example.

The problem is that some other code with insert or update query can be launched during the async pause after begin or insert. Then this extra query is run in the transaction. This is not a problem when the transaction is committed. But if the transaction is rolled back the change made by this extra query is also rolled back. Hoops we've just unpredictably lost data without any error message.

I thought about this issue and I think that one solution is to create a wrapper class that will make sure that:

  • Only one transaction is running at the same time.
  • When transaction is running only queries which belong to the transaction are executed.
  • All the extra queries are queued and executed after the current transaction is finished.
  • All attempts to start a transaction when one is already running will also get queued.

But it sounds like too complicated solution. Is there a better approach? How do you deal with this problem?

like image 986
Strix Avatar asked Oct 09 '13 12:10

Strix


2 Answers

At first, I would like to state that I have no experience with SQLite. My answer is based on quick study of node-sqlite3.

The biggest problem with your code IMHO is that you try to write to DB from different locations. As I understand SQLite, you have no control of different parallel "connections" as you have in PostgreSQL, so you probably need to wrap all your communication with DB. I modified your example to use always insertData wrapper. Here is the modified function:

function insertData(callback, cmds) {
  // start a transaction
  db.serialize(function() {
    db.run("BEGIN;");
    //console.log('insertData -> begin');
    // do multiple inserts
    cmds.forEach(function(item) {
      db.run("INSERT INTO data (t) VALUES (?)", item, function(e) {
        if (e) {
          console.log('error');
          // rollback here
        } else {
          //console.log(item);
        }
      });
    });
    // all done
    //here should be commit
    //console.log('insertData -> commit');
    db.run("ROLLBACK;", function(e) {
      return callback();
    });
  });
}

Function is called with this code:

init(function() {
  // insert with transaction
  function doTransactionInsert(e) {
    if (e) return console.log(e);
    setTimeout(insertData, 10, doTransactionInsert, ['all', 'your', 'base', 'are', 'belong', 'to', 'us']);
  }

  doTransactionInsert();

  // Insert increasing integers 0, 1, 2, ...
  var i=0;

  function doIntegerInsert() {
    //console.log('integer insert');
    insertData(function(e) {
      if (e) return console.log(e);
      setTimeout(doIntegerInsert, 9);
    }, [i++]);
  }

  ...

I made following changes:

  • added cmds parameter, for simplicity I added it as last parameter but callback should be last (cmds is an array of inserted values, in final implementation it should be an array of SQL commands)
  • changed db.exec to db.run (should be quicker)
  • added db.serialize to serialize requests inside transaction
  • ommited callback for BEGIN command
  • leave out slide and some underscore

Your test implementation now works fine for me.

like image 157
ivoszz Avatar answered Sep 23 '22 15:09

ivoszz


I have end up doing full wrapper around sqlite3 to implement locking the database in a transaction. When DB is locked all queries are queued and executed after the current transaction is over.

https://github.com/Strix-CZ/sqlite3-transactions

like image 31
Strix Avatar answered Sep 24 '22 15:09

Strix