Skip to content

SQLite/DuckDB Concurrency Errors Within a Single Process #82

@breckcs

Description

@breckcs

What happens?

  1. Write to a SQLite database using a SQLite client.
  2. Reading concurrently from the same SQLite database within the same process leads to various errors:
    1. attempt to write a readonly database errors.
    2. A crash due to EXC_BAD_ACCESS (SIGBUS).
    3. database is locked errors.

I am unable to produce similar errors when reading concurrently with a SQLite client.

Six test scenarios are detailed below.

I'm happy to help with further debugging and/or updates to code or documentation related to this issue.

To Reproduce

  1. Clone duckdb_sqlite_scanner_concurrency_tests.
  2. Use Cargo to run each test individually.

✅ 1) SQLite Writer, SQLite Reader, No WAL

Run the test:

cargo test test_sqlite_writer_sqlite_reader_no_wal -- --nocapture

It should pass:

...
SQLite COUNT : 9998
SQLite COUNT : 9999
SQLite COUNT : 10000
test tests::test_sqlite_writer_sqlite_reader_no_wal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 39.56s

✅ 2) SQLite Writer, SQLite Reader, With WAL

Run the test:

cargo test test_sqlite_writer_sqlite_reader_with_wal -- --nocapture

It should pass:

...
SQLite COUNT : 9998
SQLite COUNT : 9999
SQLite COUNT : 10000
test tests::test_sqlite_writer_sqlite_reader_with_wal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 16.27s

🔴 3) SQLite Writer, DuckDB Reader, No WAL

Run the test:

cargo test test_sqlite_writer_duckdb_reader_no_wal -- --nocapture

It should fail:

running 1 test
thread '<unnamed>' panicked at src/main.rs:194:84:
called `Result::unwrap()` on an `Err` value: DuckDBFailure(Error { code: Unknown, extended_code: 1 }, Some("Invalid Error: Failed to prepare query \"SELECT type FROM sqlite_master WHERE lower(name)=lower('users');\": attempt to write a readonly database"))

I don't expect DuckDB to be mutating anyting in this test, but even if I remove the READ_ONLY from the ATTACH statement, I get a similar error:

DuckDB COUNT : 627
thread '<unnamed>' panicked at src/main.rs:194:84:
called `Result::unwrap()` on an `Err` value: DuckDBFailure(Error { code: Unknown, extended_code: 1 }, Some("Invalid Error: Failed to prepare query \"SELECT ROWID FROM \"users\" WHERE ROWID BETWEEN 0 AND 122879\": attempt to write a readonly database"))

DuckDB is ultimately using the SQLite client to read, so this scenario should be equivalent to the first test scenario.

Questions:

  1. Is DuckDB opening the SQLite database in an incompatible mode?
  2. Does sqlite3.c used by both Rusqlite and duckdb-rs need to be binary compatible?

🔴 4) SQLite Writer, DuckDB Reader, With WAL

Run the test:

cargo test test_sqlite_writer_duckdb_reader_with_wal -- --nocapture

The process crashes with a EXC_BAD_ACCESS (SIGBUS) error:

DuckDB COUNT : 1630
DuckDB COUNT : 1633
DuckDB COUNT : 1634
error: test failed, to rerun pass `--bin duckdb_sqlite_scanner_concurrency_tests`

Caused by:
  process didn't exit successfully: `/Users/cbreck/github/duckdb_sqlite_scanner_concurrency_tests/target/debug/deps/duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4 test_sqlite_writer_duckdb_reader_with_wal --nocapture` (signal: 10, SIGBUS: access to undefined memory)

If I run it in the debugger, I get the following console output:

2024-01-24 05:55:49.165465-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb9ParseInfoE, N6duckdb23CreateTableFunctionInfoE, N6duckdb23CreateTableFunctionInfoE.
2024-01-24 05:55:49.165892-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb9ParseInfoE, N6duckdb23CreateTableFunctionInfoE, N6duckdb23CreateTableFunctionInfoE.
2024-01-24 05:55:49.168720-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb12CatalogEntryE, N6duckdb16SQLiteTableEntryE, N6duckdb17TableCatalogEntryE.
2024-01-24 05:55:49.172696-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb12CatalogEntryE, N6duckdb16SQLiteTableEntryE, N6duckdb17TableCatalogEntryE.
2024-01-24 05:55:49.188474-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb12CatalogEntryE, N6duckdb16SQLiteTableEntryE, N6duckdb17TableCatalogEntryE.
2024-01-24 05:55:49.218949-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb12CatalogEntryE, N6duckdb16SQLiteTableEntryE, N6duckdb17TableCatalogEntryE.
2024-01-24 05:55:49.278716-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb12CatalogEntryE, N6duckdb16SQLiteTableEntryE, N6duckdb17TableCatalogEntryE.
2024-01-24 05:55:49.397928-0800 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4[31472:2612509] dynamic_cast error 2: One or more of the following type_info's has hidden visibility or is defined in more than one translation unit. They should all have public visibility. N6duckdb12CatalogEntryE, N6duckdb16SQLiteTableEntryE, N6duckdb17TableCatalogEntryE.
Exception: EXC_BAD_ACCESS (code=10, address=0x1085a57f0)

