1 of 48

Visualizing the 6 rules of Primary-Backup

CSE 452 Spring 2026

2 of 48

Announcements

  • Lab 1, Lab 1 design doc, Pset 1 due today (04/08)
  • Lab 2 design doc, Pset 2 due next Friday (04/17)

3 of 48

Motivating Primary-Backup:

What will we have accomplished in lab 1?

  • Exactly-once execution of RPCs despite unreliable network conditions

What do we aim to achieve in lab 2?

  • To understand replication
  • Tolerate some node failures (ViewServer is still a single point of failure)

4 of 48

General Flow

Pool of Idle Servers

Client

Primary

Backup

View Server

Request

Forward Request

Forward

Ack

Response

Ping

Ping

Ping

GetView

What messages do they send/receive?

5 of 48

General Flow

Pool of Idle Servers

Client

Primary

Backup

View Server

Request

Forward Request

Forward

Ack

Response

Ping

Ping

Ping

GetView

What messages do they send/receive?

ViewReply (VR)

VR

VR

VR

6 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)

7 of 48

Rule 1: What if we did not forward requests before executing them? (primary changes)

Client

Server A

Server B

{}

Server C

{}

Append(foo->bar)

AppendOk(bar)

View 2

Primary: A

Backup: B

{foo: bar}

Append(foo->bar)

8 of 48

Rule 1: What if we did not forward requests before executing them? (primary changes)

Client

Server A

{foo: bar}

Server B

{}

Server C

{}

Get(foo)

View 3

Primary: B

Backup: C

KeyNotFound

9 of 48

Rule 1: What if we did not forward requests before executing them? (different views)

Client

Server A

Server B

{foo: bar}

Server C

{foo: bar}

Append(foo->baz)

View 3

Primary: B

Backup: C

AppendRes(barbaz)

Suppose both the client and Server A do not get the new View…

Append(foo->baz)

View 2

Primary: A

Backup: B

View 2

Primary: A

Backup: B

View 3

Primary: B

Backup: C

{foo: bar}

{foo: barbaz}

10 of 48

Rule 1: What if we did not forward requests before executing them? (different views)

Client

Server A

{foo: barbaz}

Server B

{foo: bar}

Server C

{foo: bar}

View 3

Primary: B

Backup: C

When the client eventually

gets the new view…

Get(foo)

GetRes(bar)

View 3

Primary: B

Backup: C

View 3

Primary: B

Backup: C

11 of 48

Rule 1: Solution

Primary must wait for backup to execute the command before executing it and replying to client.

…could there be an optimization on read commands…?

12 of 48

Rule 1: Solution

Primary must wait for backup to execute the command before executing it and replying to client.

…could there be an optimization on read commands…?

Yes, as long as you are okay with stale reads.

13 of 48

14 of 48

Rule 1: Takeaway

Forwarding!

  • Only execute a command on the Primary after receiving an acknowledgement from the Backup during forwarding
  • The backup is at most one command ahead of the primary at all time.
  • Hint: You may want to use a timer to ensure forwarded messages get processed. (A forwarded message could get dropped)
  • Note: In lab 2, the primary-backup will only handle one client request at a time

15 of 48

Aside: What happens if you try to process more than one client request at a time?

Primary

Backup

Put(“a”, “foo”)

Put(“a”, “foo”)

“a” -> “foo”

Client 1

Put(“a”, “foo”)

Client 2

Put(“a”, “bar”)

Put(“a”, “bar”)

Put(“a”, “bar”)

“a” -> “bar”

“a” -> “bar”

“a” -> “foo”

16 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)

17 of 48

Rule 2: Problem & Takeaway

View 2

Primary: A

Backup: B

View 3

Primary: B

Backup: C

View 4

Primary: C

Backup: D

View 5

Primary: D

Backup: B

Delayed Forward or a

Duplicated Forward

18 of 48

Rule 2: Problem & Takeaway

Takeaway: Have the view number on all messages

View 2

Primary: A

Backup: B

View 3

Primary: B

Backup: C

View 4

Primary: C

Backup: D

View 5

Primary: D

Backup: B

Delayed Forward or a

Duplicated Forward

19 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)

20 of 48

Rule 3: What if the new Primary was not in the previous view?

Client

Server A

{}

Server B

{}

Server C

{}

Server D

{}

