Individual-task/validate/PartitionsStorageSets_explained.md

32 KiB
Raw Blame History

Разбор PartitionsStorageSets.tla

Файл PartitionsStorageSets.tla описывает упрощенную TLA+ модель для проверки логики партиций, которые хранят множество ключей в виде полуоткрытых интервалов [lo; hi).

Модель намеренно не проверяет AVL и QSBR. Мы считаем, что:

  • AVL корректно находит нужную партицию;
  • QSBR корректно защищает lifetime нод;
  • переменная tree абстрактно означает "те partition id, которые сейчас достижимы из дерева".

Главная цель модели: проверить, что операции split/shrink не создают дыр, пересечений и потерянных ключей.

Заголовок модуля

---- MODULE PartitionsStorageSets ----
EXTENDS Naturals, FiniteSets

MODULE объявляет имя TLA+ модуля.

EXTENDS Naturals, FiniteSets подключает стандартные модули:

  • Naturals - натуральные числа, диапазоны 0..N, арифметика;
  • FiniteSets - операции над конечными множествами, например Cardinality.

Комментарий про абстракцию

(*
This model validates only the partition/set logic.
AVL tree lookup and QSBR are abstracted away as the variable `tree`,
which contains ids of currently reachable live partitions.
*)

Это важное ограничение модели.

Мы не моделируем:

  • форму AVL-дерева;
  • балансировку;
  • указатели;
  • QSBR epochs;
  • освобождение памяти;
  • lock-free детали дерева.

Вместо этого считаем, что tree - это множество id живых партиций, которые доступны через дерево.

Константы

CONSTANTS
  PartitionIds,
  Threads,
  NoThread,
  KeyCount,
  MinPartitionSize,
  MaxPartitionSize,
  MaxLoad

Константы задаются в .cfg файле.

PartitionIds

PartitionIds

Множество всех id партиций, которые модель может использовать.

Пример из PartitionsStorageSetsSmoke.cfg:

PartitionIds = {p0, p1, p2}

Важно: PartitionIds - это не только живые партиции. Это пул всех возможных id. Часть из них может быть DEAD.

Threads

Threads

Множество потоков, которые могут брать locks и выполнять операции.

Пример:

Threads = {t0}

Для более интересных конкурентных сценариев можно поставить:

Threads = {t0, t1}

NoThread

NoThread

Специальное значение, означающее "mutex никем не захвачен".

Пример:

NoThread = NoThread

Это выглядит странно, но в TLA+ справа NoThread в .cfg задает символическое значение.

KeyCount

KeyCount

Количество ключей в модели.

Если KeyCount = 3, то ключи:

0, 1, 2

В модели полный диапазон ключей задается как [0; KeyCount).

MinPartitionSize

MinPartitionSize

Нижний порог размера партиции.

Если load < MinPartitionSize и партиций больше одной, партиция считается NEED_SHRINK.

MaxPartitionSize

MaxPartitionSize

Верхний нормальный порог размера партиции.

Если load > MaxPartitionSize, партиция считается NEED_SPLIT.

MaxLoad

MaxLoad

Технический максимум для поля load, чтобы пространство состояний было конечным.

TLC должен перебрать конечное число состояний. Поэтому нельзя разрешить load быть любым натуральным числом.

Предположения о константах

ASSUME NoThread \notin Threads
ASSUME KeyCount > 0
ASSUME MinPartitionSize > 0
ASSUME MaxPartitionSize >= MinPartitionSize
ASSUME MaxLoad > MaxPartitionSize
ASSUME Cardinality(PartitionIds) >= 3

ASSUME задает требования к конфигурации.

NoThread \notin Threads

ASSUME NoThread \notin Threads

Специальное значение NoThread не должно совпадать с реальным потоком.

Иначе было бы невозможно отличить:

  • lock свободен;
  • lock захвачен потоком с id NoThread.

KeyCount > 0

ASSUME KeyCount > 0

В модели должен быть хотя бы один ключ.

MinPartitionSize > 0

ASSUME MinPartitionSize > 0

Минимальный размер должен быть положительным.

MaxPartitionSize >= MinPartitionSize

ASSUME MaxPartitionSize >= MinPartitionSize

Верхний порог не должен быть меньше нижнего.

MaxLoad > MaxPartitionSize

ASSUME MaxLoad > MaxPartitionSize

Нужно, чтобы в модели вообще могли появляться состояния NEED_SPLIT.

Если MaxLoad <= MaxPartitionSize, то load > MaxPartitionSize был бы невозможен.

Cardinality(PartitionIds) >= 3

ASSUME Cardinality(PartitionIds) >= 3

Нужно минимум 3 id:

  • один начальный живой partition;
  • два новых partition для split.

Переменные состояния

VARIABLES
  partitions,
  tree,
  locks

Это состояние модели.

В TLA+ каждое состояние - это конкретные значения всех переменных из VARIABLES.

partitions

partitions хранит данные всех partition id.

Тип:

partitions \in [PartitionIds -> PartitionRecord]

То есть это функция:

partition id -> record партиции

tree

tree - множество partition id, которые сейчас доступны через дерево.

Важно:

p \in tree

означает, что партиция живая и достижима.

p \notin tree

означает, что партиция недоступна из дерева. В этой модели такие партиции должны быть dead = TRUE.

locks

locks хранит владельца mutex для каждой партиции.

Тип:

locks \in [PartitionIds -> (Threads \cup {NoThread})]

То есть:

  • locks[p] = NoThread - lock свободен;
  • locks[p] = t - lock захвачен потоком t.

Список всех переменных

vars == <<partitions, tree, locks>>

vars нужен для temporal specification:

Spec == Init /\ [][Next]_vars

Запись [Next]_vars означает:

либо происходит действие Next,
либо состояние не меняется по переменным vars

Это стандартный способ записывать спецификацию в TLA+.

Множество ключей

Keys == 0..(KeyCount - 1)

Если KeyCount = 8, то:

Keys = 0..7

То есть полный диапазон ключей:

[0; KeyCount)

Тип записи партиции

PartitionRecord ==
  [ lo   : 0..KeyCount,
    hi   : 0..KeyCount,
    dead : BOOLEAN,
    load : 0..MaxLoad ]

Каждая партиция - это record с полями:

  • lo - левая граница интервала;
  • hi - правая граница интервала;
  • dead - является ли партиция мертвой;
  • load - абстрактный размер множества внутри партиции.

Интервал полуоткрытый:

[lo; hi)

То есть ключ k принадлежит партиции, если:

lo <= k < hi

Почему lo и hi имеют тип 0..KeyCount, а не Keys:

  • ключи идут от 0 до KeyCount - 1;
  • правая граница последнего интервала равна KeyCount;
  • поэтому hi = KeyCount должно быть допустимо.

Проверка принадлежности ключа партиции

KeyInPartition(k, p) ==
  /\ partitions[p].lo <= k
  /\ k < partitions[p].hi

KeyInPartition(k, p) истинно, если ключ k лежит внутри интервала партиции p.

Пример:

partition = [lo |-> 2, hi |-> 5]

Тогда партиция содержит ключи:

2, 3, 4

И не содержит 5.

Вычисляемый статус партиции

Status(p) ==
  IF partitions[p].dead THEN "DEAD"
  ELSE IF partitions[p].load > MaxPartitionSize THEN "NEED_SPLIT"
  ELSE IF partitions[p].load < MinPartitionSize /\ Cardinality(tree) > 1 THEN "NEED_SHRINK"
  ELSE "ALIVE"

Статус не хранится отдельным полем. Он вычисляется из текущего состояния.

Это полезно, потому что нельзя получить противоречивое состояние вида:

status = "ALIVE", но load > MaxPartitionSize

DEAD

IF partitions[p].dead THEN "DEAD"

Если dead = TRUE, партиция считается мертвой независимо от load.

NEED_SPLIT

ELSE IF partitions[p].load > MaxPartitionSize THEN "NEED_SPLIT"

Если партиция живая и ее нагрузка больше верхнего порога, ее надо split.

NEED_SHRINK

ELSE IF partitions[p].load < MinPartitionSize /\ Cardinality(tree) > 1 THEN "NEED_SHRINK"

Если партиция живая, маленькая, и в дереве больше одной партиции, ее можно shrink.

Условие Cardinality(tree) > 1 нужно потому, что последнюю оставшуюся партицию нельзя удалить.

ALIVE

ELSE "ALIVE"

Все остальные живые партиции считаются нормальными.

Начальное состояние Init

Init ==
  \E p \in PartitionIds:
    /\ tree = {p}
    /\ partitions =
      [ q \in PartitionIds |->
          IF q = p THEN
            [ lo |-> 0,
              hi |-> KeyCount,
              dead |-> FALSE,
              load |-> MinPartitionSize ]
          ELSE
            [ lo |-> 0,
              hi |-> 0,
              dead |-> TRUE,
              load |-> 0 ] ]
    /\ locks = [q \in PartitionIds |-> NoThread]

Init описывает все допустимые начальные состояния.

Здесь используется существование:

\E p \in PartitionIds:

Это значит: TLC может выбрать любой p из PartitionIds как начальную живую партицию.

Начальное дерево

tree = {p}

В начале есть ровно одна живая партиция.

Она покрывает весь диапазон ключей.

Начальное значение partitions

partitions =
  [ q \in PartitionIds |->
      IF q = p THEN
        ...
      ELSE
        ... ]

Это функция по всем partition id.

Для выбранного p создается живая партиция:

[ lo |-> 0,
  hi |-> KeyCount,
  dead |-> FALSE,
  load |-> MinPartitionSize ]

Она покрывает весь диапазон:

[0; KeyCount)

Для всех остальных q создаются мертвые пустые партиции:

[ lo |-> 0,
  hi |-> 0,
  dead |-> TRUE,
  load |-> 0 ]

Начальные locks

locks = [q \in PartitionIds |-> NoThread]

Все mutex свободны.

TypeInvariant

TypeInvariant ==
  /\ partitions \in [PartitionIds -> PartitionRecord]
  /\ tree \subseteq PartitionIds
  /\ locks \in [PartitionIds -> (Threads \cup {NoThread})]
  /\ \A p \in tree:
      /\ partitions[p].dead = FALSE
      /\ partitions[p].lo < partitions[p].hi
      /\ partitions[p].hi <= KeyCount
  /\ \A p \in PartitionIds \ tree:
      partitions[p].dead = TRUE

TypeInvariant проверяет базовую корректность формы состояния.

Это не главный алгоритмический инвариант, но он почти всегда нужен в TLA+ моделях.

Тип partitions

partitions \in [PartitionIds -> PartitionRecord]

Для каждого partition id есть корректная запись с полями:

  • lo;
  • hi;
  • dead;
  • load.

Тип tree

tree \subseteq PartitionIds

Дерево не может ссылаться на неизвестный partition id.

Тип locks

locks \in [PartitionIds -> (Threads \cup {NoThread})]

Для каждой партиции lock либо свободен, либо принадлежит одному из потоков.

Все партиции в tree живые и непустые

\A p \in tree:
  /\ partitions[p].dead = FALSE
  /\ partitions[p].lo < partitions[p].hi
  /\ partitions[p].hi <= KeyCount

Если партиция достижима из дерева:

  • она не dead;
  • ее интервал непустой;
  • правая граница не выходит за общий диапазон.

Все партиции вне tree мертвые

\A p \in PartitionIds \ tree:
  partitions[p].dead = TRUE

Если partition id не достижим из дерева, он должен быть dead.

Это сильное упрощение модели. В реальной системе из-за QSBR может существовать старая нода, которую еще кто-то читает. Но для текущей модели мы проверяем только логическую структуру живых интервалов.

Инвариант уникального владельца ключа

UniqueOwnerForEveryKey ==
  \A k \in Keys:
    \E p \in tree:
      /\ KeyInPartition(k, p)
      /\ \A q \in tree:
          KeyInPartition(k, q) => q = p

Это главный инвариант модели.

Он говорит:

для каждого ключа существует ровно одна живая партиция, которая содержит этот ключ

Разберем по частям.

\A k \in Keys:

Для каждого ключа.

\E p \in tree:

Существует живая партиция p.

KeyInPartition(k, p)

Эта партиция содержит ключ.

\A q \in tree:
  KeyInPartition(k, q) => q = p

Любая другая живая партиция q, которая содержит этот ключ, на самом деле является той же самой партицией p.

Иначе говоря:

  • нет дыр;
  • нет пересечений;
  • каждый ключ покрыт ровно одним интервалом.

Если этот инвариант падает, значит split или shrink испортил покрытие диапазона.

Инвариант DeadPartitionsAreOutsideTree

DeadPartitionsAreOutsideTree ==
  \A p \in PartitionIds:
    partitions[p].dead => p \notin tree

Если партиция мертвая, она не должна быть достижима из дерева.

Этот инвариант частично пересекается с TypeInvariant, но он более явно выражает важное свойство структуры данных.

Действие Lock

Lock(t, p) ==
  /\ p \in tree
  /\ locks[p] = NoThread
  /\ locks' = [locks EXCEPT ![p] = t]
  /\ UNCHANGED <<partitions, tree>>

Lock(t, p) моделирует взятие mutex партиции p потоком t.

Предусловия:

p \in tree

Можно брать lock только у живой достижимой партиции.

locks[p] = NoThread

Lock должен быть свободен.

Изменение:

locks' = [locks EXCEPT ![p] = t]

В следующем состоянии владелец lock для p - поток t.

Остальное не меняется:

UNCHANGED <<partitions, tree>>

Действие Unlock

Unlock(t, p) ==
  /\ locks[p] = t
  /\ locks' = [locks EXCEPT ![p] = NoThread]
  /\ UNCHANGED <<partitions, tree>>

Unlock(t, p) моделирует освобождение mutex.

Предусловие:

locks[p] = t

Только владелец lock может его освободить.

Изменение:

locks' = [locks EXCEPT ![p] = NoThread]

В следующем состоянии lock свободен.

Действие GrowPartition

\* Abstract user mutation of the set stored inside a locked partition.
GrowPartition(t, p) ==
  /\ p \in tree
  /\ locks[p] = t
  /\ partitions[p].load < MaxLoad
  /\ partitions' = [partitions EXCEPT ![p].load = @ + 1]
  /\ UNCHANGED <<tree, locks>>

Это абстрактное действие, которое увеличивает размер множества внутри партиции.

Оно нужно, чтобы в модели могла появиться партиция со статусом NEED_SPLIT.

Предусловия:

p \in tree

Партиция живая.

locks[p] = t

Поток держит mutex этой партиции.

partitions[p].load < MaxLoad

Нельзя выйти за техническую верхнюю границу MaxLoad.

Изменение:

partitions' = [partitions EXCEPT ![p].load = @ + 1]

@ означает старое значение изменяемого поля.

То есть:

new load = old load + 1

Действие ShrinkPartitionLoad

ShrinkPartitionLoad(t, p) ==
  /\ p \in tree
  /\ locks[p] = t
  /\ partitions[p].load > 0
  /\ partitions' = [partitions EXCEPT ![p].load = @ - 1]
  /\ UNCHANGED <<tree, locks>>

Это абстрактное действие, которое уменьшает размер множества внутри партиции.

Оно нужно, чтобы в модели могла появиться партиция со статусом NEED_SHRINK.

Предусловия:

  • партиция живая;
  • поток держит ее lock;
  • load > 0.

Изменение:

new load = old load - 1

tree и locks не меняются.

Действие SplitPartition

SplitPartition(t, p) ==
  /\ p \in tree
  /\ locks[p] = t
  /\ Status(p) = "NEED_SPLIT"
  /\ partitions[p].lo + 1 < partitions[p].hi
  /\ \E left \in PartitionIds \ tree:
      \E right \in (PartitionIds \ tree) \ {left}:
        \E splitKey \in (partitions[p].lo + 1)..(partitions[p].hi - 1):
          \E leftLoad \in 0..partitions[p].load:
            LET rightLoad == partitions[p].load - leftLoad IN
            /\ rightLoad \in 0..MaxLoad
            /\ leftLoad \in 0..MaxLoad
            /\ partitions' =
              [ partitions EXCEPT
                  ![p].dead = TRUE,
                  ![left] =
                    [ lo |-> partitions[p].lo,
                      hi |-> splitKey,
                      dead |-> FALSE,
                      load |-> leftLoad ],
                  ![right] =
                    [ lo |-> splitKey,
                      hi |-> partitions[p].hi,
                      dead |-> FALSE,
                      load |-> rightLoad ] ]
            /\ tree' = (tree \ {p}) \cup {left, right}
            /\ locks' =
              [ locks EXCEPT
                  ![p] = NoThread,
                  ![left] = NoThread,
                  ![right] = NoThread ]

SplitPartition моделирует разбиение одной большой партиции на две.

Предусловия split

p \in tree

Разбиваем только живую партицию.

locks[p] = t

Поток держит lock этой партиции.

Status(p) = "NEED_SPLIT"

Разбиение разрешено только если партиция слишком большая.

partitions[p].lo + 1 < partitions[p].hi

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

Например, [0; 2) можно разделить на [0; 1) и [1; 2).

А [0; 1) разделить нельзя.

Выбор новых partition id

\E left \in PartitionIds \ tree:
  \E right \in (PartitionIds \ tree) \ {left}:

TLC недетерминированно выбирает два свободных id:

  • left;
  • right.

Они должны быть вне текущего tree.

