A Java library implementing an adapted Chandy-Lamport distributed snapshot algorithm that enables recording consistent global states and restoring them after node failures in distributed systems communicating over Java RMI.
Created by Emanuele Triuzzi, Giancarlo Sorrentino, Vincenzo Riccio
Politecnico di Milano — Distributed Systems, A.Y. 2020-2021
Professors: Gianpaolo Cugola, Alessandro Margara
- Overview
- Goals
- Library Assumptions
- Architecture
- The Snapshot Algorithm
- The Restore Algorithm
- Formal Correctness Proofs
- Network Management
- Snapshot Scenario — Basic Workflow
- Example Application — Distributed Bank Transfers
- Project Structure
- Getting Started
This library provides an abstract class Snapshottable<S, M> that allows any node in a distributed system to:
- Record consistent snapshots of the global state, including local process states and in-transit messages across communication channels
- Restore the system to a previously saved consistent state after one or more node failures
The snapshot and restore procedures are both based on an adapted version of the Chandy-Lamport algorithm, extended with crash detection, Lamport clock-based snapshot identification, and a coordinated restore protocol. The library is implemented in Java and communicates over RMI (Remote Method Invocation).
The library is message and state agnostic: it is built on two generic types — M (messages) and S (state) — whose only requirement is being Serializable. The application itself defines the structure of its state and exchangeable messages.
| Goal | Description |
|---|---|
| G1 | Record a consistent global state of a distributed system |
| G2 | Let the system recover a consistent state after one (or more) node failures |
The library operates under the following assumptions:
- No mid-snapshot crashes — Nodes do not crash in the middle of a snapshot
- Reliable FIFO channels — Communication channels are reliable, unidirectional, and FIFO ordered
- Full connectivity — There is a communication path between any two processes in the system
- Non-interference — The snapshot algorithm does not interfere with the normal execution of the processes
- Network awareness — The library is aware of all the network changes
- Topology stability — The network topology does not change when a snapshot or a restore is running
- Message independence — The outcome of the elaboration of received messages is independent from the moment in which they are elaborated
- Connection awareness — Each node must be aware of its incoming and outgoing connections
- Bounded re-join — A failed node re-joins the network in a limited amount of time
- Graceful leave — When a node wants to leave the network, it informs all the nodes it is connected to
- Unique identification — Each node is uniquely identified by its IP address
- Snapshot integrity — The library's saved snapshots can only be modified or deleted by the library itself
The library is composed of four core components that work together to provide transparent snapshot and restore capabilities.
| Component | Type | Role |
|---|---|---|
Snapshottable<S, M> |
Abstract Class | Core class that nodes extend. Manages Lamport clocks, snapshot coordination, marker propagation, crash detection, and state persistence. Extends UnicastRemoteObject for RMI. |
Snapshot<S, M> |
Class | Represents a single snapshot instance. Stores the process state S and a map of channel states (queues of in-transit messages M keyed by sender). |
SnapInt |
Remote Interface | Defines the RMI contract: startSnapshot(id), restore(id), and getClock(). Enables remote snapshot/restore coordination. |
ConnInt |
Interface | Abstracts a network connection with getHost(), getPort(), and getName(). Applications implement this to define their connection model. |
The following diagram shows the complete class structure, including both the library (snaplib) and the example application (snaptest) packages.
The library achieves application independence through two generic type parameters:
Snapshottable<S extends Serializable, M extends Serializable>
S— The state type. Represents the full internal state of a node. The application defines what constitutes its state (e.g., a ledger, a counter, a game board).M— The message type. Represents the messages exchanged between nodes. The application defines the message structure (e.g., a transfer command, a chat message).
Both types must be Serializable so that the library can persist snapshots to disk and transmit markers over RMI.
The snapshot algorithm is based on the Chandy-Lamport marker-based approach, adapted to work with Java RMI and extended with Lamport clocks for snapshot identification.
Each snapshot is uniquely identified by an ID composed of:
<Lamport Clock Value> . <Initiator Host Address>
- The Lamport clock value of the initiator at the moment the snapshot starts
- The unique identifier (IP address) of the initiator
The library increments its local Lamport clock only when starting a new snapshot instance. This structure enables:
- Happens-before relationships between snapshots with different Lamport clocks, which is necessary for the restore procedure to lead the system to a consistent state
- Multiple concurrent snapshots running in parallel, since each snapshot has a unique ID
When a process initiates a snapshot or receives a snapshot marker for the first time:
- Records its internal state — The current state
Sis deep-copied (via serialization) and stored in a newSnapshot<S, M>object - Sends a marker — A snapshot marker, embedding the snapshot ID, is sent on all outgoing channels to neighboring nodes
- Starts recording messages — The node begins recording all incoming messages on every incoming channel, associating them with the snapshot
- Processes subsequent markers — When a marker for the same snapshot arrives from an incoming node, the node stops recording messages from that channel
- Completes the local snapshot — When the node has received the marker from all incoming channels, the local snapshot is considered complete and is persisted to disk as a
.snapfile
If the node has already started a snapshot with the given ID (i.e., it receives a duplicate marker), it simply removes the sender from its pending set and checks for completion.
The unique ID structure allows multiple snapshots to run concurrently. The library maintains separate state for each active snapshot:
runningSnapshots: Map<String, Snapshot<S, M>>— Maps snapshot IDs to their in-progress snapshot objectsincomingStatus: Map<String, Set<String>>— Maps snapshot IDs to the set of incoming nodes from which the marker has not yet been received
Each incoming message is checked against all active snapshots: if the sender has not yet sent a marker for a given snapshot, the message is recorded in that snapshot.
The restore algorithm allows the system to recover from node failures by rolling back to a previously saved consistent snapshot.
When a node recovers from a failure or receives a restore marker for the first time:
- Selects the snapshot — If it is the failed node, it selects the local snapshot with the greatest Lamport clock. If receiving a restore marker, it uses the specified snapshot ID. In case of ambiguity, a common rule is applied.
- Restores local state — The node restores its local state to the saved one, including resetting the Lamport clock value to the one associated with the selected snapshot.
- Processes saved messages — All messages included in the selected snapshot are replayed, ensuring the node reaches the exact state it was in when the snapshot was taken plus any in-transit messages.
- Cancels active snapshots — All currently running snapshots are discarded, and the incoming status maps are cleared.
- Sends restore markers — A restore marker, embedding the ID of the selected snapshot, is sent on each outgoing channel.
When a node receives the marker from all incoming channels, the local restore can be considered complete.
During the restore process, strict rules govern message handling:
- Messages from nodes that have not already sent the restore marker are discarded, since those nodes may still be in an inconsistent state
- Messages from nodes that have already sent the restore marker can be safely processed, since they are generated from a node in a consistent state
- New snapshot requests from non-restored nodes are also ignored to prevent inconsistent snapshots from being created
The restore algorithm is designed to handle multiple node failures. If a second failure occurs during an ongoing restore, the algorithm detects this condition and restarts the restore process from scratch, selecting the appropriate snapshot for the new failure scenario.
Unlike snapshots, only one restore can be active at a time across the network. This is because, assuming a snapshot completes successfully, all nodes will have that snapshot saved. If a snapshot didn't complete, nodes that had already started it will discard it during restore, converging to the last fully completed snapshot.
This section provides a formal proof that the adapted Chandy-Lamport algorithm implemented by this library correctly records consistent global states and that the restore procedure recovers the system to a valid state.
Consider a distributed system modeled as a directed graph
-
$P = {p_1, p_2, \ldots, p_n}$ is a finite set of processes (nodes) -
$C \subseteq P \times P$ is a set of unidirectional FIFO channels, where$c_{ij} \in C$ denotes the channel from$p_i$ to$p_j$
Each process
-
Internal event — a local state transition on
$p_i$ -
Send event
$\mathrm{send}(m, c_{ij})$ — process$p_i$ sends message$m$ on channel$c_{ij}$ -
Receive event
$\mathrm{recv}(m, c_{ij})$ — process$p_j$ receives message$m$ from channel$c_{ij}$
We define a Lamport clock
where
Definition 1 (Happens-Before). The happens-before relation
(i)
(ii)
Definition 2 (Cut). A cut
Definition 3 (Consistent Cut). A cut
Equivalently, no message is recorded as received without its corresponding send also being recorded.
Definition 4 (Global State). A global state
where
Definition 5 (Channel State). For a given snapshot, the recorded state of channel
where
Theorem. The global state
Proof. We prove this by contradiction.
Suppose the recorded global state
Since
From
From
Now, in the adapted Chandy-Lamport algorithm, when
By the FIFO property of channel
From the algorithm's marker rule, upon receiving marker
Combining (3) and (4):
This gives us:
But (5) directly contradicts (2), which states
Theorem. If the network graph
Proof. We proceed by induction on the distance from the initiator.
Base case. The initiator
Inductive step. Assume all processes at distance
Since
For each process
Therefore, every local snapshot completes, and the global snapshot terminates.
Theorem. After the restore procedure completes, the global state of the system is equivalent to the consistent global state
Proof. Let
Part (a): State restoration. Each process
Part (b): Message replay. Each process
Part (c): Message filtering during restore. The algorithm discards messages from processes that have not yet sent the restore marker. Let
Part (d): Convergence. By Theorem 2 (substituting restore markers for snapshot markers), the restore propagation terminates in finite time over the strongly connected graph. After termination,
where
The system has thus reached a globally consistent state equivalent to
The network topology is built incrementally while maintaining a strongly connected graph at every step. This is essential because the Chandy-Lamport algorithm requires a path between any two processes.
The network creation follows this mechanism:
- First node — The initiator starts alone in the network
- Joining the network — When a new node joins, it must create both an incoming and an outgoing connection with an existing node, guaranteeing the graph remains strongly connected. A snapshot marker is triggered immediately after joining to capture the new topology.
- Adding connections — Additional single point-to-point connections can be added between any two nodes at any time. Each connection change triggers a new snapshot to record the updated topology.
When a node wants to leave the network, it must verify that its removal would not break the strongly connected property of the graph.
In the diagram above:
- Node G cannot leave safely because removing it would disconnect V and E (G is a bridge node)
- Node E can leave safely because V and G would remain strongly connected through their direct bidirectional link
The disconnect procedure:
- The leaving node removes all its incoming connections, notifying each peer
- The leaving node removes all its outgoing connections, notifying each peer
- The last disconnected peer triggers a snapshot to capture the new topology
- The leaving node transfers its state (e.g., customer ledgers) to another node before exiting
This section walks through a complete snapshot scenario with three nodes — V (Vincenzo), G (Giancarlo), and E (Emanuele) — connected in a strongly connected graph.
G starts a snapshot. G records its current state S_G1, creates a new snapshot object, and sends snapshot markers (SM) on all its outgoing channels — to V and to E. G's Lamport clock is used in the snapshot ID.
| Node | Actual State | Saved State | Saved MSGs |
|---|---|---|---|
| V | S_V1 | — | — |
| G | S_G1 | S_G1 | — |
| E | S_E1 | — | — |
Meanwhile, G receives messages from V and E, which arrive before their respective snapshot markers. These messages (M_E1, M_V1) are saved in G's local snapshot since G has not yet received the snapshot marker from those senders.
| Node | Actual State | Saved State | Saved MSGs |
|---|---|---|---|
| V | S_V1 | — | — |
| G | S_G3 | S_G1 | M_E1, M_V1 |
| E | S_E1 | — | — |
V and E receive the snapshot marker from G. Upon receiving the marker for the first time, each node:
- Records its current state (S_V1 for V, S_E1 for E)
- Forwards the snapshot marker on its outgoing channels
- Starts recording incoming messages for this snapshot
Since E has no pending messages from any channel before receiving the marker, its saved messages are None.
| Node | Actual State | Saved State | Saved MSGs |
|---|---|---|---|
| V | S_V1 | S_V1 | — |
| G | S_G3 | S_G1 | M_E1, M_V1 |
| E | S_E1 | S_E1 | None |
The snapshot ends for each node. Once a node receives the marker from all its incoming channels, its local snapshot is complete. No further messages are saved for this snapshot. The snapshot is persisted to disk.
| Node | Actual State | Saved State | Saved MSGs |
|---|---|---|---|
| V | S_V2 | S_V1 | None |
| G | S_G4 | S_G1 | M_E1, M_V1 |
| E | S_E2 | S_E1 | None |
The saved states (S_V1, S_G1, S_E1) together with the recorded channel messages form a consistent global snapshot of the distributed system at a logical point in time.
The repository includes a complete example application that demonstrates the library in a realistic scenario: a distributed system of banks performing money transfers.
Each node in the system represents a bank, holding customer ledgers (a map of customer names to balances). Banks are connected via RMI and can:
- Transfer money — A bank debits a local customer and sends an RMI call to the recipient bank, which credits the recipient customer
- Transfer ledgers — When a bank is shutting down, it can transfer all its customer accounts to another bank
- Connect/disconnect — Banks can dynamically join or leave the network
The global state to preserve is the total amount of money spread across all banks in the network. The Chandy-Lamport snapshot ensures that this invariant is captured consistently, even while transfers are in-flight.
The example application implements:
| Class | Role |
|---|---|
Node |
Extends Snapshottable<State, Message>. Represents a bank node with transfer operations, network management, and snapshot restoration via reflection-based message replay. |
State |
The serializable state containing the ledger (customer balances) and the sets of incoming/outgoing connections. |
Message |
A serializable message that stores a method name, parameter types, and parameters — enabling reflective replay during restore. |
Connection |
Implements ConnInt. Represents a connection to another bank identified by host, port, and name. |
PublicInt |
Remote interface exposing bank operations: transfer, transferLedger, addConn, removeConn. |
Main |
Entry point with an interactive CLI for performing transfers, managing connections, and triggering snapshots. |
Each bank node requires two XML configuration files in its working directory:
config.xml — Node configuration:
<config>
<host>192.168.1.10</host>
<port>1099</port>
<name>Hype</name>
<defaultCustomer>Emanuele</defaultCustomer>
<newNetwork>true</newNetwork> <!-- true only for the first node -->
<incoming> <!-- required when joining an existing network -->
<host>192.168.1.20</host>
<port>1099</port>
<name>Intesa</name>
</incoming>
<outgoing>
<host>192.168.1.20</host>
<port>1099</port>
<name>Intesa</name>
</outgoing>
<canLeaveNetworkSafely>true</canLeaveNetworkSafely>
</config>ledgers.xml — Initial customer balances:
<ledgers>
<user>
<name>Emanuele</name>
<balance>150</balance>
</user>
<user>
<name>Lia</name>
<balance>50</balance>
</user>
<user>
<name>Lorenzo</name>
<balance>100</balance>
</user>
</ledgers>To run a node:
java -jar Node.jarThe interactive CLI provides the following operations:
- A — Trigger a snapshot
- B — Perform a generic bank transfer
- E / G / V — Quick transfers to predefined recipients
- C — Connect to a new outgoing node
- CC — Print all connections
- S — Print all customer balances
- X — Shut down the bank (transfer customers to another bank and leave the network)
.
├── README.md
├── .gitignore
├── docs/
│ ├── generate_diagrams.py # Python script to regenerate all diagrams
│ └── images/
│ ├── architecture.png # Library component overview
│ ├── class_diagram.png # Full class diagram (library + example)
│ ├── snapshot_algorithm.png # Snapshot algorithm flow
│ ├── restore_algorithm.png # Restore algorithm flow
│ ├── network_creation.png # Network creation steps
│ ├── network_changes.png # Safe vs unsafe disconnect
│ ├── snapshot_scenario.png # Step-by-step snapshot walkthrough
│ └── bank_example.png # Bank transfer example architecture
├── SnapLib/ # The library
│ ├── src/it/polimi/ds/ricciosorrentinotriuzzi/snaplib/
│ │ ├── Snapshottable.java # Core abstract class
│ │ ├── Snapshot.java # Snapshot data structure
│ │ ├── SnapInt.java # Remote interface
│ │ └── ConnInt.java # Connection interface
│ └── out/artifacts/SnapLib/
│ └── SnapLib.jar # Compiled library
└── Node/ # Example application
├── src/it/polimi/ds/ricciosorrentinotriuzzi/snaptest/
│ ├── Node.java # Bank node implementation
│ ├── State.java # Bank state (ledger + connections)
│ ├── Message.java # Reflective message for replay
│ ├── Connection.java # ConnInt implementation
│ ├── PublicInt.java # Remote bank interface
│ └── Main.java # Interactive CLI entry point
├── lib/ # Dependencies
│ ├── SnapLib.jar
│ ├── commons-configuration-1.10.jar
│ ├── commons-collections-3.2.2.jar
│ ├── commons-lang-2.6.jar
│ └── commons-logging-1.2.jar
└── out/artifacts/Node/
└── Node.jar # Compiled example application
Add SnapLib.jar to your project's classpath. The library has no external dependencies beyond the Java standard library (specifically java.rmi).
To use the library, create a class that extends Snapshottable<S, M> with your application-specific state and message types:
public class MyNode extends Snapshottable<MyState, MyMessage> {
private MyState state;
public MyNode(int port) throws RemoteException, AlreadyBoundException {
super(port); // Registers RMI, checks for crashes, starts restore if needed
state = new MyState();
}
@Override
public MyState getState() {
return state;
}
@Override
public String getHost() {
return "192.168.1.10"; // This node's unique identifier
}
@Override
public Set<ConnInt> getInConn() {
return state.getIncomingConnections();
}
@Override
public Set<ConnInt> getOutConn() {
return state.getOutgoingConnections();
}
@Override
public void restoreSnapshot(Snapshot<MyState, MyMessage> snapshot) {
this.state = snapshot.getState();
for (MyMessage msg : snapshot.getMessages()) {
// Replay each message to reach the consistent state
processMessage(msg);
}
}
}Key points:
- Both
MyStateandMyMessagemust implementSerializable - Call
addMessage(sender, message)when receiving a message from another node, so the library can record in-transit messages during snapshots - Call
shouldDiscard(sender)before processing messages to check if they should be discarded during a restore - Call
initiateSnapshot()to start a new snapshot - Call
applyNetworkChange()after any topology change to trigger a snapshot - Call
safeExit()before graceful shutdown to prevent false crash detection on next startup - Call
joinNetwork(host, port)to synchronize Lamport clocks when joining an existing network