The call stack is as follows:

(lldb) thread backtrace
* thread #3, stop reason = EXC_BAD_ACCESS (code=10, address=0x1085a57f0)
  * frame #0: 0x0000000104462fac duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`walIndexAppend(pWal=0x000000011fa04c00, iFrame=626, iPage=8) at sqlite3.c:65512:30
    frame #1: 0x0000000104462098 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`walFrames(pWal=0x000000011fa04c00, szPage=4096, pList=0x000000012001c638, nTruncate=8, isCommit=1, sync_flags=10) at sqlite3.c:68303:10
    frame #2: 0x0000000104461814 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3WalFrames(pWal=0x000000011fa04c00, szPage=4096, pList=0x000000012001c638, nTruncate=8, isCommit=1, sync_flags=10) at sqlite3.c:68350:10
    frame #3: 0x000000010445fa68 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`pagerWalFrames(pPager=0x000000012000b400, pList=0x000000012001c638, nTruncate=8, isCommit=1) at sqlite3.c:59594:8
    frame #4: 0x000000010443b260 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3PagerCommitPhaseOne(pPager=0x000000012000b400, zSuper=0x0000000000000000, noSync=0) at sqlite3.c:62867:14
    frame #5: 0x000000010443b7b4 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3BtreeCommitPhaseOne(p=0x0000600002e140a0, zSuperJrnl=0x0000000000000000) at sqlite3.c:73975:10
    frame #6: 0x00000001044770fc duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`vdbeCommit(db=0x000000011f804da0, p=0x00000001100136d0) at sqlite3.c:86887:14
    frame #7: 0x0000000104476378 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3VdbeHalt(p=0x00000001100136d0) at sqlite3.c:87297:16
    frame #8: 0x000000010447cde8 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3VdbeExec(p=0x00000001100136d0) at sqlite3.c:93436:8
    frame #9: 0x000000010443eac0 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3Step(p=0x00000001100136d0) at sqlite3.c:90247:10
    frame #10: 0x0000000104438eb8 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`sqlite3_step(pStmt=0x00000001100136d0) at sqlite3.c:90308:16
    frame #11: 0x00000001029eeae8 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`rusqlite::raw_statement::RawStatement::step(self=0x000000016d82a8a8) at raw_statement.rs:106:18
    frame #12: 0x00000001029ff058 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`rusqlite::statement::Statement::execute_with_bound_parameters(self=0x000000016d82a8a0) at statement.rs:652:17
    frame #13: 0x00000001029ef470 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`rusqlite::statement::Statement::execute(self=0x000000016d82a8a0, params=([0] = 1612)) at statement.rs:114:9
    frame #14: 0x00000001029ee744 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`rusqlite::Connection::execute::{{closure}}::{{closure}}((null)=() @ 0x000000016d82a6af) at lib.rs:622:69
    frame #15: 0x00000001029ed5d8 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`core::result::Result<T,E>::and_then(self=(Ok = core::result::Result<(), rusqlite::error::Error>::Ok @ 0x000000016d82a788, Err = core::result::Result<(), rusqlite::error::Error>::Err @ 0x000000016d82a788), op=rusqlite::{impl#4}::execute::{closure#0}::{closure_env#0}<[i64; 1]> @ 0x000000016d82a7c8) at result.rs:1320:22
    frame #16: 0x00000001029ee6d8 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`rusqlite::Connection::execute::{{closure}}(stmt=rusqlite::statement::Statement @ 0x000000016d82a8a0) at lib.rs:622:34
    frame #17: 0x00000001029ed834 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`core::result::Result<T,E>::and_then(self=core::result::Result<rusqlite::statement::Statement, rusqlite::error::Error>::Ok @ 0x000000016d82a970, op=rusqlite::{impl#4}::execute::{closure_env#0}<[i64; 1]> @ 0x000000016d82a840) at result.rs:1320:22
    frame #18: 0x00000001029ee63c duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`rusqlite::Connection::execute(self=0x000000016d82aa50, sql="INSERT INTO users VALUES ('Alice', (?));", params=([0] = 1612)) at lib.rs:621:9
    frame #19: 0x0000000102a001f8 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`duckdb_sqlite_scanner_concurrency_tests::tests::sqlite_writer_duckdb_reader_with_wal::{{closure}} at main.rs:225:17
    frame #20: 0x00000001029eaf98 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::sys_common::backtrace::__rust_begin_short_backtrace(f=duckdb_sqlite_scanner_concurrency_tests::tests::sqlite_writer_duckdb_reader_with_wal::{closure_env#0} @ 0x000000016d82ac80) at backtrace.rs:154:18
    frame #21: 0x00000001029fb070 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} at mod.rs:529:17
    frame #22: 0x00000001029fe180 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once(self=core::panic::unwind_safe::AssertUnwindSafe<std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure_env#0}<duckdb_sqlite_scanner_concurrency_tests::tests::sqlite_writer_duckdb_reader_with_wal::{closure_env#0}, ()>> @ 0x000000016d82acd0, (null)=() @ 0x000000016d82accf) at unwind_safe.rs:272:9
    frame #23: 0x0000000102a05300 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::panicking::try::do_call(data=0x000000016d82ad78) at panicking.rs:552:40
    frame #24: 0x0000000102a05da4 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`__rust_try + 32
    frame #25: 0x0000000102a04910 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::panicking::try(f=core::panic::unwind_safe::AssertUnwindSafe<std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure_env#0}<duckdb_sqlite_scanner_concurrency_tests::tests::sqlite_writer_duckdb_reader_with_wal::{closure_env#0}, ()>> @ 0x000000016d82ad98) at panicking.rs:516:19
    frame #26: 0x00000001029f9c70 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::thread::Builder::spawn_unchecked_::{{closure}} at panic.rs:142:14
    frame #27: 0x00000001029f9c64 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::thread::Builder::spawn_unchecked_::{{closure}} at mod.rs:528:30
    frame #28: 0x00000001029fba44 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`core::ops::function::FnOnce::call_once{{vtable.shim}}((null)=0x000060000031c0c0, (null)=() @ 0x000000016d82af67) at function.rs:250:5
    frame #29: 0x00000001045fd35c duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::sys::unix::thread::Thread::new::thread_start [inlined] <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once at boxed.rs:2007:9 [opt]
    frame #30: 0x00000001045fd350 duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::sys::unix::thread::Thread::new::thread_start [inlined] <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once at boxed.rs:2007:9 [opt]
    frame #31: 0x00000001045fd34c duckdb_sqlite_scanner_concurrency_tests-80eb8bd2605a4fd4`std::sys::unix::thread::Thread::new::thread_start at thread.rs:108:17 [opt]
    frame #32: 0x000000018b44a034 libsystem_pthread.dylib`_pthread_start + 136

