Проверка согласованности онлайн-копий SQLite для iOS на облачном Mac

Проверка согласованности онлайн-копий SQLite для iOS на облачном Mac

Проверка согласованности онлайн-копий SQLite для iOS на облачном Mac

Клиент просит добавить в релизную версию приложения функцию «Экспорт базы данных», не прерывая ввод заказов. Команда проверила её на облачном Mac: файл скачивается, но после восстановления в базе не хватает нескольких записей. Причина может быть не в передаче файла, а в том, что основной файл SQLite скопировали прямо во время работы базы. Сначала нужно выбрать способ создания согласованной резервной копии, а затем проверить восстановление. Успешное копирование файла само по себе не означает, что результат готов к передаче клиенту.

Сначала выберите API, а не копируйте файлы работающей базы

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

Последовательное копирование основного файла и соседних файлов WAL и SHM тоже не гарантирует, что все они отражают один и тот же момент времени. Эксперименты в командной строке на облачном Mac нужны для проверки поведения базы. При интеграции в iOS следует использовать существующие в приложении пути к базе и механизмы управления соединениями.

Резервное копирование без остановки работы гарантирует согласованность базы, но не завершённость бизнес-процесса. Если заказ и его позиции записываются в нескольких транзакциях, резервная копия может попасть в промежуток между ними.

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

Резервное копирование на Swift с ограничением времени

В примере ниже копируется только база main. Целевой файл размещается в отдельном каталоге для этой задачи и ещё не должен существовать; исходный файл, напротив, уже должен быть на месте. Сохраните код в backup.swift и запустите его на облачном Mac с установленными инструментами командной строки.

import Foundation
import SQLite3

enum BackupFailure: Error {
    case invalidArguments
    case sqlite(Int32)
    case timeout
}

func openDB(_ path: String, flags: Int32) throws -> OpaquePointer {
    var db: OpaquePointer?
    let rc = sqlite3_open_v2(path, &db, flags, nil)
    guard rc == SQLITE_OK, let handle = db else {
        if let db = db { sqlite3_close(db) }
        throw BackupFailure.sqlite(rc)
    }
    return handle
}

func backup(source: String, target: String) throws {
    guard !FileManager.default.fileExists(atPath: target) else {
        throw BackupFailure.invalidArguments
    }
    let src = try openDB(source, flags: SQLITE_OPEN_READONLY)
    defer { sqlite3_close(src) }
    let dst = try openDB(
        target,
        flags: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
    )
    var complete = false
    defer {
        sqlite3_close(dst)
        if !complete {
            try? FileManager.default.removeItem(atPath: target)
        }
    }
    guard let job = sqlite3_backup_init(dst, "main", src, "main") else {
        throw BackupFailure.sqlite(sqlite3_errcode(dst))
    }
    let deadline = ProcessInfo.processInfo.systemUptime + 10
    var rc: Int32 = SQLITE_OK
    repeat {
        if ProcessInfo.processInfo.systemUptime >= deadline {
            rc = SQLITE_INTERRUPT
            break
        }
        rc = sqlite3_backup_step(job, 64)
        if rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED {
            Thread.sleep(forTimeInterval: 0.02)
        }
    } while rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED
    let finish = sqlite3_backup_finish(job)
    guard rc == SQLITE_DONE else {
        if rc == SQLITE_INTERRUPT { throw BackupFailure.timeout }
        throw BackupFailure.sqlite(rc)
    }
    guard finish == SQLITE_OK else {
        throw BackupFailure.sqlite(finish)
    }
    complete = true
}

guard CommandLine.arguments.count == 3 else {
    throw BackupFailure.invalidArguments
}
try backup(source: CommandLine.arguments[1], target: CommandLine.arguments[2])
xcrun swiftc backup.swift -o backup -lsqlite3
mkdir -m 700 backup-run
./backup source.sqlite backup-run/snapshot.sqlite
sqlite3 backup-run/snapshot.sqlite \
  'PRAGMA integrity_check; PRAGMA foreign_key_check;'

Десять секунд — общий лимит для этого эксперимента, а не универсальный норматив для сервиса. Другие потоки не должны одновременно работать с целевым соединением. В примере не установлен обработчик ожидания занятости; если приложение настраивает такой обработчик самостоятельно, время ожидания внутри него тоже нужно учитывать в общем лимите.

Проверьте границы транзакций при конкурентной записи

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

Для каждой копии проверьте, что заявленное в пакете количество позиций совпадает с фактическим, нет позиций без соответствующего пакета и номера пакетов не повторяются. Снимки могут содержать разное количество полных пакетов, но ни один из них не должен содержать только часть транзакции.

Прогресс копирования не означает, что снимок уже зафиксирован

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

Если нужен снимок на фиксированный момент времени, можно явно начать транзакцию чтения в исходном соединении и выполнить чтение, чтобы сформировать снимок, а затем запустить резервное копирование. По завершении транзакцию нужно своевременно закрыть. В режиме WAL длительная транзакция чтения может мешать продвижению контрольных точек, поэтому следует также отслеживать рост журнала. Влияние блокировок в разных режимах журналирования нужно проверять отдельно.

Проверки сбоев должны исключать выдачу некорректной копии

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

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

Ожидаемый результат integrity_check — только ok, а foreign_key_check не должен возвращать ни одной строки. Даже если обе проверки пройдены, нужно проверить агрегированные бизнес-показатели и user_version: корректная структура ещё не означает корректность бизнес-данных.

Проверка завершена только после восстановления по отдельному пути

Закройте соединение с резервной копией, скопируйте результат в новый каталог задачи и откройте его заново без файлов WAL и SHM исходной базы. Сначала выполните запросы только на чтение, затем на копии, которую можно удалить, проверьте добавление, изменение, удаление и повторное открытие. Это должно подтвердить, что приложение действительно может продолжить работу с базой.

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

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

Часто задаваемые вопросы

Можно ли копировать только основной файл базы в режиме WAL?

Это ненадёжный способ онлайн-резервирования: зафиксированные записи могут оставаться в WAL. Используйте API онлайн-копирования SQLite либо другой способ экспорта с проверенной согласованностью.

Достаточно ли успешного integrity_check?

Нет. Нужны foreign_key_check, проверка бизнес-инвариантов и версии схемы, а также открытие и восстановление базы из независимого каталога.

Онлайн-копия всегда отражает состояние на момент запуска?

Нет. При пошаговом копировании запись из других соединений может перезапустить процесс. Для фиксированного снимка спроектируйте читающую транзакцию источника и оцените рост WAL.

Срок аренды под график проекта

Подходит для разовых сборок и выпуска приложения, дополнительной мощности на неделе релиза и работы над проектами разных клиентов. Если команда постоянно загружена сборками и у вас уже есть собственное оборудование, сначала сравните покупку и аренду.

VMSSD предоставляет выделенные физические Mac mini — не виртуальные машины. Выберите срок аренды: день, неделю, месяц или квартал. По окончании срока аренда прекращается без продления. Актуальную доступность моделей и локаций смотрите в панели управления.

Превратите чтение в план действий

Оцените сроки проекта и нагрузку на сборки, а затем выберите модель и срок аренды. Если пользуетесь сервисом впервые, начните со списка перед оформлением заказа.