I think that this project is suffering from a misunderstanding of SQLITE_LOCK vs SQLITE_BUSY. I'd like to start a discussion about this, and why anyone has ever encouraged to enabled shared cache mode to solve a SQLITE_LOCK error.
The SQLITE_BUSY result code indicates that the database file could not be written (or in some cases read) because of concurrent activity by some other database connection, usually a database connection in a separate process.
The SQLITE_LOCKED result code indicates that a write operation could not continue because of a conflict within the same database connection or a conflict with a different database connection that uses a shared cache.
https://www.sqlite.org/cvstrac/wiki?p=DatabaseIsLocked
Without shared cache mode:
SQLITE_BUSY is expected to happen with any multithreaded application. This just means that two connections are conflicting with each other. You need to set up a busy timeout so that SQLite can automatically retry in this case. Problem solved.
SQLITE_LOCKED only happens if a single SQLite connection is doing something that doesn't make sense, such as dropping a table while still being in the middle of a SELECT statement. If someone is getting this error, either they're doing something crazy, or a connection was returned to the pool without being ready to be returned to the pool, because statements are still executing without being finalized.
Without shared cache mode, these are the only problems that you should expect.
But we've encouraged everyone to enable shared cache mode, so now the problem has changed. In shared cache mode, there is really only ever a single connection to the database at a time, and all "connections" are just sharing the same connection.
With shared cache mode:
SQLITE_BUSY pretty much doesn't happen. You can't conflict with other connections if there is only a single connection.
SQLITE_LOCKED will happen much more often. Now, normal, seemingly separate connections will get SQLITE_LOCKED if they happen to run conflicting statements at the same time. Worse, you can't set a busy timeout to fix it. If a lock error happens, it's failed. The only way around this now is to support the unlock_notify API, which this driver currently does not support. So we dig our heels in even further and limit the connection pool to only a single connection.
So basically, it seems incorrect to me that you should ever suggest people turn on shared cache mode to fix SQLITE_LOCKED errors. It should only increase the frequency of SQLITE_LOCKED. Is there something that I'm misunderstanding? Is there a reason I'm not understanding why SQLITE_LOCKED errors are unavoidable without shared cache mode?
So we dig our heels in even further and limit the connection pool to only a single connection.
Also of note is the fact that if you only have a single connection to a given database, using a shared cache is pointless and just adds unnecessary overhead.
No doubt adding to the confusion is the fact that when getting the error message, the SQLite library itself turns SQLITE_BUSY into "database is locked" and SQLITE_LOCKED into "database table is locked". It's very easy to see the former error message and mistake it for SQLITE_LOCKED.
As for why shared cache is recommended, one of the earliest bug reports about "database is locked" I can find is #39. The reason turning on the shared cache fixes the problem is:
sqlite3_reset on a prepared statement for which you are fetching results, that connection maintains a read lock on the database.SQLITE_BUSY. (If you have a busy timeout, it will retry until the timeout before returning.)Taking these points all together, it seems like people were trying to do something that, while allowed by SQLite, is disallowed by the standard library. The standard library then opens a second connection behind the scenes, and the user gets a mysterious SQLITE_BUSY error.
The driver does seem to support the unlock notify API now, provided you build with the sqlite_unlock_notify tag.
It seems to me that there a few ways to use this library correctly:
SQLITE_BUSY.SQLITE_LOCKED.)rows.Next() loop. In this case, it does not matter whether you use WAL or a rollback journal, nor does it matter whether you use a private or shared cache.tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
rows, err := tx.Query(...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(...); err != nil {
return err
}
if err := tx.Exec(...); err != nil {
return err
}
}
if err := rows.Err(); err != nil {
return err
}
return tx.Commit()
DB.Conn method instead of a transaction.c, err := db.Conn(ctx)
if err != nil {
return err
}
defer c.Close()
rows, err := c.QueryContext(ctx, ...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(...); err != nil {
return err
}
if err := c.ExecContext(ctx, ...); err != nil {
return err
}
}
if err := rows.Err(); err != nil {
return err
}
return c.Close()
No activity / response; closed.
Most helpful comment
Also of note is the fact that if you only have a single connection to a given database, using a shared cache is pointless and just adds unnecessary overhead.
No doubt adding to the confusion is the fact that when getting the error message, the SQLite library itself turns
SQLITE_BUSYinto "database is locked" andSQLITE_LOCKEDinto "database table is locked". It's very easy to see the former error message and mistake it forSQLITE_LOCKED.As for why shared cache is recommended, one of the earliest bug reports about "database is locked" I can find is #39. The reason turning on the shared cache fixes the problem is:
sqlite3_reseton a prepared statement for which you are fetching results, that connection maintains a read lock on the database.SQLITE_BUSY. (If you have a busy timeout, it will retry until the timeout before returning.)Taking these points all together, it seems like people were trying to do something that, while allowed by SQLite, is disallowed by the standard library. The standard library then opens a second connection behind the scenes, and the user gets a mysterious
SQLITE_BUSYerror.The driver does seem to support the unlock notify API now, provided you build with the sqlite_unlock_notify tag.
It seems to me that there a few ways to use this library correctly:
SQLITE_BUSY.SQLITE_LOCKED.)rows.Next()loop. In this case, it does not matter whether you use WAL or a rollback journal, nor does it matter whether you use a private or shared cache.DB.Connmethod instead of a transaction.