It always fails in the walIndexAppend function in the sqlite3.c client.

DuckDB is ultimately using the SQLite client to read, so this scenario should be equivalent to the second test scenario.

Questions:

  1. Is DuckDB opening the SQLite database in an incompatible mode?
  2. Does sqlite3.c used by both Rusqlite and duckdb-rs need to be binary compatible?

I'm using RustRover as my debugger.

image

🔴 5) DuckDB Writer, DuckDB Reader, No WAL

In an attempt to eliminate binary compatibility issues between the Rusqlite and duckdb-rs clients, I wrote to the SQLite database and read from the SQLite database using only the duckdb-rs client.

Run the test:

cargo test test_duckdb_writer_duckdb_reader_no_wal -- --nocapture

It fails with:

...
DuckDB COUNT : 360
DuckDB COUNT : 361
DuckDB COUNT : 362
thread '<unnamed>' panicked at src/main.rs:291:22:
called `Result::unwrap()` on an `Err` value: DuckDBFailure(Error { code: Unknown, extended_code: 1 }, Some("Invalid Error: Failed to execute query \"COMMIT\": database is locked"))
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread '<unnamed>' panicked at src/main.rs:317:84:
called `Result::unwrap()` on an `Err` value: DuckDBFailure(Error { code: Unknown, extended_code: 1 }, Some("Invalid Error: Failed to prepare query \"SELECT ROWID FROM \"users\" WHERE ROWID BETWEEN 0 AND 122879\": database is locked"))
thread 'tests::test_duckdb_writer_duckdb_reader_no_wal' panicked at src/main.rs:326:23:
called `Result::unwrap()` on an `Err` value: Any { .. }
test tests::test_duckdb_writer_duckdb_reader_no_wal ... FAILED

failures:

failures:
    tests::test_duckdb_writer_duckdb_reader_no_wal

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 5 filtered out; finished in 2.07s

There have been other reports of the database is locked error. See #78.

🔴 6) DuckDB Writer, DuckDB Reader, With WAL

Just for completeness, I also used only the duckdb-rs client to read and write to the SQLite database with WAL model enabled. Note, I don't see a way to create the SQLite database with WAL enabled using the DuckDB client, so the database is first created using the Rusqlite client, then closed and opened with the duckdb-rs client before writing to it.

Run the test:

cargo test test_duckdb_writer_duckdb_reader_with_wal -- --nocapture

This fails with:

running 1 test
thread '<unnamed>' panicked at src/main.rs:395:18:
called `Result::unwrap()` on an `Err` value: DuckDBFailure(Error { code: Unknown, extended_code: 1 }, Some("Invalid Error: Failed to prepare query \"SELECT type FROM sqlite_master WHERE lower(name)=lower('users');\": database is locked"))

OS:

Mac OS X arm64

SQLite Version:

3.44.0

DuckDB Version:

0.9.2

DuckDB Client:

duckdb-rs (Rust)

Full Name:

Colin Breck

Affiliation:

Tesla

Have you tried this on the latest main branch?

  • I agree

Have you tried the steps to reproduce? Do they include all relevant data and configuration? Does the issue you report still appear there?

  • I agree

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions