mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(observability): skip SQLITE_FULL "database or disk is full" (Sentry TAURI-RUST-B6N) (#3672)
This commit is contained in:
@@ -506,14 +506,55 @@ pub fn expected_error_kind(message: &str) -> Option<ExpectedErrorKind> {
|
||||
/// That string carries no "no space left on device" text, so anchor
|
||||
/// additionally on the cross-platform `StorageFull` ErrorKind token (std maps
|
||||
/// ENOSPC / `ERROR_DISK_FULL` / `ERROR_HANDLE_DISK_FULL` all to
|
||||
/// `ErrorKind::StorageFull`). This is defense-in-depth for the genuinely
|
||||
/// `ErrorKind::StorageFull`).
|
||||
///
|
||||
/// A fifth shape comes from SQLite itself. When the engine detects the
|
||||
/// disk-full condition during its own page bookkeeping (journal/WAL extension)
|
||||
/// before the next syscall surfaces an errno, rusqlite renders the `SQLITE_FULL`
|
||||
/// result code as `"database or disk is full"` (Sentry TAURI-RUST-B6N, hit at
|
||||
/// `memory_store::unified::documents::tx.commit()` during
|
||||
/// `openhuman.memory_doc_ingest`). `SQLITE_FULL` has only two causes:
|
||||
/// genuine ENOSPC/ERROR_DISK_FULL (always the case in practice — the same
|
||||
/// burst always produces an os-error-28/112 sibling event) or a
|
||||
/// `max_page_count` PRAGMA cap (we set none).
|
||||
///
|
||||
/// The rusqlite `Display` for `SQLITE_FULL` is exactly the five words
|
||||
/// `"database or disk is full"` — no preamble, no trailing context. Our local
|
||||
/// memory-store write call-sites wrap it with `format!("<verb>: {e}")`
|
||||
/// (e.g. `"commit tx: ..."` / `"clear_namespace commit tx: ..."` in
|
||||
/// `memory_store::unified::documents`), so the phrase always lands as the
|
||||
/// **suffix** of the local emit. Anchor on suffix, not `contains`, so the
|
||||
/// silencer does not match a non-2xx backend response body whose payload
|
||||
/// happens to mention the same phrase (e.g. an `api.tinyhumans.ai` 5xx whose
|
||||
/// server-side SQLite is full). Non-2xx backend bodies are framed by
|
||||
/// `integrations::client::post` / `composio::client` as `"Backend returned
|
||||
/// <status> <reason> for <METHOD> <url>: <detail>"` — an operator-actionable
|
||||
/// server/storage failure that must still surface to Sentry. As
|
||||
/// defense-in-depth for the edge case where the backend body itself ends with
|
||||
/// the phrase, reject any message that also carries the `"backend returned "`
|
||||
/// envelope prefix (codex CR on #3672, mirrors the precedent set by
|
||||
/// [`is_backend_user_error_message`]).
|
||||
///
|
||||
/// This is defense-in-depth for the genuinely
|
||||
/// unpreventable **write** paths (a write can't succeed on a full disk); the
|
||||
/// read path no longer emits this error at all (it degrades to a lock-free
|
||||
/// read — see `AuthProfilesStore::load`).
|
||||
fn is_disk_full_message(lower: &str) -> bool {
|
||||
lower.contains("no space left on device")
|
||||
if lower.contains("no space left on device")
|
||||
|| lower.contains("not enough space on the disk")
|
||||
|| lower.contains("storagefull")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// SQLITE_FULL — see the fifth-shape section above for the scoping
|
||||
// rationale. Suffix anchor (after trimming trailing whitespace and
|
||||
// punctuation that closures / JSON wrappers commonly append) pins to the
|
||||
// local-emit shape; the negative `"backend returned "` guard rejects the
|
||||
// remote envelope as a second line of defense.
|
||||
let trimmed = lower.trim_end_matches(|c: char| {
|
||||
c.is_ascii_whitespace() || matches!(c, '.' | ',' | ';' | ':' | '"' | '\'')
|
||||
});
|
||||
trimmed.ends_with("database or disk is full") && !lower.contains("backend returned ")
|
||||
}
|
||||
|
||||
/// Detect the literal `"Config loading timed out"` string produced by
|
||||
@@ -2818,6 +2859,13 @@ mod tests {
|
||||
// only the `ErrorKind` debug + os_code survive — no "no space left
|
||||
// on device" text. Must still classify via the StorageFull anchor.
|
||||
"Failed to create auth profile lock (kind=Some(StorageFull), os_code=Some(28))",
|
||||
// SQLITE_FULL rendering from rusqlite — engine-level disk-full
|
||||
// detection during page-bookkeeping (journal/WAL extension) that
|
||||
// beats the next syscall to the errno. Production hit at
|
||||
// `memory_store::unified::documents::tx.commit()` during
|
||||
// `openhuman.memory_doc_ingest`, in the same burst that emits
|
||||
// os-error-112 siblings (Sentry TAURI-RUST-B6N).
|
||||
"commit tx: database or disk is full",
|
||||
] {
|
||||
assert_eq!(
|
||||
expected_error_kind(raw),
|
||||
@@ -2840,6 +2888,55 @@ mod tests {
|
||||
expected_error_kind("not enough memory to allocate buffer"),
|
||||
None
|
||||
);
|
||||
// The SQLite anchor pins to the exact `"database or disk is full"`
|
||||
// phrase. Generic prose that mentions a full database for unrelated
|
||||
// reasons (e.g. duplicate-row complaints, application-level capacity
|
||||
// talk) must not be silenced.
|
||||
assert_eq!(
|
||||
expected_error_kind("upsert failed: database is full of duplicates"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
expected_error_kind("user quota: database is full for this tier"),
|
||||
None
|
||||
);
|
||||
// A non-2xx backend body whose payload contains the SQLITE_FULL phrase
|
||||
// (e.g. `api.tinyhumans.ai` server-side SQLite is full) is an
|
||||
// operator-actionable storage failure, not the user's local disk —
|
||||
// must still surface to Sentry. `integrations::client::post` frames
|
||||
// these as `"Backend returned <status> <reason> for POST <url>:
|
||||
// <detail>"` (codex CR on #3672). The suffix anchor excludes the
|
||||
// embedded-in-JSON case; the negative `"backend returned "` guard
|
||||
// covers the rare case where the body itself ends with the phrase.
|
||||
assert_eq!(
|
||||
expected_error_kind(
|
||||
"Backend returned 500 Internal Server Error for POST \
|
||||
https://api.tinyhumans.ai/agent-integrations/composio/list: \
|
||||
{\"error\":\"database or disk is full\"}"
|
||||
),
|
||||
None,
|
||||
"remote-backend body must surface"
|
||||
);
|
||||
assert_eq!(
|
||||
expected_error_kind(
|
||||
"Backend returned 500 Internal Server Error for POST \
|
||||
https://api.tinyhumans.ai/agent-integrations/composio/list: \
|
||||
database or disk is full"
|
||||
),
|
||||
None,
|
||||
"remote-backend body must surface even when the body itself ends with the phrase"
|
||||
);
|
||||
// Non-suffix occurrences in other body framings (no `"Backend
|
||||
// returned"` prefix) are also excluded by the suffix anchor — locks
|
||||
// in the primary defense layer.
|
||||
assert_eq!(
|
||||
expected_error_kind(
|
||||
"Embedding API error (500 Internal Server Error): \
|
||||
{\"error\":\"database or disk is full\",\"retry\":true}"
|
||||
),
|
||||
None,
|
||||
"embedded-in-JSON body must surface"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user