feat: add tla config for model

This commit is contained in:
hhhh 2026-06-14 23:55:35 +03:00
parent 8758541668
commit 57d048661a
7 changed files with 1592 additions and 0 deletions

3
.gitignore vendored
View file

@ -51,6 +51,9 @@ bin/
# Debug information files # Debug information files
*.dwo *.dwo
# TLA+ states
states/
# CMake # CMake
*cmake*/ *cmake*/
/CMakeLists.txt /CMakeLists.txt

View file

@ -0,0 +1,15 @@
SPECIFICATION Spec
INVARIANTS
TypeInvariant
UniqueOwnerForEveryKey
DeadPartitionsAreOutsideTree
CONSTANTS
PartitionIds = {p0, p1, p2, p3, p4}
Threads = {t0, t1}
NoThread = NoThread
KeyCount = 8
MinPartitionSize = 2
MaxPartitionSize = 4
MaxLoad = 8

View file

@ -0,0 +1,15 @@
SPECIFICATION Spec
INVARIANTS
TypeInvariant
UniqueOwnerForEveryKey
DeadPartitionsAreOutsideTree
CONSTANTS
PartitionIds = {p0, p1, p2, p3, p4, p5, p6, p7}
Threads = {t0, t1, t2, t3}
NoThread = NoThread
KeyCount = 1024
MinPartitionSize = 2
MaxPartitionSize = 8
MaxLoad = 9

View file

@ -0,0 +1,191 @@
---- MODULE PartitionsStorageSets ----
EXTENDS Naturals, FiniteSets
(*
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.
*)
CONSTANTS
PartitionIds,
Threads,
NoThread,
KeyCount,
MinPartitionSize,
MaxPartitionSize,
MaxLoad
ASSUME NoThread \notin Threads
ASSUME KeyCount > 0
ASSUME MinPartitionSize > 0
ASSUME MaxPartitionSize >= MinPartitionSize
ASSUME MaxLoad > MaxPartitionSize
ASSUME Cardinality(PartitionIds) >= 3
VARIABLES
partitions,
tree,
locks
vars == <<partitions, tree, locks>>
Keys == 0..(KeyCount - 1)
PartitionRecord ==
[ lo : 0..KeyCount,
hi : 0..KeyCount,
dead : BOOLEAN,
load : 0..MaxLoad ]
KeyInPartition(k, p) ==
/\ partitions[p].lo <= k
/\ k < partitions[p].hi
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"
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]
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
UniqueOwnerForEveryKey ==
\A k \in Keys:
\E p \in tree:
/\ KeyInPartition(k, p)
/\ \A q \in tree:
KeyInPartition(k, q) => q = p
DeadPartitionsAreOutsideTree ==
\A p \in PartitionIds:
partitions[p].dead => p \notin tree
Lock(t, p) ==
/\ p \in tree
/\ locks[p] = NoThread
/\ locks' = [locks EXCEPT ![p] = t]
/\ UNCHANGED <<partitions, tree>>
Unlock(t, p) ==
/\ locks[p] = t
/\ locks' = [locks EXCEPT ![p] = NoThread]
/\ UNCHANGED <<partitions, tree>>
\* 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>>
ShrinkPartitionLoad(t, p) ==
/\ p \in tree
/\ locks[p] = t
/\ partitions[p].load > 0
/\ partitions' = [partitions EXCEPT ![p].load = @ - 1]
/\ UNCHANGED <<tree, locks>>
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 ]
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]
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]
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)
Spec == Init /\ [][Next]_vars
====

View file

@ -0,0 +1,15 @@
SPECIFICATION Spec
INVARIANTS
TypeInvariant
UniqueOwnerForEveryKey
DeadPartitionsAreOutsideTree
CONSTANTS
PartitionIds = {p0, p1, p2}
Threads = {t0}
NoThread = NoThread
KeyCount = 3
MinPartitionSize = 1
MaxPartitionSize = 2
MaxLoad = 3

File diff suppressed because it is too large Load diff

33
validate/README.md Normal file
View file

@ -0,0 +1,33 @@
# Валидация
## Описание структуры данных (partitions_storage)
1) Внутри данной структуры данных хранятся множества в виде полуоткрытых интервалов ([X; Y), [Y; Z), ... )
2) В процессе работы множество может быть разбито на два более маленьких для увелечения производительности доступа
3) В процессе работы множество может быть присаеденено к другому если оно стало слишком маленьким
4) Запросы, которые может поддерживать:
- `get(key)` - отдает множество, в которому принадлежит данный ключ, множество находится в данный момент под mutex_lock
## Описание работы структуры данных
1) Внутри используется персистентное AVL дерево, которое является lock-free и в узлах, хранит как раз так называемые множества
2) У партиции может быть несколько состояний в момент когда пользователь взяль mutex lock и прочитал состояние:
- ALIVE ( 16 <= `partition.size()` <= 128)
- NEED_SPLIT ( `partition.size()` > 128)
- NEED_SHRINK ( 16 > `partition.size()` && partitions_count > 1)
- DEAD (особое состояние)
3) Как происходит get:
- сначала получаем из AVL дерева ноду на партицию (она защищена QSBR) и берем у нее mutex lock
- проверяем состояние, если DEAD, то возвращаемся к предыдущему шагу. Если ALIVE то возвращаем, иначе выполняем соответствующие команды
4) Как происходит split:
- так как над портицией держится lock то мы пемечяем ее как DEAD
- разделяем на две по некоторому ключу
- вставляем в AVL дерево за место ноды связку из таких node (left, split_key, right)
5) Как происходит shrink (shrink происходит когда множеств больше чем одно):
- отпускаем lock
- находим предыдущею до нас множество и берем у него lock, проверяя не является ли она DEAD
- берем lock у нашей партиции и проверяем что она не DEAD
- находим следующую за нами множество и берем у него lock, проверяя не является ли она DEAD
- помечаем нашу партицию как DEAD
- "убираем" нашу партицию из дерева и запоминаем каким ребенком мы были для предка (left, right)
- если мы были правым ребенком то передаем все множество в следующее множество
- если мы были левым ребенком то передаем все множество в прошлое множество
6) В начальный момент времени существует либо только одно множество, либо несколько их, которые сбалансированы