Перейти к содержимому

Что несёт в себе ошибка

Каждая точка апи падает тем набором, который в ней возможен. Конструктор try_ отвечает четырьмя способами, которыми структура может не открыться; запись через Kv — шестью, которыми заворачивают сырую запись. Набор их перечисляет — значит, match по нему полный, и компилятор такой его и держит.

Каждый набор — обычный std::error::Error. Поэтому с отказом делают одно из двух, и оба дёшевы: разбирают его или отдают дальше.

let refused = match Panel::try_new_with(&store) {
Ok(_) => return Ok(()),
Err(why) => why,
};
let said = match refused {
OpenStruct::WillNotRead { at, why } => format!("{at} holds something else: {why}"),
OpenStruct::Taken(taken) => format!("{} already holds it", taken.held_by),
OpenStruct::NotAPath(why) => format!("that is not a path: {why}"),
OpenStruct::Store(disk) => format!("the store: {disk}"),
};

Ветки _ нет, и ради этого всё и сказано типом. Заглушка _ — это место, куда незаметно проваливается тот, кто ждал «отказали», а встретил чуть другое; здесь ей нечего ловить. А когда у вызова появится новый способ упасть, у всех, кто разбирал все, перестанет компилироваться — а именно это им и надо сообщить.

Все эти наборы исчерпывающие, по той же причине.

Вариант несёт всё, что было известно там, где его подняли: место вместе с владельцем, место вместе с причиной, обе стороны столкновения. OpenStruct::Taken несёт четыре вещи — какое место, кто его хотел, через какой путь оно занято и кем, — потому что столкновение читается только по всем четырём, а друг о друге объявления не знают. Лишнее поле можно не смотреть; нужное на месте.

fn with_anyhow(store: &amethystate::Store) -> anyhow::Result<()> {
store.set(["ui", "width"], &800u32)?;
Ok(())
}
fn with_a_box(store: &amethystate::Store) -> Result<(), Box<dyn Error + Send + Sync>> {
store.set(["ui", "height"], &600u32)?;
Ok(())
}

Send + Sync + 'static, Display в одну строку и source(), уходящий к причине. Поэтому anyhow, eyre и Box<dyn Error> берут набор простым ?, и махнуть рукой ничего не стоит.

набор откуда
OpenStruct try_new, try_new_with, try_load, try_load_with, Open::try_new_with, AmeStateSlice::try_load_slice, new_with_id, new_with_id_under, Schema::open, Kv::cell
OpenStore StoreBuilder::build, migrate, located
LoadMap Kv::map и собственный конструктор поля-карты
ReadValue Store::get
WriteValue Store::set, Store::delete, записи Field::durable, ReactiveCell::set/update/modify, ReactiveMap::insert
KvWrite Kv::get, Kv::set, Kv::remove
ScanKeys Store::scan_keys, Store::scan_prefix, Kv::keys
Flush save_now, close, flush_prefix
RunStep каждый метод MigrationContext и то, что отдаёт сам шаг

Они пересекаются и всё равно остаются разными типами. У WriteValue есть Absent и SourceGone, до которых сырая запись через Kv не дотягивается; у KvWrite есть Declared, до которого не дотягивается запись через поле. Общих вариантов пять. Одним набором каждый вызывающий читал бы ветки, которые у него выстрелить не могут.

У двух дверей набора нет вовсе: то, что их останавливает, — ошибка в коде, а не обстоятельство. Простые конструкторы — new, new_with, load, load_with, load_slice — паникуют с тем, что ответил бы их двойник try_. А Field::set, update и modify принимают любую запись: значение, которое движок не удержит, вызывает панику с именем поля, запись в закрытый store отбрасывается с предупреждением в логе, а о диске, который не берёт записи, сообщает store, а не запись. Запись, которая отвечает Result, — Field::durable.

У карты набор свой, а не общий с OpenStruct, потому что её открывают поверх того, что уже лежит под ней, — и она встречает два отказа, которых больше не встречает никто: сохранённый ключ, который ей не запись, и запись, чьё имя не читается как тип ключа. У шага миграции набор свой, потому что различию, которое ему нужно, — «эта запись не то, что я ждал», мимо чего шаг часто может пройти, против «диск сломан», мимо чего не может, — больше негде жить.

Field::try_get стоит особняком, и намеренно: это не запись и не отказ store, а то, о чём поле и store не договорились. Он отвечает Disagreement — путь и одна из четырёх причин, — и это тоже обычный Error. Что значит каждая причина: Когда значение не читается.

Вариант Store(..) любого набора несёт то, что сказал движок, целиком — как Report из error_stack. И каждый вариант, который отчёт классифицировал, тоже: WillNotRead, WillNotEncode и TooDeep держат отчёт, а не отрендеренное предложение, потому что цифры и есть диагноз.

let refused = store.get::<u16>(["port"]).unwrap_err();
let amethystate::store::ReadValue::WillNotRead { at, why } = refused else {
panic!("the bytes are there and they are not a u16")
};

Частей у отчёта две, и enum из них только первая:

  • цепочка контекстов — что упало, на каждом уровне, который знал, что падает;
  • вложения — подробности, которые несут типами, а не предложениями.

Достать отчёт из набора — такой же match, как любой другой:

