a73x

b982a55a

fix(server): a guest's address is judged on its own terms, not the fleet's

a73x   2026-08-06 09:12

Commit message
fix(server): a guest's address is judged on its own terms, not the fleet's

The fleet allocated every host a /24 and then checked reported addresses
against it, which read a host's guest subnet as something the fleet decides.
It does not. A Mac's guests live on whatever subnet vmnet runs, and no
allocation will ever contain one, so every address a Mac reported was thrown
away and assigned_ip stayed blank for the life of the guest — no console, no
MCP, no boot-gate, and a fleet view that showed a running VM at no address.

RecordVMStatus now asks whether the value names a guest anything could reach:
it parses, and it is not the unspecified address, loopback, link-local, or
multicast. That is exactly the set the old comment claimed to be rejecting,
and it needs to know nothing about any host. An address that fails still does
not block the transition — a failed VM needs status=failed persisted whatever
its address says.

RecordVMStatus also returns the address it wrote, empty when it wrote none,
and syncsvc caches that instead of the value it sent. A guard that silently
blanked its argument and reported success is what let the status cache
remember addresses that were never stored: the cache then read every later
report of the same address as unchanged and skipped the write, so the one
retry that would have corrected it never ran. The tracker's claim to mirror
the UPDATE's SET clause is now true rather than aspirational.

