Worker: separate contexts of upstream buffer
The upstream buffer takes care of two things: communication with Flamenco Manager (first context) and buffering things in a SQLite database (second context). This commit separates those two contexts, so that shutting down the application isn't going to prevent buffering things in the database.
This commit is contained in:
parent
959a235c59
commit
cc20b850ec
@ -21,6 +21,13 @@ import (
|
|||||||
// TODO: pull the SQLite stuff out of this file into a more global place, so
|
// TODO: pull the SQLite stuff out of this file into a more global place, so
|
||||||
// that other areas of Flamenco Worker can also use it.
|
// that other areas of Flamenco Worker can also use it.
|
||||||
|
|
||||||
|
// Note that there are two contexts used in this file. One (`dbCtx`) is for
|
||||||
|
// database access and is a local, short-lived, background context. The other
|
||||||
|
// (`ctx`) is the one that's passed from the caller, which should indicate the
|
||||||
|
// global worker context. If that context is done, queueing updates in the
|
||||||
|
// database will still work, but all communication with Flamenco Manager will
|
||||||
|
// halt.
|
||||||
|
|
||||||
// UpstreamBufferDB implements the UpstreamBuffer interface using a database as backend.
|
// UpstreamBufferDB implements the UpstreamBuffer interface using a database as backend.
|
||||||
type UpstreamBufferDB struct {
|
type UpstreamBufferDB struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
@ -35,6 +42,7 @@ type UpstreamBufferDB struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultUpstreamFlushInterval = 30 * time.Second
|
const defaultUpstreamFlushInterval = 30 * time.Second
|
||||||
|
const databaseContextTimeout = 10 * time.Second
|
||||||
|
|
||||||
var _ UpstreamBuffer = (*UpstreamBufferDB)(nil)
|
var _ UpstreamBuffer = (*UpstreamBufferDB)(nil)
|
||||||
|
|
||||||
@ -54,7 +62,7 @@ func NewUpstreamBuffer(client FlamencoClient, clock TimeService) (*UpstreamBuffe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OpenDB opens the database. Must be called once before using.
|
// OpenDB opens the database. Must be called once before using.
|
||||||
func (ub *UpstreamBufferDB) OpenDB(ctx context.Context, databaseFilename string) error {
|
func (ub *UpstreamBufferDB) OpenDB(dbCtx context.Context, databaseFilename string) error {
|
||||||
if ub.db != nil {
|
if ub.db != nil {
|
||||||
return errors.New("upstream buffer database already opened")
|
return errors.New("upstream buffer database already opened")
|
||||||
}
|
}
|
||||||
@ -64,13 +72,13 @@ func (ub *UpstreamBufferDB) OpenDB(ctx context.Context, databaseFilename string)
|
|||||||
return fmt.Errorf("opening %s: %w", databaseFilename, err)
|
return fmt.Errorf("opening %s: %w", databaseFilename, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.PingContext(ctx); err != nil {
|
if err := db.PingContext(dbCtx); err != nil {
|
||||||
return fmt.Errorf("accessing %s: %w", databaseFilename, err)
|
return fmt.Errorf("accessing %s: %w", databaseFilename, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ub.db = db
|
ub.db = db
|
||||||
|
|
||||||
if err := ub.prepareDatabase(ctx); err != nil {
|
if err := ub.prepareDatabase(dbCtx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +92,7 @@ func (ub *UpstreamBufferDB) SendTaskUpdate(ctx context.Context, taskID string, u
|
|||||||
ub.dbMutex.Lock()
|
ub.dbMutex.Lock()
|
||||||
defer ub.dbMutex.Unlock()
|
defer ub.dbMutex.Unlock()
|
||||||
|
|
||||||
queueSize, err := ub.queueSize(ctx)
|
queueSize, err := ub.queueSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to determine upstream queue size: %w", err)
|
return fmt.Errorf("unable to determine upstream queue size: %w", err)
|
||||||
}
|
}
|
||||||
@ -93,7 +101,7 @@ func (ub *UpstreamBufferDB) SendTaskUpdate(ctx context.Context, taskID string, u
|
|||||||
if queueSize > 0 {
|
if queueSize > 0 {
|
||||||
log.Debug().Int("queueSize", queueSize).
|
log.Debug().Int("queueSize", queueSize).
|
||||||
Msg("task updates already queued, immediately queueing new update")
|
Msg("task updates already queued, immediately queueing new update")
|
||||||
return ub.queueTaskUpdate(ctx, taskID, update)
|
return ub.queueTaskUpdate(taskID, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to deliver the update.
|
// Try to deliver the update.
|
||||||
@ -101,7 +109,7 @@ func (ub *UpstreamBufferDB) SendTaskUpdate(ctx context.Context, taskID string, u
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("task", taskID).
|
log.Warn().Err(err).Str("task", taskID).
|
||||||
Msg("error communicating with Manager, going to queue task update for sending later")
|
Msg("error communicating with Manager, going to queue task update for sending later")
|
||||||
return ub.queueTaskUpdate(ctx, taskID, update)
|
return ub.queueTaskUpdate(taskID, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Manager responded, so no need to queue this update, even when there was an error.
|
// The Manager responded, so no need to queue this update, even when there was an error.
|
||||||
@ -117,7 +125,7 @@ func (ub *UpstreamBufferDB) SendTaskUpdate(ctx context.Context, taskID string, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close releases the database. It does not try to flush any pending items.
|
// Close releases the database. It does not try to flush any pending items.
|
||||||
func (ub *UpstreamBufferDB) Close(ctx context.Context) error {
|
func (ub *UpstreamBufferDB) Close() error {
|
||||||
if ub.db == nil {
|
if ub.db == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -131,11 +139,11 @@ func (ub *UpstreamBufferDB) Close(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prepareDatabase creates the database schema, if necessary.
|
// prepareDatabase creates the database schema, if necessary.
|
||||||
func (ub *UpstreamBufferDB) prepareDatabase(ctx context.Context) error {
|
func (ub *UpstreamBufferDB) prepareDatabase(dbCtx context.Context) error {
|
||||||
ub.dbMutex.Lock()
|
ub.dbMutex.Lock()
|
||||||
defer ub.dbMutex.Unlock()
|
defer ub.dbMutex.Unlock()
|
||||||
|
|
||||||
tx, err := ub.db.BeginTx(ctx, nil)
|
tx, err := ub.db.BeginTx(dbCtx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("beginning database transaction: %w", err)
|
return fmt.Errorf("beginning database transaction: %w", err)
|
||||||
}
|
}
|
||||||
@ -144,7 +152,7 @@ func (ub *UpstreamBufferDB) prepareDatabase(ctx context.Context) error {
|
|||||||
stmt := `CREATE TABLE IF NOT EXISTS task_update_queue(task_id VARCHAR(36), payload BLOB)`
|
stmt := `CREATE TABLE IF NOT EXISTS task_update_queue(task_id VARCHAR(36), payload BLOB)`
|
||||||
log.Debug().Str("sql", stmt).Msg("creating database table")
|
log.Debug().Str("sql", stmt).Msg("creating database table")
|
||||||
|
|
||||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
if _, err := tx.ExecContext(dbCtx, stmt); err != nil {
|
||||||
return fmt.Errorf("creating database table: %w", err)
|
return fmt.Errorf("creating database table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,15 +163,18 @@ func (ub *UpstreamBufferDB) prepareDatabase(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ub *UpstreamBufferDB) queueSize(ctx context.Context) (int, error) {
|
func (ub *UpstreamBufferDB) queueSize() (int, error) {
|
||||||
if ub.db == nil {
|
if ub.db == nil {
|
||||||
log.Panic().Msg("no database opened, unable to inspect upstream queue")
|
log.Panic().Msg("no database opened, unable to inspect upstream queue")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbCtx, dbCtxCancel := context.WithTimeout(context.Background(), databaseContextTimeout)
|
||||||
|
defer dbCtxCancel()
|
||||||
|
|
||||||
var queueSize int
|
var queueSize int
|
||||||
|
|
||||||
err := ub.db.
|
err := ub.db.
|
||||||
QueryRowContext(ctx, "SELECT count(*) FROM task_update_queue").
|
QueryRowContext(dbCtx, "SELECT count(*) FROM task_update_queue").
|
||||||
Scan(&queueSize)
|
Scan(&queueSize)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@ -176,12 +187,15 @@ func (ub *UpstreamBufferDB) queueSize(ctx context.Context) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ub *UpstreamBufferDB) queueTaskUpdate(ctx context.Context, taskID string, update api.TaskUpdateJSONRequestBody) error {
|
func (ub *UpstreamBufferDB) queueTaskUpdate(taskID string, update api.TaskUpdateJSONRequestBody) error {
|
||||||
if ub.db == nil {
|
if ub.db == nil {
|
||||||
log.Panic().Msg("no database opened, unable to queue task updates")
|
log.Panic().Msg("no database opened, unable to queue task updates")
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := ub.db.BeginTx(ctx, nil)
|
dbCtx, dbCtxCancel := context.WithTimeout(context.Background(), databaseContextTimeout)
|
||||||
|
defer dbCtxCancel()
|
||||||
|
|
||||||
|
tx, err := ub.db.BeginTx(dbCtx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("beginning database transaction: %w", err)
|
return fmt.Errorf("beginning database transaction: %w", err)
|
||||||
}
|
}
|
||||||
@ -195,7 +209,7 @@ func (ub *UpstreamBufferDB) queueTaskUpdate(ctx context.Context, taskID string,
|
|||||||
stmt := `INSERT INTO task_update_queue (task_id, payload) VALUES (?, ?)`
|
stmt := `INSERT INTO task_update_queue (task_id, payload) VALUES (?, ?)`
|
||||||
log.Debug().Str("sql", stmt).Str("task", taskID).Msg("inserting task update")
|
log.Debug().Str("sql", stmt).Str("task", taskID).Msg("inserting task update")
|
||||||
|
|
||||||
if _, err := tx.ExecContext(ctx, stmt, taskID, blob); err != nil {
|
if _, err := tx.ExecContext(dbCtx, stmt, taskID, blob); err != nil {
|
||||||
return fmt.Errorf("queueing task update: %w", err)
|
return fmt.Errorf("queueing task update: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +229,7 @@ func (ub *UpstreamBufferDB) Flush(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// See if we need to flush at all.
|
// See if we need to flush at all.
|
||||||
queueSize, err := ub.queueSize(ctx)
|
queueSize, err := ub.queueSize()
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return fmt.Errorf("unable to determine queue size: %w", err)
|
return fmt.Errorf("unable to determine queue size: %w", err)
|
||||||
@ -237,7 +251,10 @@ func (ub *UpstreamBufferDB) Flush(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ub *UpstreamBufferDB) flushFirstItem(ctx context.Context) (done bool, err error) {
|
func (ub *UpstreamBufferDB) flushFirstItem(ctx context.Context) (done bool, err error) {
|
||||||
tx, err := ub.db.BeginTx(ctx, nil)
|
dbCtx, dbCtxCancel := context.WithTimeout(context.Background(), databaseContextTimeout)
|
||||||
|
defer dbCtxCancel()
|
||||||
|
|
||||||
|
tx, err := ub.db.BeginTx(dbCtx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("beginning database transaction: %w", err)
|
return false, fmt.Errorf("beginning database transaction: %w", err)
|
||||||
}
|
}
|
||||||
@ -250,7 +267,7 @@ func (ub *UpstreamBufferDB) flushFirstItem(ctx context.Context) (done bool, err
|
|||||||
var taskID string
|
var taskID string
|
||||||
var blob []byte
|
var blob []byte
|
||||||
|
|
||||||
err = tx.QueryRowContext(ctx, stmt).Scan(&rowID, &taskID, &blob)
|
err = tx.QueryRowContext(dbCtx, stmt).Scan(&rowID, &taskID, &blob)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows:
|
||||||
// Flush operation is done.
|
// Flush operation is done.
|
||||||
@ -268,7 +285,7 @@ func (ub *UpstreamBufferDB) flushFirstItem(ctx context.Context) (done bool, err
|
|||||||
// than to discard it and ignore it ever happened.
|
// than to discard it and ignore it ever happened.
|
||||||
logger.Warn().Err(err).
|
logger.Warn().Err(err).
|
||||||
Msg("unable to unmarshal queued task update, discarding")
|
Msg("unable to unmarshal queued task update, discarding")
|
||||||
if err := ub.discardRow(ctx, tx, rowID); err != nil {
|
if err := ub.discardRow(tx, rowID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return false, tx.Commit()
|
return false, tx.Commit()
|
||||||
@ -295,17 +312,20 @@ func (ub *UpstreamBufferDB) flushFirstItem(ctx context.Context) (done bool, err
|
|||||||
Msg("queued task update discarded by Manager, unknown reason")
|
Msg("queued task update discarded by Manager, unknown reason")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ub.discardRow(ctx, tx, rowID); err != nil {
|
if err := ub.discardRow(tx, rowID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return false, tx.Commit()
|
return false, tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ub *UpstreamBufferDB) discardRow(ctx context.Context, tx *sql.Tx, rowID int64) error {
|
func (ub *UpstreamBufferDB) discardRow(tx *sql.Tx, rowID int64) error {
|
||||||
|
dbCtx, dbCtxCancel := context.WithTimeout(context.Background(), databaseContextTimeout)
|
||||||
|
defer dbCtxCancel()
|
||||||
|
|
||||||
stmt := `DELETE FROM task_update_queue WHERE rowid = ?`
|
stmt := `DELETE FROM task_update_queue WHERE rowid = ?`
|
||||||
log.Trace().Str("sql", stmt).Int64("rowID", rowID).Msg("un-queueing task update")
|
log.Trace().Str("sql", stmt).Int64("rowID", rowID).Msg("un-queueing task update")
|
||||||
|
|
||||||
_, err := tx.ExecContext(ctx, stmt, rowID)
|
_, err := tx.ExecContext(dbCtx, stmt, rowID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("un-queueing task update: %w", err)
|
return fmt.Errorf("un-queueing task update: %w", err)
|
||||||
}
|
}
|
||||||
@ -334,7 +354,7 @@ func (ub *UpstreamBufferDB) periodicFlushLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rollbackTransaction(tx *sql.Tx) {
|
func rollbackTransaction(tx *sql.Tx) {
|
||||||
if err := tx.Rollback(); err != nil {
|
if err := tx.Rollback(); err != nil && err != sql.ErrTxDone {
|
||||||
log.Error().Err(err).Msg("rolling back transaction")
|
log.Error().Err(err).Msg("rolling back transaction")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ func TestUpstreamBufferCloseUnopened(t *testing.T) {
|
|||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
ub, _ := mockUpstreamBufferDB(t, mockCtrl)
|
ub, _ := mockUpstreamBufferDB(t, mockCtrl)
|
||||||
err := ub.Close(context.Background())
|
err := ub.Close()
|
||||||
assert.NoError(t, err, "Closing without opening should be OK")
|
assert.NoError(t, err, "Closing without opening should be OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ func TestUpstreamBufferManagerUnavailable(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check the queue size, it should have an item queued.
|
// Check the queue size, it should have an item queued.
|
||||||
queueSize, err := ub.queueSize(ctx)
|
queueSize, err := ub.queueSize()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, queueSize)
|
assert.Equal(t, 1, queueSize)
|
||||||
|
|
||||||
@ -94,10 +94,10 @@ func TestUpstreamBufferManagerUnavailable(t *testing.T) {
|
|||||||
|
|
||||||
// Queue should be empty now.
|
// Queue should be empty now.
|
||||||
ub.dbMutex.Lock()
|
ub.dbMutex.Lock()
|
||||||
queueSize, err = ub.queueSize(ctx)
|
queueSize, err = ub.queueSize()
|
||||||
ub.dbMutex.Unlock()
|
ub.dbMutex.Unlock()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, queueSize)
|
assert.Equal(t, 0, queueSize)
|
||||||
|
|
||||||
assert.NoError(t, ub.Close(ctx))
|
assert.NoError(t, ub.Close())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user