Выбор точки split

\E splitKey \in (partitions[p].lo + 1)..(partitions[p].hi - 1):

splitKey выбирается строго внутри интервала.

Старый интервал:

[lo; hi)

Новые интервалы:

[lo; splitKey)
[splitKey; hi)

Распределение load

\E leftLoad \in 0..partitions[p].load:
  LET rightLoad == partitions[p].load - leftLoad IN

Модель не знает, как реальные элементы распределяются между половинами.

Поэтому она разрешает любое распределение:

leftLoad + rightLoad = oldLoad

Это полезная абстракция: TLC проверяет больше вариантов, включая неудобные.

Изменение partitions

partitions' =
  [ partitions EXCEPT
      ![p].dead = TRUE,
      ![left] =
        [ lo |-> partitions[p].lo,
          hi |-> splitKey,
          dead |-> FALSE,
          load |-> leftLoad ],
      ![right] =
        [ lo |-> splitKey,
          hi |-> partitions[p].hi,
          dead |-> FALSE,
          load |-> rightLoad ] ]

В следующем состоянии:

  • старая партиция p становится dead;
  • left становится живой партицией [old.lo; splitKey);
  • right становится живой партицией [splitKey; old.hi).

Изменение tree

tree' = (tree \ {p}) \cup {left, right}

В дереве:

  • удаляем старую партицию p;
  • добавляем две новые.

Изменение locks

locks' =
  [ locks EXCEPT
      ![p] = NoThread,
      ![left] = NoThread,
      ![right] = NoThread ]

После split locks освобождены.

Это упрощение модели. Если в C++ реализации get возвращает пользователю уже locked partition, возможно позднее стоит уточнить это поведение.

Действие ShrinkToPrev

ShrinkToPrev(t, p) ==
  /\ p \in tree
  /\ locks[p] = t
  /\ Status(p) = "NEED_SHRINK"
  /\ \E prev \in tree \ {p}:
      /\ locks[prev] = t
      /\ partitions[prev].hi = partitions[p].lo
      /\ partitions[prev].load + partitions[p].load <= MaxLoad
      /\ partitions' =
        [ partitions EXCEPT
            ![prev].hi = partitions[p].hi,
            ![prev].load = partitions[prev].load + partitions[p].load,
            ![p].dead = TRUE ]
      /\ tree' = tree \ {p}
      /\ locks' = [locks EXCEPT ![p] = NoThread, ![prev] = NoThread]

ShrinkToPrev моделирует слияние маленькой партиции p с предыдущей соседней партицией.

Предусловия

p \in tree

Удалять можно только живую партицию.

locks[p] = t

Поток держит lock удаляемой партиции.

Status(p) = "NEED_SHRINK"

Партиция маленькая и есть больше одной живой партиции.

Выбор предыдущей партиции

\E prev \in tree \ {p}:

TLC выбирает другую живую партицию prev.

locks[prev] = t

Тот же поток должен держать lock соседа.

partitions[prev].hi = partitions[p].lo

prev должна быть настоящим левым соседом.

Например:

prev = [0; 3)
p    = [3; 5)

Ограничение load

partitions[prev].load + partitions[p].load <= MaxLoad

Нужно, чтобы после слияния load не вышел за технический максимум модели.

Изменение partitions

partitions' =
  [ partitions EXCEPT
      ![prev].hi = partitions[p].hi,
      ![prev].load = partitions[prev].load + partitions[p].load,
      ![p].dead = TRUE ]

Сосед prev расширяется вправо:

prev: [prev.lo; prev.hi)
p:    [p.lo; p.hi)

становится:

prev: [prev.lo; p.hi)

Партиция p становится мертвой.

Изменение tree

tree' = tree \ {p}

Удаляем p из дерева.

Изменение locks

locks' = [locks EXCEPT ![p] = NoThread, ![prev] = NoThread]

После shrink locks освобождаются.

Действие ShrinkToNext

ShrinkToNext(t, p) ==
  /\ p \in tree
  /\ locks[p] = t
  /\ Status(p) = "NEED_SHRINK"
  /\ \E next \in tree \ {p}:
      /\ locks[next] = t
      /\ partitions[p].hi = partitions[next].lo
      /\ partitions[next].load + partitions[p].load <= MaxLoad
      /\ partitions' =
        [ partitions EXCEPT
            ![next].lo = partitions[p].lo,
            ![next].load = partitions[next].load + partitions[p].load,
            ![p].dead = TRUE ]
      /\ tree' = tree \ {p}
      /\ locks' = [locks EXCEPT ![p] = NoThread, ![next] = NoThread]

