max_replication_slots часто воспринимают как счётчик — «сколько слотов разрешено». На самом деле это длина массива в разделяемой памяти, и почти все наблюдаемые свойства параметра — отказ стартовать при понижении, обязательный рестарт, поведение pg_upgrade — следуют из этого факта. Разберём механику: как массив выделяется, как в нём создаются, захватываются, сохраняются и инвалидируются слоты, и что из этого следует для эксплуатации.
Что именно задаёт параметр
При регистрации разделяемой памяти размер под слоты вычисляется как смещение до массива плюс произведение (max_replication_slots + max_repack_replication_slots) на sizeof(ReplicationSlot):
size = offsetof(ReplicationSlotCtlData, replication_slots);
size = add_size(size,
mul_size(max_replication_slots + max_repack_replication_slots,
sizeof(ReplicationSlot)));Массив выделяется один раз при старте. При инициализации цикл проходит по всем max_replication_slots + max_repack_replication_slots элементам и настраивает каждый слот — active_proc, спинлок, LWLock, условную переменную. max_replication_slots — это длина массива в разделяемой памяти, и это всё, чем он является: он не решает, кто может создать слот, сколько WAL слот удерживает и когда заброшенный слот будет очищен. Он решает лишь, сколько в массиве записей, а массив выделяется один раз при старте, поэтому значение нельзя изменить без перезапуска.
Отсюда два практических следствия. Если понизить значение ниже числа слотов, уже лежащих на диске, сервер откажется стартовать с ошибкой FATAL: too many replication slots active before shutdown, где «active» означает «существует». pg_upgrade с 17-й версии применяет то же правило при переносе логических слотов: max_replication_slots нового кластера должен быть не меньше числа логических слотов старого, иначе --check останавливается.
Сколько слотов нужно на издателе
На издателе нужно по одному логическому слоту на каждую подписку, плюс ещё по одному на каждый worker синхронизации таблицпроцесс, копирующий таблицу при начальной синхронизации подписки. Слоты синхронизации называются pg_<subscription oid>_sync_<table oid>_<system identifier> и удаляются по завершении копирования. Поэтому подписчик с настройкой по умолчанию max_sync_workers_per_subscription = 2 может занимать три записи на издателе, пока догоняет.
Слот самой подписки создаётся при CREATE SUBSCRIPTION, а слоты worker'ов синхронизации таблиц — позже, когда они запускаются. Если на издателе max_replication_slots = 1, слот подписки займёт единственную запись, и тогда каждый tablesync worker не сможет создать свой слот, завершится и будет перезапускаться каждые wal_retrieve_retry_interval. Таблицы останутся в pg_subscription_rel в состоянии srsubstate = 'd' бесконечно, а в логе подписчика ошибка будет повторяться каждые пять секунд. Сама ошибка — all replication slots are in use с подсказкой Free one or increase "max_replication_slots". Поднятие лимита на издателе (с перезапуском) исправляет это без изменения подписки.
Устройство структуры слота в общей памяти
Структура ReplicationSlot защищена двумя механизмами. Флаг in_use, определяющий, определён ли слот, изменяется при добавлении или удалении под LWLockоблегчённая блокировка разделяемой памяти ReplicationSlotControlLock: владеющий слотом бэкенд берёт его в эксклюзивном режиме при обновлении флага, а читатели — в разделяемом при просмотре данных слота. Отдельные поля защищены мьютексом (slock_t mutex), который обновляет только владеющий слотом бэкенд; владелец не берёт этот мьютекс при чтении своих полей, а другие бэкенды берут его при чтении данных чужого слота.
Поля just_dirtied и dirty лежат рядом с мьютексом, причём комментарий указывает, что мьютекс находится на той же кэш-линии, что и effective_xmin.
effective_xmin для логического декодирования хранит последний xmin, реально записанный на диск, а для потоковой репликации совпадает с persistent-значением data.xmin — это нужно, чтобы не удалять данные, ещё необходимые для декодирования, даже после сбоя. Поле data (ReplicationSlotPersistentData) переживает выключения и сбои; io_in_progress_lock отмечает, что кто-то выполняет ввод-вывод по слоту, а active_cv — условная переменная, сигнализируемая при изменении active_proc.
Создание слота
ReplicationSlotCreate принимает имя слота, флаг db_specific (создаётся ли слот для конкретной базы данных: при истинном значении в данные слота записывается идентификатор текущей базы, при ложном — InvalidOid), значение persistency (переживёт ли слот перезапуск и крах), а также флаги two_phase (разрешает ли слот двухфазные транзакции), repack (создаётся ли слот для перепаковки), failover (будет ли слот синхронизироваться на standby) и synced (создан ли слот в результате синхронизации с primary). Сначала она обнуляет persistent-данные слота и заполняет их: имя, база данных (MyDatabaseId при db_specific, иначе InvalidOid), persistency, two_phase, failover, synced. Затем инициализируются поля только разделяемой памяти (just_dirtied, dirty, effective_xmin, candidate_* и т.д.), после чего слот создаётся на диске вызовом CreateSlotOnDisk(slot).
Далее под эксклюзивной блокировкой ReplicationSlotControlLock выставляется slot->in_use = true, под спинлоком — slot->active_proc = MyProcNumber, и MyReplicationSlot начинает указывать на слот. Для логического слота создаётся запись статистики, после чего отпускается ReplicationSlotAllocationLock и широковещательно сигналится active_cv.
Отдельного внимания заслуживает persistency. Различаются три её вида: RS_PERSISTENT — слот сохраняется на диск и переживает перезапуск и крах; RS_EPHEMERAL — слот существует только пока его удерживает текущий процесс и удаляется при освобождении или откате транзакции; RS_TEMPORARY — временный слот, который тоже удаляется при ошибке, но, в отличие от эфемерного, не привязан к удержанию процессом. В walsender при CREATE_REPLICATION_SLOT слот изначально создаётся как RS_EPHEMERAL (или RS_TEMPORARY, если запрошен временный), потому что это «allows us to nicely handle errors during initialization because it'll get dropped if this transaction fails». В конце, если слот не temporary, вызывается ReplicationSlotPersist(), делающий его persistent. В slotfuncs.c та же логика: после успешного создания if (!temporary) ReplicationSlotPersist(); и затем ReplicationSlotRelease().
Захват слота
ReplicationSlotAcquire сначала под общим ReplicationSlotControlLock находит слот и под спинлоком слота выставляет владельца: если слот свободен (active_proc == INVALID_PROC_NUMBER), туда записывается MyProcNumber и сбрасывается inactive_since в 0; если слот уже занят другим процессом, active_proc просто читается. Затем берётся pid владельца через GetPGProcByNumber(active_proc)->pid. Если active_proc != MyProcNumber, то при nowait=false бэкенд засыпает на условной переменной s->active_cv и после пробуждения повторяет попытку (goto retry), а при nowait=true сразу выдаёт ошибку ERRCODE_OBJECT_IN_USE с сообщением replication slot "%s" is active for PID %d.
После успешного захвата слот присваивается MyReplicationSlot, и только тогда, если error_if_invalid=true и s->data.invalidated != RS_INVAL_NONE, выбрасывается ошибка ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE с деталью о причине инвалидации. Проверка делается после присвоения намеренно — чтобы избежать гонки с checkpointer, который иначе мог бы инвалидировать слот сразу после проверки. Сброс inactive_since выполняется под спинлоком слота: в ветке свободного слота вызывается ReplicationSlotSetInactiveSince(s, 0, false) внутри SpinLockAcquire/SpinLockRelease, что, согласно комментарию, нужно «to avoid race conditions with slot invalidation».
Именование и валидация
ReplicationSlotValidateNameInternal проверяет имя в несколько шагов. Пустое имя отвергается с кодом ERRCODE_INVALID_NAME и сообщением replication slot name "%s" is too short. Имя длиной не менее NAMEDATALEN отвергается с кодом ERRCODE_NAME_TOO_LONG и сообщением replication slot name "%s" is too long. Каждый символ должен быть строчной латинской буквой (a–z), цифрой (0–9) или подчёркиванием (_); иначе возвращается ERRCODE_INVALID_NAME с сообщением replication slot name "%s" contains invalid character и подсказкой Replication slot names may only contain lower case letters, numbers, and the underscore character.
Если allow_reserved_name ложно и имя равно CONFLICT_DETECTION_SLOT (проверяется через IsSlotForConflictCheck), возвращается ERRCODE_RESERVED_NAME с сообщением replication slot name "%s" is reserved и подсказкой The name "%s" is reserved for the conflict detection slot.
Failover-включённые временные слоты запрещены, потому что временные слоты не будут синхронизированы на standby: «Do not allow users to create failover enabled temporary slots, because temporary slots will not be synced to the standby.» Это ограничение не применяется при синхронизации слотов: «However, failover enabled temporary slots can be created during slot synchronization.» Попытка включить failover для временного слота вне синхронизации даёт ERRCODE_FEATURE_NOT_SUPPORTED с сообщением cannot enable failover for a temporary replication slot.
Список synchronized_standby_slots
Список слотов в GUC synchronized_standby_slots валидируется функцией check_synchronized_standby_slots, которая вызывает вспомогательную validate_sync_standby_slots. Сначала строка разбирается на список идентификаторов через SplitIdentifierString; если синтаксис списка некорректен, в деталь ошибки записывается List syntax is invalid. и валидация завершается неудачей. Затем каждый элемент проверяется ReplicationSlotValidateNameInternal; при ошибке в GUC-контекст передаются код ошибки, сообщение и, если есть, подсказка, после чего валидация завершается неудачей. Если валидация не прошла или список пуст, функция освобождает память и завершается с прежним результатом.
При успешной валидации вычисляется размер структуры SyncStandbySlotsConfigData, память выделяется через guc_malloc, и данные сохраняются в extra. При ошибке валидации GUC не принимает новое значение, а assign_synchronized_standby_slots не вызывается, поэтому synchronized_standby_slots_config остаётся прежним.
Сохранение слота на диск
SaveSlotToPath сначала под спинлоком читает флаг dirty и сбрасывает just_dirtied, затем, если dirty не был установлен, сразу возвращается без записи. Далее берётся эксклюзивная блокировка io_in_progress_lock, заполняется структура ReplicationSlotOnDisk (magic, версия, длина, CRC32C, копия slot->data) и открывается временный файл state.tmp через OpenTransientFile с O_CREAT|O_EXCL|O_WRONLY|PG_BINARY.
При ошибке записи сохраняется errno, закрывается файл, удаляется временный файл через unlink, снимается блокировка, и если errno не был установлен, подставляется ENOSPC:
errno = save_errno ? save_errno : ENOSPC;После успешной записи выполняется pg_fsync временного файла, закрытие, rename в постоянный путь state, а затем в критической секции — fsync самого файла, каталога слота и PG_REPLSLOT_DIR. По завершении под спинлоком слот помечается как негрязный, если за время записи его не успели снова изменить: if (!slot->just_dirtied) slot->dirty = false;, и запоминаются last_saved_confirmed_flush и last_saved_restart_lsn.
При старте StartupReplicationSlots обходит каталог слотов. Если находит оставшийся каталог с суффиксом .tmp (крах во время создания или удаления слота), удаляет его через rmtree и делает fsync каталога PG_REPLSLOT_DIR, иначе восстанавливает слот через RestoreSlotFromDisk.
Восстановление слота при старте
RestoreSlotFromDisk сначала читает только ту часть statefile, которая гарантированно не зависит от версии: read(fd, &cp, ReplicationSlotOnDiskConstantSize). Затем проверяет magic, version и length, после чего дочитывает остаток файла размером cp.length и проверяет CRC. Если cp.length не совпадает с ожидаемым ReplicationSlotOnDiskV2Size, возникает PANIC о повреждённой длине.
Если persistency слота не RS_PERSISTENT (то есть слот был ephemeral), слот не восстанавливается, а его каталог удаляется через rmtree(slotdir, true), после чего делается fsync_fname(PG_REPLSLOT_DIR, true) и возврат. Для persistent-слота выполняется memcpy(&slot->data, &cp.slotdata, sizeof(ReplicationSlotPersistentData)) и инициализируются поля в памяти, включая effective_xmin, effective_catalog_xmin, last_saved_confirmed_flush, last_saved_restart_lsn, а также candidate_* в Invalid*. Если свободного слота не нашлось, выдаётся FATAL: too many replication slots active before shutdown.
Удаление слота
ReplicationSlotDropAcquired окончательно удаляет уже захваченный текущим бэкендом слот: обнуляет MyReplicationSlot и вызывает ReplicationSlotDropPtr, а при try_disable дополнительно просит чекпоинтер отключить логическое декодирование. В ReplicationSlotDropPtr сначала берётся ReplicationSlotAllocationLock в режиме LW_EXCLUSIVE, чтобы никто параллельно не создавал слот с тем же именем, и лишь затем формируются пути path и tmppath.
После этого директория слота переименовывается из path в tmppath через rename, и при успехе выполняется fsync переименованной директории и родительской PG_REPLSLOT_DIR внутри критической секции, чтобы изменения были на диске в crash-safe виде. Только после этого берётся ReplicationSlotControlLock в режиме LW_EXCLUSIVE, под которым слот помечается как неиспользуемый (in_use = false) и active_proc сбрасывается в INVALID_PROC_NUMBER, а затем ожидающие процессы будятся через ConditionVariableBroadcast.
Переименование делается до освобождения ReplicationSlotControlLock, потому что слот должен быть «определённо gone» до того, как под этим локом его пометят мёртвым: «The slot is definitely gone. Lock out concurrent scans of the array long enough to kill it». ReplicationSlotAllocationLock освобождается в самом конце, чтобы никто не начал создавать слот, пока идёт очистка остатков старого.
Если rename не удался, слот помечается неактивным (active_proc = INVALID_PROC_NUMBER) и ожидающие будятся, а ошибка выдаётся как WARNING для непостоянных слотов и ERROR для постоянных.
Причины инвалидации
DetermineSlotInvalidationCause последовательно проверяет возможные причины и возвращает первую, для которой слот подходит под инвалидацию. Всего причин четыре:
- потеря WAL — если
restart_lsnслота валиден и меньшеoldestLSN— LSN самого старого WAL-сегмента, который ещё существует на диске; - конфликт с горизонтом xid — для логического слота, когда
effective_xminилиeffective_catalog_xminне превосходитsnapshotConflictHorizon— границу xid, до которой снимки уже не могут быть построены; - неподходящий уровень WAL — если слот логический (причина, связанная с требованием
wal_level); - простой дольше заданного порога.
CanInvalidateIdleSlot требует одновременно четырёх условий: задан ненулевой idle_replication_slot_timeout_secs, restart_lsn валиден, inactive_since > 0 и слот не находится в состоянии «синхронизируется во время recovery»:
return (idle_replication_slot_timeout_secs != 0 &&
XLogRecPtrIsValid(s->data.restart_lsn) &&
s->inactive_since > 0 &&
!(RecoveryInProgress() && s->data.synced));Текст причины формирует ReportSlotInvalidation через switch по cause. Для потери WAL вычисляется превышение oldestLSN - restart_lsn и добавляется совет увеличить max_slot_wal_keep_size. Для конфликта с горизонтом xid текст сообщает о конфликте с xid horizon. Для неподходящего уровня WAL — о требовании wal_level >= logical либо о наличии логического слота при wal_level = replica. Для простоя дольше порога — о превышении idle_replication_slot_timeout и совете его увеличить.
Инвалидация по таймауту простоя
Инвалидация по idle_replication_slot_timeout выполняется в InvalidatePossiblyObsoleteSlot. Если среди возможных причин есть RS_INVAL_IDLE_TIMEOUT, текущее время now берётся один раз через GetCurrentTimestamp() до захвата спинлока — чтобы избежать накладных расходов системного вызова при удержании спинлока. Затем под спинлоком вызывается DetermineSlotInvalidationCause, которая для idle-таймаута проверяет CanInvalidateIdleSlot и, если слот подходит, сравнивает s->inactive_since с now через TimestampDifferenceExceedsSeconds; при превышении порога она записывает s->inactive_since в выходной параметр и возвращает RS_INVAL_IDLE_TIMEOUT.
После освобождения спинлока, если причина — RS_INVAL_IDLE_TIMEOUT, длительность простоя вычисляется через TimestampDifference(inactive_since, now, ...), то есть используется то же самое now, зафиксированное до захвата спинлока.
Если слот уже кем-то захвачен (active_proc != INVALID_PROC_NUMBER), процесс готовится к ожиданию на условной переменной слота, освобождает ReplicationSlotControlLock, при смене PID владельца сигнализирует ему (SIGTERM или SignalRecoveryConflict) и ждёт освобождения слота. После ожидания он снова захватывает ReplicationSlotControlLock в режиме LW_SHARED и через continue повторяет цикл. Повторная проверка допускает, что слот мог продвинуть restart_lsn или xmin и перестать конфликтовать — тогда инвалидация не нужна: ресурсы, о которых беспокоились (сегменты WAL или кортежи), ещё не удалены, и инвалидировать слот незачем.
Инвалидация при удалении WAL
InvalidateObsoleteReplicationSlots принимает маску возможных причин possible_causes и для каждого используемого слота вызывает InvalidatePossiblyObsoleteSlot. Для причины RS_INVAL_WAL_REMOVED слот инвалидируется, если его restart_lsn валиден и меньше oldestLSN, вычисленного из oldestSegno через XLogSegNoOffsetToRecPtr.
После успешной инвалидации слот помечается грязным и сохраняется, а функция запоминает invalidated и, для логических слотов, invalidated_logical. Если что-либо было инвалидировано, пересчитываются ресурсные лимиты вызовами ReplicationSlotsComputeRequiredXmin(false) и ReplicationSlotsComputeRequiredLSN(). Если был инвалидирован логический слот и не осталось ни одного валидного логического слота, вызывается RequestDisableLogicalDecoding().
Как считаются oldest xmin и oldest restart_lsn
ReplicationSlotsComputeRequiredXmin обходит все слоты (до max_replication_slots + max_repack_replication_slots), пропуская неиспользуемые и инвалидированные, и под спинлоком слота читает effective_xmin и effective_catalog_xmin. Для каждого валидного значения она берёт минимум по TransactionIdPrecedes, накапливая agg_xmin и agg_catalog_xmin, после чего передаёт их в ProcArraySetReplicationSlotXmin.
ReplicationSlotsComputeRequiredLSN аналогично обходит слоты, читает persistency, restart_lsn, invalidated и last_saved_restart_lsn, пропускает инвалидированные, и для каждого валидного restart_lsn берёт минимальный, после чего вызывает XLogSetReplicationSlotMinimumLSN(min_required).
Для persistent-слота (persistency == RS_PERSISTENT) при валидном last_saved_restart_lsn и restart_lsn > last_saved_restart_lsn значение restart_lsn заменяется на last_saved_restart_lsn. Причина: сегменты между last_saved_restart_lsn и restart_lsn могут понадобиться persistent-слоту при краше базы, тогда как non-persistent слоты краш не переживают, и для них last_saved_restart_lsn не важен.
ReplicationSlotsComputeLogicalRestartLSN делает то же самое, но только для логических слотов (SlotIsLogical) и возвращает минимальный restart_lsn, не сохраняя его в xlog.
Блокировка ReplicationSlotControlLock берётся в shared-режиме, чтобы уменьшить конкуренцию, и удерживается до обновления xmin-значений слотов. При already_locked предполагается, что ReplicationSlotControlLock и ProcArrayLock уже взяты эксклюзивно, причём порядок захвата — сначала ReplicationSlotControlLock, затем ProcArrayLock: «It is crucial that the caller first acquires the ReplicationSlotControlLock, followed by the ProcArrayLock, to prevent any undetectable deadlocks since this function acquires them in that order.»
Синхронизация слотов на standby
Slotsync worker перезапускается с интервалом SLOTSYNC_RESTART_INTERVAL_SEC, равным 10 секундам:
#define SLOTSYNC_RESTART_INTERVAL_SEC 10fetch_remote_slots получает список слотов с праймари, выполняя SQL-запрос к pg_catalog.pg_replication_slots с фильтром WHERE failover and NOT temporary, выбирая столбцы slot_name, plugin, confirmed_flush_lsn, restart_lsn, catalog_xmin, two_phase, two_phase_at, failover, database, invalidation_reason; если передан список имён, добавляется условие AND slot_name IN (...).
Результат обрабатывается построчно: для каждого кортежа создаётся RemoteSlot, поля заполняются через slot_getattr. NULL LSN и Xmin возможны, если слот инвалидирован на праймари, и обрабатываются так: confirmed_lsn и restart_lsn при isnull получают InvalidXLogRecPtr, catalog_xmin — InvalidTransactionId, two_phase_at — InvalidXLogRecPtr, а invalidated при isnull становится RS_INVAL_NONE.
Если restart_lsn, confirmed_lsn или catalog_xmin невалидны, но слот не инвалидирован (RS_INVAL_NONE), это означает состояние RS_EPHEMERAL, и такой слот не синхронизируется: он освобождается через pfree(remote_slot) и не добавляется в список; иначе слот добавляется в remote_slot_list через lappend.
Синхронизация отдельного слота
Перед синхронизацией synchronize_one_slot ищет слот по имени через SearchNamedReplicationSlot. Если слот найден, но не помечен как synced, это означает, что на standby уже существует пользовательский слот с тем же именем, и выдаётся ERROR. Затем слот захватывается через ReplicationSlotAcquire, и причина инвалидации копируется с удалённого сервера только если локальный слот ещё не инвалидирован (slot->data.invalidated == RS_INVAL_NONE && remote_slot->invalidated != RS_INVAL_NONE), после чего вызываются ReplicationSlotMarkDirty и ReplicationSlotSave.
Если локальный слот уже инвалидирован, синхронизация пропускается, после чего слот освобождается через ReplicationSlotRelease. Для временного слота (RS_TEMPORARY) вызывается update_and_persist_local_synced_slot, иначе — update_local_synced_slot с проверкой, что remote_slot->confirmed_lsn не меньше slot->data.confirmed_flush. Если слот не найден, он создаётся как временный через ReplicationSlotCreate с RS_TEMPORARY, но только если remote_slot->invalidated == RS_INVAL_NONE. Временные слоты выбираются вместо эфемерных, чтобы они переживали освобождение и не пересоздавались каждый цикл синхронизации, пока restart_lsn или catalog_xmin удалённого слота не догнали.
Синхронизация не поддерживается на каскадном standby: primary должен был бы ждать все каскадные standby, иначе логические подписчики могут опередить один из каскадных standby, который планируется к повышению. Поэтому при remote_in_recovery выдаётся ERROR с сообщением cannot synchronize replication slots from a standby server.
Что видит клиент в pg_replication_slots
Поле active показывает, стримится ли слот сейчас; active_pid — PID сессии, которая стримит данные (NULL, если слот неактивен); restart_lsn — LSN самого старого WAL, который ещё может понадобиться потребителю слота; confirmed_flush_lsn — LSN, до которого логический потребитель подтвердил получение данных; wal_status — доступность WAL-файлов слота; safe_wal_size — сколько байт можно записать в WAL, чтобы слот не перешёл в состояние lost; invalidation_reason — причина инвалидации (NULL, если слот не инвалидирован); synced — признак логического слота, синхронизированного с primary.
Отличить инвалидацию по таймауту простоя от потери WAL можно по полю invalidation_reason: при таймауте там будет idle_timeout («the slot has remained inactive longer than the configured idle_replication_slot_timeout duration»), при потере WAL — wal_removed («the required WAL has been removed»). В коде этим значениям соответствуют RS_INVAL_IDLE_TIMEOUT и RS_INVAL_WAL_REMOVED, а строка выводится через GetSlotInvalidationCauseName(cause). safe_wal_size равен NULL для потерянных слотов и при max_slot_wal_keep_size = -1, а wal_status может быть lost, если слот больше не пригоден.
Какие ошибки увидит клиент
При исчерпании всех слотов (когда свободного слота в массиве нет) клиент получает ошибку с кодом ERRCODE_CONFIGURATION_LIMIT_EXCEEDED и сообщением all replication slots are in use, а также подсказку освободить один слот или увеличить соответствующий параметр (max_replication_slots либо max_repack_replication_slots).
Для invalidated-слота известно, что такие слоты пропускаются при вычислении минимально требуемого LSN и логического restart LSN, а при захвате с error_if_invalid=true выдаётся ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE с деталью о причине инвалидации.
При попытке включить failover для временного слота клиент получает ERRCODE_FEATURE_NOT_SUPPORTED с сообщением cannot enable failover for a temporary replication slot, если только это не происходит в процессе синхронизации слотов.
Почему persistent-слот сначала создаётся как ephemeral
Постоянный слот изначально создаётся как эфемерный (RS_EPHEMERAL), чтобы при ошибке во время инициализации он автоматически удалялся при откате транзакции, а в конце помечается постоянным через ReplicationSlotPersist(). Это решает проблему краха в середине создания: если слот остался эфемерным, при восстановлении он не восстанавливается, а удаляется. Временные слоты (RS_TEMPORARY) создаются временными сразу, поскольку они всё равно удаляются при ошибке.
При освобождении эфемерного слота вызывается ReplicationSlotDropAcquired и запрашивается отключение логического декодирования. На диске создание идёт через временный каталог с суффиксом .tmp, который при крахе удаляется, а готовый слот переименовывается в постоянный путь.
Гонки и порядок блокировок
Ключевая гонка — между захватом слота и его инвалидацией. После того как слот сделан активным, нужно проверить инвалидацию, потому что иначе checkpointer может инвалидировать слот сразу после проверки: «We need to check for invalidation after making the slot ours to avoid the possible race condition with the checkpointer that can otherwise invalidate the slot immediately after the check.» Проверка выполняется уже после присвоения MyReplicationSlot, и при error_if_invalid и s->data.invalidated != RS_INVAL_NONE выбрасывается ошибка.
При освобождении слота ReplicationSlotRelease для эфемерного слота вызывает ReplicationSlotDropAcquired, а для остальных под спинлоком slot->mutex сбрасывает active_proc и время неактивности, затем будит ожидающих через ConditionVariableBroadcast. При удалении ReplicationSlotDropPtr берёт ReplicationSlotAllocationLock в режиме LW_EXCLUSIVE, переименовывает каталог слота, а затем под ReplicationSlotControlLock в режиме LW_EXCLUSIVE очищает active_proc и in_use, после чего будит ожидающих.
Порядок блокировок зафиксирован: вызывающий должен сначала взять ReplicationSlotControlLock, а затем ProcArrayLock, чтобы избежать незаметных взаимоблокировок.
Что из этого следует на практике
- Рестарт неизбежен.
max_replication_slots— длина массива, выделяемого один раз при старте. Менять значение можно только с перезапуском, и планировать его нужно заранее. - Понижать ниже числа слотов на диске нельзя. Сервер откажется стартовать с
FATAL: too many replication slots active before shutdown, где «active» означает «существует». То же правило применяетpg_upgradeс 17-й версии для логических слотов. - На издателе закладывайте запас под синхронизацию. Одна подписка может занимать до трёх записей при
max_sync_workers_per_subscription = 2, пока идёт начальное копирование. Нехватка слотов не даёт явной ошибки подписки, а приводит к бесконечному циклу перезапуска tablesync worker'ов с таблицами в состоянииsrsubstate = 'd'. - Слот, инвалидированный по таймауту, отличается от потерявшего WAL. Смотрите
invalidation_reason:idle_timeoutпротивwal_removed. Это разные причины и разные способы устранения. - Persistent-слот удерживает WAL по
last_saved_restart_lsn, а не по текущемуrestart_lsn.