Foundations to FrontierPart IX: The graduate coreBezhig

G08: Cloud systems, partial failure, and consistency

6 exercises, each with a complete solution. 1 figure. 1 code listing.

On this page
  1. Prerequisites and observable goals
  2. The central question: what does each participant know?
  3. Safety, liveness, and user-visible contracts
  4. Quorum intersection is a combinatorial fact
  5. Worked counterexample: the read that goes backward
  6. Logical clocks do not create real-time knowledge
  7. Consensus requires a complete protocol
  8. Retries, idempotent effects, and transactional boundaries
  9. Reliability targets and overload
  10. Independent practice
  11. Smallest useful hints
  12. Complete solutions: G08
  13. Readiness and continuation

Prerequisites and observable goals#

Before

foundation C10–C14, G01 invariants, and G07 network/queue models.

After

Specify a failure model, reason about incomplete requests and quorum intersections, and explain why availability mechanisms do not automatically provide linearizability.

The central question: what does each participant know?#

In one sequential process, a function either returns or raises. Across a network, a sender may not know whether the receiver ran, committed, crashed, or replied into a broken connection. A distributed design must specify both system state and each participant’s knowledge. Treat timeouts as missing knowledge, not proof of nonexecution.

Cloud infrastructure adds mechanisms for provisioning and isolation, but it does not repeal these facts. A virtual machine virtualizes a hardware environment; containers ordinarily isolate processes while sharing a host kernel. Replicas on one host can share its power, storage, kernel, and networking failure. Zones or regions may change the failure correlation but do not imply absolute independence. Define a failure domain operationally: which components can disappear together under a stated event?

Safety, liveness, and user-visible contracts#

A safety property says something bad never happens: a job is never charged twice under one accepted request ID, or a committed register read does not move backward under a linearizable history. A liveness property says something good eventually happens under stated assumptions: an accepted job eventually completes when a worker and storage remain available and retries continue fairly. Permanent outages make unconditional eventual-completion claims untenable.

Linearizability asks whether each completed operation can be placed at one point between its invocation and response, respecting real-time order of nonoverlapping operations, so that the resulting sequential history satisfies the object’s specification. Eventual convergence, by contrast, does not itself promise a fresh value for each read. “Strong consistency” is too vague until a particular contract is named.

The CAP result concerns the inability to guarantee both its specified consistency and availability properties when partitions are possible in the relevant model. It is not a menu where every practical database permanently selects exactly two of three independent features. During a partition, an operation may wait or fail to preserve a consistency guarantee; a different operation may safely return stale or local data under another contract. State the operation, fault, and guarantee rather than repeating a slogan.

Quorum intersection is a combinatorial fact#

For NN replicas, every read set of size RR intersects every write set of size WW exactly when R+W>NR+W>N. If they were disjoint, their union would have R+WR+W distinct members, impossible when this exceeds NN. Conversely, when R+W≤NR+W\le N, choose disjoint subsets of those sizes. Similarly, two write quorums intersect when 2W>N2W>N.

These facts say sets overlap. They do not specify timestamp generation, failure recovery, conflicting writes, read repair, or which incomplete operations may be visible. Therefore they are ingredients of a protocol proof, not a proof of a complete register implementation.

Worked counterexample: the read that goes backward#

Take three replicas initially holding version 0, value old. A writer sends version 1, value new, but only replica A stores it before the writer stalls. Use read/write quorum sizes two. A first read queries A and B and returns the maximum version, new. After that read completes, a second read queries B and C and returns old.

Every completed read contacted two replicas; R+W=4>3R+W=4>3 still holds as a set-size statement. The new write never completed a write quorum. If the first read exposes it, a linearization would need to account for the observed new value; a later nonoverlapping read cannot then revert to old in the absence of another write. A proper register protocol needs additional machinery, such as an appropriate write-back phase and versioning rules. This trace is deliberately unsafe; the lab is a counterexample generator, not a recommended quorum storage implementation.

Time moves downward. The drawing shows chosen query sets and observations, not a implemented transport. The ordering of the two reads is essential to the counterexample.

Logical clocks do not create real-time knowledge#

The happened-before relation includes program order within a process, message send before its receive, and transitive closure. Lamport clocks increment at local events and set the receiver to max⁡(local,message)+1\max(\text{local},\text{message})+1. Then a→ba\to b implies L(a)<L(b)L(a)<L(b). The converse is false: two unrelated events may have ordered clock numbers. Adding a process-ID tie-break makes a total order of timestamps but does not prove causality or elapsed time. [G-LAMPORT]

For process P starting at zero, a send gets timestamp 1. Process Q at 4 receives it and moves to 5. Q’s receive is later causally than that send even though its larger value partly reflects unrelated local events. A five-unit logical-clock difference is not five milliseconds. Physical clock synchronization has different assumptions and uncertainties.

Consensus requires a complete protocol#

A replicated log needs agreement about entries and ordering despite failures, not merely a leader-shaped box. Raft separates leader election, log replication, and safety rules, using terms, quorum decisions, and log restrictions. Majority intersection helps, but leadership terms and the rules governing which entries may commit are essential. This chapter does not implement Raft, and its two-quorum enumeration is not evidence that a consensus implementation is correct. [G-RAFT]

Choose an existing, well-understood system when a real service needs consensus. For study, start by stating the assumptions (crash versus Byzantine behavior, storage persistence, message loss/delay, membership changes), tracing an election or failover from an actual protocol, and testing safety in a bounded model. Do not combine convenient fragments from different protocols and assume their proofs compose.

Retries, idempotent effects, and transactional boundaries#

An idempotent mathematical operation satisfies f(f(x))=f(x)f(f(x))=f(x), but request deduplication is a protocol-level contract. A request ID must bind to a payload; storing only the ID can let a changed request silently inherit an unrelated old answer. The deduplication record and effect must be atomic relative to failures. “Check ID, change state, later save ID” has a crash window that allows repetition.

GP2 uses an outbox row committed with the producer’s job, a consumer receipt committed with the score, and a producer acknowledgement after consumer success. A crash after consumer commit but before acknowledgement causes redelivery, which the consumer detects. This gives at-most-one recorded effect per valid ID/payload under its transaction assumptions and eventual attempts when recovery proceeds. It is not exactly-once network delivery, and it does not survive arbitrary corrupt storage or guarantee that an unrelated external side effect was atomic.

Reliability targets and overload#

Define a service-level indicator from user-visible events, then a target and window. If an availability objective is 99.9% over a 30-day window measured by elapsed time, its error budget is .001(30)(24)(60)=43.2.001(30)(24)(60)=43.2 minutes. A request-based indicator has a request-count denominator instead. Percentile latency must state success filtering and workload; excluding failures can make a broken service appear fast. An SLO is an engineering target, not a warranty. [G-SLO]

Under overload, bound queues and retry work. Use backoff with randomness and attempt/deadline limits; otherwise clients can synchronize and amplify failure. Separate transient failures from permanent bad requests. Preserve enough logs, metrics, and trace identifiers to explain which boundary failed, while keeping secrets and personal data out of logs. [G-OVERLOAD]

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

G08.01CSolution

For N=5,R=2,W=3N=5,R=2,W=3, construct disjoint read and write sets or prove none exist. Repeat for R=3,W=3R=3,W=3.

G08.02TSolution

Explain the three-replica incomplete-write counterexample in terms of the real-time order of the reads. Why does merely saying “majority reads” not repair it?

G08.03CSolution

P has logical clock 2 and sends a message after incrementing. Q has clock 7 and receives it. Find the send and receive timestamps. Does L(a)<L(b)L(a)<L(b) prove aa caused bb?

G08.04TSolution

An API commits a payment effect and then inserts its deduplication ID in a separate transaction. Give a crash trace and redesign the atomic boundary without claiming exactly-once network transport. Use a hypothetical classroom effect, not real payments.

G08.05CSolution

Compute the elapsed-time error budget for 99.95% availability over 28 days. Explain how a request-based budget would instead be computed.

G08.06LSolution

Implement the bounded quorum enumerator, logical clock, and deliberately unsafe register trace in starters/distributed.py. Exhaustively check the intersection condition for small replica counts and preserve the read-regression counterexample as an expected test. State explicitly which code is not a safe storage protocol.

Smallest useful hints#

For .01, compare the union size with NN. For .04, place a crash between the two commits. For .06, an expected counterexample is a successful test of a bad idea, not a failed implementation of a good one.

Complete solutions: G08#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

G08.01Show solution

Strict inequality matters

For sizes two and three among five, choose R={0,1}R=\{0,1\} and W={2,3,4}W=\{2,3,4\}; they are disjoint. Equality R+W=NR+W=N is insufficient. With sizes three and three, a disjoint union would require six members among five, impossible. Intersection is guaranteed, but protocol correctness requires more than set sizes.

Back to G08.01
G08.02Show solution

An exposed incomplete write can make a later read stale

The first read returns version 1 from A and finishes before the second starts. The second sees only B and C at version 0. In a linearizable single-register history without intervening writes, returning new then old in that nonoverlapping order is invalid. Both reads used a majority, but neither propagated the discovered value or ensured it was finalized by an appropriate protocol. More replicas alone do not specify the missing rule.

Back to G08.02
G08.03Show solution

Causal order implies clock order, not conversely

The send increments P from 2 to 3. Q receives and sets its clock to max⁡(7,3)+1=8\max(7,3)+1=8. An ordered pair of timestamp values does not prove causation; unrelated events can also receive 3 and 8. The clock is a consistent ordering aid, not a stopwatch or complete causal relation.

Back to G08.03
G08.04Show solution

Record the effect and its identity together

The first transaction commits the effect, then the process crashes before storing the ID. The client retries; lookup finds no ID and repeats the effect. Put the effect and request-ID/payload/outcome record in the same atomic transaction when they share a transactional store. A retry with the same payload reads the prior outcome; a changed payload under the ID is rejected. An external effect in another system needs a compatible protocol or compensation; one local transaction cannot magically include arbitrary remote state.

Back to G08.04
G08.05Show solution

Use the stated denominator

The allowed fraction is 1−.9995=.00051-.9995=.0005. A 28-day window has 28⋅24⋅60=4032028\cdot24\cdot60=40320 minutes, giving 20.16 minutes. A request-based budget for MM eligible requests is .0005M.0005M unsuccessful requests under the SLI definition. Requests are not interchangeable with minutes when traffic varies.

Back to G08.05
G08.06Show solution

Complete reference counterexample tools

The enumerator returns a witness pair of disjoint sets or None. Its purpose is to make the combinatorial theorem tangible for N≤12N\le12; enumeration is not a production strategy for choosing quorums. The logical-clock implementation validates timestamps. The register function intentionally returns a new value followed by an old value to preserve the unsafe trace.

graduate_code/solutions/distributed.pyPython
"""G08: bounded combinatorial checks, not a production storage protocol."""
from dataclasses import dataclass
from itertools import combinations


def disjoint_quorums(n, read_size, write_size):
    if any(type(x) is not int for x in (n,read_size,write_size)):
        raise ValueError("strict integer sizes required")
    if not 1 <= n <= 12 or not 1 <= read_size <= n or not 1 <= write_size <= n:
        raise ValueError("bounded valid quorum sizes required")
    for readers in combinations(range(n),read_size):
        complement = [i for i in range(n) if i not in readers]
        # Enumerate write quorums disjoint from this read set only.
        for writers in combinations(complement,write_size):
            return set(readers),set(writers)
    return None


@dataclass
class LogicalClock:
    value: int = 0

    def __post_init__(self):
        if type(self.value) is not int or self.value < 0:
            raise ValueError("nonnegative integer clock required")

    def tick(self):
        self.value += 1
        return self.value

    def receive(self, timestamp):
        if type(timestamp) is not int or timestamp < 0:
            raise ValueError("nonnegative integer message timestamp required")
        self.value = max(self.value,timestamp)+1
        return self.value


def unsafe_register_trace():
    """Deliberately violates linearizable read behavior; never use as storage."""
    replicas = {name:(0,"old") for name in "ABC"}
    replicas["A"] = (1,"new")  # incomplete write, no write quorum
    first = max(replicas[name] for name in "AB")
    second = max(replicas[name] for name in "BC")
    return first,second


if __name__ == "__main__":
    print(disjoint_quorums(5,2,3))
    print(unsafe_register_trace())
Back to G08.06

Readiness and continuation#

Advance when you can distinguish a timeout from rollback, quorum overlap from a register protocol, and causal order from a timestamp comparison. Continue with a single complete consensus protocol and its model before attempting an implementation. GP2 applies a smaller transaction/retry argument you can test locally without claiming real distributed deployment.