internal/server/store/store.go
Old New
@@ -1016,48 +1016,39 @@ func (s *Store) ForceRemoveHost(id string) (int, error) {
1016 return int(purged), nil 1016 return int(purged), nil
1017 } 1017 }
1018 1018
1019 // ipWithinHostCIDR reports whether ip falls inside the owning host's bridge_cidr. 1019 // usableGuestAddress reports whether ip is an address a guest could actually be
1020 // A missing row, an unparseable stored CIDR, or an unparseable ip all yield 1020 // reached on. It asks nothing about topology: a host's guests live wherever that
1021 // (false, nil) — the IP simply cannot be validated, so callers drop it. Only a 1021 // host's network puts them — inside a Linux bridge the fleet allocated, or on
1022 // genuine DB error on the lookup is surfaced. 1022 // whatever subnet macOS's vmnet happens to run — and the fleet is told that
1023 func (s *Store) ipWithinHostCIDR(vmID, ip string) (bool, error) { 1023 // subnet, it does not decide it. So the only thing worth rejecting here is a
1024 var cidrStr string 1024 // value that names no reachable guest under any topology: unparseable, the
1025 switch err := s.db.QueryRow( 1025 // unspecified address, loopback, link-local (the 169.254/16 a guest reports when
1026 `SELECT h.bridge_cidr FROM vms v JOIN hosts h ON h.id = v.host_id WHERE v.id=?`, vmID, 1026 // DHCP never answered), or multicast.
1027 ).Scan(&cidrStr); { 1027 func usableGuestAddress(ip string) bool {
1028 case errors.Is(err, sql.ErrNoRows):
1029 return false, nil
1030 case err != nil:
1031 return false, fmt.Errorf("lookup host cidr: %w", err)
1032 }
1033 prefix, err := netip.ParsePrefix(cidrStr)
1034 if err != nil {
1035 return false, nil
1036 }
1037 addr, err := netip.ParseAddr(ip) 1028 addr, err := netip.ParseAddr(ip)
1038 if err != nil { 1029 if err != nil {
1039 return false, nil 1030 return false
1040 } 1031 }
1041 return prefix.Contains(addr), nil 1032 return !addr.IsUnspecified() && !addr.IsLoopback() &&
1033 !addr.IsLinkLocalUnicast() && !addr.IsLinkLocalMulticast() && !addr.IsMulticast()
1042 } 1034 }
1043 1035
1044 func (s *Store) RecordVMStatus(id, status, lastErr, ip string) error { 1036 // RecordVMStatus persists one VM's (status, last_error, assigned_ip) and returns
1045 // A reported IP outside the owning host's bridge_cidr (a DHCP hiccup, a 1037 // the address it wrote — empty when it wrote none, either because the report
1046 // link-local/APIPA address, or an agent bug) must NOT block the (status, 1038 // carried none or because the address was unusable.
1047 // last_error) transition: a genuinely-failed VM still needs status=failed 1039 //
1048 // persisted durably. So an out-of-CIDR (or unparseable) IP is DROPPED — 1040 // An unusable address must NOT block the (status, last_error) transition: a
1049 // assigned_ip keeps its prior value via the CASE below — while the status 1041 // genuinely-failed VM still needs status=failed persisted durably. So it is
1050 // still writes. This preserves the integrity guarantee (a bogus IP never 1042 // DROPPED — assigned_ip keeps its prior value via the CASE below — while the
1051 // lands in the row) without stalling the transition or spamming per-tick 1043 // status still writes.
1052 // rejection warnings from syncsvc.applyReport. 1044 //
1053 if ip != "" { 1045 // The returned address is what makes that drop visible to the caller. A guard
1054 within, err := s.ipWithinHostCIDR(id, ip) 1046 // that silently blanks its argument and reports success is how syncsvc's status
1055 if err != nil { 1047 // cache came to remember addresses that were never stored, and then suppress
1056 return err 1048 // every later report that would have corrected them.
1057 } 1049 func (s *Store) RecordVMStatus(id, status, lastErr, ip string) (string, error) {
1058 if !within { 1050 if ip != "" && !usableGuestAddress(ip) {
1059 ip = "" 1051 ip = ""
1060 }
1061 } 1052 }
1062 1053
1063 res, err := s.db.Exec( 1054 res, err := s.db.Exec(
@@ -1065,13 +1056,13 @@ func (s *Store) RecordVMStatus(id, status, lastErr, ip string) error {
1065 status, lastErr, ip, ip, id, 1056 status, lastErr, ip, ip, id,
1066 ) 1057 )
1067 if err != nil { 1058 if err != nil {
1068 return err 1059 return "", err
1069 } 1060 }
1070 n, _ := res.RowsAffected() 1061 n, _ := res.RowsAffected()
1071 if n == 0 { 1062 if n == 0 {
1072 return sql.ErrNoRows 1063 return "", sql.ErrNoRows
1073 } 1064 }
1074 return nil 1065 return ip, nil
1075 } 1066 }
1076 1067
1077 // vmColumns is the positional column list every VM SELECT must use, so the 1068 // vmColumns is the positional column list every VM SELECT must use, so the
internal/server/store/store_test.go
Old New
@@ -271,7 +271,8 @@ func TestDesiredStateMutationsBumpEpochButStatusWritesDoNot(t *testing.T) {
271 e2, _ := s.Epoch() 271 e2, _ := s.Epoch()
272 assert.Equal(t, e1+1, e2, "power edit bumps") 272 assert.Equal(t, e1+1, e2, "power edit bumps")
273 273
274 require.NoError(t, s.RecordVMStatus("vm1", "ready", "", "10.77.1.2")) 274 _, err := s.RecordVMStatus("vm1", "ready", "", "10.77.1.2")
275 require.NoError(t, err)
275 e3, _ := s.Epoch() 276 e3, _ := s.Epoch()
276 assert.Equal(t, e2, e3, "agent-reported status does NOT bump") 277 assert.Equal(t, e2, e3, "agent-reported status does NOT bump")
277 278
@@ -311,27 +312,53 @@ func TestDesiredForHostIncludesTombstonedAndEpochConsistently(t *testing.T) {
311 assert.NotNil(t, vms[0].DeletedAt, "tombstoned rows stay in the snapshot until acked") 312 assert.NotNil(t, vms[0].DeletedAt, "tombstoned rows stay in the snapshot until acked")
312 } 313 }
313 314
314 func TestRecordVMStatusDropsOutOfCIDRIPButKeepsStatus(t *testing.T) { 315 // TestRecordVMStatusKeepsReachableAddressesAndDropsUnusableOnes pins what the
315 s := newStore(t) 316 // address guard asks. It does not ask where a host's guests live: a Mac's guests
316 h := enrollHost(t, s) // 10.77.1.0/24 317 // sit on whatever subnet vmnet runs, which no fleet allocation will ever
317 require.NoError(t, s.CreateVM(VM{ID: "vm1", HostID: h.ID, Name: "a", ImageURL: "u", 318 // contain. It asks only whether the value names a guest anything could reach.
318 ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"})) 319 // Whatever the answer, the (status, last_error) transition still writes.
319 320 func TestRecordVMStatusKeepsReachableAddressesAndDropsUnusableOnes(t *testing.T) {
320 // A good IP lands. 321 const prior = "10.77.1.9"
321 require.NoError(t, s.RecordVMStatus("vm1", "ready", "", "10.77.1.9")) 322 for _, tc := range []struct {
322 vm, err := s.GetVM("vm1") 323 name string
323 require.NoError(t, err) 324 ip string
324 assert.Equal(t, "10.77.1.9", vm.AssignedIP) 325 want string // the address the row should hold afterwards
325 326 }{
326 // An out-of-CIDR IP alongside a failed transition must NOT block the status 327 {"a guest on the host's own bridge", "10.77.1.20", "10.77.1.20"},
327 // write, and must NOT overwrite the prior good IP with the bogus value. 328 {"a guest on a subnet the fleet never allocated", "192.168.64.7", "192.168.64.7"},
328 require.NoError(t, s.RecordVMStatus("vm1", "failed", "boom", "10.77.2.9"), 329 {"unspecified", "0.0.0.0", prior},
329 "out-of-CIDR IP must not fail the status write") 330 {"loopback", "127.0.0.1", prior},
330 vm, err = s.GetVM("vm1") 331 {"link-local: DHCP never answered", "169.254.11.2", prior},
331 require.NoError(t, err) 332 {"multicast", "224.0.0.1", prior},
332 assert.Equal(t, "failed", vm.Status, "status transition must persist despite the bad IP") 333 {"unparseable", "not-an-ip", prior},
333 assert.Equal(t, "boom", vm.LastError) 334 {"none reported", "", prior},
334 assert.Equal(t, "10.77.1.9", vm.AssignedIP, "bogus IP must be dropped, prior kept") 335 } {
336 t.Run(tc.name, func(t *testing.T) {
337 s := newStore(t)
338 h := enrollHost(t, s) // 10.77.1.0/24
339 require.NoError(t, s.CreateVM(VM{ID: "vm1", HostID: h.ID, Name: "a", ImageURL: "u",
340 ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
341 _, err := s.RecordVMStatus("vm1", "ready", "", prior)
342 require.NoError(t, err)
343
344 written, err := s.RecordVMStatus("vm1", "failed", "boom", tc.ip)
345 require.NoError(t, err, "the address must never fail the status write")
346
347 vm, err := s.GetVM("vm1")
348 require.NoError(t, err)
349 assert.Equal(t, "failed", vm.Status, "the status transition persists regardless")
350 assert.Equal(t, "boom", vm.LastError)
351 assert.Equal(t, tc.want, vm.AssignedIP)
352
353 // What it says it wrote is what it wrote: the address on a keep, and
354 // nothing at all on a drop — the caller cannot tell them apart otherwise.
355 if tc.want == prior {
356 assert.Empty(t, written, "a dropped address must be reported as written-nothing")
357 } else {
358 assert.Equal(t, tc.ip, written)
359 }
360 })
361 }
335 } 362 }
336 363
337 func TestEnrollmentCIDRWorksForNonSlash16Pools(t *testing.T) { 364 func TestEnrollmentCIDRWorksForNonSlash16Pools(t *testing.T) {
@@ -363,7 +390,7 @@ func TestEnrollmentFailsWhenPoolExhausted(t *testing.T) {
363 390
364 func TestRecordVMStatusUnknownVMErrors(t *testing.T) { 391 func TestRecordVMStatusUnknownVMErrors(t *testing.T) {
365 s := newStore(t) 392 s := newStore(t)
366 err := s.RecordVMStatus("nope", "ready", "", "") 393 _, err := s.RecordVMStatus("nope", "ready", "", "")
367 assert.Error(t, err) 394 assert.Error(t, err)
368 } 395 }
369 396
internal/server/syncsvc/syncsvc.go
Old New
@@ -31,7 +31,8 @@ const defaultWriteTimeout = 30 * time.Second
31 // direct s.st call) lets tests substitute a counting fake to prove that 31 // direct s.st call) lets tests substitute a counting fake to prove that
32 // unchanged reports perform no write. 32 // unchanged reports perform no write.
33 type vmStatusRecorder interface { 33 type vmStatusRecorder interface {
34 RecordVMStatus(id, status, lastErr, ip string) error 34 // RecordVMStatus returns the address it wrote, empty when it wrote none.
35 RecordVMStatus(id, status, lastErr, ip string) (string, error)
35 } 36 }
36 37
37 // Service is the QUIC server end of the agent reconcile stream. 38 // Service is the QUIC server end of the agent reconcile stream.
@@ -381,8 +382,9 @@ func (s *Service) applyReport(hostID string, rep *pb.ActualStateReport) {
381 } 382 }
382 vmID := v.GetVmId() 383 vmID := v.GetVmId()
383 // Pass the RAW reported ip to RecordVMStatus to preserve its 384 // Pass the RAW reported ip to RecordVMStatus to preserve its
384 // empty-ip-keeps-prior UPDATE semantics. 385 // empty-ip-keeps-prior UPDATE semantics, and cache the address it
385 err := s.tracker.writeThrough(vmID, phase, v.GetLastError(), v.GetIp(), func() error { 386 // reports back rather than the one we sent it.
387 err := s.tracker.writeThrough(vmID, phase, v.GetLastError(), v.GetIp(), func() (string, error) {
386 return s.recorder.RecordVMStatus(vmID, phase, v.GetLastError(), v.GetIp()) 388 return s.recorder.RecordVMStatus(vmID, phase, v.GetLastError(), v.GetIp())
387 }) 389 })
388 if err != nil { 390 if err != nil {
internal/server/syncsvc/syncsvc_test.go
Old New
@@ -212,7 +212,7 @@ type countingRecorder struct {
212 calls int 212 calls int
213 } 213 }
214 214
215 func (c *countingRecorder) RecordVMStatus(id, status, lastErr, ip string) error { 215 func (c *countingRecorder) RecordVMStatus(id, status, lastErr, ip string) (string, error) {
216 c.mu.Lock() 216 c.mu.Lock()
217 c.calls++ 217 c.calls++
218 c.mu.Unlock() 218 c.mu.Unlock()
@@ -292,6 +292,43 @@ func TestReportWithValidIPUpdatesRegistryAndStore(t *testing.T) {
292 }, 2*time.Second, 20*time.Millisecond) 292 }, 2*time.Second, 20*time.Millisecond)
293 } 293 }
294 294
295 // TestReportRecoversFromAnUnusableAddress walks the whole path a guest takes
296 // when its first answer is a bad one: an address the store refuses must not
297 // poison the write path against the address that follows it. A host whose guests
298 // live outside anything the fleet allocated — a Mac's vmnet, say — reports one
299 // of those on every tick, so "the second report is treated as unchanged" means
300 // blank forever, not blank once.
301 func TestReportRecoversFromAnUnusableAddress(t *testing.T) {
302 f := setup(t)
303 require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
304 ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
305 c := mustDial(t, f)
306 c.recv(t)
307
308 ready := func(ip string) *pb.AgentMessage {
309 return &pb.AgentMessage{Msg: &pb.AgentMessage_Report{
310 Report: &pb.ActualStateReport{Vms: []*pb.ActualVM{
311 {VmId: "vm1", Power: "running", Phase: "ready", Ip: ip},
312 }}}}
313 }
314
315 // DHCP never answered: the guest reports the address it made up for itself.
316 c.send(t, ready("169.254.11.2"))
317 require.Eventually(t, func() bool {
318 vms, _ := f.st.ListVMs()
319 return len(vms) == 1 && vms[0].Status == "ready"
320 }, 2*time.Second, 20*time.Millisecond, "the status must write even so")
321 vms, _ := f.st.ListVMs()
322 require.Empty(t, vms[0].AssignedIP, "an unusable address must not land")
323
324 // DHCP answers, on a subnet no fleet allocation contains.
325 c.send(t, ready("192.168.64.7"))
326 require.Eventually(t, func() bool {
327 vms, _ := f.st.ListVMs()
328 return len(vms) == 1 && vms[0].AssignedIP == "192.168.64.7"
329 }, 2*time.Second, 20*time.Millisecond, "the address that works must land")
330 }
331
295 func TestHardDeleteTriggersRepush(t *testing.T) { 332 func TestHardDeleteTriggersRepush(t *testing.T) {
296 f := setup(t) 333 f := setup(t)
297 require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a", 334 require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
internal/server/syncsvc/tracker.go
Old New
@@ -30,32 +30,41 @@ func newStatusTracker() *statusTracker {
30 // disagreeing with the row — a divergence that would then suppress every later 30 // disagreeing with the row — a divergence that would then suppress every later
31 // identical report indefinitely. 31 // identical report indefinitely.
32 // 32 //
33 // write performs the durable RecordVMStatus call; it runs ONLY when the durable 33 // write performs the durable RecordVMStatus call and RETURNS THE ADDRESS IT
34 // triple (status, last_error, effective assigned_ip) changed, and the cache is 34 // WROTE; it runs ONLY when the report could still change the row, and the cache
35 // updated ONLY if write returns nil, so a rejected write never suppresses the 35 // is updated ONLY if write returns nil, so a rejected write never suppresses the
36 // next retry. write receives no arguments: the caller passes the RAW reported ip 36 // next retry. write receives no arguments: the caller passes the RAW reported ip
37 // to RecordVMStatus (whose UPDATE keeps the prior ip on empty), while the cache 37 // to RecordVMStatus (whose UPDATE keeps the prior ip on empty).
38 // stores the effective ip computed here.
39 // 38 //
40 // The effective ip mirrors RecordVMStatus's UPDATE SET clause 39 // The tracker never decides what an address means. It caches what the write says
41 // `assigned_ip = CASE WHEN ?=” THEN assigned_ip ELSE ? END`: an EMPTY reported 40 // it stored, folded with that UPDATE's own empty-keeps-prior rule
42 // ip keeps the prior stored ip (prior.ip=="" when there is no prior entry); a 41 // (`assigned_ip = CASE WHEN ?='' THEN assigned_ip ELSE ? END`), so the cache
43 // non-empty ip replaces it. 42 // holds what the row holds. An address the store DROPPED therefore leaves the
44 func (t *statusTracker) writeThrough(vmID, status, lastErr, ip string, write func() error) error { 43 // cache on the prior value rather than the rejected one, and the next report
44 // carrying a good address reads as a change and lands. Deciding the effective ip
45 // here instead — from the value the agent REPORTED rather than the one the store
46 // WROTE — is how a permanently blank assigned_ip survived every report that
47 // would have filled it.
48 func (t *statusTracker) writeThrough(vmID, status, lastErr, ip string, write func() (string, error)) error {
45 t.mu.Lock() 49 t.mu.Lock()
46 defer t.mu.Unlock() 50 defer t.mu.Unlock()
47 prior, ok := t.last[vmID] 51 prior, ok := t.last[vmID]
48 effIP := ip 52 // Decide on the RAW ip. Whether the store keeps it is the store's business;
49 if effIP == "" { 53 // all this needs is that a SKIPPED report is one that provably changes
50 effIP = prior.ip 54 // nothing — an empty ip leaves assigned_ip alone, and an ip already stored
51 } 55 // rewrites it to itself.
52 if ok && prior.status == status && prior.lastErr == lastErr && prior.ip == effIP { 56 if ok && prior.status == status && prior.lastErr == lastErr &&
57 (ip == "" || ip == prior.ip) {
53 return nil // unchanged — skip the write 58 return nil // unchanged — skip the write
54 } 59 }
55 if err := write(); err != nil { 60 written, err := write()
61 if err != nil {
56 return err // rejected — do not cache; let the next report retry 62 return err // rejected — do not cache; let the next report retry
57 } 63 }
58 t.last[vmID] = vmStatus{status: status, lastErr: lastErr, ip: effIP} 64 if written == "" {
65 written = prior.ip
66 }
67 t.last[vmID] = vmStatus{status: status, lastErr: lastErr, ip: written}
59 return nil 68 return nil
60 } 69 }
61 70
internal/server/syncsvc/tracker_test.go
Old New
@@ -8,11 +8,12 @@ import (
8 "github.com/stretchr/testify/require" 8 "github.com/stretchr/testify/require"
9 ) 9 )
10 10
11 // write is a tiny helper that runs writeThrough with a SUCCESSFUL durable write, 11 // write is a tiny helper that runs writeThrough with a SUCCESSFUL durable write
12 // returning whether a write actually happened (the triple changed). 12 // that STORES the address it was given, returning whether a write actually
13 // happened (the report could still change the row).
13 func (t *statusTracker) write(vmID, status, lastErr, ip string) bool { 14 func (t *statusTracker) write(vmID, status, lastErr, ip string) bool {
14 wrote := false 15 wrote := false
15 _ = t.writeThrough(vmID, status, lastErr, ip, func() error { wrote = true; return nil }) 16 _ = t.writeThrough(vmID, status, lastErr, ip, func() (string, error) { wrote = true; return ip, nil })
16 return wrote 17 return wrote
17 } 18 }
18 19
@@ -71,13 +72,18 @@ func TestStatusTrackerFailedWriteNotCached(t *testing.T) {
71 72
72 // The durable write FAILS: writeThrough must surface the error and NOT cache. 73 // The durable write FAILS: writeThrough must surface the error and NOT cache.
73 sentinel := errors.New("record rejected") 74 sentinel := errors.New("record rejected")
74 if err := tr.writeThrough("vm1", "ready", "", "10.0.0.1", func() error { return sentinel }); err != sentinel { 75 if err := tr.writeThrough("vm1", "ready", "", "10.0.0.1", func() (string, error) {
76 return "", sentinel
77 }); err != sentinel {
75 t.Fatalf("failed write must surface its error, got %v", err) 78 t.Fatalf("failed write must surface its error, got %v", err)
76 } 79 }
77 // Because the write failed (uncached), the next report must still attempt it — 80 // Because the write failed (uncached), the next report must still attempt it —
78 // a failed write must never suppress the retry. 81 // a failed write must never suppress the retry.
79 attempted := false 82 attempted := false
80 if err := tr.writeThrough("vm1", "ready", "", "10.0.0.1", func() error { attempted = true; return nil }); err != nil { 83 if err := tr.writeThrough("vm1", "ready", "", "10.0.0.1", func() (string, error) {
84 attempted = true
85 return "10.0.0.1", nil
86 }); err != nil {
81 t.Fatalf("retry write errored: %v", err) 87 t.Fatalf("retry write errored: %v", err)
82 } 88 }
83 if !attempted { 89 if !attempted {
@@ -89,6 +95,43 @@ func TestStatusTrackerFailedWriteNotCached(t *testing.T) {
89 } 95 }
90 } 96 }
91 97
98 // TestStatusTrackerDroppedAddressNotCached pins the half of the invariant that a
99 // silent guard broke: the cache holds what the STORE says it stored, never what
100 // the agent merely reported. A store that drops an address says so by returning
101 // nothing, and the tracker must then leave the cache where the row is — because
102 // a cache that remembers a rejected address treats every later report of it as
103 // "unchanged" and suppresses the write forever, which is how a guest could run
104 // for days with a blank assigned_ip while its agent reported the address on
105 // every single tick.
106 func TestStatusTrackerDroppedAddressNotCached(t *testing.T) {
107 tr := newStatusTracker()
108 dropped := func() (string, error) { return "", nil } // the store rejected it
109
110 require.NoError(t, tr.writeThrough("vm1", "ready", "", "169.254.11.2", dropped))
111 tr.mu.Lock()
112 cached := tr.last["vm1"].ip
113 tr.mu.Unlock()
114 require.Empty(t, cached, "a rejected address must not be cached as stored")
115
116 // The same rejected address again still reaches the store: nothing has been
117 // written, so there is nothing to skip.
118 attempted := false
119 require.NoError(t, tr.writeThrough("vm1", "ready", "", "169.254.11.2", func() (string, error) {
120 attempted = true
121 return "", nil
122 }))
123 require.True(t, attempted, "a repeat of a rejected address must still attempt the write")
124
125 // And the address that finally works lands, rather than being mistaken for a
126 // repeat of something already stored.
127 if !tr.write("vm1", "ready", "", "10.0.0.5") {
128 t.Fatal("a good address after a rejected one must write")
129 }
130 if tr.write("vm1", "ready", "", "10.0.0.5") {
131 t.Fatal("once stored, the same address must not write again")
132 }
133 }
134
92 // TestStatusTrackerWriteThroughAtomic pins the invariant Fix D restores: under 135 // TestStatusTrackerWriteThroughAtomic pins the invariant Fix D restores: under
93 // concurrent conflicting reports for one VM, the cached triple always equals the 136 // concurrent conflicting reports for one VM, the cached triple always equals the
94 // LAST durable write. writeThrough holds the lock across write+commit, so the 137 // LAST durable write. writeThrough holds the lock across write+commit, so the
@@ -101,11 +144,11 @@ func TestStatusTrackerWriteThroughAtomic(t *testing.T) {
101 lastWritten := "" // status of the most recent successful durable write 144 lastWritten := "" // status of the most recent successful durable write
102 145
103 writer := func(status string) { 146 writer := func(status string) {
104 _ = tr.writeThrough("vm1", status, "", "10.0.0.1", func() error { 147 _ = tr.writeThrough("vm1", status, "", "10.0.0.1", func() (string, error) {
105 mu.Lock() 148 mu.Lock()
106 lastWritten = status // recorded inside writeThrough's critical section 149 lastWritten = status // recorded inside writeThrough's critical section
107 mu.Unlock() 150 mu.Unlock()
108 return nil 151 return "10.0.0.1", nil
109 }) 152 })
110 } 153 }
111 154