Works as intended. "our code base does not use transactions at all" does NOT mean that there are no transactions, just that SQLite uses *implicit* transactions, i.e. every statement is in it's own transaction.
"we can share a connection between threads as long as we don't read/write into the same table at the same time" is NOT a correct assumption. The assertion you need to prove for SQLite running in multi-threaded mode is "we can share a connection between threads as long as the connection is not used simultaneously in two or more threads". What you program is doing is guaranteed to provoke SQLITE_BUSY_SNAPSHOT errors. Your first thread is creating an implicit transaction on connection FIRST by reading from table FIRST. Your second thread is creating an implicit transaction on connection SECOND by writing into table SECOND. The implied COMMIT "invalidates" the snapshot on connection FIRST. Your third thread now attempts to insert into table THIRD on connection FIRST. But connection FIRST is within an READ transaction started by your first thread, so it has to escalate it's transaction to a WRITE transaction. But because it is in an "invalid" snapshot, it cannot do so. This is well documented behaviour. https://sqlite.org/isolation.html "Another example: X starts a read transaction using BEGIN and SELECT, then Y makes a changes to the database using UPDATE. Then X tries to make a change to the database using UPDATE. The attempt by X to escalate its transaction from a read transaction to a write transaction fails with an SQLITE_BUSY_SNAPSHOT error because the snapshot of the database being viewed by X is no longer the latest version of the database. If X were allowed to write, it would fork the history of the database file, which is something SQLite does not support. In order for X to write to the database, it must first release its snapshot (using ROLLBACK for example) then start a new transaction with a subsequent BEGIN. " -----Ursprüngliche Nachricht----- Von: sqlite-users [mailto:sqlite-users-boun...@mailinglists.sqlite.org] Im Auftrag von Kira Backes Gesendet: Montag, 12. August 2019 09:33 An: sqlite-users@mailinglists.sqlite.org Betreff: [EXTERNAL] [sqlite] SQLITE_BUSY (SQLITE_BUSY_SNAPSHOT) due to a simple SELECT query -- Documentation needed! Dear sqlite mailing list, I had to spend 2 days debugging a SQLITE_BUSY_SNAPSHOT, and reading the documentation did not help me. Our code base does not use transactions at all (we have a segmented code base protected by mutexes for a whole section, so reads/writes do not conflict ever). We neither had a crashing sqlite connection nor a corrupted database file. Even reading everything in isolation did not help me, since we're supposed to be able to share a connection between threads as long as we do not read/write into the same table at the same time, which we were able to ensure due to the section mutexes. After thinking a very long time about this I found the reason: You absolutely can not share a WAL connection between threads or risk SQLITE_BUSY events. Yes, you heard right. If connection A runs a SELECT query in table A, then connection B inserts something into table B, and then you try in a concurrent thread to INSERT into table C using connection A you will get SQLITE_BUSY_SNAPSHOT errors until all SELECT queries are finished on connection A (in my case this took about a minute because I was reading some cache tables with several threads, so there was never a second where all queries were finished...). So I had SQLITE_BUSY_SNAPSHOT errors for a full minutes even though I never used transactions anywhere... Just because I had some long running SELECT statements in unrelated tables it made the connection completely unusable. This is not a theoretical case, this can happen *VERY* easily and as far as I can tell this is not documented anywhere (and believe me, I've ready nearly every single page of the sqlite3 documentation). So you should really really document this very easy to trigger case. And if you don't believe me: since I'm a nice girl I've written a unit test (C++, catch2) for this which reliably reproduces this behavior: TEST_CASE("Test SQLITE_BUSY_SNAPSHOT", "[sqlite3]") { std::string db_name{"test_sqlite3_busy_snapshot.sqlite3"}; if (std::filesystem::exists(db_name)) { REQUIRE(std::filesystem::remove(db_name)); } std::mt19937_64 engine{std::random_device{}()}; std::uniform_int_distribution<int64_t> u(INT64_MIN, INT64_MAX); auto open_db_fn = [&]{ sqlite3* handle; REQUIRE(sqlite3_open_v2(db_name.c_str(), &handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr) == SQLITE_OK); REQUIRE(sqlite3_exec(handle, "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA secure_delete=FAST;", nullptr, nullptr, nullptr) == SQLITE_OK); REQUIRE(sqlite3_busy_timeout(handle, 600'000) == SQLITE_OK); return handle; }; std::string insert_into_FIRST = "INSERT INTO `FIRST_table` VALUES (?)"s; std::string insert_into_SECOND = "INSERT INTO `SECOND_table` VALUES (?)"s; std::string insert_into_THIRD = "INSERT INTO `THIRD_table` VALUES (?)"s; // initialize db, insert 4 rows into FIRST table { sqlite3* init_handle{open_db_fn()}; REQUIRE(sqlite3_exec(init_handle, "CREATE TABLE `FIRST_table` (`a` LONG)", nullptr, nullptr, nullptr) == SQLITE_OK); REQUIRE(sqlite3_exec(init_handle, "CREATE TABLE `SECOND_table` (`a` LONG)", nullptr, nullptr, nullptr) == SQLITE_OK); REQUIRE(sqlite3_exec(init_handle, "CREATE TABLE `THIRD_table` (`a` LONG)", nullptr, nullptr, nullptr) == SQLITE_OK); sqlite3_stmt* stmt{nullptr}; REQUIRE(sqlite3_prepare_v3(init_handle, insert_into_FIRST.c_str(), -1, 0, &stmt, nullptr) == SQLITE_OK); for (int i{0}; i != 4; ++i) { REQUIRE(sqlite3_bind_int64(stmt, 1, u(engine)) == SQLITE_OK); REQUIRE(sqlite3_step(stmt) == SQLITE_DONE); REQUIRE(sqlite3_reset(stmt) == SQLITE_OK); } REQUIRE(sqlite3_finalize(stmt) == SQLITE_OK); REQUIRE(sqlite3_close(init_handle) == SQLITE_OK); } // init handle is closed, from here on we have a clean state sqlite3* FIRST_handle{open_db_fn()}; sqlite3* SECOND_handle{open_db_fn()}; std::atomic_bool shall_continue_read_from_FIRST = true; // Continuously read from FIRST table using FIRST handle std::thread thread_read_from_FIRST{[&] { sqlite3_stmt* stmt; REQUIRE(sqlite3_prepare_v3(FIRST_handle, "SELECT * FROM `FIRST_table`", -1, SQLITE_PREPARE_PERSISTENT, &stmt, nullptr) == SQLITE_OK); while (shall_continue_read_from_FIRST) { while (sqlite3_step(stmt) == SQLITE_ROW) { sqlite3_column_int(stmt, 0); std::this_thread::sleep_for(std::chrono::milliseconds{5}); } sqlite3_reset(stmt); } sqlite3_finalize(stmt); }}; // Insert once into SECOND table (untouched before) using SECOND handle (unused before) { sqlite3_stmt* stmt; REQUIRE(sqlite3_prepare_v3(SECOND_handle, insert_into_SECOND.c_str(), -1, SQLITE_PREPARE_PERSISTENT, &stmt, nullptr) == SQLITE_OK); REQUIRE(sqlite3_bind_int64(stmt, 1, u(engine)) == SQLITE_OK); REQUIRE(sqlite3_step(stmt) == SQLITE_DONE); REQUIRE(sqlite3_reset(stmt) == SQLITE_OK); sqlite3_finalize(stmt); } // Insert once into THIRD table (untouched before) using FIRST handle (which is still doing a select in another thread somewhere) { sqlite3_stmt* stmt; REQUIRE(sqlite3_prepare_v3(FIRST_handle, insert_into_THIRD.c_str(), -1, 0, &stmt, nullptr) == SQLITE_OK); REQUIRE(sqlite3_bind_int64(stmt, 1, u(engine)) == SQLITE_OK); REQUIRE(sqlite3_step(stmt) == SQLITE_BUSY); REQUIRE(sqlite3_extended_errcode(FIRST_handle) == SQLITE_BUSY_SNAPSHOT); REQUIRE(sqlite3_reset(stmt) == SQLITE_BUSY); REQUIRE(sqlite3_extended_errcode(FIRST_handle) == SQLITE_BUSY_SNAPSHOT); sqlite3_finalize(stmt); } // Stop select on FIRST handle and FIRST table in other thread shall_continue_read_from_FIRST = false; thread_read_from_FIRST.join(); // Retry insert into THIRD table using FIRST handle { sqlite3_stmt* stmt; REQUIRE(sqlite3_prepare_v3(FIRST_handle, insert_into_THIRD.c_str(), -1, 0, &stmt, nullptr) == SQLITE_OK); REQUIRE(sqlite3_bind_int64(stmt, 1, u(engine)) == SQLITE_OK); REQUIRE(sqlite3_step(stmt) == SQLITE_DONE); REQUIRE(sqlite3_reset(stmt) == SQLITE_OK); sqlite3_finalize(stmt); } std::filesystem::remove(db_name); } kind regards, Kira Backes _______________________________________________ sqlite-users mailing list sqlite-users@mailinglists.sqlite.org http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users ___________________________________________ Gunter Hick | Software Engineer | Scientific Games International GmbH | Klitschgasse 2-4, A-1130 Vienna | FN 157284 a, HG Wien, DVR: 0430013 | (O) +43 1 80100 - 0 May be privileged. May be confidential. Please delete if not the addressee. _______________________________________________ sqlite-users mailing list sqlite-users@mailinglists.sqlite.org http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users