mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 02:44:59 +00:00
* First pass at RetryWithTx * Refactor RetryWithTx to take a writable bool (instead of reads, writes) * Implment DirectiveMethodDiff This commit adds support for a Directive to contain only the diffs (as opposed to the full Directive). * Update controller tests to allow for DirectiveMethodDiff (over Full) * Update RetryWithTx to retry on duplicate key constraint. If two concurrent processes call IngestShard() for the same shard, both were trying to insert the same job into the jobs table. That resulted in a duplicate key error from the database. We want to include that error in the list of errors for which RetryWithTx should retry. * Remove unused method: Directive.TranslatePartitions() * Replace query in a loop with a single query We had a query which was looking to see if a job already existed. That query was inside a loop, and could potentially generate 256 queries (for example). This commit replaces that logic so that we use a single query wiht an `IN ()` clause. * Convert to directive version-by-address This commit uses a separate directive version per address. It moves the version get/increment back inside the buildDirective method so that if two concurrent processes are building a directive for the same address, one of them will get rolled back trying to commit the version update. * Migration for directive version by address * Add a comment about DirectiveVersion lock/unlock logic * Remove AddLastWins * fix linter * handle error in walkdir * fix test failures from removing AddLastWins
175 lines
4.8 KiB
Go
175 lines
4.8 KiB
Go
package dax_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestTransaction(t *testing.T) {
|
|
t.Run("retryWithTx", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
log := logger.StderrLogger
|
|
trans, err := sqldb.NewTransactor(sqldb.GetTestConfigRandomDB("retry_with_tx"), log) // TODO running migrations takes kind of a long time, consolidate w/ other SQL tests
|
|
require.NoError(t, err, "connecting")
|
|
require.NoError(t, trans.Start())
|
|
|
|
orgID1 := dax.OrganizationID("acme")
|
|
db1 := &dax.Database{
|
|
ID: "db1id",
|
|
Name: "db1",
|
|
Options: dax.DatabaseOptions{
|
|
WorkersMin: 1,
|
|
},
|
|
}
|
|
qdb1 := dax.NewQualifiedDatabase(orgID1, db1)
|
|
qdbid1 := qdb1.QualifiedID()
|
|
schemar := sqldb.NewSchemar(log)
|
|
|
|
// The purpose of this test is to ensure that we're enforcing repeatable
|
|
// read isolation level on writes. It tests the RetryWithTx function by
|
|
// retrying a write that fails and ensuring that it succeeds on the next
|
|
// retry. It performs the following steps:
|
|
//
|
|
// tx1: create db1 with units 1
|
|
// tx2: read db1 (units should be 1)
|
|
// wait... on chan "wait2"
|
|
// read db1 again (units should still be 1) << repeatableread
|
|
// set units to 2
|
|
// tx3 set units to 3
|
|
// tx4 read db1 (units should be 3)
|
|
// close "wait2"
|
|
// tx5 read db1 (units should be 2)
|
|
|
|
wait2 := make(chan struct{})
|
|
wait3 := make(chan struct{})
|
|
done := make(chan struct{})
|
|
|
|
// tx1
|
|
tx1 := func(tx dax.Transaction, writable bool) error {
|
|
dt, ok := tx.(*sqldb.DaxTransaction)
|
|
require.True(t, ok)
|
|
|
|
require.NoError(t, schemar.CreateDatabase(dt, qdb1))
|
|
|
|
return nil
|
|
}
|
|
require.NoError(t, dax.RetryWithTx(ctx, trans, tx1, true, 1))
|
|
|
|
// tx2
|
|
|
|
// tx2cnt tracks the number of times that tx2 has been called. We need
|
|
// this because we expect it to read different values depending on which
|
|
// call it's on. And we only want it to close channels the first time
|
|
// through.
|
|
var tx2cnt int
|
|
|
|
// exp contains the values that we expect tx2 to read (for
|
|
// "workers-min") on the respective call.
|
|
exp := map[int]int{
|
|
0: 1,
|
|
1: 3,
|
|
}
|
|
tx2 := func(tx dax.Transaction, writable bool) error {
|
|
dt, ok := tx.(*sqldb.DaxTransaction)
|
|
require.True(t, ok)
|
|
|
|
qdb, err := schemar.DatabaseByID(dt, qdbid1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, exp[tx2cnt], qdb.Options.WorkersMin)
|
|
if tx2cnt == 0 {
|
|
close(wait3)
|
|
}
|
|
|
|
// Wait until tx3 commits before trying to do anything else.
|
|
<-wait2
|
|
|
|
qdb, err = schemar.DatabaseByID(dt, qdbid1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, exp[tx2cnt], qdb.Options.WorkersMin)
|
|
|
|
// Increment tx2cnt for the next time tx2 gets called.
|
|
tx2cnt++
|
|
|
|
return schemar.SetDatabaseOption(dt, qdbid1, dax.DatabaseOptionWorkersMin, "2")
|
|
}
|
|
|
|
// Run the calls to tx2 in a go routine because we want to mimic
|
|
// concurrent attempt to read/write the same data.
|
|
go func() {
|
|
require.NoError(t, dax.RetryWithTx(ctx, trans, tx2, true, 2))
|
|
|
|
// After the second call of tx2 completes, close the done channel
|
|
// so that tx5 can proceed and verify that tx2 eventually got to
|
|
// commit its transaction.
|
|
close(done)
|
|
}()
|
|
|
|
// Wait until tx2 does its first read of the data before allowing tx3 to
|
|
// begin.
|
|
select {
|
|
case <-wait3:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("expected close of channel: wait3")
|
|
}
|
|
|
|
// tx3
|
|
tx3 := func(tx dax.Transaction, writable bool) error {
|
|
dt, ok := tx.(*sqldb.DaxTransaction)
|
|
require.True(t, ok)
|
|
|
|
qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, qdb.Options.WorkersMin)
|
|
|
|
require.NoError(t, schemar.SetDatabaseOption(dt, qdbid1, dax.DatabaseOptionWorkersMin, "3"))
|
|
|
|
return nil
|
|
}
|
|
|
|
require.NoError(t, dax.RetryWithTx(ctx, trans, tx3, true, 1))
|
|
|
|
// tx4
|
|
tx4 := func(tx dax.Transaction, writable bool) error {
|
|
dt, ok := tx.(*sqldb.DaxTransaction)
|
|
require.True(t, ok)
|
|
|
|
qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, qdb.Options.WorkersMin)
|
|
|
|
return nil
|
|
}
|
|
|
|
require.NoError(t, dax.RetryWithTx(ctx, trans, tx4, false, 1))
|
|
|
|
// Close wait2 so that tx2 can continue retrying transactions.
|
|
close(wait2)
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("expected close of channel: done")
|
|
}
|
|
|
|
// tx5
|
|
tx5 := func(tx dax.Transaction, writable bool) error {
|
|
dt, ok := tx.(*sqldb.DaxTransaction)
|
|
require.True(t, ok)
|
|
|
|
qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, qdb.Options.WorkersMin)
|
|
|
|
return nil
|
|
}
|
|
|
|
require.NoError(t, dax.RetryWithTx(ctx, trans, tx5, false, 1))
|
|
})
|
|
|
|
}
|