Distributed Systems

Fault Tolerance

Dependability, reliable communication, and recovery
≈ 37 min read · 8049 words

Every protocol studied so far assumed that processes and channels are reliable. That assumption is now dropped: processes may crash, channels may lose packets, and the system must keep working anyway. This chapter is about tolerating partial failure, which is the defining challenge of a distributed system. We first fix the vocabulary (dependability, faults, failure modes, redundancy), then walk through what can go wrong in the simplest possible system, a single client and a single server, and progressively scale up to reliable group communication and, finally, to recovery: how a process that has crashed is brought back to a consistent state.

Reaching internal agreement inside a group (the consensus problem, FloodSet, the Byzantine generals, commit protocols, Raft) is a topic in its own right and is deferred to the next chapter, Agreement. Here the group only needs to communicate reliably, not to decide.

1. Why Be Fault Tolerant? Dependability#

A dependable system is one we can rely on: it operates as expected even in unforeseen situations. Dependability is not a single property but a bundle of four.

Dependablewe can rely on itAvailabilityready when neededReliabilityruns long without failSafetyfails without harmMaintainabilityeasy to modify

Availability means the system is ready to use whenever we need it: when we look for it, we find it running. Reliability means the system runs continuously for a long time without interruption.

Availability is not reliability

Consider a system that goes down for one millisecond every hour. It is highly available, about 99.999% (“five nines”) of the time it is up and it restarts almost immediately, yet it is deeply unreliable, because it never runs for more than an hour without an interruption. We usually want both.

Safety means that even under the worst unforeseen conditions the system never puts us in a harmful situation. A safe word processor may be unreliable or unavailable (it crashes when the power is cut), but it never loses our data, because it saves continuously. It can fail, but it fails gracefully. Maintainability means the system is easy to modify. This is an internal property, but it strongly influences the other three: if we can easily change the system, we can correct the situations that make it crash, hang, or lose data.

2. Faults, Errors, and Failures#

These three terms are distinct and form a causal chain.

Faultroot causeErrorbad internal stateFailureobservable outcomebad code, dead batterydangling pointer, no powerthe process crashes

Testing puts failures in evidence (it finds inputs that make the system fail), but the goal is always to identify and fix the underlying fault, not just to observe its symptom.

Some faults are under our control (a programmer’s mistake, a battery we can keep charged); others are not (a power cut, a severed cable). In a centralised system an uncontrollable hardware fault is equivalent to a total crash: there is a single machine, and nothing can be done. In a distributed system such a fault typically affects only part of the runtime. We do not want the failure of a single disk in a data centre to bring down the whole system. Tolerating partial failure is exactly the core problem of distributed fault tolerance.

3. Classifying Failures#

3.1 By duration#

Type Description
Transient Occurs once and does not recur, or recurs very rarely. Generally not a serious problem.
Permanent Persists until explicitly corrected (a crashed disk). It will not fix itself, but it is diagnosable and repairable.
Intermittent Appears and disappears unpredictably (a flaky Wi-Fi link). The hardest to diagnose.

Intermittent faults are the most problematic: the system crashes, you re-run it, and this time the fault does not reappear, so pinning down the root cause becomes very difficult.

3.2 By failure mode#

FailuresOmissionskips an actionByzantinewrong outputTimingbound violatedprocess: crashfail-stop / silent / safechannel: dropsend / channel / receiveprocess: wrong codechannel: corruptedCRC turns channel corruption into omission; process Byzantine faults are rare (usually an attack)

Omission failures occur when a component fails to perform an expected action.

Byzantine (arbitrary) failures occur when a component keeps operating but produces incorrect results: a process executes the wrong program (omitting steps or inventing new ones), or a channel delivers a packet whose contents have been corrupted.

Timing failures exist only in a synchronous system: one of the assumed bounds is violated, for example a packet arrives later than the maximum assumed latency. Since almost every algorithm we study assumes synchrony, timing failures are worth naming.

In practice:

4. Masking Failures with Redundancy#

The general tool for tolerating faults is redundancy, in three flavours.

Information redundancy adds redundant data so errors can be detected and corrected. A Hamming code in a packet can not only detect a flipped bit but reconstruct the original, with no retransmission.

Time redundancy detects an error and retries: send a packet, notice the missing acknowledgement, resend. This works well for transient and intermittent faults.

Physical redundancy deploys multiple copies (replicas) of a component so that if one fails the others continue. This is the most common strategy in distributed systems, and the oldest: biology got there first. Examples include RAID disk mirroring and process replication.

