Skip to content

Commit dbc56cf

Browse files
authored
Merge pull request #7514 from cloudflare/jolio/sqlite-suppo-ufummb
Sqlite: support adding specific error context to internal exceptions
2 parents b9fb237 + 757419a commit dbc56cf

3 files changed

Lines changed: 80 additions & 11 deletions

File tree

‎src/workerd/util/sqlite-test.c++‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,35 @@ KJ_TEST("SQLite extended error codes in messages") {
10931093
}
10941094
}
10951095

1096+
KJ_TEST("SQLite error context is appended to internal errors only") {
1097+
auto dir = kj::newInMemoryDirectory(kj::nullClock());
1098+
SqliteDatabase::Vfs vfs(*dir);
1099+
SqliteDatabase db(vfs, kj::Path({"foo"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY);
1100+
db.setErrorContext(kj::str("actorId = abc123"));
1101+
1102+
db.run("CREATE TABLE things (id INTEGER PRIMARY KEY)");
1103+
db.run("INSERT INTO things VALUES (1)");
1104+
1105+
// Failures while preparing and while stepping both carry the context.
1106+
KJ_EXPECT_THROW_MESSAGE("no such table: nonexistent: SQLITE_ERROR; actorId = abc123",
1107+
db.run("SELECT * FROM nonexistent"));
1108+
KJ_EXPECT_THROW_MESSAGE(
1109+
"SQLITE_CONSTRAINT_PRIMARYKEY); actorId = abc123", db.run("INSERT INTO things VALUES (1)"));
1110+
1111+
// Errors reported by the regulator do not.
1112+
class ReportingRegulator: public SqliteDatabase::Regulator {
1113+
public:
1114+
void onError(kj::Maybe<int> sqliteErrorCode, kj::StringPtr message) const override {
1115+
kj::throwFatalException(KJ_EXCEPTION(FAILED, "reported", message));
1116+
}
1117+
};
1118+
static ReportingRegulator regulator;
1119+
auto exception = KJ_ASSERT_NONNULL(kj::runCatchingExceptions(
1120+
[&]() { db.run({.regulator = regulator}, "SELECT * FROM nonexistent"); }));
1121+
KJ_EXPECT(exception.getDescription().contains("no such table: nonexistent"), exception);
1122+
KJ_EXPECT(!exception.getDescription().contains("abc123"), exception);
1123+
}
1124+
10961125
class MockRollbackCallback {
10971126
public:
10981127
kj::Function<void()> create() {
@@ -1704,10 +1733,11 @@ KJ_TEST("SQLite memory metering tracks allocations correctly") {
17041733
"memory should decrease when running `PRAGMA shrink_memory`");
17051734
}
17061735

1707-
KJ_TEST("I/O exceptions pass through SQLite") {
1736+
KJ_TEST("I/O exceptions pass through SQLite with the error context") {
17081737
auto dir = kj::atomicRefcounted<ErrorInjectableDirectory>();
17091738
SqliteDatabase::Vfs vfs(*dir);
17101739
SqliteDatabase db(vfs, kj::Path({"db"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY);
1740+
db.setErrorContext(kj::str("actorId = abc123"));
17111741

17121742
db.run({.regulator = SqliteDatabase::TRUSTED}, kj::str(R"(
17131743
CREATE TABLE IF NOT EXISTS things (
@@ -1728,9 +1758,16 @@ KJ_TEST("I/O exceptions pass through SQLite") {
17281758
INSERT INTO things(value) VALUES (456);
17291759
)"));
17301760
}));
1731-
KJ_EXPECT(exception.getDescription() == "test-vfs-error", exception);
1761+
KJ_EXPECT(exception.getDescription() == "test-vfs-error; actorId = abc123", exception);
17321762
auto disposition = KJ_ASSERT_NONNULL(exception.getDetail(SENTRY_TAG_DETAIL_ID));
17331763
KJ_EXPECT(disposition.asChars() == "NOSENTRY"_kj, exception);
1764+
1765+
// Application-visible exceptions pass through unchanged.
1766+
KJ_ASSERT_NONNULL(dir->dbFile)->error = KJ_EXCEPTION(FAILED, "jsg.Error: test-vfs-error");
1767+
auto tunneled = KJ_ASSERT_NONNULL(kj::runCatchingExceptions([&]() {
1768+
db.run({.regulator = SqliteDatabase::TRUSTED}, "INSERT INTO things(value) VALUES (789)");
1769+
}));
1770+
KJ_EXPECT(tunneled.getDescription() == "jsg.Error: test-vfs-error", tunneled);
17341771
}
17351772

17361773
void testCriticalError(const char* expectedErrorMessage,

‎src/workerd/util/sqlite.c++‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "strings.h"
88

9+
#include <workerd/jsg/exception.h>
910
#include <workerd/util/autogate.h>
1011
#include <workerd/util/sentry.h>
1112

@@ -196,7 +197,19 @@ void tagSentry(kj::Exception& e, kj::StringPtr tag) {
196197
}
197198
}
198199

199-
[[noreturn]] void throwSentryException(kj::Exception&& e, kj::StringPtr tag) {
200+
// Applies the context given to `SqliteDatabase::setErrorContext()`, if any. Tunneled exceptions
201+
// are left untouched because their description is shown to the application; a VFS callback may
202+
// throw one (e.g. when a write exceeds a storage limit).
203+
void appendErrorContext(kj::Exception& e, kj::Maybe<kj::StringPtr> errorContext) {
204+
if (jsg::isTunneledException(e.getDescription())) return;
205+
KJ_IF_SOME(context, errorContext) {
206+
e.setDescription(kj::str(e.getDescription(), "; ", context));
207+
}
208+
}
209+
210+
[[noreturn]] void throwSentryException(
211+
kj::Exception&& e, kj::StringPtr tag, kj::Maybe<kj::StringPtr> errorContext) {
212+
appendErrorContext(e, errorContext);
200213
tagSentry(e, tag);
201214
kj::throwFatalException(kj::mv(e));
202215
}
@@ -231,8 +244,9 @@ class SqliteCallScope {
231244
vfsErrorListener = nullptr;
232245
}
233246

234-
void rethrowVfsError() {
247+
void rethrowVfsError(kj::Maybe<kj::StringPtr> errorContext) {
235248
KJ_IF_SOME(e, error) {
249+
appendErrorContext(e, errorContext);
236250
// Slight hack: The exception already has a stack trace attached which should include the
237251
// current stack, but `kj::throwFatalException()` would re-append the current stack trace
238252
// to the exception. We can avoid that by calling
@@ -257,13 +271,14 @@ class SqliteCallScope {
257271

258272
// Like KJ_REQUIRE() but give the Regulator a chance to report the error. `errorMessage` is either
259273
// the return value of sqlite3_errmsg() or a string literal containing a similarly
260-
// application-approriate error message. A reference called `regulator` must be in-scope.
274+
// application-approriate error message. A reference called `regulator` must be in-scope, as must
275+
// a `getErrorContext()` method (i.e. this must be used within SqliteDatabase or Query).
261276
// sqliteErrorCode is a kj::Maybe<int> and represents the error code from sqlite.
262277
#define SQLITE_REQUIRE_WITH_TAG(condition, sqliteErrorCode, sentryTag, errorMessage, ...) \
263278
if (!(condition)) { \
264279
regulator->onError(sqliteErrorCode, errorMessage); \
265-
throwSentryException( \
266-
KJ_EXCEPTION(FAILED, "SQLite failed", errorMessage, ##__VA_ARGS__), sentryTag); \
280+
throwSentryException(KJ_EXCEPTION(FAILED, "SQLite failed", errorMessage, ##__VA_ARGS__), \
281+
sentryTag, getErrorContext()); \
267282
}
268283

269284
#define SQLITE_REQUIRE(condition, sqliteErrorCode, errorMessage, ...) \
@@ -275,12 +290,12 @@ class SqliteCallScope {
275290
do { \
276291
SqliteCallScope sqliteCallScope; \
277292
int _ec = code; \
278-
if (_ec != SQLITE_OK) sqliteCallScope.rethrowVfsError(); \
293+
if (_ec != SQLITE_OK) sqliteCallScope.rethrowVfsError(kj::none); \
279294
if (_ec != SQLITE_OK) { \
280295
throwSentryException( \
281296
KJ_EXCEPTION( \
282297
FAILED, kj::str(sqlite3_errstr(_ec), ": ", namedErrorCode(_ec)), ##__VA_ARGS__), \
283-
"SENTRY_DO"_kj); \
298+
"SENTRY_DO"_kj, kj::none); \
284299
} \
285300
} while (false)
286301

@@ -293,7 +308,7 @@ class SqliteCallScope {
293308
/* SQLITE_MISUSE doesn't put error info on the database object, so check it separately */ \
294309
KJ_ASSERT(_ec != SQLITE_MISUSE, "SQLite misused: " #code, ##__VA_ARGS__); \
295310
handleCriticalError(_ec, dbErrorMessage(_ec, db), sqliteCallScope.getException()); \
296-
if (_ec == SQLITE_IOERR) sqliteCallScope.rethrowVfsError(); \
311+
if (_ec == SQLITE_IOERR) sqliteCallScope.rethrowVfsError(getErrorContext()); \
297312
SQLITE_REQUIRE(_ec == SQLITE_OK, _ec, dbErrorMessage(_ec, db), ##__VA_ARGS__); \
298313
} while (false)
299314

@@ -306,7 +321,7 @@ class SqliteCallScope {
306321
do { \
307322
KJ_ASSERT(error != SQLITE_MISUSE, "SQLite misused: " code, ##__VA_ARGS__); \
308323
handleCriticalError(error, dbErrorMessage(error, db), sqliteCallScope.getException()); \
309-
if (error == SQLITE_IOERR) sqliteCallScope.rethrowVfsError(); \
324+
if (error == SQLITE_IOERR) sqliteCallScope.rethrowVfsError(getErrorContext()); \
310325
SQLITE_REQUIRE_WITH_TAG( \
311326
error != SQLITE_BUSY, error, "NOSENTRY"_kj, dbErrorMessage(error, db), ##__VA_ARGS__); \
312327
SQLITE_REQUIRE(error == SQLITE_OK, error, dbErrorMessage(error, db), ##__VA_ARGS__); \

‎src/workerd/util/sqlite.h‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ class SqliteDatabase {
255255
onCriticalErrorCallback = kj::mv(callback);
256256
}
257257

258+
// Identifies this database in internal error logs and Sentry reports, e.g. so that a
259+
// SQLITE_IOERR, etc. can be traced back to the specific database file that produced
260+
// it. `context` (e.g. "id = 1234") is appended as "; <context>" to the description of
261+
// exceptions thrown.
262+
void setErrorContext(kj::String context) {
263+
errorContext = kj::mv(context);
264+
}
265+
258266
SqliteMemoryScope enterMemoryScope();
259267

260268
// Returns true if a transaction was automatically rolled due to a critical error.
@@ -389,6 +397,7 @@ class SqliteDatabase {
389397
kj::Maybe<kj::Function<void(kj::StringPtr errorMessage, kj::Maybe<kj::Exception> maybeException)>>
390398
onCriticalErrorCallback;
391399
kj::Maybe<kj::Function<void(SqliteDatabase&)>> afterResetCallback;
400+
kj::Maybe<kj::String> errorContext;
392401

393402
kj::List<ResetListener, &ResetListener::link> resetListeners;
394403

@@ -433,6 +442,10 @@ class SqliteDatabase {
433442
kj::StringPtr errorMessage,
434443
kj::Maybe<const kj::Exception&> exception);
435444

445+
kj::Maybe<kj::StringPtr> getErrorContext() {
446+
return errorContext.map([](kj::String& s) -> kj::StringPtr { return s; });
447+
}
448+
436449
enum Multi { SINGLE, MULTI };
437450

438451
// A pair of a compiled statement, and a description of the interesting state changes it applies.
@@ -769,6 +782,10 @@ class SqliteDatabase::Query final: private ResetListener {
769782
db.handleCriticalError(errorCode, errorMessage, maybeException);
770783
}
771784

785+
kj::Maybe<kj::StringPtr> getErrorContext() {
786+
return db.getErrorContext();
787+
}
788+
772789
// Some reasonable automatic conversions.
773790

774791
inline void bind(uint column, int value) {

0 commit comments

Comments
 (0)