Append(foo->bar)

Append(foo->bar)

AppendRes(bar)

AppendRes(bar)

View 2

Primary: A

Backup: B

Server A

{foo: bar}

Server B

{foo: bar}

21 of 48

Rule 3: What if the new Primary was not in the previous view?

Client

Server A

{foo: bar}

Server B

{foo: bar}

Server C

{}

Server D

{}

View 3

Primary: C

Backup: D

Get(foo)

KeyNotFound()

Get(foo)

KeyNotFound()

22 of 48

Rule 3: Takeaway

  • Primary or backup in the previous view will have the most up to date application state. (recall rule 1)
  • An idle server could have an empty application state.

23 of 48

Possible Transitions?

The new primary MUST have been in the previous view

View i

Primary: A

Backup: B

View i+1

Primary: A

Backup: NULL

View i

Primary: A

Backup: B

View i+1

Primary: B

Backup: NULL

View i

Primary: A

Backup: B

View i+1

Primary: B

Backup: A

Some Possible? Transitions

24 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)

25 of 48

Rule 4: What if a non-primary responds to a client?

Server C

{}

View 4

Primary: B

Backup: null

Client

Server A

{}

Server B

{}

Put(a->bar)

View 2

Primary: A

Backup: B

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 3

Primary: B

Backup: C

26 of 48

Rule 4: What if a non-primary responds to a client?

Server C

{}

View 4

Primary: B

Backup: null

Client

Server A

{a:bar}

Server B

{}

Put(a->bar)

View 2

Primary: A

Backup: B

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 3

Primary: B

Backup: C

27 of 48

Rule 4: What if a non-primary responds to a client?

Server C

{}

View 4

Primary: B

Backup: null

Client

Server A

{a:bar}

Server B

{}

PutOk()

View 2

Primary: A

Backup: B

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 3

Primary: B

Backup: C

28 of 48

Rule 4: What if a non-primary responds to a client?

Server C

{}

View 4

Primary: B

Backup: null

Client

Server A

{a:bar}

Server B

{}

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 3

Primary: B

Backup: C

29 of 48

Rule 4: What if a non-primary responds to a client?

Server C

{}

View 4

Primary: B

Backup: null

Client

Server A

{a:bar}

Server B

{}

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 3

Primary: B

Backup: C

Get(a)

30 of 48

Rule 4: What if a non-primary responds to a client?

Server C

{}

View 4

Primary: B

Backup: null

Client

Server A

{a:bar}

Server B

{}

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 4

Primary: B

Backup: null

View 3

Primary: B

Backup: C

KeyNotFound()

31 of 48

Rule 4: Takeaway

  • Client is not guaranteed that they will be talking to the primary
  • Any node could be on an outdated view
  • Servers should only process/respond to client if they believe they are the primary

32 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)

33 of 48

Recap: State Transfer (State Machine Replication)

  • Why do we state transfer?
    • To bring new backup (formerly idle server) up to date.
  • When do we do a state transfer?
    • If primary receives a new view with a new backup - need to do state transfer
    • V1: {S1, S2}, V2: {S1, S3}
  • What do we transfer?
    • Application state (In lab 2, that’s your entire AMOApplication)

34 of 48

Rule 5: Problem

View 1

Primary: A

Backup: B

Server A

{a:bar}

View 1

Primary: A

Backup: B

Everyone is in view 1…

Server B

{a:bar}

View 1

Primary: A

Backup: B

Server C

{}

View 1

Primary: A

Backup: B

VS

35 of 48

Rule 5: Problem

View 1

Primary: A

Backup: B

Server A

{a:bar}

Server B

{a:bar}

View 1

Primary: A

Backup: B

View 1

Primary: A

Backup: B

VS

Server B dies, ViewServer moves on to view 2. Server C is the “new” backup

View 2

Primary: A

Backup: C

Server C

{}

View 1

Primary: A

Backup: B

36 of 48

Rule 5: Problem

Server A

{a:bar}

View 1

Primary: A

Backup: B

VS

Server A dies and Server C is made primary (without view 2 ever being acked by A)

View 2

Primary: A

Backup: C

View 3

Primary: C

Backup: null

Server C

{}

View 1

Primary: A

Backup: B

Server B

{a:bar}

View 1

Primary: A

Backup: B

View 3

Primary: C

Backup: null

37 of 48

Rule 5: Problem

Server A

{a:bar}

View 1

Primary: A

Backup: B

VS

But when client shows up again..its not looking good:/

View 3

Primary: C

Backup: null

Server C

{}

Server B

{a:bar}

View 1

Primary: A

Backup: B

View 3

Primary: C

Backup: null

Client

View 3

Primary: C

Backup: null

KeyNotFound()

Get(a)

38 of 48

Rule 5: Problem Explained

  • The issue was changing the view before view 2 was acknowledged by the current primary (Server A) at the time
    • Prerequisite of an acknowledgement includes a completed state transfer to bring a “new” backup up to speed
    • If this is not done we lose app state which leads to a violation of the safety property

39 of 48

Rule 5: Ideal Flow

Server A

{a:bar}

Server B

{a:bar}

View 1

Primary: A

Backup: B

View 1

Primary: A

Backup: null

VS

Ideally, right after B dies, Server A completes a state transfer

View 2

Primary: A

Backup: C

Server C

{}

View 1

Primary: A

Backup: B

StateTransfer((“a” -> “bar”))

Server C

{a:bar}

View 2

Primary: A

Backup: C

View 2

Primary: A

Backup: C

40 of 48

Rule 5: Ideal Flow

Server A

{a:bar}

Server B

{a:bar}

View 2

Primary: A

Backup: C

View 1

Primary: A

Backup: null

VS

And send an ack to ViewServer, to indicate state sync between all involved servers in the current view

View 2

Primary: A

Backup: C

Server C

{a:bar}

View 2

Primary: A

Backup: C

View2 ack

41 of 48

Rule 5: Takeaway

Server A

{a:bar}

View 1

Primary: A

Backup: null

VS

So that when views change and C becomes primary, everything remains consistent

View 3

Primary: C

Backup: null

Server C

{a:bar}

Server B

{a:bar}

View 1

Primary: A

Backup: null

View 3

Primary: C

Backup: null

Client

View 3

Primary: C

Backup: null

GetResult(bar)

Get(a)

42 of 48

Recap: State Transfer (Subtle details)

  • Recall that the ViewServer must not transition onto a new view until the current view has been acknowledged by the primary.
    • During state transfer, primary can just stop sending out pings.
    • Or primary can continue pinging the ViewServer with old view.
  • Backup can receive duplicated/late state transfer messages �(i.e. a state transfer message is duplicated/come later).
    • We need to guarantee that state on backup is only overwritten once per view change (A common source of bugs)
  • What happens if the state transfer ack gets dropped?
    • Resend state transfer message, but backup should not overwrite their state if they have already the state transfer

43 of 48

Executing Duplicate State Transfer Requests

Primary

Backup

StateTransfer((“a” -> “foo”))

StateTransfer((“a” -> “foo”))

StateTransferAck

“a” -> “foo”

“a” -> “foo”

44 of 48

Executing Duplicate State Transfer Requests

Primary

Backup

StateTransfer((“a” -> “foo”))

“a” -> “foobarbaz”

“a” -> “foobarbaz”

“a” -> “foo”

StateTransferAck

Once backup becomes the next primary, clients could get unexpected results.

Takeaway: Execute a state transfer once per view. The backup should still always respond to a state transfer request acknowledging the request.

45 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)

46 of 48

Rule 6: What if we processed requests during state transfer?

Primary

Backup

StateTransfer((“a” -> “foo”))

“a” -> “foo”

Client 1

Put(“a”, “bar”)

Put(“a”, “bar”)

Put(“a”, “bar”)

“a” -> “foo”

“a” -> “bar”

“a” -> “bar”

StateTransferAck

47 of 48

Rule 6: Takeaway

  • State transfer will overwrite the state of the new backup so don’t process requests during a state transfer, otherwise the results could be overwritten.
  • Primary should ignore client requests while in the middle of a state transfer.

48 of 48

Rules:

  1. Primary must wait for backup to accept/execute each op before doing op and replying to client
  2. Backup must accept forwarded requests only if view is correct
  3. Primary in view i+1 must have been backup or primary in view i
  4. Non-primary must reject client requests
  5. ViewServer cannot move on to view i+1 until ack from primary in view i is received
  6. Every operation must be before or after state transfer (Problem with processing requests during a state transfer)