blob: 13fa9fb39a1c785b2a05146567c4db2ec204975e [file] [log] [blame]
---- MODULE chain_lock ----
EXTENDS TLC, Integers, Sequences
CONSTANTS
THREADS,
NULL,
UNLOCKED,
INVALID_TOKEN,
ERR_OK,
ERR_RETRY,
ERR_CYCLE_DETECTED,
ERR_INVALID_STATE
\* To enable symmetry across threads, uncomment this as well as the SYMMETRY
\* statement in the model config.
\* SymmetrySets == Permutations(THREADS)
(* A node set is a finite set representing the state of a graph. It consists
of a set of sequences, with the first element equal to the node ID, and the
second element equal to the downstream node (or NULL).
*)
NodeSet(g) == { << i, g[i] >> : i \in DOMAIN g }
(* AllNodes is a finite set containing all of the node ids in a graph encoded as
a sequence. It is basically just the DOMAIN of the sequence (which is why
the set is finite).
*)
AllNodes(g)== DOMAIN g
(* TerminalNodes is the set of node IDs for which the downstream node is NULL *)
TerminalNodes(g)== { n \in AllNodes(g) : g[n] = NULL }
(* UpstreamNodes is the set of node IDs for which the downstream node is not NULL *)
UpstreamNodes(g)== { n \in AllNodes(g) : g[n] /= NULL }
(* DownstreamLinks is the set of unique downstream links in a graph *)
DownstreamLinks(g)== { g[n] : n \in AllNodes(g) }
(* HeadNodes is the set of nodes in a graph for which there does not exist a node which links to them. *)
HeadNodes(g)== { n \in AllNodes(g) : n \notin DownstreamLinks(g) }
(* Our graph is "closed" iff every node in the graph has either a NULL
downstream link, or there exists a node in the graph whose ID is equal to the
node's downstream link *)
GraphIsClosed(g) ==
/\ \A x \in NodeSet(g): (x[2] = NULL) \/ \E y \in NodeSet(g): x[2] = y[1]
(* Whether or not a path through a graph starting at a given node is acyclic or
not is computed recursively from the graph definition, a starting node, and a
set of visited nodes which is initalized to the empty set.
The path is acyclic if:
1) The downstream link from the starting node is NULL, OR
2a) The starting node is not in the visited set, AND
2b) The path starting from the downstream node is acyclic when recusively
evaluating with the visited set unioned with the set contianing just the
starting node.
*)
RECURSIVE PathIsAcyclicEtc(_, _, _, _)
PathIsAcyclicEtc(g, start, visited, path) ==
(g[start] = NULL) \/
((start \notin visited \/ Print(<< "Cycle Detected",
<< "Graph", NodeSet(g) >>,
<< "Cycle", Append(path, start) >>
>> , FALSE))
/\ PathIsAcyclicEtc(g, g[start], visited \union {start}, Append(path, start)))
PathIsAcyclic(g, start) == PathIsAcyclicEtc(g, start, {}, << >>)
(* A graph is acyclic if the paths starting from every node are all acyclic. *)
GraphIsAcyclic(g) ==
\A start \in DOMAIN g: PathIsAcyclic(g, start)
(* for this spec, a graph is considered valid if it is both closed, and acyclic *)
GraphIsValid(g) == GraphIsClosed(g) /\ GraphIsAcyclic(g)
(* Edge operations are encoded as a tuple of the form << NodeId, NewDownstreamNodeId >>.
The set of all valid add-edge operations for a graph |g| is the cross of the
set of terminal nodes with the set of all nodes. Basically, any node which
does not already have a downstream link can be linked to any node in the
graph.
The set of all valid remove-edge operations for a graph |g| is the cross of
the set of upstream nodes with { NULL }. Basically, any node which has a
downstream link can have that link removed.
The set of all valid operations is just the union of the two.
*)
AllValidAddEdgeOperations(g) == TerminalNodes(g) \X AllNodes(g)
AllValidRemoveEdgeOperations(g) == UpstreamNodes(g) \X { NULL }
AllValidEdgeOperations(g) == AllValidAddEdgeOperations(g) \union
AllValidRemoveEdgeOperations(g)
(* MutatedGraph is an operator which produced g', the graph after op has been
performed on g
*)
MutatedGraph(g, op) == [ x \in DOMAIN g |-> IF x = op[1] THEN op[2] ELSE g[x] ]
(*
+-----+ +-----+ +-----+ +-----+ +-----+
| 1 | | 2 | | 5 | | 8 | | 9 |
+-----+ +-----+ +-----+ +-----+ +-----+
| | |
+---------- v
| +-----+
v | 6 |
+-----+ +-----+
| 3 | |
+-----+ v
| +-----+
v | 7 |
+-----+ +-----+
| 4 |
+-----+
*)
INITIAL_PI_GRAPH == << (* 1 *) 3,
(* 2 *) 3,
(* 3 *) 4,
(* 4 *) NULL,
(* 5 *) 6,
(* 6 *) 7,
(* 7 *) NULL,
(* 8 *) NULL,
(* 9 *) NULL
>>
(*--algorithm chain_lock
variables
graph_state = INITIAL_PI_GRAPH,
token_generator = 1,
genlock_state = [ node_id \in AllNodes(graph_state) |-> UNLOCKED ],
ret_vals = [ t \in THREADS |-> NULL ],
tokens = [ t \in THREADS |-> INVALID_TOKEN ];
macro UnlockNode(id) begin
assert genlock_state[id] = tokens[self];
genlock_state[id] := UNLOCKED;
end macro;
procedure LockNode(id)
variables observed
begin
(* try to CMPX UNLOCKED -> |gen|, if we succeed, we own the lock and can get out *)
lock_node_start:
if genlock_state[id] = UNLOCKED then
genlock_state[id] := tokens[self];
ret_vals[self] := << ERR_OK >>;
return;
else
observed := genlock_state[id];
end if;
(* Someone else owns the lock. There are three options to consider here.
1) Their token id was greater than ours. We have priority, just spin
waiting for them to either finish, or encounter our path and back off.
2) Their token was is less than ours. They have priority, and may be
waiting for us to get out of their way. Back off, releasing our
lock-path and try again later.
3) Their token was equal to our token. We have detected a cycle
and should abort the operation.
*)
lock_node_contested:
if observed > tokens[self] then
goto lock_node_start;
elsif observed < tokens[self] then
ret_vals[self] := << ERR_RETRY >>;
return;
else
(* Attach the current graph state to the error code. We will use it to
assert that if we had reported that a cycle was detected, that there
actually would have been one had we applied our operation.
*)
ret_vals[self] := << ERR_CYCLE_DETECTED, graph_state >>;
return;
end if
end procedure;
procedure PerformOperation(op)
variables next_node, locked = << >>;
begin
(* Obtain a token ID for our operation *)
get_token:
tokens[self] := token_generator;
token_generator := token_generator + 1;
lock_first_node:
call LockNode(op[1]);
lock_first_node_result:
if ret_vals[self][1] /= ERR_OK then
goto unlock;
else
locked := Append(locked, op[1]);
end if;
(* Check to see if our operation is still valid, or if we should fast abort.
1) If our downstream node has already been set to what we want, we are
already finished.
2) If we are adding an edge, but the node already has an edge added, we
need to abort the operation with "invalid state"
If everthing looks OK, we need to lock the path up until we reach terminal
node in the graph, then actually mutate our graph. The next node to
visit/lock will be either the edge we plan to add, or the current edge (if
we plan to delete it)
*)
operation_check:
if graph_state[op[1]] = op[2] then
(* our return value is already set to OK *)
goto unlock;
elsif (graph_state[op[1]] /= NULL) /\ (op[2] /= NULL) then
ret_vals[self][1] := << ERR_INVALID_STATE >>;
else
next_node := IF op[2] /= NULL THEN op[2] ELSE graph_state[op[1]];
end if;
lock_path:
(* OK, now just follow the path until the end, locking as we go. *)
while next_node /= NULL do
call LockNode(next_node);
lock_path_result:
if ret_vals[self][1] /= ERR_OK then
goto unlock;
else
locked := Append(locked, next_node);
next_node := graph_state[next_node];
end if;
end while;
(* Success! Perform our operation. *)
mutate_graph:
graph_state[op[1]] := op[2];
(* Unlock our locked set, front to back *)
unlock:
while Len(locked) > 0 do
UnlockNode(Head(locked));
locked := Tail(locked);
end while;
maybe_spin:
(* If our last lock-op result was retry, go ahead and try again. Otherwise,
we are done, and our result (either OK or CYCLE_DETECTED is already stored
in our ret_val).
*)
if ret_vals[self][1] = ERR_RETRY then
goto lock_first_node;
else
(* If the final result of attempting to perform our operation was
CYCLE_DETECTED, assert that there actually would have been a cycle in the
graph if we had executed our operation.
*)
assert (ret_vals[self][1] /= ERR_CYCLE_DETECTED) \/
~GraphIsAcyclic(MutatedGraph(ret_vals[self][2], op));
return;
end if;
end procedure
fair process threads \in THREADS begin
update_edge:
with operation \in AllValidRemoveEdgeOperations(INITIAL_PI_GRAPH) do
call PerformOperation(operation);
end with;
done:
skip;
end process;
end algorithm; *)
\* BEGIN TRANSLATION (chksum(pcal) = "128ab4fe" /\ chksum(tla) = "ac0580b6")
CONSTANT defaultInitValue
VARIABLES graph_state, token_generator, genlock_state, ret_vals,
tokens, pc, stack, id, observed, op, next_node, locked
vars == << graph_state, token_generator, genlock_state, ret_vals,
tokens, pc, stack, id, observed, op, next_node, locked >>
ProcSet == (THREADS)
Init == (* Global variables *)
/\ graph_state = INITIAL_PI_GRAPH
/\ token_generator = 1
/\ genlock_state = [ node_id \in AllNodes(graph_state) |-> UNLOCKED ]
/\ ret_vals = [ t \in THREADS |-> NULL ]
/\ tokens = [ t \in THREADS |-> INVALID_TOKEN ]
(* Procedure LockNode *)
/\ id = [ self \in ProcSet |-> defaultInitValue]
/\ observed = [ self \in ProcSet |-> defaultInitValue]
(* Procedure PerformOperation *)
/\ op = [ self \in ProcSet |-> defaultInitValue]
/\ next_node = [ self \in ProcSet |-> defaultInitValue]
/\ locked = [ self \in ProcSet |-> << >>]
/\ stack = [self \in ProcSet |-> << >>]
/\ pc = [self \in ProcSet |-> "update_edge"]
lock_node_start(self) == /\ pc[self] = "lock_node_start"
/\ IF genlock_state[id[self]] = UNLOCKED
THEN /\ genlock_state' = [genlock_state EXCEPT ![id[self]] = tokens[self]]
/\ ret_vals' = [ret_vals EXCEPT ![self] = << ERR_OK >>]
/\ pc' = [pc EXCEPT ![self] = Head(stack[self]).pc]
/\ observed' = [observed EXCEPT ![self] = Head(stack[self]).observed]
/\ id' = [id EXCEPT ![self] = Head(stack[self]).id]
/\ stack' = [stack EXCEPT ![self] = Tail(stack[self])]
ELSE /\ observed' = [observed EXCEPT ![self] = genlock_state[id[self]]]
/\ pc' = [pc EXCEPT ![self] = "lock_node_contested"]
/\ UNCHANGED << genlock_state, ret_vals,
stack, id >>
/\ UNCHANGED << graph_state, token_generator,
tokens, op, next_node, locked >>
lock_node_contested(self) == /\ pc[self] = "lock_node_contested"
/\ IF observed[self] > tokens[self]
THEN /\ pc' = [pc EXCEPT ![self] = "lock_node_start"]
/\ UNCHANGED << ret_vals, stack, id,
observed >>
ELSE /\ IF observed[self] < tokens[self]
THEN /\ ret_vals' = [ret_vals EXCEPT ![self] = << ERR_RETRY >>]
/\ pc' = [pc EXCEPT ![self] = Head(stack[self]).pc]
/\ observed' = [observed EXCEPT ![self] = Head(stack[self]).observed]
/\ id' = [id EXCEPT ![self] = Head(stack[self]).id]
/\ stack' = [stack EXCEPT ![self] = Tail(stack[self])]
ELSE /\ ret_vals' = [ret_vals EXCEPT ![self] = << ERR_CYCLE_DETECTED, graph_state >>]
/\ pc' = [pc EXCEPT ![self] = Head(stack[self]).pc]
/\ observed' = [observed EXCEPT ![self] = Head(stack[self]).observed]
/\ id' = [id EXCEPT ![self] = Head(stack[self]).id]
/\ stack' = [stack EXCEPT ![self] = Tail(stack[self])]
/\ UNCHANGED << graph_state, token_generator,
genlock_state, tokens, op,
next_node, locked >>
LockNode(self) == lock_node_start(self) \/ lock_node_contested(self)
get_token(self) == /\ pc[self] = "get_token"
/\ tokens' = [tokens EXCEPT ![self] = token_generator]
/\ token_generator' = token_generator + 1
/\ pc' = [pc EXCEPT ![self] = "lock_first_node"]
/\ UNCHANGED << graph_state, genlock_state, ret_vals,
stack, id, observed, op, next_node,
locked >>
lock_first_node(self) == /\ pc[self] = "lock_first_node"
/\ /\ id' = [id EXCEPT ![self] = op[self][1]]
/\ stack' = [stack EXCEPT ![self] = << [ procedure |-> "LockNode",
pc |-> "lock_first_node_result",
observed |-> observed[self],
id |-> id[self] ] >>
\o stack[self]]
/\ observed' = [observed EXCEPT ![self] = defaultInitValue]
/\ pc' = [pc EXCEPT ![self] = "lock_node_start"]
/\ UNCHANGED << graph_state, token_generator,
genlock_state, ret_vals, tokens,
op, next_node, locked >>
lock_first_node_result(self) == /\ pc[self] = "lock_first_node_result"
/\ IF ret_vals[self][1] /= ERR_OK
THEN /\ pc' = [pc EXCEPT ![self] = "unlock"]
/\ UNCHANGED locked
ELSE /\ locked' = [locked EXCEPT ![self] = Append(locked[self], op[self][1])]
/\ pc' = [pc EXCEPT ![self] = "operation_check"]
/\ UNCHANGED << graph_state,
token_generator,
genlock_state, ret_vals,
tokens, stack, id,
observed, op, next_node >>
operation_check(self) == /\ pc[self] = "operation_check"
/\ IF graph_state[op[self][1]] = op[self][2]
THEN /\ pc' = [pc EXCEPT ![self] = "unlock"]
/\ UNCHANGED << ret_vals, next_node >>
ELSE /\ IF (graph_state[op[self][1]] /= NULL) /\ (op[self][2] /= NULL)
THEN /\ ret_vals' = [ret_vals EXCEPT ![self][1] = << ERR_INVALID_STATE >>]
/\ UNCHANGED next_node
ELSE /\ next_node' = [next_node EXCEPT ![self] = IF op[self][2] /= NULL THEN op[self][2] ELSE graph_state[op[self][1]]]
/\ UNCHANGED ret_vals
/\ pc' = [pc EXCEPT ![self] = "lock_path"]
/\ UNCHANGED << graph_state, token_generator,
genlock_state, tokens, stack, id,
observed, op, locked >>
lock_path(self) == /\ pc[self] = "lock_path"
/\ IF next_node[self] /= NULL
THEN /\ /\ id' = [id EXCEPT ![self] = next_node[self]]
/\ stack' = [stack EXCEPT ![self] = << [ procedure |-> "LockNode",
pc |-> "lock_path_result",
observed |-> observed[self],
id |-> id[self] ] >>
\o stack[self]]
/\ observed' = [observed EXCEPT ![self] = defaultInitValue]
/\ pc' = [pc EXCEPT ![self] = "lock_node_start"]
ELSE /\ pc' = [pc EXCEPT ![self] = "mutate_graph"]
/\ UNCHANGED << stack, id, observed >>
/\ UNCHANGED << graph_state, token_generator,
genlock_state, ret_vals, tokens, op,
next_node, locked >>
lock_path_result(self) == /\ pc[self] = "lock_path_result"
/\ IF ret_vals[self][1] /= ERR_OK
THEN /\ pc' = [pc EXCEPT ![self] = "unlock"]
/\ UNCHANGED << next_node, locked >>
ELSE /\ locked' = [locked EXCEPT ![self] = Append(locked[self], next_node[self])]
/\ next_node' = [next_node EXCEPT ![self] = graph_state[next_node[self]]]
/\ pc' = [pc EXCEPT ![self] = "lock_path"]
/\ UNCHANGED << graph_state, token_generator,
genlock_state, ret_vals, tokens,
stack, id, observed, op >>
mutate_graph(self) == /\ pc[self] = "mutate_graph"
/\ graph_state' = [graph_state EXCEPT ![op[self][1]] = op[self][2]]
/\ pc' = [pc EXCEPT ![self] = "unlock"]
/\ UNCHANGED << token_generator, genlock_state,
ret_vals, tokens, stack, id,
observed, op, next_node, locked >>
unlock(self) == /\ pc[self] = "unlock"
/\ IF Len(locked[self]) > 0
THEN /\ Assert(genlock_state[(Head(locked[self]))] = tokens[self],
"Failure of assertion at line 138, column 3 of macro called at line 240, column 5.")
/\ genlock_state' = [genlock_state EXCEPT ![(Head(locked[self]))] = UNLOCKED]
/\ locked' = [locked EXCEPT ![self] = Tail(locked[self])]
/\ pc' = [pc EXCEPT ![self] = "unlock"]
ELSE /\ pc' = [pc EXCEPT ![self] = "maybe_spin"]
/\ UNCHANGED << genlock_state, locked >>
/\ UNCHANGED << graph_state, token_generator, ret_vals,
tokens, stack, id, observed, op,
next_node >>
maybe_spin(self) == /\ pc[self] = "maybe_spin"
/\ IF ret_vals[self][1] = ERR_RETRY
THEN /\ pc' = [pc EXCEPT ![self] = "lock_first_node"]
/\ UNCHANGED << stack, op, next_node, locked >>
ELSE /\ Assert((ret_vals[self][1] /= ERR_CYCLE_DETECTED) \/
~GraphIsAcyclic(MutatedGraph(ret_vals[self][2], op[self])),
"Failure of assertion at line 256, column 5.")
/\ pc' = [pc EXCEPT ![self] = Head(stack[self]).pc]
/\ next_node' = [next_node EXCEPT ![self] = Head(stack[self]).next_node]
/\ locked' = [locked EXCEPT ![self] = Head(stack[self]).locked]
/\ op' = [op EXCEPT ![self] = Head(stack[self]).op]
/\ stack' = [stack EXCEPT ![self] = Tail(stack[self])]
/\ UNCHANGED << graph_state, token_generator,
genlock_state, ret_vals, tokens, id,
observed >>
PerformOperation(self) == get_token(self) \/ lock_first_node(self)
\/ lock_first_node_result(self)
\/ operation_check(self) \/ lock_path(self)
\/ lock_path_result(self)
\/ mutate_graph(self) \/ unlock(self)
\/ maybe_spin(self)
update_edge(self) == /\ pc[self] = "update_edge"
/\ \E operation \in AllValidRemoveEdgeOperations(INITIAL_PI_GRAPH):
/\ /\ op' = [op EXCEPT ![self] = operation]
/\ stack' = [stack EXCEPT ![self] = << [ procedure |-> "PerformOperation",
pc |-> "done",
next_node |-> next_node[self],
locked |-> locked[self],
op |-> op[self] ] >>
\o stack[self]]
/\ next_node' = [next_node EXCEPT ![self] = defaultInitValue]
/\ locked' = [locked EXCEPT ![self] = << >>]
/\ pc' = [pc EXCEPT ![self] = "get_token"]
/\ UNCHANGED << graph_state, token_generator,
genlock_state, ret_vals, tokens, id,
observed >>
done(self) == /\ pc[self] = "done"
/\ TRUE
/\ pc' = [pc EXCEPT ![self] = "Done"]
/\ UNCHANGED << graph_state, token_generator, genlock_state,
ret_vals, tokens, stack, id, observed, op,
next_node, locked >>
threads(self) == update_edge(self) \/ done(self)
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == /\ \A self \in ProcSet: pc[self] = "Done"
/\ UNCHANGED vars
Next == (\E self \in ProcSet: LockNode(self) \/ PerformOperation(self))
\/ (\E self \in THREADS: threads(self))
\/ Terminating
Spec == /\ Init /\ [][Next]_vars
/\ \A self \in THREADS : /\ WF_vars(threads(self))
/\ WF_vars(PerformOperation(self))
/\ WF_vars(LockNode(self))
Termination == <>(\A self \in ProcSet: pc[self] = "Done")
\* END TRANSLATION
GraphStateIsValid == GraphIsValid(graph_state)
====