| index 4431920ab..62c1eb22a 100644 |
| --- a/internal/rollout/config_test.go |
| +++ b/internal/rollout/config_test.go |
| @@ -57,3 +57,63 @@ func TestASleepersAckGoesOutAtDeliveryAsHibernating(t *testing.T) { |
| t.Fatalf("ack = %+v, want Hibernating and not Restarted", got) |
| } |
| } |
| + |
| +// Spec §8.1: a revision is acknowledged only once active. A held ack that a |
| +// newer delivery bypassed must never be released afterwards, or it would be |
| +// written after the newer ack with a revision that is no longer delivered. |
| +func TestANewerAckSupersedesTheHeldOne(t *testing.T) { |
| + t.Run("revert to the active revision", func(t *testing.T) { |
| + var wrote []string |
| + write := func(a appconfig.ConfigAck) { wrote = append(wrote, a.Host) } |
| + tr := &configTrack{delivered: "c2", active: "c1"} |
| + if now := tr.deferAck(appconfig.ConfigAck{Host: "held-c2"}, write); now != nil { |
| + t.Fatal("c2's ack was not held while c1 is active") |
| + } |
| + // The config reverts to c1, which is the active revision. |
| + tr.delivered = "c1" |
| + now := tr.deferAck(appconfig.ConfigAck{Host: "c1"}, write) |
| + if now == nil { |
| + t.Fatal("c1's ack waited though c1 is active") |
| + } |
| + now() |
| + // The in-flight generation finishes on c2. |
| + if run, ok := tr.activate("c2"); run != nil || ok { |
| + t.Fatal("the stale c2 ack was released after a newer ack went out") |
| + } |
| + if len(wrote) != 1 || wrote[0] != "c1" { |
| + t.Fatalf("wrote %v, want only the c1 ack", wrote) |
| + } |
| + }) |
| + t.Run("sleep then hibernating ack", func(t *testing.T) { |
| + var wrote []string |
| + write := func(a appconfig.ConfigAck) { wrote = append(wrote, a.Host) } |
| + tr := &configTrack{delivered: "c2", active: "c1"} |
| + if now := tr.deferAck(appconfig.ConfigAck{Host: "held-c2"}, write); now != nil { |
| + t.Fatal("c2's ack was not held while c1 is active") |
| + } |
| + // The instance sleeps and c3 is delivered to it. |
| + tr.delivered, tr.sleeping = "c3", true |
| + now := tr.deferAck(appconfig.ConfigAck{Host: "c3"}, write) |
| + if now == nil { |
| + t.Fatal("a sleeper's ack waited") |
| + } |
| + now() |
| + if run, ok := tr.activate("c2"); run != nil || ok { |
| + t.Fatal("the stale c2 ack was released after the Hibernating ack went out") |
| + } |
| + if len(wrote) != 1 || wrote[0] != "c3" { |
| + t.Fatalf("wrote %v, want only the c3 ack", wrote) |
| + } |
| + }) |
| +} |
| + |
| +// A held ack is released only for the revision that is delivered now, even |
| +// when delivered moved on without another ack arriving. |
| +func TestAHeldAckIsNotReleasedForARevisionNoLongerDelivered(t *testing.T) { |
| + tr := &configTrack{delivered: "c2", active: "c1"} |
| + tr.deferAck(appconfig.ConfigAck{Host: "a"}, func(appconfig.ConfigAck) { t.Fatal("wrote a stale ack") }) |
| + tr.delivered = "c3" |
| + if run, ok := tr.activate("c2"); run != nil || ok { |
| + t.Fatal("released an ack for c2 while c3 is delivered") |
| + } |
| +} |