Individual-task/validate/PartitionsStorageSets_explained.md

1320 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Разбор `PartitionsStorageSets.tla`
Файл `PartitionsStorageSets.tla` описывает упрощенную TLA+ модель для проверки логики партиций, которые хранят множество ключей в виде полуоткрытых интервалов `[lo; hi)`.
Модель намеренно не проверяет AVL и QSBR. Мы считаем, что:
- AVL корректно находит нужную партицию;
- QSBR корректно защищает lifetime нод;
- переменная `tree` абстрактно означает "те partition id, которые сейчас достижимы из дерева".
Главная цель модели: проверить, что операции split/shrink не создают дыр, пересечений и потерянных ключей.
## Заголовок модуля
```tla
---- MODULE PartitionsStorageSets ----
EXTENDS Naturals, FiniteSets
```
`MODULE` объявляет имя TLA+ модуля.
`EXTENDS Naturals, FiniteSets` подключает стандартные модули:
- `Naturals` - натуральные числа, диапазоны `0..N`, арифметика;
- `FiniteSets` - операции над конечными множествами, например `Cardinality`.
## Комментарий про абстракцию
```tla
(*
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 живых партиций, которые доступны через дерево.
## Константы
```tla
CONSTANTS
PartitionIds,
Threads,
NoThread,
KeyCount,
MinPartitionSize,
MaxPartitionSize,
MaxLoad
```
Константы задаются в `.cfg` файле.
### `PartitionIds`
```tla
PartitionIds
```
Множество всех id партиций, которые модель может использовать.
Пример из `PartitionsStorageSetsSmoke.cfg`:
```tla
PartitionIds = {p0, p1, p2}
```
Важно: `PartitionIds` - это не только живые партиции. Это пул всех возможных id. Часть из них может быть `DEAD`.
### `Threads`
```tla
Threads
```
Множество потоков, которые могут брать locks и выполнять операции.
Пример:
```tla
Threads = {t0}
```
Для более интересных конкурентных сценариев можно поставить:
```tla
Threads = {t0, t1}
```
### `NoThread`
```tla
NoThread
```
Специальное значение, означающее "mutex никем не захвачен".
Пример:
```tla
NoThread = NoThread
```
Это выглядит странно, но в TLA+ справа `NoThread` в `.cfg` задает символическое значение.
### `KeyCount`
```tla
KeyCount
```
Количество ключей в модели.
Если `KeyCount = 3`, то ключи:
```tla
0, 1, 2
```
В модели полный диапазон ключей задается как `[0; KeyCount)`.
### `MinPartitionSize`
```tla
MinPartitionSize
```
Нижний порог размера партиции.
Если `load < MinPartitionSize` и партиций больше одной, партиция считается `NEED_SHRINK`.
### `MaxPartitionSize`
```tla
MaxPartitionSize
```
Верхний нормальный порог размера партиции.
Если `load > MaxPartitionSize`, партиция считается `NEED_SPLIT`.
### `MaxLoad`
```tla
MaxLoad
```
Технический максимум для поля `load`, чтобы пространство состояний было конечным.
TLC должен перебрать конечное число состояний. Поэтому нельзя разрешить `load` быть любым натуральным числом.
## Предположения о константах
```tla
ASSUME NoThread \notin Threads
ASSUME KeyCount > 0
ASSUME MinPartitionSize > 0
ASSUME MaxPartitionSize >= MinPartitionSize
ASSUME MaxLoad > MaxPartitionSize
ASSUME Cardinality(PartitionIds) >= 3
```
`ASSUME` задает требования к конфигурации.
### `NoThread \notin Threads`
```tla
ASSUME NoThread \notin Threads
```
Специальное значение `NoThread` не должно совпадать с реальным потоком.
Иначе было бы невозможно отличить:
- lock свободен;
- lock захвачен потоком с id `NoThread`.
### `KeyCount > 0`
```tla
ASSUME KeyCount > 0
```
В модели должен быть хотя бы один ключ.
### `MinPartitionSize > 0`
```tla
ASSUME MinPartitionSize > 0
```
Минимальный размер должен быть положительным.
### `MaxPartitionSize >= MinPartitionSize`
```tla
ASSUME MaxPartitionSize >= MinPartitionSize
```
Верхний порог не должен быть меньше нижнего.
### `MaxLoad > MaxPartitionSize`
```tla
ASSUME MaxLoad > MaxPartitionSize
```
Нужно, чтобы в модели вообще могли появляться состояния `NEED_SPLIT`.
Если `MaxLoad <= MaxPartitionSize`, то `load > MaxPartitionSize` был бы невозможен.
### `Cardinality(PartitionIds) >= 3`
```tla
ASSUME Cardinality(PartitionIds) >= 3
```
Нужно минимум 3 id:
- один начальный живой partition;
- два новых partition для split.
## Переменные состояния
```tla
VARIABLES
partitions,
tree,
locks
```
Это состояние модели.
В TLA+ каждое состояние - это конкретные значения всех переменных из `VARIABLES`.
### `partitions`
`partitions` хранит данные всех partition id.
Тип:
```tla
partitions \in [PartitionIds -> PartitionRecord]
```
То есть это функция:
```text
partition id -> record партиции
```
### `tree`
`tree` - множество partition id, которые сейчас доступны через дерево.
Важно:
```text
p \in tree
```
означает, что партиция живая и достижима.
```text
p \notin tree
```
означает, что партиция недоступна из дерева. В этой модели такие партиции должны быть `dead = TRUE`.
### `locks`
`locks` хранит владельца mutex для каждой партиции.
Тип:
```tla
locks \in [PartitionIds -> (Threads \cup {NoThread})]
```
То есть:
- `locks[p] = NoThread` - lock свободен;
- `locks[p] = t` - lock захвачен потоком `t`.
## Список всех переменных
```tla
vars == <<partitions, tree, locks>>
```
`vars` нужен для temporal specification:
```tla
Spec == Init /\ [][Next]_vars
```
Запись `[Next]_vars` означает:
```text
либо происходит действие Next,
либо состояние не меняется по переменным vars
```
Это стандартный способ записывать спецификацию в TLA+.
## Множество ключей
```tla
Keys == 0..(KeyCount - 1)
```
Если `KeyCount = 8`, то:
```tla
Keys = 0..7
```
То есть полный диапазон ключей:
```text
[0; KeyCount)
```
## Тип записи партиции
```tla
PartitionRecord ==
[ lo : 0..KeyCount,
hi : 0..KeyCount,
dead : BOOLEAN,
load : 0..MaxLoad ]
```
Каждая партиция - это record с полями:
- `lo` - левая граница интервала;
- `hi` - правая граница интервала;
- `dead` - является ли партиция мертвой;
- `load` - абстрактный размер множества внутри партиции.
Интервал полуоткрытый:
```text
[lo; hi)
```
То есть ключ `k` принадлежит партиции, если:
```text
lo <= k < hi
```
Почему `lo` и `hi` имеют тип `0..KeyCount`, а не `Keys`:
- ключи идут от `0` до `KeyCount - 1`;
- правая граница последнего интервала равна `KeyCount`;
- поэтому `hi = KeyCount` должно быть допустимо.
## Проверка принадлежности ключа партиции
```tla
KeyInPartition(k, p) ==
/\ partitions[p].lo <= k
/\ k < partitions[p].hi
```
`KeyInPartition(k, p)` истинно, если ключ `k` лежит внутри интервала партиции `p`.
Пример:
```text
partition = [lo |-> 2, hi |-> 5]
```
Тогда партиция содержит ключи:
```text
2, 3, 4
```
И не содержит `5`.
## Вычисляемый статус партиции
```tla
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"
```
Статус не хранится отдельным полем. Он вычисляется из текущего состояния.
Это полезно, потому что нельзя получить противоречивое состояние вида:
```text
status = "ALIVE", но load > MaxPartitionSize
```
### `DEAD`
```tla
IF partitions[p].dead THEN "DEAD"
```
Если `dead = TRUE`, партиция считается мертвой независимо от `load`.
### `NEED_SPLIT`
```tla
ELSE IF partitions[p].load > MaxPartitionSize THEN "NEED_SPLIT"
```
Если партиция живая и ее нагрузка больше верхнего порога, ее надо split.
### `NEED_SHRINK`
```tla
ELSE IF partitions[p].load < MinPartitionSize /\ Cardinality(tree) > 1 THEN "NEED_SHRINK"
```
Если партиция живая, маленькая, и в дереве больше одной партиции, ее можно shrink.
Условие `Cardinality(tree) > 1` нужно потому, что последнюю оставшуюся партицию нельзя удалить.
### `ALIVE`
```tla
ELSE "ALIVE"
```
Все остальные живые партиции считаются нормальными.
## Начальное состояние `Init`
```tla
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` описывает все допустимые начальные состояния.
Здесь используется существование:
```tla
\E p \in PartitionIds:
```
Это значит: TLC может выбрать любой `p` из `PartitionIds` как начальную живую партицию.
### Начальное дерево
```tla
tree = {p}
```
В начале есть ровно одна живая партиция.
Она покрывает весь диапазон ключей.
### Начальное значение `partitions`
```tla
partitions =
[ q \in PartitionIds |->
IF q = p THEN
...
ELSE
... ]
```
Это функция по всем partition id.
Для выбранного `p` создается живая партиция:
```tla
[ lo |-> 0,
hi |-> KeyCount,
dead |-> FALSE,
load |-> MinPartitionSize ]
```
Она покрывает весь диапазон:
```text
[0; KeyCount)
```
Для всех остальных `q` создаются мертвые пустые партиции:
```tla
[ lo |-> 0,
hi |-> 0,
dead |-> TRUE,
load |-> 0 ]
```
### Начальные locks
```tla
locks = [q \in PartitionIds |-> NoThread]
```
Все mutex свободны.
## TypeInvariant
```tla
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`
```tla
partitions \in [PartitionIds -> PartitionRecord]
```
Для каждого partition id есть корректная запись с полями:
- `lo`;
- `hi`;
- `dead`;
- `load`.
### Тип `tree`
```tla
tree \subseteq PartitionIds
```
Дерево не может ссылаться на неизвестный partition id.
### Тип `locks`
```tla
locks \in [PartitionIds -> (Threads \cup {NoThread})]
```
Для каждой партиции lock либо свободен, либо принадлежит одному из потоков.
### Все партиции в `tree` живые и непустые
```tla
\A p \in tree:
/\ partitions[p].dead = FALSE
/\ partitions[p].lo < partitions[p].hi
/\ partitions[p].hi <= KeyCount
```
Если партиция достижима из дерева:
- она не `dead`;
- ее интервал непустой;
- правая граница не выходит за общий диапазон.
### Все партиции вне `tree` мертвые
```tla
\A p \in PartitionIds \ tree:
partitions[p].dead = TRUE
```
Если partition id не достижим из дерева, он должен быть `dead`.
Это сильное упрощение модели. В реальной системе из-за QSBR может существовать старая нода, которую еще кто-то читает. Но для текущей модели мы проверяем только логическую структуру живых интервалов.
## Инвариант уникального владельца ключа
```tla
UniqueOwnerForEveryKey ==
\A k \in Keys:
\E p \in tree:
/\ KeyInPartition(k, p)
/\ \A q \in tree:
KeyInPartition(k, q) => q = p
```
Это главный инвариант модели.
Он говорит:
```text
для каждого ключа существует ровно одна живая партиция, которая содержит этот ключ
```
Разберем по частям.
```tla
\A k \in Keys:
```
Для каждого ключа.
```tla
\E p \in tree:
```
Существует живая партиция `p`.
```tla
KeyInPartition(k, p)
```
Эта партиция содержит ключ.
```tla
\A q \in tree:
KeyInPartition(k, q) => q = p
```
Любая другая живая партиция `q`, которая содержит этот ключ, на самом деле является той же самой партицией `p`.
Иначе говоря:
- нет дыр;
- нет пересечений;
- каждый ключ покрыт ровно одним интервалом.
Если этот инвариант падает, значит split или shrink испортил покрытие диапазона.
## Инвариант `DeadPartitionsAreOutsideTree`
```tla
DeadPartitionsAreOutsideTree ==
\A p \in PartitionIds:
partitions[p].dead => p \notin tree
```
Если партиция мертвая, она не должна быть достижима из дерева.
Этот инвариант частично пересекается с `TypeInvariant`, но он более явно выражает важное свойство структуры данных.
## Действие `Lock`
```tla
Lock(t, p) ==
/\ p \in tree
/\ locks[p] = NoThread
/\ locks' = [locks EXCEPT ![p] = t]
/\ UNCHANGED <<partitions, tree>>
```
`Lock(t, p)` моделирует взятие mutex партиции `p` потоком `t`.
Предусловия:
```tla
p \in tree
```
Можно брать lock только у живой достижимой партиции.
```tla
locks[p] = NoThread
```
Lock должен быть свободен.
Изменение:
```tla
locks' = [locks EXCEPT ![p] = t]
```
В следующем состоянии владелец lock для `p` - поток `t`.
Остальное не меняется:
```tla
UNCHANGED <<partitions, tree>>
```
## Действие `Unlock`
```tla
Unlock(t, p) ==
/\ locks[p] = t
/\ locks' = [locks EXCEPT ![p] = NoThread]
/\ UNCHANGED <<partitions, tree>>
```
`Unlock(t, p)` моделирует освобождение mutex.
Предусловие:
```tla
locks[p] = t
```
Только владелец lock может его освободить.
Изменение:
```tla
locks' = [locks EXCEPT ![p] = NoThread]
```
В следующем состоянии lock свободен.
## Действие `GrowPartition`
```tla
\* 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`.
Предусловия:
```tla
p \in tree
```
Партиция живая.
```tla
locks[p] = t
```
Поток держит mutex этой партиции.
```tla
partitions[p].load < MaxLoad
```
Нельзя выйти за техническую верхнюю границу `MaxLoad`.
Изменение:
```tla
partitions' = [partitions EXCEPT ![p].load = @ + 1]
```
`@` означает старое значение изменяемого поля.
То есть:
```text
new load = old load + 1
```
## Действие `ShrinkPartitionLoad`
```tla
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`.
Изменение:
```text
new load = old load - 1
```
`tree` и `locks` не меняются.
## Действие `SplitPartition`
```tla
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
```tla
p \in tree
```
Разбиваем только живую партицию.
```tla
locks[p] = t
```
Поток держит lock этой партиции.
```tla
Status(p) = "NEED_SPLIT"
```
Разбиение разрешено только если партиция слишком большая.
```tla
partitions[p].lo + 1 < partitions[p].hi
```
Интервал должен быть достаточно большим, чтобы его можно было разделить на два непустых интервала.
Например, `[0; 2)` можно разделить на `[0; 1)` и `[1; 2)`.
А `[0; 1)` разделить нельзя.
### Выбор новых partition id
```tla
\E left \in PartitionIds \ tree:
\E right \in (PartitionIds \ tree) \ {left}:
```
TLC недетерминированно выбирает два свободных id:
- `left`;
- `right`.
Они должны быть вне текущего `tree`.
### Выбор точки split
```tla
\E splitKey \in (partitions[p].lo + 1)..(partitions[p].hi - 1):
```
`splitKey` выбирается строго внутри интервала.
Старый интервал:
```text
[lo; hi)
```
Новые интервалы:
```text
[lo; splitKey)
[splitKey; hi)
```
### Распределение load
```tla
\E leftLoad \in 0..partitions[p].load:
LET rightLoad == partitions[p].load - leftLoad IN
```
Модель не знает, как реальные элементы распределяются между половинами.
Поэтому она разрешает любое распределение:
```text
leftLoad + rightLoad = oldLoad
```
Это полезная абстракция: TLC проверяет больше вариантов, включая неудобные.
### Изменение `partitions`
```tla
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`
```tla
tree' = (tree \ {p}) \cup {left, right}
```
В дереве:
- удаляем старую партицию `p`;
- добавляем две новые.
### Изменение `locks`
```tla
locks' =
[ locks EXCEPT
![p] = NoThread,
![left] = NoThread,
![right] = NoThread ]
```
После split locks освобождены.
Это упрощение модели. Если в C++ реализации `get` возвращает пользователю уже locked partition, возможно позднее стоит уточнить это поведение.
## Действие `ShrinkToPrev`
```tla
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` с предыдущей соседней партицией.
### Предусловия
```tla
p \in tree
```
Удалять можно только живую партицию.
```tla
locks[p] = t
```
Поток держит lock удаляемой партиции.
```tla
Status(p) = "NEED_SHRINK"
```
Партиция маленькая и есть больше одной живой партиции.
### Выбор предыдущей партиции
```tla
\E prev \in tree \ {p}:
```
TLC выбирает другую живую партицию `prev`.
```tla
locks[prev] = t
```
Тот же поток должен держать lock соседа.
```tla
partitions[prev].hi = partitions[p].lo
```
`prev` должна быть настоящим левым соседом.
Например:
```text
prev = [0; 3)
p = [3; 5)
```
### Ограничение load
```tla
partitions[prev].load + partitions[p].load <= MaxLoad
```
Нужно, чтобы после слияния `load` не вышел за технический максимум модели.
### Изменение `partitions`
```tla
partitions' =
[ partitions EXCEPT
![prev].hi = partitions[p].hi,
![prev].load = partitions[prev].load + partitions[p].load,
![p].dead = TRUE ]
```
Сосед `prev` расширяется вправо:
```text
prev: [prev.lo; prev.hi)
p: [p.lo; p.hi)
```
становится:
```text
prev: [prev.lo; p.hi)
```
Партиция `p` становится мертвой.
### Изменение `tree`
```tla
tree' = tree \ {p}
```
Удаляем `p` из дерева.
### Изменение `locks`
```tla
locks' = [locks EXCEPT ![p] = NoThread, ![prev] = NoThread]
```
После shrink locks освобождаются.
## Действие `ShrinkToNext`
```tla
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`.
### Выбор следующей партиции
```tla
\E next \in tree \ {p}:
```
TLC выбирает другую живую партицию `next`.
```tla
partitions[p].hi = partitions[next].lo
```
`next` должна быть настоящим правым соседом.
Например:
```text
p = [3; 5)
next = [5; 8)
```
### Изменение интервала
```tla
![next].lo = partitions[p].lo
```
Правая партиция расширяется влево:
```text
p: [p.lo; p.hi)
next: [next.lo; next.hi)
```
становится:
```text
next: [p.lo; next.hi)
```
Партиция `p` становится мертвой и удаляется из `tree`.
## Действие `Next`
```tla
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 в следующую партицию.
Запись:
```tla
\/
```
означает логическое "или".
Запись:
```tla
\E t \in Threads, p \in PartitionIds:
```
означает, что TLC перебирает все варианты потока `t` и партиции `p`.
Если предусловия действия не выполняются, этот вариант просто невозможен.
## Спецификация `Spec`
```tla
Spec == Init /\ [][Next]_vars
```
`Spec` - это полное описание поведения системы.
Оно говорит:
1. Первое состояние должно удовлетворять `Init`.
2. Каждый следующий шаг должен удовлетворять `[Next]_vars`.
`[]` означает "всегда".
`[Next]_vars` означает:
- либо произошел один из переходов `Next`;
- либо состояние осталось тем же по переменным `vars`.
## Конец модуля
```tla
====
```
Так заканчивается TLA+ модуль.
## Как читать counterexample
Если TLC найдет ошибку, он покажет последовательность состояний:
```text
State 1
State 2
State 3
...
```
Для этой модели нужно смотреть прежде всего:
- `tree`;
- `partitions`;
- `locks`;
- какое действие было между состояниями.
Если нарушился `UniqueOwnerForEveryKey`, значит произошло одно из двух:
- появился ключ, который не принадлежит ни одной живой партиции;
- появился ключ, который принадлежит двум живым партициям.
Практически это означает ошибку в логике split/shrink или слишком слабое предусловие действия.
## Как запускать
Быстрый smoke-test:
```bash
java -jar /home/hhhh/bin/toolbox/tla2tools.jar \
-config validate/PartitionsStorageSetsSmoke.cfg \
validate/PartitionsStorageSets.tla
```
Более тяжелая проверка:
```bash
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.
Это осознанно. Сейчас модель проверяет только базовую безопасность интервалов:
```text
каждый ключ всегда принадлежит ровно одной живой партиции
```
Следующий логичный шаг - уточнить `shrink`, добавив состояние операции по шагам:
1. отпустили lock своей партиции;
2. нашли prev;
3. взяли prev lock;
4. снова взяли свой lock;
5. нашли next;
6. взяли next lock;
7. проверили DEAD;
8. удалили текущую партицию.
Тогда модель начнет проверять больше конкурентных interleavings.