fn what_the_store_said(why: LoadMap) -> StorageResult<()> {
match why {
LoadMap::EntryWillNotRead { why, .. } => Err(why.into_report()),
other => panic!("an entry was expected to be at fault: {other}"),
}
}
let refused = store.kv().map::<String, u64>("labels").unwrap_err();
let report = what_the_store_said(refused).unwrap_err();
let context = report.current_context();
let sentence = report.to_string();

context — это StorageError::Codec, а sentence — the value could not be encoded or decoded.

current_context() — самый внешний контекст. Варианты StorageError называют операцию, которая упала, а не то, обо что она упала: Write, Scan, Codec. Два разных движка, не сумевших записать, дают один и тот же контекст, а различаются кадрами под ним, — и это сделано нарочно: тому, кто решает, что делать дальше, важно, легла ли запись, а не redb или serde_json сказал «нет».

StorageError помечен #[non_exhaustive], в отличие от наборов. Это собственный список диска, он растёт вместе с движками, и его читают, а не разбирают по веткам.

Каждый факт — свой тип, поэтому из сообщения ничего не парсится обратно. Живут они в amethystate::errors::facts: ключ, префикс, под которым шло сканирование, запись, на которой оно споткнулось, файл, который читали, размер значения. Каждый — newtype над тем, что несёт, а подпись живёт в его Display.

facts::all::<T, _> отдаёт все факты одного типа, начиная с самого глубокого:

let refused = store.kv().map::<String, u64>("ports").unwrap_err();
let report = what_the_store_said(refused).unwrap_err();
let entries: Vec<&Entry> = facts::all::<Entry, _>(&report).collect();
let prefixes: Vec<&Prefix> = facts::all::<Prefix, _>(&report).collect();

В entries один Entry("http"), в prefixes один Prefix("ports"). Карта, не открывшаяся из-за одной плохой записи, говорит, из-за какой именно, — и это ровно то, что печать через {} выбрасывает.

Спросить факт, которого в отчёте нет, — получить ничего:

let refused = store.kv().map::<String, u64>("ports").unwrap_err();
let report = what_the_store_said(refused).unwrap_err();
let key = facts::all::<Key, _>(&report).next();

key — это None: этот отчёт никогда не был про один ключ.

Какие факты в отчёте, зависит от того, кто был на стеке: их прикладывает тот, кто их знал, и Key не приложит код, видевший только префикс. Читать их надо как улики, которые есть, когда есть, — не как схему. Прикладывают лениво: на успешном пути не строится ничего.

Это тот слой, от которого наборы и избавляют. Лезть сюда стоит, когда пишешь строчку в лог или отчёт о баге, — а не когда решаешь, что делать дальше.

{} набора — одна строка, написанная для того, кому это чинить. {:?} отчёта под ним даёт всё: каждый контекст цепочки и факты под тем кадром, который их приложил.

store.set(["ports", "http"], &"text".to_string())?;
let undecodable = store.kv().map::<String, u64>("ports").unwrap_err();
the entry at ports.http will not read back
the value could not be encoded or decoded
├╴as: u64
├╴value bytes: 5
├╴prefix: ports
├╴entry: http
│
├─▶ Erased codec error: wrong msgpack marker FixStr(4)
│
╰─▶ wrong msgpack marker FixStr(4)
let wrong_key = store.kv().map::<Id<u16>, String>("ports").unwrap_err();
`http` under ports will not read as a Id<u16>

Под этим отчёта нет, и не надо: вариант уже назвал карту, запись и тип, которым её имя не прочиталось.

let names_nothing = store.set(Vec::<String>::new(), &1u32).unwrap_err();
the write was given no path to land at: a path of no levels names the whole store - say `StorePath::root()` to mean it
let shallow = StoreBuilder::new(settings)
.limits(|l| l.key_depth(4))
.build()?;
let too_deep = shallow.set(["a", "b", "c", "d", "e"], &1u32).unwrap_err();
a.b.c.d.e is deeper than this store reads back
deeper than this store reads back
├╴key: a.b.c.d.e
├╴levels: 5, and the limit is 4
├╴set by: limits(|l| l.key_depth(..))
╰╴what is stored here spends the same budget - this store reads 512 levels in all

Последний — образец, к которому стоит стремиться, когда отказ пишешь ты сам: предложение называет, что отвергли, а факты под ним отвечают на вопрос, который читатель сейчас задаст, — чей это был предел и во что он обошёлся.

Значит, {:?} отчёта — это то, что кладут в лог, а {} набора — та самая одна строка, которую читает человек.

Report этот трейт не реализует, поэтому под баунд E: Error он не подходит — а его требует и anyhow::Error::new, и thiserror через #[source], и любая своя обёртка над чужой ошибкой. Наборы этот баунд удовлетворяют, поэтому у границы вопрос не встаёт; встаёт он, когда отчёт уже вынут и переходить надо ему самому. Дорога — into_error, и она ничего не теряет:

let std_error = report.into_error();
let sentence = std_error.to_string();
let whole = format!("{std_error:?}");

sentence — то же самое the value could not be encoded or decoded, а в whole по-прежнему лежит entry: http. Обёртка держит отчёт за собой, а не расплющивает его. as_error — заимствующий близнец, чтобы отдать отчёт, не отдавая его насовсем.

error_stack переэкспортирован как amethystate::error_stack, а Report лежит ещё и в amethystate::errors, — так что назвать его в своей сигнатуре не стоит ни одной своей зависимости и не разъедется с этим крейтом по версиям.