inputmodule 1module 2module 3faultyvotermajorityout

The classic hardware pattern is Triple Modular Redundancy (TMR): each sub-module is triplicated and its outputs pass through a majority voter, so a single faulty sub-module is silently outvoted.

4.1 How much redundancy? Crash versus Byzantine#

The degree of replication needed depends on the type of failure being tolerated. Suppose we want an external client to obtain a correct answer from a replicated component.

Crash: k + 1 replicasokokone survivor is enoughByzantine: 2k + 1 replicas838majority outvotes the liar

Byzantine faults are therefore far more expensive to tolerate than crashes. This distinction (k+1k+1 vs 2k+12k+1) returns, in a sharper form, when the group must reach an internal decision rather than answer an outside client, which is the subject of the Agreement chapter.

5. Reliable Client-Server Communication#

To see how much can go wrong in practice, take the simplest possible distributed system: one client and one server exchanging messages.

5.1 Reliable point-to-point communication and TCP#

The foundation is reliable point-to-point delivery, usually provided by TCP. TCP masks channel omission with acknowledgements and retransmission (time redundancy), but with important caveats.

In short, TCP solves the vast majority of channel-level problems, but completely masking every failure is impossible: in some situations TCP gives up and raises an exception to the application.

5.2 The RPC illusion and its failure modes#

RPC aims to give the caller the illusion of a local procedure call: transparent, reliable, executed exactly once. A local call fails only if the whole machine crashes; a remote call has many more failure modes.

clientserverrequestreply1 cannot locate2 request lost3 server crashes4 reply lost5 client crashes
  1. The server cannot be located at the outset.
  2. The request is lost before reaching the server.
  3. The server crashes during execution.
  4. The reply is lost after the server has finished.
  5. The client crashes after sending the request.

Case 1 can be handled by raising an exception or redirecting to another server. The rest are harder, and the fundamental obstacle is that the client cannot tell them apart: if no reply arrives, it does not know whether the request was lost, the server crashed before executing, the server crashed after executing, or the reply was lost.

5.3 Delivery semantics: at-most-once, at-least-once, exactly-once#

Consider a concrete print server: the client sends a document, the server prints it and returns a confirmation. The channel is reliable (TCP, no partitions). The only source of failure is the server crashing at an unpredictable point: before, during, or after printing.

Two things are under our control:

Enumerating every client strategy against both server strategies and every crash timing (assuming a single crash) gives the following. Outcomes are OK (printed once), DUP (printed twice), ZERO (never printed); C marks the crash point.

Client reissue strategy MPC MC(P) C(MP) PMC PC(M) C(PM)
Always reissue DUP OK OK DUP DUP OK
Never reissue OK ZERO ZERO OK OK ZERO
Reissue only when ACKed DUP OK ZERO DUP OK ZERO
Reissue only when not ACKed OK ZERO OK OK DUP OK

(The first three columns are the M then P server strategy, the last three P then M.) No row is “OK” across all six crash timings: every strategy risks a duplicate or a missing print somewhere.

the server can crash before / during / after the workprint (P)M?M?crashAt-most-oncenever retry: may ZEROAt-least-onceretry: may DUPExactly-oncecannot be guaranteedno strategy avoids both a duplicate and a missed print across all crash timings
Semantics How to obtain it Trade-off
At-most-once Client sends once, never retries. The document may never be printed.
At-least-once Server confirms after printing; client retries until confirmed. The document may be printed more than once.
Exactly-once Cannot be guaranteed. See below.
Exactly-once is impossible in general

When the server can crash at an arbitrary point, no combination of client and server strategies eliminates both the risk of zero executions and the risk of duplicates. At-most-once and at-least-once are each easy; exactly-once is not achievable in the general case.

A partial fix for duplicates is to attach a unique ID to each request, keep a history of processed IDs on the server, and discard (only re-acknowledging) any request already seen. This gives exactly-once in practice, but it needs, in principle, unbounded memory for the history (a delayed request could arrive arbitrarily late) and therefore assumptions on maximum traffic and delay to bound that memory. If those assumptions are violated (a timing failure), a duplicate may still slip through.

5.4 Lost replies and idempotency#

When the reply is lost, the client faces the same dilemma and the same risk of a duplicate on retry. The practical escape is to make operations idempotent: executing the request several times has the same effect as executing it once. This is automatic for stateless servers: “what is 5+35+3?” always returns 88, however many times it is asked. With idempotent operations, at-least-once is enough, since duplicate executions are harmless. This is a major argument for stateless, functional server design.

5.5 Client crashes: orphan computations#

A client crash seems harmless but can leave an orphan on the server. If the client’s request spawned a server thread, that thread may block indefinitely waiting to confirm delivery of its reply (especially over TCP) after the client has vanished.

clientserver threadorphan, waitingrequestExterminationlog + killReincarnationepoch broadcastGentle reinc.resume if possibleExpirationtime-to-livea crashed client orphans a server thread; expiration (TTL) is the common fix

Killing an orphan thread is conceptually simple but practically involved: the mechanism to force-terminate a thread must exist, and any resources it opened (files, locks, connections) must be cleaned up.

6. Process Resilience and Groups#

The primary technique for surviving process crashes is replication: instead of a single server, run a group of processes in the same role, so that if one crashes the others carry on.

This is easy for stateless services (there is no state to keep in sync; a client is simply directed to any surviving member). For stateful services the replicas must stay synchronised: a state change at one replica must reach all others, so that a client redirected to a different replica after a crash finds the state it expects.

Flat grouppeers, no SPOF, needs agreementCoordinated groupCsimple, but the coordinator is a SPOF (re-elect)

A further difficulty is membership management: knowing who is still alive. A crashed process cannot announce its own crash, and a network partition can make some processes unreachable without their having crashed, so a surviving partition may wrongly conclude the others are dead. Distinguishing a crashed process from a merely unreachable one is, in general, impossible.

6.1 How big must the group be?#

For an external client querying the group, the sizing mirrors section 4.1: tolerating kk crashes needs k+1k+1 members (any survivor answers); tolerating kk Byzantine members needs 2k+12k+1 (contact all of them, take the majority vote). To tolerate one Byzantine member, query 3 and vote; to tolerate two, query 5.

Deciding inside the group is harder

When the group must reach an internal decision (elect a coordinator, agree to commit or abort) rather than answer an outside observer, a Byzantine member can mislead different peers differently, and the requirement rises to 3k+13k+1. That is the consensus problem, developed in the Agreement chapter. Here we only need the members to exchange state reliably, which is the next section.

7. Reliable Group Communication#

A stateful replicated group keeps its members in sync by exchanging messages, so it needs reliable multicast: a message sent to the group is reliably delivered to all its members. Assume for now that processes do not fail but the channel may lose packets.

7.1 The point-to-point approach and ACK implosion#

One option is a separate TCP connection from the sender to each member. It works, but on a shared broadcast medium (Ethernet, Wi-Fi) it wastefully retransmits the same message over the same wire. A better base is an underlying broadcast/multicast protocol (for example UDP multicast) with reliability added on top.

The naive way to add reliability, TCP-style positive acknowledgements, causes ACK implosion: when all goes well (the common case) the sender is flooded by an ACK from every receiver at once.

7.2 Scalable Reliable Multicast (SRM)#

The fix inverts the acknowledgement model: receivers stay silent and send a negative acknowledgement (NACK) only when they detect a missing packet. A gap in sequentially numbered packets (receiving 1, 2, 3, 5, 7) reveals that 4 and 6 were missed, possibly after a short wait for out-of-order delivery.

But if a packet is lost near the sender, every receiver misses it and they would all NACK at once, recreating the implosion. The solution is feedback suppression:

senderpackets 1..7R1: gap at 4R2: gap at 4R3: gap at 4first NACK (broadcast)the others hear it and cancel their random timers
  1. On detecting a loss, a receiver does not NACK immediately; it starts a random-delay timer.
  2. When the timer expires it sends the NACK in broadcast (not just to the sender).
  3. If another receiver’s NACK arrives first, it cancels its own timer: the loss has already been reported.

This keeps the number of NACKs per lost packet to one or two regardless of group size. The full protocol (sequential numbering, NACKs, random timers, broadcast NACKs) is Scalable Reliable Multicast (SRM), also called non-hierarchical feedback control. Its cost is that every member processes every NACK, even for packets it received correctly. SRM suits shared broadcast media precisely because it leverages the broadcast nature of the medium for both data and NACK suppression.

7.3 Hierarchical feedback control#

For very large groups even flat SRM generates too much traffic. The group is then organised into a hierarchy of subgroups: each subgroup uses SRM internally, a leader is elected per subgroup, and leaders communicate by unicast. The subgroups form a tree rooted at the sender; a leader requests missing messages from its parent and may drop a buffered message only once all its own receivers and all its child leaders have it. A crashed leader is re-elected. This trades coordination complexity for much lower load at scale.

8. Reliable Multicast with Faulty Processes: Virtual Synchrony#

Now let processes crash as well. Reliable delivery alone is no longer enough: we must keep the group consistent across membership changes. The building block is the distinction between receiving a message (the communication layer buffers it) and delivering it (handing it to the application only once some condition holds). That buffering is exactly what lets the middleware enforce ordering and atomicity.

8.1 Close synchrony versus virtual synchrony#

The ideal, close synchrony, would treat messages as sent instantaneously and reliably: a process crashes either before sending (nobody receives the message) or after sending (everyone receives it), and receivers always learn of a crash after any final message from the departing process. This is impossible, because both message propagation and crash notification take time.

The achievable approximation is virtual synchrony (Birman and Joseph, 1987).

P1P2P3P1 sends, then crashesview changea message from the crashing sender reaches all members or none, before the new view installs

Virtual synchrony is a weaker but implementable property. Informally, it requires:

In other words, group membership changes (a process joining or leaving/crashing) act as epoch boundaries. All messages exchanged within an epoch are fully contained within that epoch, no message bleeds across the membership boundary. This applies symmetrically to processes joining the group: a process should not receive messages from a new member before being informed that the member joined.

Formally, a multicast sent during a particular group view (membership epoch) is delivered either to all members of that view or to none. Group membership changes delimit epochs, and messaging and membership changes appear in a consistent order across all processes. The same constraint applies to a joining member: no process should receive messages from a new member before being told it has joined. The two situations that stay unacceptable are partial delivery and receiving a crash notification before the message.

8.2 Message ordering#

Virtual synchrony says nothing about the order of messages within an epoch. That is an orthogonal choice with two dimensions. Causal strength ranges from unordered (no guarantee), through FIFO (messages from the same sender are delivered in send order), to causal (if sending A causally precedes sending B, everyone delivers A before B). Orthogonally, delivery may be non-totally ordered or totally ordered (all receivers deliver messages in the same order, even if that order differs from real time). The six combinations have standard names:

Causal strength Without total order With total order
Unordered Reliable multicast Atomic multicast
FIFO FIFO multicast FIFO atomic multicast
Causal Causal multicast Causal atomic multicast

The Lamport-clock-based ordering from the Synchronization chapter is exactly a causal atomic multicast: it respects causal order (via timestamps) and is totally ordered (via a deterministic tie-break), delivering in the same order everywhere. Virtual synchrony is layered on top of whichever of the six guarantees is chosen.

8.3 An implementation: the ISIS protocol#

How ISIS enforces virtual synchrony (view-change flush)

The ISIS middleware (Birman et al., 1991) implements virtual synchrony for small replicated groups (typically 3 to 5 replicas) over reliable, FIFO point-to-point channels. The delicate moment is a sender crashing mid-broadcast, with some replicas having the last message and others not.

A message is stable once every current member has acknowledged it; until then it is unstable and must be retained. When a process detects a crash it triggers a view change:

  1. it notifies all other members;
  2. all members suspend delivery of new messages to the application (the epoch freezes);
  3. every member sends all its unstable messages to all others (a flush);
  4. once a member has flushed and received everyone’s flush, it signals ready;
  5. when all are ready, the new view (without the crashed process) is installed and delivery resumes.

This guarantees that a message received by at least one surviving member before the crash is delivered to all survivors before the new view installs: no message is seen by some but not others across a membership boundary. ISIS assumes at most one crash during the flush; its per-message acknowledgement is expensive, which is why it targets small groups of replicated servers rather than large broadcast networks.

9. Recovery#

Fault tolerance so far kept the system running despite a crash. Recovery is the complementary question: when a crashed process comes back, how is it returned to a correct state?

9.1 Backward versus forward recovery#

Backward recoverycheckpointcrashroll backForward recoverycorruptedreconstructedcontinue

Backward recovery returns to a previously known-good state. TCP retransmission is an example: not receiving an ACK, the sender conceptually steps back to “message not yet sent” and resends. So is reloading a periodically saved state from disk. Forward recovery repairs the erroneous state and continues: an error-correcting code reconstructs a corrupted packet and completes the reception without going back; a streaming player interpolates a dropped frame and plays on. For crashing processes in distributed systems, backward recovery dominates.

9.2 Checkpointing#

When a process crashes and restarts, the global state may be inconsistent. Checkpointing periodically saves a consistent global state to stable storage, so that on failure the system reloads it and resumes.

The right checkpoint is a consistent cut (in the sense of the distributed-snapshot algorithm from the Synchronization chapter): a state in which no process has recorded a message as received that the sender does not record as sent. Such a state may never have occurred at a single real instant, yet it is causally valid and therefore a legitimate restart point.

Coordinated checkpointing, built on the global snapshot protocol, guarantees consistency by construction but is expensive: every node coordinates and saves its state at a precise point, writing a potentially large amount of data. Despite the cost it is used in production; enabling fault tolerance in Apache Flink, for instance, activates coordinated checkpointing across all nodes, so that any crash restarts the job from the last checkpoint.

9.3 Logging#

As an alternative to saving the full state, each process can log every operation it performs and, on restart, replay the log to reconstruct its state up to the crash. Each log record is small (cheap per step), but recovery may replay a very long history. In practice the two are combined: periodic checkpoints plus a log of the operations between them. On failure the system reloads the last checkpoint and replays only the log since, keeping both the checkpoint cost and the replay cost manageable. Logging is developed further in section 11.

10. Uncoordinated Checkpointing and the Recovery Line#

10.1 Uncoordinated checkpointing and the domino effect#

Coordinated checkpointing is costly at runtime. Uncoordinated (independent) checkpointing is the cheap alternative: each process saves its own local state to disk independently, with no coordination and without saving channel contents. The price is paid at recovery.

Naively reloading each process’s most recent local checkpoint can yield a globally inconsistent state, for instance one in which P2 has “received” a message that P1 has no record of sending. Such a state is causally invalid. The system must therefore search backwards through the stored checkpoints for the most recent set that forms a consistent cut. In the worst case, rolling one process back orphans a message and forces another rollback, which orphans a further message, and so on, cascading all the way to the start.

P1P2crasheach rollback orphans a message and forces an earlier one: the cascade can reach the start

Whether this happens depends on the checkpoint pattern: with luck the latest checkpoints already form a consistent cut; without it, no pair does and the whole history is lost. The goal is to find the recovery line, the most recent consistent cut among the stored checkpoints.

P1P2P3failurerecovery linethe most recent set of local checkpoints that forms a consistent cut (no orphan message)

10.2 Tracking dependencies between checkpoints#

To find a consistent cut without a centralised snapshot protocol, each process labels its execution into intervals (the periods between consecutive checkpoints) and records inter-process dependencies cheaply:

These records are small (one per received message’s interval, not per message) and are saved with each local checkpoint. No global coordination happens during normal execution. When a failure occurs, all processes (including the recovered one) send their checkpoint history and dependency records to a central coordinator, which reconstructs the full interval-dependency graph and computes the recovery line.

Where the exercise begins

The professor stresses that building the interval-dependency graph is the genuinely distributed, hard part, and it is assumed done. The exam exercise starts from the ready-made graph and asks only for a local computation on it: applying one of the two algorithms below. “The stupid part of the problem is the exercise; the complex part is understanding how everything works.”

10.3 The two algorithms#

Both algorithms turn interval dependencies into edges between checkpoints, then read off the recovery line. They must, and do, give the same answer.

Rollback-dependency graph (RDG).

  1. Turn each interval dependency into a directed edge from the closing checkpoint of the sender’s interval to the closing checkpoint of the receiver’s interval (the checkpoint that ends each interval).
  2. Add the implicit horizontal edges from each checkpoint to the next one of the same process (a checkpoint always depends on its predecessor).
  3. Mark the failed states (the volatile state each crashed process was in at crash time, drawn at the right end).
  4. Propagate the mark forward along all edges, message and horizontal alike; every checkpoint reached becomes marked.
  5. The recovery line is, for each process, its most recent unmarked checkpoint.

Checkpoint-dependency graph (CDG).

  1. Turn each interval dependency into an edge from the opening checkpoint of the sender’s interval (the one that starts it) to the closing checkpoint of the receiver’s interval. (This is the only difference from RDG: opening instead of closing on the sender side.) Keep the horizontal edges.
  2. Remove the failed states outright.
  3. Take a tentative recovery line = each process’s most recent surviving checkpoint.
  4. If any checkpoint in the line can reach another checkpoint in the line (directly or transitively, following edges), remove the destination (arrival) checkpoint of that dependency, rolling that process back one step.
  5. Repeat step 4 until no checkpoint in the line reaches another. The result is the recovery line.
Two easy slips

The horizontal same-process edges are always present, even when the diagram omits them to reduce clutter, and they must be included in the propagation (RDG) and reachability (CDG). And in CDG it is the arrival checkpoint of an offending dependency that is removed, never the source.

10.4 A fully worked example#

The following is the standard example from the course slides, with P0 and P1 both failing. The interval-dependency graph (checkpoints as bars, messages as arrows) is on the left; the two graphs derived from it are below.

The space-time diagram. Four processes with local checkpoints c_{i,x} and application messages; P0 and P1 crash (the two failures at the right). This is the input to both algorithms.
The two derived graphs. Left, the rollback-dependency graph: the two failure states are initially marked, the mark propagates forward and taints the third checkpoints of P0 and P1, and the surviving most-recent checkpoints form the recovery line. Right, the checkpoint-dependency graph reaches the identical line by iteratively removing the arrival of any in-line dependency.

Both methods return the same recovery line: P0 and P1 roll back to c0,2c_{0,2} and c1,2c_{1,2} (their later checkpoints are tainted by the failures they exchanged messages from), while the non-failed P2 and P3 keep their last checkpoints. The lesson to carry into the exam is the mechanical procedure, not the specific values: mark the failures and propagate (RDG), or take the latest surviving set and prune arrivals until no in-line dependency remains (CDG).

10.5 Coordinated checkpointing revisited#

The alternative to all of the above is to coordinate the checkpoints so a consistent cut is guaranteed by construction. A simple coordinator-based scheme: the coordinator asks every process to checkpoint; each saves its state and stops sending new messages until cleared; when all have confirmed, the coordinator broadcasts clearance. Blocking sends during the window is essential, since it prevents an in-flight message from creating a dependency that crosses the checkpoint boundary. The downside is that the whole system blocks for the duration. The practical alternative is the non-blocking global snapshot (Chandy-Lamport) algorithm. Two refinements cut the cost further: an incremental snapshot checkpoints only the processes that actually matter for recovery, and communication-induced checkpointing piggybacks information on application messages so a receiver can decide whether it too must checkpoint to keep the global state consistent, blending uncoordinated runtime cost with coordinated consistency.

10.6 Coordinated versus uncoordinated: the trade-off#

Approach Runtime cost Recovery cost Consistency
Coordinated (global snapshot) High: all processes synchronise periodically Low: the snapshot is always a valid consistent cut Guaranteed by construction
Uncoordinated (independent + dependency tracking) Low: each process acts alone; lightweight tagging High: collect all data, build the graph, compute the line Found only after failure; domino effect possible

Uncoordinated checkpointing is preferred when minimising runtime overhead matters and failures are rare; coordinated checkpointing when recovery must be fast and predictable.

11. Logging in Detail#

11.1 When logging works: piecewise determinism#

Logging assumes execution is piecewise deterministic: between the receipt of two messages a process behaves entirely deterministically, with no other influencing inputs (keystrokes, random numbers, interrupts). Under this assumption, replaying the same sequence of messages reproduces the same sequence of states.

11.2 Stable and unstable messages, orphans#

A message MM is stable if it can no longer be lost, that is, it has been written to stable storage (by the sender before sending, or by the receiver after receipt). An unstable MM lives only in volatile memory. For each unstable MM two sets are defined:

A surviving process QQ is an orphan with respect to MM if QQ \in Depend(MM) but every process in Copy(MM) has crashed: MM can never be replayed, yet QQ’s state assumes it was received. Replaying the log without MM would place QQ in a state it never actually passed through. To rule orphans out entirely it suffices to ensure that every process in Depend(MM) is also in Copy(MM): then MM is lost only if all dependent processes crashed too, so no survivor is ever orphaned.

11.3 Pessimistic versus optimistic logging#

In pessimistic logging the system guarantees no orphan is ever created. The rule: a process must stabilise a message before sending anything that causally depends on it. The simplest realisation is that the sender writes MM to its log just before transmitting, so MM is always on stable storage in Copy(MM). It is “pessimistic” because it assumes crashes are frequent and pays the I/O cost on every message; the reward is clean recovery, with no orphan detection needed.

In optimistic logging messages are logged asynchronously, cutting the per-message overhead but allowing orphans to appear on failure. Recovery must then compute the Depend and Copy sets, identify the orphans, and force them to roll back to a checkpoint before the dependency (or restart). The orphan-detection machinery is intricate and not detailed here.

11.4 Combining checkpointing and logging#

12. Exam Questions#

The exercises below are drawn from past exams of this course. Worked solutions are unofficial (the instructor does not publish official ones); where the recovery-line method is used, it follows exactly the two-graph procedure of section 10.3 and the worked example of section 10.4.

12.1 Classifying failures#

Exam of 6 September 2024, question 1

Discuss (classify) the various types of failure that may happen in a distributed system, and give a practical example of each.

Solution

Classify along two axes (section 3).

By duration. Transient: a cosmic-ray bit flip that never recurs. Permanent: a disk whose controller has died and stays dead until replaced. Intermittent: a loose network cable that drops packets only when the rack vibrates, so a re-run often “works”.

By mode. Omission (an expected action is skipped): a process crash (fail-stop if detectable, fail-silent if it just goes quiet, fail-safe if it emits a recognisably wrong output), or a channel dropping a packet (send / channel / receive omission). Byzantine / arbitrary (the component runs on but is wrong): a process executing corrupted code and returning 1111 for 3+53+5, or a channel delivering a corrupted packet. Timing (synchronous systems only): a reply that arrives after the assumed maximum latency, violating an assumed bound.

In practice channels tend to fail Byzantine (corruption), but a CRC turns that into omission; processes tend to fail by omission (crash), Byzantine process faults being rare and usually the mark of an attack.

12.2 Reliable group communication, with and without faulty processes#

Exam of 14 February 2024, question 4

Describe how to efficiently provide reliable group (multicast) communication among a set of reliable processes. How do things change if processes may fail? Explain how the problem changes and how to address the new situation.

Solution

Reliable processes, lossy channel (section 7). Building on a broadcast/multicast medium, positive ACKs cause ACK implosion. Invert the scheme: receivers stay silent and send a NACK only on detecting a gap in the sequence numbers. To avoid a NACK storm when a packet is lost near the source, use feedback suppression: each receiver waits a random time and then sends its NACK in broadcast; hearing another’s NACK cancels its own timer, so only one or two NACKs are sent per loss. This is Scalable Reliable Multicast. For very large groups, organise a hierarchy of subgroups, each running SRM internally with an elected leader, leaders linked by unicast.

If processes may fail (section 8), reliable delivery is no longer enough: a sender may crash mid-broadcast, leaving some members with its last message and others without. We now require virtual synchrony: a message multicast in a given group view is delivered to all members of that view or to none, and no member is told of the crash before receiving the message. It is implemented (as in ISIS) with a view-change protocol: on detecting a crash, members freeze delivery and flush their unstable messages (received but not yet acknowledged by everyone) to all others, so any message that reached at least one survivor reaches them all before the new view (without the crashed process) is installed. Message ordering within a view (unordered / FIFO / causal, each optionally totally ordered) is an orthogonal guarantee layered underneath.

12.3 Scalable reliable multicast#

Exam of 6 September 2024, question 4

Describe scalable reliable multicast in detail, clarifying the problem it solves and the assumptions it makes.

Solution

Problem. Deliver a message reliably to every member of a group, efficiently, over a medium that can lose packets. Assumptions. An underlying broadcast/multicast medium (Ethernet, Wi-Fi, UDP multicast) that is cheap to broadcast on but unreliable; non-faulty processes (SRM addresses channel loss, not process crashes); packets carry sequence numbers and the sender’s flow is roughly continuous, so a gap is detectable.

Mechanism (section 7.2). Avoid ACK implosion by using negative acknowledgements: a receiver detects a loss as a gap in the sequence numbers (after a short wait for reordering) and, instead of ACKing every packet, NACKs only the missing one. Avoid NACK implosion (many receivers missing the same packet lost near the source) with feedback suppression: on detecting a loss, start a random timer; on expiry, send the NACK in broadcast; if another receiver’s NACK is heard first, cancel the timer. So at most one or two NACKs per lost packet, independent of group size. The trade-off is that every member processes every NACK, and the retransmission is likewise multicast. This design is also called non-hierarchical feedback control; for very large groups it is nested inside a hierarchy of subgroups.

12.4 Computing the recovery line#

Exams of 17 January 2024 (Q3), 14 February 2024 (Q3), and 6 September 2024 (Q3)

Calculate the recovery line for the two diagrams below using the rollback-dependency graph for the first one and the checkpoint-dependency graph for the second one. (The January paper adds: explain when this kind of graph is used and how the data to build it is collected.)

All three exams pose the identical exercise on different diagrams. The method is the two-graph procedure of section 10.3, demonstrated end to end on the worked example of section 10.4; the when/how part is answered by section 10.2 (each process tags messages with its interval id, receivers record cross-interval dependencies, and on a failure all of this is shipped to a central coordinator that builds the graph and runs the algorithm).

17 Jan 2024, first diagram (use RDG). P1 and P3 fail. 17 Jan 2024, second diagram (use CDG). P1 and P3 fail.

14 Feb 2024, first diagram (use RDG). P3 fails. 14 Feb 2024, second diagram (use CDG). P1 fails.

6 Sep 2024, first diagram (use RDG). P1 fails. 6 Sep 2024, second diagram (use CDG). P4 fails.

Solution (method and how to read the answer off each figure)

For every one of these six diagrams, proceed exactly as in the worked example of section 10.4.

Using the RDG (first diagram of each pair).

  1. Redraw the checkpoints and add, for each tagged message, an edge from the closing checkpoint of the sender’s interval to the closing checkpoint of the receiver’s interval; add the horizontal per-process edges.
  2. Mark the crash state(s) of the failed process(es) (17 Jan: P1 and P3; 14 Feb: P3; 6 Sep: P1) and propagate the mark forward along every edge.
  3. Read off, per process, the most recent unmarked checkpoint.

Using the CDG (second diagram of each pair).

  1. Same, but each message edge runs from the opening checkpoint of the sender’s interval to the closing checkpoint of the receiver’s interval; drop the crash states.
  2. Take the latest surviving checkpoint of each process; while some checkpoint in the line reaches another in the line, delete the arrival checkpoint of that dependency; stop when none does.

Reading the outcome. The failed process itself falls back to its last saved checkpoint (its volatile post-crash state is gone). The rollback then cascades only to a process that received a message which the rollback turns into an orphan (a receive with no matching send), and such a process is pulled back one checkpoint at a time until the cut is consistent; every other process keeps its most recent checkpoint. Concretely, a survivor is forced to roll back when it received a message that a crashed process sent in its final (now-lost) interval, or, transitively, from an interval that itself had to be rolled back. Trace each message in the printed diagram against this rule to obtain the line; both graphs must agree.

Because these worked answers are unofficial and depend on reading the exact endpoints of hand-drawn arrows, the reliable takeaway is the procedure and the fact that both graphs yield the same line, which is what the exam grades. As the instructor put it, “do your best to show that you captured the point and made the reasoning.”

12.5 Where to find the rest of this paper’s questions#

The other questions on these papers belong to neighbouring chapters: the FloodSet algorithm (17 Jan Q4) and Raft (17 Jan Q7, 6 Sep Q7) are in Agreement; the consistency schedules (17 Jan Q5, 6 Sep Q5) in Replication and Consistency; clock synchronization (14 Feb Q2, 6 Sep Q2) in Synchronization; Chord (14 Feb Q6) in Peer-to-Peer; the dataflow model (14 Feb Q7, 6 Sep Q6) in Distributed Analytics.

13. Glossary#

Term Meaning
Dependability Umbrella property: availability, reliability, safety, maintainability.
Availability Fraction of time the system is ready to use.
Reliability Ability to run continuously for a long time without interruption.
Fault / Error / Failure Root cause / bad internal state / observable loss of service.
Omission failure A component skips an expected action (crash, dropped packet).
Byzantine failure A component keeps running but produces wrong output.
TMR Triple Modular Redundancy: triplicate a module and majority-vote its output.
At-most-once / at-least-once Delivery semantics: never a duplicate (may lose) / never a loss (may duplicate).
Idempotent An operation whose repeated execution has the same effect as one execution.
Orphan A server computation left running by a crashed client; or, in logging, a survivor depending on a message that can no longer be replayed.
Reliable multicast Deliver a message to all group members despite channel loss.
SRM Scalable Reliable Multicast: NACK-based reliable multicast with random-timer feedback suppression.
Virtual synchrony A message multicast in a group view reaches all members of that view or none, ordered consistently with membership changes.
Atomic multicast Totally ordered reliable multicast.
Consistent cut / recovery line A global state with no orphan message; the most recent such set of checkpoints.
Coordinated / uncoordinated checkpointing Processes checkpoint together (consistent by construction) / independently (cheap, may domino).
Domino effect A cascade of rollbacks that can discard the whole execution history.
RDG / CDG Rollback- / Checkpoint-dependency graph: the two algorithms that compute the recovery line.
Piecewise determinism Between two message receipts, a process behaves deterministically (the logging assumption).
Pessimistic / optimistic logging Stabilise before dependent sends (no orphans) / log asynchronously (orphans possible, rolled back).

Compiled from G. Cugola's lectures (A.Y. 2025/26), slides + class transcriptions. Not official course material.