-
Notifications
You must be signed in to change notification settings - Fork 199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implementing a TLA+ Specification: EWD998Chan #75
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The implementation took two hours and did not involve running the code. Disclaimer: The code first used a home-grown wire format that we replaced with Json to remove error-prone ad-hoc parsing.
The resolution of this translation error took approximately 15 minutes and required the execution of the code using a debugger. [Bug]
Notice that logging happes at two points in time when the system is in a consistent state; a) when a message is received^1 and b) when a message is sent. 1 A message is conceptually received, not when the receiver thread accepts the message from the ServerSocket, but when the message is taken out of the inbox. While it might be tempting to log in the receiver thread, it would break the happen-before relationship of some log statements. For example, it could appear as if a node first received a payload message that activated the node, and it then passes the token (which a node is only allowed to do when it is inactive).
The approach by Ron Pressler [1] does not work well for validating a trace that is extremely partial, i.e., it only contains messages that cross the network. EWD998's trace neither has the values of all variables nor logs every state of the system. Using non-determinism to define the missing values and states is not practical, as we end up in a TLA+ fragment that TLC cannot handle, such as the following sub-formula of the next-state relation, where the first conjunct defines an infinite set of functions from which the second conjunction filters an infinite subset of functions. ```tla SomeAct == /\ ... /\ inbox' \in [Node -> Seq(Msg)] /\ \E idx \in DOMAIN inbox[Initiator]': inbox[Initiator][idx]' = Trace[TLCGet("level")].msg /\ ... ``` A workaround is to define TLC state and action constraints that match each line in the trace to the current state of a prefix of some behavior defined by the original spec. The trace can only be mapped to a behavior iff TLC can match every line in the trace to a state in the prefix. If not, a TLC postcondition will be violated the unmatched logline. [1] https://www.youtube.com/watch?v=TP3SY0EUV2A
…al clock, which is why the trace/log file is unordered. For example, the receiver of a message might log the receipt of a message before the sender logs the send event. The canonical solution in distributed systems is a vector clock. Note that EWD998ChanID models a vector clock to interface with https://bestchai.bitbucket.io/shiviz/
… ShiViz (https://bestchai.bitbucket.io/shiviz). Regex: ^{"event":"(<|>|d)","node":(?<host>[0-9]?),"pkt":{("snd":(?<src>[0-9]?),"rcv":(?<rcv>[0-9]?),"msg":(?<event>({"type":"tok","q":-?[0-9]*,"color":"(white|black)"}|{"type":"(pl|trm|w|d)"})),|)"vc":(?<clock>.*)}}
…98Chan models token passing as a single, atomic step, but in a real system, passing the token is not atomic. The token is transmitted via the network. To handle this problem, this technique introduces (explicit) stuttering steps, where the variables remain unchanged, but the length of the behavior increases.
…nodes deactivating are not logged in the implementation. This leads to a rejected trace when a seemingly active node passes the token. TLC's limitation in supporting the composition of actions (compare section 7.3 (page 76ff) of Lamport's Specifying Systems) prevents the TraceSpec from defining its next-state relation by conjoining Deactivate \cdot PassToken. However, this limitation can be overcome by defining a suitable action that deactivates the node when it passes the token.
… missing state changes in the trace/log file, and we see no reason why this approach won't generalize beyond this particular example. With the EWD998 implementation, however, we can (consistently) log when a node deactivates, thereby increasing the coverage of trace validation by including more system state.
…rmination ("trm") messages in the implementation, which are not included in the EWD998 model. This represents the first discrepancy that has been discovered between the specifications and the actual code, but it is something that could have easily been spotted through a manual comparison between the two. To resolve this issue, the TraceNextConstraint has been defined to allow for "trm" messages and is based on finite stuttering, similar to the way the IsRecvToken is handled.
The SendMsg action in the high-level specification does not allow a message to be sent from a node to itself. While self-messaging is safe and does not violate the properties of the specification, the implementation will be changed instead.
…nerating approximately 10k states. The initiator initiates another token round even when the previous token round is conclusive. The system is in this state after a token round with all nodes white and inactive except for an active but white initiator. This is a first attempt at a bugfix. ``` << "Failed matching the trace to (a prefix of) a behavior:", [ pkt |-> [ vc |-> [ 1 |-> 342, 3 |-> 300, 0 |-> 216, 2 |-> 341, 5 |-> 313, 4 |-> 313, 6 |-> 320, 7 |-> 413 ], msg |-> [type |-> "pl"], snd |-> 0, rcv |-> 7 ], event |-> "<" ], "TLA+ debugger breakpoint hit count 2576" >> FALSE Error: Assumption line 212, col 5 to line 215, col 84 of module EWD998ChanTrace is false. ... ```
after generating approximately 10k states." and finally implement the EWD998 specification.
lemmy
force-pushed
the
mku-ewd998_impl
branch
5 times, most recently
from
April 12, 2023 03:54
a3304ed
to
fcae11a
Compare
Signed-off-by: Markus Alexander Kuppe <[email protected]>
… EWD998Chan where possible. Signed-off-by: Markus Alexander Kuppe <[email protected]>
…allowed ('config' was unexpected) [Build]
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Implementation Process 0d341ee
I implemented the EWD998 algorithm based on the TLA+ specification EWD998Chan. The implementation took two hours, during which I did not run or debug the code. Copy-pasting snippets from the specification to the Java code made the translation straightforward, with the exception of developing the low-level network receiver thread, which is abstracted away in the specification.
Disclaimer: Initially, the code utilized a custom wire format, which I later replaced with JSON to eliminate error-prone ad-hoc parsing.
[Divergence] Debugging and Error Resolution 2e24bf9
When running the implementation for the first time, I discovered a bug. A node that receives a token must retain the token across the while loop. Resolving this translation error took approximately 15 minutes and required executing the code using a debugger.
Adding JSON Logging 69f2708
In order to add JSON logging, I identified the appropriate places to log where the system is in a consistent state. Logging occurs at two points in the implementation:
inbox
. Note that logging in the receiver thread would break the happen-before relationship of some log statements due to multithreading, leading to inconsistencies.Trace Validation 5e0b9f5
The trace validation mapping, as suggested by Ron Pressler, is not well-suited for validating a trace that is extremely partial. EWD998's trace neither has the values of all variables nor logs every state of the system.
Using non-determinism to axiomatically define the missing values tends to become impractical, as it quickly results in a TLA+ fragment that TLC cannot handle. For example, consider the following sub-formula of the next-state relation where the first conjunct defines an infinite set of functions, from which the second conjunction filters an infinite subset:
A constructive definition, on the other hand, is cumbersome and, thus, error-prone in TLA+. My workaround involves defining TLC state and action constraints that match each line in the trace to the current state of a prefix of some behavior defined by the original, high-level specification. A trace is accepted iff TLC can match every line in the trace to the corresponding state in a (prefix of) a high-level spec behavior.
Absence of Global Clock 61d6be7
A fundamental problem of distributed systems is the absence of a global clock, which is why the trace/log file may be causally unordered. For example, the receiver of a message might log the receipt of a message before the sender logs the corresponding send event. The canonical solution in distributed systems is a vector clock, which is modeled in EWD998ChanID to interface with ShiViz (https://bestchai.bitbucket.io/shiviz).
Updating JSON Logging for ShiViz Compatibility 7897628
I included the node's identity in the JSON logging to ensure compatibility with ShiViz. The following regex was used:
Introducing Stuttering Steps for Token Passing 98c4d40
In the high-level TLA+ specification EWD998Chan, token passing is modeled as a single atomic step. However, in real-world systems, token passing is not atomic and is transmitted via the network. To address this issue, I introduced explicit stuttering steps, where variables remain unchanged, at the price of the length of the behavior increasing.
Handling Unlogged Deactivation Events ad8988f
Trace validation faces challenges such as the absence of node deactivation events in the logs, which leads to a rejected trace when a seemingly active node passes the token. The limitations of TLC in supporting the composition of actions (as discussed in section 7.3, pages 76ff, of Lamport's Specifying Systems) hinder the TraceSpec from defining its next-state relation by conjoining
Deactivate \cdot PassToken
. However, this limitation can be overcome by defining a suitable action that deactivates the node when it passes the token.Related: tlaplus/tlaplus#805
Improving Trace Validation Coverage 55e9a64
The previous step demonstrated how manually composed actions can handle missing state changes in the trace or log file, and I see no reason why this approach won't generalize beyond this specific example. In the case of the EWD998 implementation, it is possible to consistently log when a node deactivates, thereby increasing the coverage of trace validation by including more system state.
[Divergence] Handling Termination Messages in Trace Validation 7f157c8
Trace validation starts to reject traces due to the presence of termination ("trm") messages in the implementation, which are not included in the EWD998 model. This discrepancy between the specifications and the actual code could have been easily identified through a manual comparison between the two. To resolve this issue, I define the
TraceNextConstraint
to allow for "trm" messages and base it on finite stuttering, similar to the way theIsRecvToken
is handled.[Divergence] SendMsg Action ff7a49a
Trace validation detects another divergence between the high-level specification and the implementation. The
SendMsg
action in the high-level specification does not allow a message to be sent from a node to itself. Although self-messaging is safe and does not violate the properties of the specification, I choose to modify the implementation to match the specification more closely.[Divergence] Single node configuration 75673c4
Trace validation identified a divergence between the specification and the code implementation. In a system with only one node, the token does not appear to be moving. To handle this edge case, I have adjusted the
IsInitiateToken
predicate to account for this scenario.Logging Exceptions 27d721c
In the code implementation, all exceptions are logged as special events in the log file. To maintain consistency with the specification, I have ensured that exceptions are logged to the trace and cause the trace validation to fail.
[Divergence] Trace Validation Divergence 5665744
Upon performing trace validation, I discovered another divergence after generating approximately 10,000 states within ~30 seconds. The initiator initiates another token round even when the previous token round is conclusive. This issue occurs when the system is in a state with all nodes being white and inactive except for an active, but white, initiator that owns the token.
[Divergence] Final Implementation 4da7373
I had to revert the (bogus) bugfix attempt above. To address the issue, I have now implemented the EWD998 specification correctly, ensuring that the divergence is eliminated and the system functions as expected. Trace validation no longer finds divergences.
Note to self: Random stuff at /Users/markus/src/TLA/_specs/examples/specifications/ewd998 [mku-TraceValidation]