featurebase/rbf/db_test.go
Seebs 4f5f3e30ea remove port_mapper because it can't work with our unrestartable server
Long story short: Once we create a server and start it, we can't start
it again. We can't close it and restart it, and we can't just start
it without closing it.

Unfortunately, if the server's config needs to change, we have a Problem
here.

This ultimately means that the retry logic for GetListeners can't actually
retry successfully; if we fail on the first attempt, we necessarily fail
on any later attempts also, and if we try to fix that, we get panics.

But!

We don't actually NEED to retry. We just need to ensure that we can
open a :0 port, extract the actual port number, and use that in places
where the port number mattered, without having to rebind it.

The only actual place we needed to rebind things was opening gRPC
servers, so we introduce a gRPC Listener that can be used instead of
trying to bind to a specified port.

In a bunch of other cases where we had similar logic to try to allocate
and then use a port, we can switch to just using a provided listener.
For instance, net/http has `Serve(net.Listener, handler)`, not just
ListenAndServe(addr, handler).

This should eliminate the weird CI failures from eaddrinuse.

NOT fixed: server/cluster_test.go/TestClusterResize_AddNode isn't working
right now. The new node isn't actually being added to the existing cluster.
I attempted this but was outsmarted by it, and I think fixing the
rest of this is worth it as a separate thing.
2021-02-24 11:25:46 -06:00

367 lines
8.7 KiB
Go

// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbf_test
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"os"
"testing"
"time"
_ "net/http/pprof"
"github.com/pilosa/pilosa/v2/rbf"
rbfcfg "github.com/pilosa/pilosa/v2/rbf/cfg"
"golang.org/x/sync/errgroup"
)
func TestDB_Open(t *testing.T) {
db := NewDB()
if err := db.Open(); err != nil {
t.Fatal(err)
} else if err := db.Close(); err != nil {
t.Fatal(err)
}
}
func TestDB_WAL(t *testing.T) {
t.Run("ErrTxTooLarge", func(t *testing.T) {
config := rbfcfg.NewDefaultConfig()
config.MaxWALSize = 4 * rbf.PageSize
db := MustOpenDB(t, config)
defer MustCloseDB(t, db)
tx := MustBegin(t, db, true)
defer tx.Rollback()
if err := tx.CreateBitmap("x"); err != nil {
t.Fatal(err)
} else if err := tx.CreateBitmap("y"); err != rbf.ErrTxTooLarge {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("Halt", func(t *testing.T) {
if testing.Short() {
t.Skip("-short enabled, skipping")
}
config := rbfcfg.NewDefaultConfig()
config.MaxWALSize = 16 * rbf.PageSize
config.MaxWALCheckpointSize = 8 * rbf.PageSize
config.MinWALCheckpointSize = 4 * rbf.PageSize
db := MustOpenDB(t, config)
defer MustCloseDB(t, db)
// Continuously run read overlapping transactions.
ctx, cancel := context.WithCancel(context.Background())
g, ctx := errgroup.WithContext(ctx)
for i := 0; i < 10; i++ {
i := i
g.Go(func() error {
time.Sleep(time.Duration(i) * 10 * time.Millisecond) // stagger
for {
if err := ctx.Err(); err != nil {
return nil
}
if err := func() error {
tx, err := db.Begin(false)
if err != nil {
return err
}
defer tx.Rollback()
time.Sleep(20 * time.Millisecond)
return nil
}(); err != nil {
return err
}
}
})
}
// Generate updates to the DB/WAL.
for i := 0; i < 100; i++ {
func() {
tx := MustBegin(t, db, true)
defer tx.Rollback()
if err := tx.CreateBitmapIfNotExists("x"); err != nil {
t.Fatal(err)
} else if _, err := tx.Add("x", uint64(i)); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err)
}
}()
}
// Stop read transactions & wait.
cancel()
if err := g.Wait(); err != nil {
t.Fatal(err)
}
})
}
func TestDB_Recovery(t *testing.T) {
// Ensure a bitmap header written without a bitmap is truncated.
t.Run("TruncPartialWALBitmap", func(t *testing.T) {
db := MustOpenDB(t)
defer MustCloseDB(t, db)
a := make([]uint64, rbf.ArrayMaxSize+100)
for i := range a {
a[i] = uint64(i)
}
// Create bitmap & generate enough values to create a bitmap container.
if tx, err := db.Begin(true); err != nil {
t.Fatal(err)
} else if err := tx.CreateBitmap("x"); err != nil {
t.Fatal(err)
} else if _, err := tx.Add("x", a...); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err)
}
// Add one additional bit in a second transaction.
tx0, err := db.Begin(true)
if err != nil {
t.Fatal(err)
} else if _, err := tx0.Add("x", uint64(len(a))); err != nil {
t.Fatal(err)
}
// Start a read-only transaction so the write tx does not checkpoint the WAL.
tx1, err := db.Begin(false)
if err != nil {
t.Fatal(err)
}
// Commit write transaction.
if err := tx0.Commit(); err != nil {
t.Fatal(err)
}
tx1.Rollback()
// Close database & truncate WAL to remove commit page & bitmap data page.
walPath, walSize := db.WALPath(), db.WALSize()
if err := db.Close(); err != nil {
t.Fatal(err)
} else if err := os.Truncate(walPath, walSize-(2*rbf.PageSize)); err != nil {
t.Fatal(err)
}
// Reopen database.
newDB := rbf.NewDB(db.Path, nil)
if err := newDB.Open(); err != nil {
t.Fatal(err)
}
defer MustCloseDB(t, newDB)
// Verify last insert was not added.
tx, err := newDB.Begin(true)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
if exists, err := tx.Contains("x", uint64(len(a))); exists || err != nil {
t.Fatalf("Contains()=<%v,%#v>", exists, err)
} else if exists, err := tx.Contains("x", uint64(len(a)-1)); !exists || err != nil {
t.Fatalf("Contains()=<%v,%#v>", exists, err)
}
tx.Rollback()
})
}
func TestDB_HasData(t *testing.T) {
db := MustOpenDB(t)
defer MustCloseDB(t, db)
// HasData should start out false.
const requireOneHotBit = true
hasAnything, err := db.HasData(!requireOneHotBit)
if err != nil {
t.Fatal(err)
}
if hasAnything {
t.Fatalf("HasData reported existing data on an empty database")
}
hasAnything, err = db.HasData(requireOneHotBit)
if err != nil {
t.Fatal(err)
}
if hasAnything {
t.Fatalf("HasData reported existing data on an empty database")
}
// check that HasData sees a committed record.
// Create bitmap with no hot bits.
if tx, err := db.Begin(true); err != nil {
t.Fatal(err)
} else if err := tx.CreateBitmap("x"); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err)
}
// HasData(false) should now report seeing the 'x' record, even though
// the value is an empty bitmap, since !requireOneHotBit.
hasAnything, err = db.HasData(!requireOneHotBit)
if err != nil {
t.Fatal(err)
}
if !hasAnything {
t.Fatalf("HasData(!requireOneHotBit) reported no data on a database that has 'x' written to it")
}
// HasData(requireOneHotBit) should report false, since we have no hot bits.
hasAnything, err = db.HasData(requireOneHotBit)
if err != nil {
t.Fatal(err)
}
if hasAnything {
t.Fatalf("HasData(requireOneHotBit) reported data on a database that has 'x' -> empty bitmap")
}
// hot up a bit.
if tx, err := db.Begin(true); err != nil {
t.Fatal(err)
} else if _, err := tx.Add("x", rand.Uint64()); err != nil {
t.Fatal(err)
} else if err := tx.Commit(); err != nil {
t.Fatal(err)
}
// Now it shouldn't matter, we should get HasData true either way
// HasData(requireOneHotBit) should report false, since we have no hot bits.
hasAnything, err = db.HasData(requireOneHotBit)
if err != nil {
t.Fatal(err)
}
if !hasAnything {
t.Fatalf("HasData should have seen the hot bit")
}
hasAnything, err = db.HasData(!requireOneHotBit)
if err != nil {
t.Fatal(err)
}
if !hasAnything {
t.Fatalf("HasData should have seen the hot bit")
}
}
// Ensures the DB can continuously write while readers are executing.
func TestDB_MultiTx(t *testing.T) {
if testing.Short() {
t.Skip("-short enabled, skipping")
}
cfg := rbfcfg.NewDefaultConfig()
db := MustOpenDB(t, cfg)
defer MustCloseDB(t, db)
// Run multiple readers in separate goroutines.
ctx, cancel := context.WithCancel(context.Background())
g, ctx := errgroup.WithContext(ctx)
for i := 0; i < 4; i++ {
g.Go(func() error {
for {
if ctx.Err() != nil {
return nil // cancelled, return no error
} else if err := func() error {
tx, err := db.Begin(false)
if err != nil {
return err
}
defer tx.Rollback()
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
for i := 0; i < rand.Intn(1000); i++ {
v := rand.Intn(1 << 20)
if _, err := tx.Contains("x", uint64(v)); err != nil {
return err
}
}
return nil
}(); err != nil {
return err
}
time.Sleep(time.Duration(rand.Intn(int(100 * time.Millisecond))))
}
})
}
// Continuously set/clear bits while readers are executing.
for i := 0; i < 1000; i++ {
func() {
tx, err := db.Begin(true)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
for j := 0; j < rand.Intn(100); j++ {
v := rand.Intn(1 << 20)
if _, err := tx.Add("x", uint64(v)); err != nil {
t.Fatal(err)
}
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
}()
}
// Stop readers & wait.
cancel()
if err := g.Wait(); err != nil {
t.Fatal(err)
}
}
// better diagnosis of deadlocks/hung situations versus just really slow "Quick" tests.
func TestMain(m *testing.M) {
l, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
port := l.Addr().(*net.TCPAddr).Port
fmt.Printf("rbf/ TestMain: online stack-traces: curl http://localhost:%v/debug/pprof/goroutine?debug=2\n", port)
go func() {
err := http.Serve(l, nil)
if err != nil {
panic(err)
}
}()
os.Exit(m.Run())
}