powersync-ja / react-native-quick-sqlite

Embedded SQLite with JSI bindings
MIT License
6 stars 2 forks source link

[Feature] Concurrent connections & transactions #2

Closed stevensJourney closed 1 year ago

stevensJourney commented 1 year ago

Description

This PR adds the ability to concurrently access a SQLite DB. The work is based off this Blog Post and adapted from https://github.com/journeyapps/sqlite_async.dart.

Multiple read connections and a single write connection are opened in WAL mode. A C++ connection pool wardens which context has access to any of the DB connections at a point in time. Asynchronous queuing is handled via the JavaScript side of JSI bindings. All SQL operations are asynchronously executed in the thread pool.

Single connection DB connections are possible by setting the number of read connections to zero.

Flow

The general flow is:

Tests

The test app was updated and now includes tests for DB connections. All tests currently pass on Android and iOS. An automated test for Android has been added to CI.

image

Related PRs

https://github.com/journeyapps/powersync-react-native-sdk/pull/7 Updated logic to use new DBAdapter API. Tested in demo application.

stevensJourney commented 1 year ago

There are currently still possibilities for concurrency issues.

Taking this example:

await db.writeTransaction(async (tx) => {
   var promises = [];
   for (var i = 0; i < 100; i++) {
      promises.push(tx.executeAsync('SELECT * FROM User'));
   }
   await Promise.all(promises);
});

While this transaction has an exclusive lock on the write connection, the individual statements are concurrently executed on different threads, which can cause issues (not sure exactly what issues, but could be anything from a crash to data corruption). Using SQLITE_OPEN_FULLMUTEX may mitigate those, but then you'd effectively use all the threads for a single transaction in this case, leaving none available for other concurrent transactions.

A good option would be to combine the ThreadPool and ConnectionPool, and have a dedicated thread and work queue per connection - never letting other threads touch the connection.

@rkistner The threadpool is now removed and each connection (inside the connection pool) has it's own thread that will queue operations inside the context lock. This will ensure only one DB operation occurs on a connection at a time. A test was also added for this use case.