Проверка снимков чтения SQLite для iOS на облачном Mac
Фоновая синхронизация уже зафиксировала новые записи, но после обновления списка по-прежнему видны старые значения. Пересоздание кеша временно устраняет проблему, однако при следующем пакетном чтении она возникает снова. В такой ситуации сначала стоит проверить, не выполняется ли чтение всё ещё в рамках одной и той же транзакции SQLite. Воспроизвести поведение с двумя независимыми соединениями на облачном Mac — более надёжный способ отделить семантику базы данных от кеширования интерфейса, чем раз за разом повторять действия через удалённый рабочий стол.
Сначала определите контракт чтения
В статье рассматриваются только локальные файловые базы данных в режиме WAL. Эти выводы нельзя переносить на базы в памяти или конфигурации с общим кешем. Подготовьте Python 3 с модулем SQLite, выполните python3 -c 'import sqlite3; print(sqlite3.sqlite_version)' и сохраните вывод вместе с результатами теста.
Критерии приёмки таковы: после первого запроса читающего соединения пишущее соединение может зафиксировать изменения; прежняя транзакция чтения продолжает видеть старое значение; новое значение становится видимым при следующем запросе только после завершения этой транзакции. «Старое» здесь означает согласованность в пределах транзакции, а не потерю данных.
Не фиксируйте транзакцию в произвольных местах при чтении нескольких таблиц, требующем согласованного представления, только ради того, чтобы список выглядел «более актуальным». Сначала определите, нужен ли операции единый снимок или каждый запрос должен получать последние зафиксированные данные.
Для теста нужны настоящий временный файл и два соединения. Два обычных соединения с :memory: имеют каждое свою базу данных и не позволяют проверить такое взаимодействие.
Создайте минимальный тест с двумя соединениями
Сохраните следующий код в snapshot_check.py и выполните python3 snapshot_check.py. Скрипт не подключается к базе приложения, а временный каталог автоматически удаляется при выходе из контекста. Результаты всех курсоров считываются сразу, после чего курсоры закрываются: это позволяет не спутать незавершённый SQL-оператор с проблемой явно открытой транзакции.
import sqlite3
import tempfile
from pathlib import Path
def one(db, sql):
cursor = db.execute(sql)
try:
return cursor.fetchone()
finally:
cursor.close()
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "snapshot.sqlite"
writer = sqlite3.connect(path, isolation_level=None)
reader = sqlite3.connect(path, isolation_level=None)
try:
assert one(writer, "PRAGMA journal_mode=WAL")[0] == "wal"
writer.execute("PRAGMA wal_autocheckpoint=0")
writer.execute("CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)")
writer.execute("INSERT INTO item VALUES(1, 'old')")
one(writer, "PRAGMA wal_checkpoint(TRUNCATE)")
reader.execute("BEGIN")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "old"
writer.execute("BEGIN IMMEDIATE")
writer.execute("UPDATE item SET value='new' WHERE id=1")
writer.execute("COMMIT")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "old"
held = one(writer, "PRAGMA wal_checkpoint(PASSIVE)")
assert held[1] > held[2], held
reader.execute("COMMIT")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "new"
released = one(writer, "PRAGMA wal_checkpoint(PASSIVE)")
assert released[1] == released[2], released
print({"held": held, "released": released, "result": "passed"})
finally:
if reader.in_transaction:
reader.rollback()
if writer.in_transaction:
writer.rollback()
reader.close()
writer.close()
Автоматическое создание контрольных точек явно отключено лишь для того, чтобы ход эксперимента можно было наблюдать. Это не рекомендация для рабочей среды. Усечение после инициализации также предназначено только для этой изолированной тестовой базы: не переносите его в базу, которая обслуживает текущие задачи.
Различайте BEGIN и первое чтение
Обычный BEGIN начинает отложенную транзакцию. Нельзя считать момент выполнения этого оператора моментом создания снимка. В приведённом примере представление для чтения формируется при первом запросе.
Проведите отдельный вариант эксперимента: перенесите запись и фиксацию изменений перед первым запросом читающего соединения. Тогда и первый запрос, и последующие запросы в той же транзакции должны возвращать new. Соответствующим образом измените проверки.
Так выявляется распространённая ошибка диагностики: в журнале есть запись «транзакция началась», и инженер полагает, что все последующие чтения отражают состояние именно на тот момент. При диагностике фиксируйте как минимум идентификаторы соединения и транзакции, момент первого запроса и место завершения транзакции, а не только вход в метод.
Нужна ли согласованность между несколькими запросами?
Например, если сначала запрашивается общее число заказов, а затем их подробные данные и оба запроса должны относиться к одному бизнес-снимку, выполняйте их в короткой транзакции чтения. Если же каждая подгрузка при прокрутке должна получать актуальные данные, не охватывайте одной транзакцией весь жизненный цикл страницы. Критерии приёмки определяются границами бизнес-операции, а не тем, какой подход выглядит более быстрым в обновлении данных.
Следите за продвижением контрольной точки, а не только за размером файла
wal_checkpoint(PASSIVE) возвращает три целых числа: признак занятости, число кадров в журнале и число кадров, перенесённых в основную базу при создании контрольной точки. Читающему соединению в тесте всё ещё нужно прежнее представление данных, поэтому часть новых кадров пока нельзя перенести. После завершения транзакции чтения повторный вызов позволяет контрольной точке продвинуться.
Не закладывайте в проверки точное число кадров, которое должна создать конкретная операция. На него влияют размер страницы, структура базы и версия библиотеки времени выполнения. Тест должен проверять соотношения между значениями.
Аналогично, если число обработанных кадров сравнялось с общим числом кадров журнала, это не означает, что WAL-файл сразу уменьшится. Файл может сохранять выделенное место для повторного использования. В рабочей среде одновременно отслеживайте длительность транзакций чтения, результаты создания контрольных точек и динамику записи. Сам по себе большой файл в каталоге — не повод удалять журнал.
Если вторая проверка не проходит, сначала найдите незавершённые транзакции чтения, незакрытые курсоры или другие открытые соединения и только затем анализируйте стратегию создания контрольных точек. Не начинайте с цикла принудительного усечения: так можно скрыть настоящую причину — длительно работающего читателя.
Перенесите тест в проверки слоя данных iOS
Эксперимент на хосте проверяет библиотеку SQLite, с которой связан используемый Python. После переноса в iOS повторите тест через реальный слой доступа к данным на целевом симуляторе или устройстве и запишите значение sqlite3_libversion() для этой среды. Результат на хосте не доказывает такое же поведение во всех средах выполнения.
Сохраните два действительно независимых соединения на нижнем уровне. Управляйте тремя этапами — «первое чтение завершено, запись зафиксирована, повторное чтение» — с помощью явных барьеров синхронизации в тесте, а не задержек через sleep. Если пул направит обе операции в одно соединение, эксперимент потеряет смысл.
В завершение проверьте четыре условия: старая транзакция читает старое значение, новая — новое, транзакции завершаются и при ошибках, а после завершения длительной транзакции чтения контрольная точка может продвигаться. Для асинхронных запросов дополнительно убедитесь, что отмена завершает работу и курсора, и транзакции. Сохраните эти проверки в регрессионных тестах. Тогда, когда список снова покажет старые данные, вы сможете сначала определить, является ли это корректным снимком или транзакция уже вышла за предусмотренные границы жизненного цикла.
Часто задаваемые вопросы
Фиксирует ли BEGIN снимок чтения сразу?
Нет, если это обычная отложенная транзакция. В приведённом тесте WAL снимок создаётся при первом фактическом чтении.
Должен ли открытый читатель сразу видеть новый коммит?
Нет. Текущая транзакция сохраняет прежний снимок. Новые данные будут видны после её завершения и повторного чтения.
Всегда ли контрольная точка уменьшает файл WAL?
Нет. Перенос страниц в основной файл базы и усечение WAL — разные операции. Одного размера файла для диагностики недостаточно.
Поручите задачи выделенному облачному Mac
Арендуйте выделенный физический Mac mini и выберите локацию и срок аренды под свой график. После активации данные для подключения появятся в консоли; если возникнут вопросы, отправьте заявку в поддержку через консоль.
Выбрать облачный Mac