ShrinkToNext моделирует слияние маленькой партиции p со следующей соседней партицией.

Это симметрично ShrinkToPrev.

Выбор следующей партиции

\E next \in tree \ {p}:

TLC выбирает другую живую партицию next.

partitions[p].hi = partitions[next].lo

next должна быть настоящим правым соседом.

Например:

p    = [3; 5)
next = [5; 8)

Изменение интервала

![next].lo = partitions[p].lo

Правая партиция расширяется влево:

p:    [p.lo; p.hi)
next: [next.lo; next.hi)

становится:

next: [p.lo; next.hi)

Партиция p становится мертвой и удаляется из tree.

Действие Next

Next ==
  \/ \E t \in Threads, p \in PartitionIds: Lock(t, p)
  \/ \E t \in Threads, p \in PartitionIds: Unlock(t, p)
  \/ \E t \in Threads, p \in PartitionIds: GrowPartition(t, p)
  \/ \E t \in Threads, p \in PartitionIds: ShrinkPartitionLoad(t, p)
  \/ \E t \in Threads, p \in PartitionIds: SplitPartition(t, p)
  \/ \E t \in Threads, p \in PartitionIds: ShrinkToPrev(t, p)
  \/ \E t \in Threads, p \in PartitionIds: ShrinkToNext(t, p)

Next описывает один шаг системы.

На каждом шаге TLC выбирает одно из действий:

  • взять lock;
  • отпустить lock;
  • увеличить load;
  • уменьшить load;
  • split;
  • shrink в предыдущую партицию;
  • shrink в следующую партицию.

Запись:

\/

означает логическое "или".

Запись:

\E t \in Threads, p \in PartitionIds:

означает, что TLC перебирает все варианты потока t и партиции p.

Если предусловия действия не выполняются, этот вариант просто невозможен.

Спецификация Spec

Spec == Init /\ [][Next]_vars

Spec - это полное описание поведения системы.

Оно говорит:

  1. Первое состояние должно удовлетворять Init.
  2. Каждый следующий шаг должен удовлетворять [Next]_vars.

[] означает "всегда".

[Next]_vars означает:

  • либо произошел один из переходов Next;
  • либо состояние осталось тем же по переменным vars.

Конец модуля

====

Так заканчивается TLA+ модуль.

Как читать counterexample

Если TLC найдет ошибку, он покажет последовательность состояний:

State 1
State 2
State 3
...

Для этой модели нужно смотреть прежде всего:

  • tree;
  • partitions;
  • locks;
  • какое действие было между состояниями.

Если нарушился UniqueOwnerForEveryKey, значит произошло одно из двух:

  • появился ключ, который не принадлежит ни одной живой партиции;
  • появился ключ, который принадлежит двум живым партициям.

Практически это означает ошибку в логике split/shrink или слишком слабое предусловие действия.

Как запускать

Быстрый smoke-test:

java -jar /home/hhhh/bin/toolbox/tla2tools.jar \
  -config validate/PartitionsStorageSetsSmoke.cfg \
  validate/PartitionsStorageSets.tla

Более тяжелая проверка:

java -jar /home/hhhh/bin/toolbox/tla2tools.jar \
  -config validate/PartitionsStorageSets.cfg \
  validate/PartitionsStorageSets.tla

Smoke-конфиг нужен для быстрой проверки синтаксиса и базовых инвариантов.

Полный конфиг перебирает гораздо больше состояний и может работать долго.

Что модель пока не проверяет

Текущая модель не проверяет:

  • корректность AVL rotations;
  • корректность lock-free публикации новых узлов;
  • ABA-проблемы;
  • QSBR epochs;
  • memory reclamation;
  • C++ memory ordering;
  • порядок взятия нескольких locks;
  • deadlock-freedom;
  • liveness.

Это осознанно. Сейчас модель проверяет только базовую безопасность интервалов:

каждый ключ всегда принадлежит ровно одной живой партиции

Следующий логичный шаг - уточнить shrink, добавив состояние операции по шагам:

  1. отпустили lock своей партиции;
  2. нашли prev;
  3. взяли prev lock;
  4. снова взяли свой lock;
  5. нашли next;
  6. взяли next lock;
  7. проверили DEAD;
  8. удалили текущую партицию.

Тогда модель начнет проверять больше конкурентных interleavings.