1
0
mirror of https://github.com/rancher/steve.git synced 2025-08-31 15:11:31 +00:00

sql: use a closure to wrap transactions (#469)

This introduces the a `WithTransaction` function, which is then used for all transactional work in Steve.

Because `WithTransaction` takes care of all `Begin`s, `Commit`s and `Rollback`s, it eliminates the problem where forgotten open transactions can block all other operations (with long stalling and `SQLITE_BUSY` errors).

This also:

- merges together the disparate `DBClient` interfaces in one only `db.Client` interface with one unexported non-test implementation. I found this much easier to follow
- refactors the transaction package in order to make it as minimal as possible, and as close to the wrapped `sql.Tx` and `sql.Stmt` functions as possible, in order to reduce cognitive load when working with this part of the codebase
- simplifies tests accordingly
- adds a couple of known files to `.gitignore`
    
Credits to @tomleb for suggesting the approach: https://github.com/rancher/lasso/pull/121#pullrequestreview-2515872507
This commit is contained in:
Silvio Moioli
2025-02-05 10:05:52 +01:00
committed by GitHub
parent 6a46a1e091
commit 772dc7577e
28 changed files with 1543 additions and 2059 deletions

View File

@@ -62,45 +62,34 @@ type Indexer struct {
var _ cache.Indexer = (*Indexer)(nil)
type Store interface {
DBClient
db.Client
cache.Store
GetByKey(key string) (item any, exists bool, err error)
GetName() string
RegisterAfterUpsert(f func(key string, obj any, tx db.TXClient) error)
RegisterAfterDelete(f func(key string, tx db.TXClient) error)
RegisterAfterUpsert(f func(key string, obj any, tx transaction.Client) error)
RegisterAfterDelete(f func(key string, tx transaction.Client) error)
GetShouldEncrypt() bool
GetType() reflect.Type
}
type DBClient interface {
BeginTx(ctx context.Context, forWriting bool) (db.TXClient, error)
QueryForRows(ctx context.Context, stmt transaction.Stmt, params ...any) (*sql.Rows, error)
ReadObjects(rows db.Rows, typ reflect.Type, shouldDecrypt bool) ([]any, error)
ReadStrings(rows db.Rows) ([]string, error)
ReadInt(rows db.Rows) (int, error)
Prepare(stmt string) *sql.Stmt
CloseStmt(stmt db.Closable) error
}
// NewIndexer returns a cache.Indexer backed by SQLite for objects of the given example type
func NewIndexer(indexers cache.Indexers, s Store) (*Indexer, error) {
tx, err := s.BeginTx(context.Background(), true)
if err != nil {
return nil, err
}
dbName := db.Sanitize(s.GetName())
createTableQuery := fmt.Sprintf(createTableFmt, dbName)
err = tx.Exec(createTableQuery)
if err != nil {
return nil, &db.QueryError{QueryString: createTableQuery, Err: err}
}
createIndexQuery := fmt.Sprintf(createIndexFmt, dbName)
err = tx.Exec(createIndexQuery)
if err != nil {
return nil, &db.QueryError{QueryString: createIndexQuery, Err: err}
}
err = tx.Commit()
err := s.WithTransaction(context.Background(), true, func(tx transaction.Client) error {
createTableQuery := fmt.Sprintf(createTableFmt, dbName)
_, err := tx.Exec(createTableQuery)
if err != nil {
return &db.QueryError{QueryString: createTableQuery, Err: err}
}
createIndexQuery := fmt.Sprintf(createIndexFmt, dbName)
_, err = tx.Exec(createIndexQuery)
if err != nil {
return &db.QueryError{QueryString: createIndexQuery, Err: err}
}
return nil
})
if err != nil {
return nil, err
}
@@ -129,9 +118,9 @@ func NewIndexer(indexers cache.Indexers, s Store) (*Indexer, error) {
/* Core methods */
// AfterUpsert updates indices of an object
func (i *Indexer) AfterUpsert(key string, obj any, tx db.TXClient) error {
func (i *Indexer) AfterUpsert(key string, obj any, tx transaction.Client) error {
// delete all
err := tx.StmtExec(tx.Stmt(i.deleteIndicesStmt), key)
_, err := tx.Stmt(i.deleteIndicesStmt).Exec(key)
if err != nil {
return &db.QueryError{QueryString: i.deleteIndicesQuery, Err: err}
}
@@ -146,7 +135,7 @@ func (i *Indexer) AfterUpsert(key string, obj any, tx db.TXClient) error {
}
for _, value := range values {
err = tx.StmtExec(tx.Stmt(i.addIndexStmt), indexName, value, key)
_, err = tx.Stmt(i.addIndexStmt).Exec(indexName, value, key)
if err != nil {
return &db.QueryError{QueryString: i.addIndexQuery, Err: err}
}