// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package state import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestGraveyard_Lifecycle(t *testing.T) { g := NewGraveyard(nil) // Make a donor state store to steal its database, all prepared for // tombstones. s := testStateStore(t) // Create some tombstones. func() { tx := s.db.WriteTxnRestore() defer tx.Abort() if err := g.InsertTxn(tx, "foo/in/the/house", 2, nil); err != nil { t.Fatalf("err: %s", err) } if err := g.InsertTxn(tx, "foo/bar/baz", 5, nil); err != nil { t.Fatalf("err: %s", err) } if err := g.InsertTxn(tx, "foo/bar/zoo", 8, nil); err != nil { t.Fatalf("err: %s", err) } if err := g.InsertTxn(tx, "some/other/path", 9, nil); err != nil { t.Fatalf("err: %s", err) } require.NoError(t, tx.Commit()) }() // Check some prefixes. func() { tx := s.db.Txn(false) defer tx.Abort() if idx, err := g.GetMaxIndexTxn(tx, "foo", nil); idx != 8 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "foo/in/the/house", nil); idx != 2 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "foo/bar/baz", nil); idx != 5 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "foo/bar/zoo", nil); idx != 8 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "some/other/path", nil); idx != 9 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "", nil); idx != 9 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "nope", nil); idx != 0 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } }() // Reap some tombstones. func() { tx := s.db.WriteTxnRestore() defer tx.Abort() if err := g.ReapTxn(tx, 6); err != nil { t.Fatalf("err: %s", err) } require.NoError(t, tx.Commit()) }() // Check prefixes to see that the reap took effect at the right index. func() { tx := s.db.Txn(false) defer tx.Abort() if idx, err := g.GetMaxIndexTxn(tx, "foo", nil); idx != 8 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "foo/in/the/house", nil); idx != 0 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "foo/bar/baz", nil); idx != 0 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "foo/bar/zoo", nil); idx != 8 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "some/other/path", nil); idx != 9 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "", nil); idx != 9 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } if idx, err := g.GetMaxIndexTxn(tx, "nope", nil); idx != 0 || err != nil { t.Fatalf("bad: %d (%s)", idx, err) } }() } func TestGraveyard_GC_Trigger(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } // Set up a fast-expiring GC. ttl, granularity := 100*time.Millisecond, 20*time.Millisecond gc, err := NewTombstoneGC(ttl, granularity) if err != nil { t.Fatalf("err: %s", err) } // Make a new graveyard and assign the GC. g := NewGraveyard(gc) gc.SetEnabled(true) // Make sure there's nothing already expiring. if gc.PendingExpiration() { t.Fatalf("should not have any expiring items") } // Create a tombstone but abort the transaction, this should not trigger // GC. s := testStateStore(t) func() { tx := s.db.WriteTxnRestore() defer tx.Abort() if err := g.InsertTxn(tx, "foo/in/the/house", 2, nil); err != nil { t.Fatalf("err: %s", err) } }() // Make sure there's nothing already expiring. if gc.PendingExpiration() { t.Fatalf("should not have any expiring items") } // Now commit. func() { tx := s.db.WriteTxnRestore() defer tx.Abort() if err := g.InsertTxn(tx, "foo/in/the/house", 2, nil); err != nil { t.Fatalf("err: %s", err) } require.NoError(t, tx.Commit()) }() // Make sure the GC got hinted. if !gc.PendingExpiration() { t.Fatalf("should have a pending expiration") } // Make sure the index looks good. select { case idx := <-gc.ExpireCh(): if idx != 2 { t.Fatalf("bad index: %d", idx) } case <-time.After(2 * ttl): t.Fatalf("should have gotten an expire notice") } } func TestGraveyard_Snapshot_Restore(t *testing.T) { g := NewGraveyard(nil) // Make a donor state store to steal its database, all prepared for // tombstones. s := testStateStore(t) // Create some tombstones. func() { tx := s.db.WriteTxnRestore() defer tx.Abort() if err := g.InsertTxn(tx, "foo/in/the/house", 2, nil); err != nil { t.Fatalf("err: %s", err) } if err := g.InsertTxn(tx, "foo/bar/baz", 5, nil); err != nil { t.Fatalf("err: %s", err) } if err := g.InsertTxn(tx, "foo/bar/zoo", 8, nil); err != nil { t.Fatalf("err: %s", err) } if err := g.InsertTxn(tx, "some/other/path", 9, nil); err != nil { t.Fatalf("err: %s", err) } require.NoError(t, tx.Commit()) }() // Verify the index was set correctly. if idx := s.maxIndex(partitionedIndexEntryName(tableTombstones, "default")); idx != 9 { t.Fatalf("bad index: %d", idx) } // Dump them as if we are doing a snapshot. dump := func() []*Tombstone { tx := s.db.Txn(false) defer tx.Abort() iter, err := g.DumpTxn(tx) if err != nil { t.Fatalf("err: %s", err) } var dump []*Tombstone for ti := iter.Next(); ti != nil; ti = iter.Next() { dump = append(dump, ti.(*Tombstone)) } return dump }() // Verify the dump, which should be ordered by key. expected := []*Tombstone{ {Key: "foo/bar/baz", Index: 5}, {Key: "foo/bar/zoo", Index: 8}, {Key: "foo/in/the/house", Index: 2}, {Key: "some/other/path", Index: 9}, } if len(expected) != len(dump) { t.Fatalf("expected %d, got %d tombstones", len(expected), len(dump)) } for i, e := range expected { if e.Key != dump[i].Key { t.Fatalf("expected key %s, got %s", e.Key, dump[i].Key) } if e.Index != dump[i].Index { t.Fatalf("expected key %s, got %s", e.Key, dump[i].Key) } } // Make another state store and restore from the dump. func() { s := testStateStore(t) func() { tx := s.db.WriteTxnRestore() defer tx.Abort() for _, stone := range dump { if err := g.RestoreTxn(tx, stone); err != nil { t.Fatalf("err: %s", err) } } require.NoError(t, tx.Commit()) }() // Verify that the restore works. if idx := s.maxIndex(partitionedIndexEntryName(tableTombstones, "default")); idx != 9 { t.Fatalf("bad index: %d", idx) } dump := func() []*Tombstone { tx := s.db.Txn(false) defer tx.Abort() iter, err := g.DumpTxn(tx) if err != nil { t.Fatalf("err: %s", err) } var dump []*Tombstone for ti := iter.Next(); ti != nil; ti = iter.Next() { dump = append(dump, ti.(*Tombstone)) } return dump }() if len(expected) != len(dump) { t.Fatalf("expected %d, got %d tombstones", len(expected), len(dump)) } for i, e := range expected { if e.Key != dump[i].Key { t.Fatalf("expected key %s, got %s", e.Key, dump[i].Key) } if e.Index != dump[i].Index { t.Fatalf("expected idx %d, got %d", e.Index, dump[i].Index) } } }() }