mirror of
https://github.com/MedUnes/go-kata.git
synced 2026-07-28 10:40:14 +07:00
Create backup.go
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package defer_cleanup_chain
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Tx models the transaction lifecycle we need for this kata.
|
||||
// Keeping this small makes it easy to provide fake implementations in tests.
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
// Rows models a streaming result set.
|
||||
// Values returns the current row payload after Next() moved to a valid item.
|
||||
type Rows interface {
|
||||
Next() bool
|
||||
Values() (id int, payload string, err error)
|
||||
Err() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// DB is the minimal database contract needed by BackupDatabase.
|
||||
// The implementation can be real SQL, in-memory, or fully mocked in tests.
|
||||
type DB interface {
|
||||
Begin(ctx context.Context) (Tx, error)
|
||||
QueryBackupRows(ctx context.Context, tx Tx) (Rows, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Connector abstracts DB creation so tests can inject controlled failures.
|
||||
type Connector func(ctx context.Context, dbURL string) (DB, error)
|
||||
|
||||
// defaultConnector is intentionally unconfigured.
|
||||
// In real code this would be wired to an actual driver in init/bootstrap code.
|
||||
var defaultConnector Connector = func(ctx context.Context, dbURL string) (DB, error) {
|
||||
return nil, errors.New("no database connector configured")
|
||||
}
|
||||
|
||||
// BackupDatabase performs the backup flow while preserving both operation and
|
||||
// cleanup errors. It uses a named return error so deferred cleanups can append
|
||||
// their own failures via errors.Join.
|
||||
func BackupDatabase(ctx context.Context, dbURL, filename string) (err error) {
|
||||
return backupDatabase(ctx, dbURL, filename, defaultConnector, os.Create)
|
||||
}
|
||||
|
||||
// backupDatabase is split from BackupDatabase to keep the public API clean while
|
||||
// allowing tests to inject open/connect behavior and force edge cases.
|
||||
func backupDatabase(
|
||||
ctx context.Context,
|
||||
dbURL, filename string,
|
||||
connect Connector,
|
||||
openFile func(name string) (*os.File, error),
|
||||
) (err error) {
|
||||
// Acquire #1: output file.
|
||||
// Defer is registered immediately so every return path closes the file.
|
||||
file, err := openFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open output file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := file.Close(); closeErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("close output file: %w", closeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Wrap the file with a buffered writer. This is another acquired resource:
|
||||
// data can remain in memory until Flush runs, so we must defer Flush as well.
|
||||
writer := bufio.NewWriter(file)
|
||||
defer func() {
|
||||
if flushErr := writer.Flush(); flushErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("flush writer: %w", flushErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Acquire #2: DB connection.
|
||||
db, err := connect(ctx, dbURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect DB: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := db.Close(); closeErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("close DB connection: %w", closeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Acquire #3: transaction.
|
||||
// We avoid manual rollback branches by using a committed flag consumed by defer.
|
||||
tx, err := db.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if committed {
|
||||
return
|
||||
}
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("rollback transaction: %w", rollbackErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Acquire #4: row stream (logical cursor/network resource).
|
||||
rows, err := db.QueryBackupRows(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query backup rows: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := rows.Close(); closeErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("close rows: %w", closeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Stream each row to the backup file.
|
||||
// We intentionally do not defer anything inside this loop.
|
||||
for rows.Next() {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return fmt.Errorf("backup canceled: %w", ctxErr)
|
||||
}
|
||||
|
||||
id, payload, rowErr := rows.Values()
|
||||
if rowErr != nil {
|
||||
return fmt.Errorf("read row values: %w", rowErr)
|
||||
}
|
||||
|
||||
if _, writeErr := fmt.Fprintf(writer, "%d,%s\n", id, payload); writeErr != nil {
|
||||
return fmt.Errorf("write backup row: %w", writeErr)
|
||||
}
|
||||
}
|
||||
if rowsErr := rows.Err(); rowsErr != nil {
|
||||
return fmt.Errorf("iterate backup rows: %w", rowsErr)
|
||||
}
|
||||
|
||||
// Commit is the success boundary.
|
||||
// If commit fails, committed remains false and deferred rollback still runs.
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
return fmt.Errorf("commit transaction: %w", commitErr)
|
||||
}
|
||||
committed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user