179 lines
4.6 KiB
Go
179 lines
4.6 KiB
Go
package database_test
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// livePragma reads a pragma off a live handle. Reading the DSN back
|
|
// would prove only that the string was built; these tests assert that
|
|
// SQLite actually applied it.
|
|
func livePragma(t *testing.T, db *gorm.DB, name string) string {
|
|
t.Helper()
|
|
|
|
var v string
|
|
|
|
row := db.Raw("pragma " + name).Row()
|
|
require.NoError(t, row.Scan(&v))
|
|
|
|
return v
|
|
}
|
|
|
|
func TestSQLiteDSNCarriesTheDurabilitySettings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dsn := database.SQLiteDSN(
|
|
"/var/lib/webhooker/webhooker.db",
|
|
database.SQLiteModeCreate,
|
|
)
|
|
|
|
assert.Contains(t, dsn, "journal_mode%28WAL%29")
|
|
assert.Contains(t, dsn, "busy_timeout%2810000%29")
|
|
assert.Contains(t, dsn, "_txlock=immediate")
|
|
assert.Contains(t, dsn, "mode=rwc")
|
|
|
|
// busy_timeout must come first. The driver runs these in order on
|
|
// every new connection, and PRAGMA journal_mode takes a lock — a
|
|
// connection opened while the database is busy would fail on that
|
|
// pragma, with no busy handler yet installed to wait it out.
|
|
assert.Less(
|
|
t,
|
|
strings.Index(dsn, "busy_timeout"),
|
|
strings.Index(dsn, "journal_mode"),
|
|
"busy_timeout must be applied before journal_mode",
|
|
)
|
|
|
|
// cache=shared turns an in-process conflict into SQLITE_LOCKED,
|
|
// which the busy handler does not retry. It must never come back.
|
|
// See https://git.eeqj.de/sneak/webhooker/issues/256.
|
|
assert.NotContains(t, strings.ToLower(dsn), "cache=shared")
|
|
}
|
|
|
|
// TestPerWebhookDBAppliesPragmasOnALiveHandle is the check the issue
|
|
// asks for by name: the settings are confirmed by querying the running
|
|
// database, not by inspecting the connection string.
|
|
func TestPerWebhookDBAppliesPragmasOnALiveHandle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mgr, lc := setupTestWebhookDBManager(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, lc.Start(ctx))
|
|
|
|
defer func() { require.NoError(t, lc.Stop(ctx)) }()
|
|
|
|
webhookID := uuid.New().String()
|
|
|
|
db, err := mgr.GetDB(webhookID)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(
|
|
t, "wal",
|
|
strings.ToLower(livePragma(t, db, "journal_mode")),
|
|
)
|
|
assert.Equal(
|
|
t, "10000", livePragma(t, db, "busy_timeout"),
|
|
)
|
|
}
|
|
|
|
func TestMainDBAppliesPragmasOnALiveHandle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
|
|
sqlDB, err := database.OpenSQLite(
|
|
filepath.Join(dir, database.MainDBFileName),
|
|
database.SQLiteModeCreate,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
defer func() { require.NoError(t, sqlDB.Close()) }()
|
|
|
|
var journal string
|
|
|
|
require.NoError(t, sqlDB.
|
|
QueryRowContext(ctx, "pragma journal_mode").
|
|
Scan(&journal))
|
|
assert.Equal(t, "wal", strings.ToLower(journal))
|
|
|
|
var busy string
|
|
|
|
require.NoError(t, sqlDB.
|
|
QueryRowContext(ctx, "pragma busy_timeout").
|
|
Scan(&busy))
|
|
assert.Equal(t, "10000", busy)
|
|
}
|
|
|
|
// TestConcurrentReaderDoesNotBlockWrites is the unit-scale form of the
|
|
// reproduction in
|
|
// https://git.eeqj.de/sneak/webhooker/issues/256: an operator's
|
|
// long-held read of their own data used to make every concurrent write
|
|
// fail. Under WAL the reader takes a snapshot and the writes proceed.
|
|
func TestConcurrentReaderDoesNotBlockWrites(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mgr, lc := setupTestWebhookDBManager(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, lc.Start(ctx))
|
|
|
|
defer func() { require.NoError(t, lc.Stop(ctx)) }()
|
|
|
|
webhookID := uuid.New().String()
|
|
|
|
db, err := mgr.GetDB(webhookID)
|
|
require.NoError(t, err)
|
|
|
|
// A second handle on the same file, holding a read transaction
|
|
// open across every write below — what `sqlite3 <db> .dump` is.
|
|
readerSQL, err := database.OpenSQLite(
|
|
mgr.DBPath(webhookID), database.SQLiteModeExisting,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
defer func() { require.NoError(t, readerSQL.Close()) }()
|
|
|
|
readerConn, err := readerSQL.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
defer func() { require.NoError(t, readerConn.Close()) }()
|
|
|
|
_, err = readerConn.ExecContext(ctx, "begin deferred")
|
|
require.NoError(t, err)
|
|
|
|
_, err = readerConn.ExecContext(
|
|
ctx, "select count(*) from events",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
for range 25 {
|
|
err = db.Transaction(func(tx *gorm.DB) error {
|
|
return tx.Create(&database.Event{
|
|
WebhookID: webhookID,
|
|
EntrypointID: uuid.New().String(),
|
|
Method: "POST",
|
|
Body: "{}",
|
|
}).Error
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = readerConn.ExecContext(ctx, "commit")
|
|
require.NoError(t, err)
|
|
|
|
var count int64
|
|
|
|
require.NoError(
|
|
t,
|
|
db.Model(&database.Event{}).Count(&count).Error,
|
|
)
|
|
assert.Equal(t, int64(25), count)
|
|
}
|