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

Что лежит на диске

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

match StoreBackend::files_layout(&store) {
Some(StoreLayout::Single { data }) => {
println!("everything is in {}", data.display());
}
Some(StoreLayout::Sidecars {
data,
meta,
data_backup,
meta_backup,
}) => {
println!("values: {}", data.display());
println!("bookkeeping: {}", meta.display());
println!(
"kept while rewriting: {}, {}",
data_backup.display(),
meta_backup.display(),
);
}
Some(StoreLayout::InMemory) => println!("nothing is on disk"),
Some(_) => println!("a layout this code does not know yet"),
None => println!("this engine does not say"),
}

Отвечает он формой, а не списком, поэтому добраться до нужного файла — это match, и никогда не поиск. У движка без отдельной бухгалтерии её и не спросишь, а у того, у кого она есть, её не может не оказаться.

Файл называют независимо от того, лежит ли он там прямо сейчас. Бэкап есть только пока идёт перезапись, а имя его нужно в обоих случаях.

Оба держат и значения, и бухгалтерию внутри одного файла, каждый в своих структурах. Рядом никто ничего не пишет.

Текстовые движки: два файла, и ещё два во время записи

Заголовок раздела «Текстовые движки: два файла, и ещё два во время записи»
файл что держит
settings.json значения
settings.json.meta бухгалтерию схемы
settings.json.bak копию данных, пока их перезаписывают
settings.json.meta.bak то же для бухгалтерии

Файл-спутник несёт то, без чего store не прочитает данные обратно: какая версия каждой структуры их записала, как выглядели её поля и что уже сделал проход миграции. Файл данных человек прочитает и сам по себе, а вот store, открывший его без спутника, остался без схемы, под которой тот записан. Оба принадлежат store, и бэкап берёт оба.

.meta и .bak дописывают ко всему имени, а не взамен расширения. Заменой на обоих файлах вышло бы settings.bak: вторая копия легла бы на первую, и у данных не осталось бы бэкапа. Заодно так получилось бы имя, которого store не создавал никогда, — settings.bak, который кто-то положил туда сам. С бухгалтерией то же самое: settings.user и settings.system в одном каталоге делили бы один settings.meta, и каждый store затирал бы чужую запись о том, какие миграции уже прошли.

Store, записанный выпуском до 0.23, держит бухгалтерию в settings.meta. Пока settings.json.meta нет, открытие читает старый файл и пишет новый. Старый остаётся на месте: его может читать и другой store.

Это страховка на открытии, а не копия, которую держат для вас. Открываясь, store читает файлы и прогоняет по прочитанному проход миграции, в памяти; только потом он пишет, и только те файлы, которые проход изменил или которых ещё не было. Каждый такой файл сперва копируют, а когда запись легла, копию убирают. У store, который запустился нормально, никакого .bak нет вовсе.

Значит, лежащий рядом .bak — это прошлое открытие, которое не доехало до конца. Вот то, что оно за собой оставляет, собранное руками:

std::fs::write(&backup, &good).unwrap();
std::fs::write(&data, "{ this never finished").unwrap();

Целый бэкап рядом с недописанным файлом данных. На следующем открытии данные не разберутся, а бэкап разберётся: store поднимется из него и пойдёт дальше, сказав об этом через tracing::warn! под таргетом amethystate.

Файл, который не разобрался, не выбрасывается: прежде чем копия встанет на его место, его переименуют в settings.json.unreadable, и всё, что в нём было, можно посмотреть. .bak уходит, как только поднятый из него файл лёг на диск. Копия отвечает один раз. Останься она лежать, она ответила бы и следующему файлу, который не разберётся, — правке руками с опечаткой, — и молча вернула бы данные, какими они были до этой правки.

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

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

Файл, который проход не изменил, не копируют и не пишут. Тот же файл может держать открытым другой store и как раз в него сохраняться, и вернуть на место то, что прочитало открытие, значило бы затереть этой старой копией записанное другим.

Удалите файл данных, и store начнётся с чистого листа. Когда нет ни файла данных, ни .bak рядом с ним, бухгалтерии больше нечего описывать, и открытие не смотрит на оставшийся .meta: объявленные карты снова получают значения по умолчанию, а каждый префикс начинается с версии, которую объявляет его структура.

Она отвечает на один вопрос — «миграция не доехала, верни файл обратно», — а спрашивают с неё и то, на что ответить ей нечем.

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

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

Возраст копии store доверять не может. Из .bak поднимаются, когда данные не читаются, — и всё равно, лежит там запись перед этой или сотня записей назад. Время изменения не ответ: оно съезжает, стоит кому-нибудь скопировать каталог.

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

Что из этого следует приложению: .bak — сообщение о сбое, а не точка восстановления. Он говорит, что открытие не доехало. Там, где данные дороги, держите свои копии: копия store принадлежит миграции, и одолжить её под бэкап нельзя.

У движка, который владеет своим файлом целиком, ничего этого нет. SQLite пишет через журнал упреждающей записи, а redb кладёт новые страницы рядом со старыми и переключает на них заголовок в две фазы, после каждой сбрасывая файл на диск. Оборванная запись либо есть целиком, либо её нет вовсе.