Это история о приложении для обмена сообщениями, которым пользуются миллиарды пользователей.
Приложение придерживается чрезвычайно строгой модели конфиденциальности. Приложение никогда не хранит данные пользователя на серверах. Все сообщения end-to-end шифруются.
Многие пользователи этого приложения, особенно на Android, регулярно удаляли и переустанавливали его. Чтобы предотвратить потерю сообщений, их резервное копирование осуществляется на SD-карту пользователя. В модели безопасности Android SD-карта является общедоступным местом хранения данных, доступным всем приложениям. Поэтому, чтобы сохранить конфиденциальность сообщений, резервные копии зашифровались.
И вдруг неожиданно поступил всплеск жалоб от пользователей на то, что приложение не смогло восстановить их сообщения. Хотя у нас были некоторые внутренние показатели, подтверждающие это, не было возможности отладить ошибку без доступа к некоторым образцам для воспроизведения ситуации.
Для пользователей мессенджеров, особенно нашего, сообщения были воспоминаниями. Потеря сообщений означала потерю ценных воспоминаний.
Мы обратились к пользователям с просьбой предоставить образцы резервных копий, чтобы воспроизвести проблемы. Несколько человек ответили. Все образцы были криптографически в порядке.
Однако базы данных SQLite в резервных копиях были сломаны. Они не загружались ни через Sqlite на Android, ни через любой Sqlite-браузер на Mac OS. Однако их можно было открыть в SQLite Shell. И до тех пор, пока запросы не пытались затронуть битую строку, они работали успешно. Работали даже агрегатные запросы типа SELECT COUNT(*).
В одном случае нарушались ограничения первичного ключа. В нескольких других случаях встречались неправильные символы Юникода. Хотя я пытался разобраться в ситуации, мне не удалось найти причину этих ошибок. Дешевые телефоны Android — странные звери. Я регулярно сталкивался с ошибками памяти, поэтому эти повреждения базы данных меня не удивили.
Но как найти и устранить такие поврежденные записи программно?
RTFM — прочитайте наконец руководство
Я продолжал читать руководство по SQLite Shell снова и снова. Пока не понял, что в принципе могу сделать дамп базы данных SQLite в виде последовательности SQL-команд, которая точно воспроизводит базу данных.
Получение дампа выглядел следующим образом.
BEGIN TRANSACTION CREATE TABLE ... INSERT INTO ... ... (100,000-1,000,000 rows) END TRANSACTION
И этот дамп работал даже для тех сломанных баз данных!
Мы решили, что потеря нескольких плохих сообщений ради восстановления всего остального будет приемлемым компромиссом. Но как это сделать?
Что, если я смогу определить плохие строки и удалить их? В сообщениях пользователя содержатся новые строки, поэтому я не могу аккуратно отделить их программно. А поскольку все происходило в транзакции, она откатывалась, как только возникала первая ошибка.
Продолжая копаться в руководстве, я обнаружил:
-- The .bail off command tells SQLite to continue executing statements even if errors occur .bail off
Вуаля!
Исправление
Финальный процесс выглядел примерно так. Пользователь переходит к обычному восстановлению базы данных, и если оно не удается, мы переходим к следующему.
// Dump database into SQL commands using SQLite shell // Remove the first 'BEGIN TRANSACTION' and the last 'END TRANSACTION' // Setup an empty database with all the tables and correct constraints // Start SQLite shell in `.bail off` mode // Restore the SQL commands from step 2 into the database using SQLite shell if (failedToRestore) { setupEmptyDatabase(); startSqliteShellAndRecover(); }
Это происходило, когда пользователь сталкивался с повреждением базы данных, и завершалось в течение 5-60 секунд.
Это действительно сработало
Помните, что агрегированные запросы на подсчет работают даже тогда, когда база данных кажется поврежденной. Таким образом, мы знали, что можем подсчитать строки как в поврежденной, так и в восстановленной базе данных.
Не раскрывая точных цифр, я могу сказать, что результаты были довольно хорошими, большинство пользователей потеряли всего несколько сообщений в процессе восстановления, и почти полная база данных была восстановлена для большинства пользователей.
Финальный слой восстановления базы данных состоял из ~200 строк кода. Возможно, самый маленький в мире. Сегодня он установлен на миллиардах устройств, готовых спасти их пользователей от повреждения базы данных.