Что лежит на диске
Сколько файлов держит 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, и никогда не поиск. У движка без отдельной бухгалтерии её и не спросишь, а у того, у кого она есть, её не может не оказаться.
Файл называют независимо от того, лежит ли он там прямо сейчас. Бэкап есть только пока идёт перезапись, а имя его нужно в обоих случаях.
redb и SQLite: один файл
Заголовок раздела «redb и SQLite: один файл»Оба держат и значения, и бухгалтерию внутри одного файла, каждый в своих структурах. Рядом никто ничего не пишет.
Текстовые движки: два файла, и ещё два во время записи
Заголовок раздела «Текстовые движки: два файла, и ещё два во время записи»| файл | что держит |
|---|---|
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 кладёт новые страницы рядом со старыми и переключает на них заголовок в две фазы, после каждой сбрасывая файл на диск. Оборванная запись либо есть целиком, либо её нет вовсе.