Рыба проекта. Минимальная функциональность

This commit is contained in:
2026-08-03 22:22:24 +03:00
commit 8c8631ac9c
80 changed files with 10618 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
package pipeline
import (
"context"
"io"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)
// Option configures a Runner.
type Option func(*runner)
// WithDumper replaces the default pg_dump dumper.
func WithDumper(dumper pgdump.Dumper) Option {
return func(r *runner) {
r.dumper = dumper
}
}
// WithEncryptor replaces the default encryptor.
func WithEncryptor(encryptor crypto.Encryptor) Option {
return func(r *runner) {
r.encryptor = encryptor
}
}
// Runner orchestrates the dump → encrypt → sink pipeline.
type runner struct {
dumper pgdump.Dumper
encryptor crypto.Encryptor
}
// NewRunner creates a pipeline runner with the given functional options.
func NewRunner(options ...Option) *runner {
r := &runner{}
for _, option := range options {
option(r)
}
return r
}
// Run executes the full backup pipeline: pg_dump → encrypt → sink.
func (r *runner) Run(
ctx context.Context,
pgDumpOpts pgdump.Options,
recipients []crypto.RecipientPub,
sink domain.Sink,
rand io.Reader,
) (retErr error) {
tx, err := sink.Begin(pgDumpOpts.Key)
if err != nil {
return err
}
defer func() {
if retErr != nil {
_ = tx.Abort()
}
}()
dumpCtx, dumpCancel := context.WithCancel(ctx)
defer dumpCancel()
pipeR, pipeW := io.Pipe()
dumpErrCh := make(chan error, 1)
go func() {
dumpErrCh <- r.dumper.Dump(dumpCtx, pgDumpOpts, pipeW)
}()
encryptErrCh := make(chan error, 1)
go func() {
encryptErrCh <- r.encryptor.Encrypt(pipeR, recipients, tx, rand)
}()
select {
case derr := <-dumpErrCh:
if derr != nil {
_ = pipeR.CloseWithError(derr)
_ = <-encryptErrCh
return derr
}
eerr := <-encryptErrCh
if eerr != nil {
return eerr
}
return tx.Commit()
case eerr := <-encryptErrCh:
if eerr != nil {
dumpCancel()
_ = pipeR.CloseWithError(eerr)
_ = <-dumpErrCh
return eerr
}
derr := <-dumpErrCh
if derr != nil {
return derr
}
return tx.Commit()
}
}
+249
View File
@@ -0,0 +1,249 @@
package pipeline
import (
"context"
"errors"
"io"
"runtime"
"testing"
"time"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)
var (
errPgDumpFailed = errors.New("pg_dump failed")
errEncryptFailed = errors.New("encryption failed")
)
type fakeSink struct {
transaction *fakeSinkTx
}
func (sink *fakeSink) Begin(key string) (domain.SinkTx, error) {
sink.transaction = &fakeSinkTx{}
return sink.transaction, nil
}
func (sink *fakeSink) List(prefix string) ([]string, error) {
return make([]string, 0), nil
}
func (sink *fakeSink) Remove(key string) error {
return nil
}
type fakeSinkTx struct {
committed bool
aborted bool
data []byte
}
func (transaction *fakeSinkTx) Write(p []byte) (int, error) {
transaction.data = append(transaction.data, p...)
return len(p), nil
}
func (transaction *fakeSinkTx) Commit() error {
transaction.committed = true
return nil
}
func (transaction *fakeSinkTx) Abort() error {
transaction.aborted = true
return nil
}
type fakeDumper struct {
writeBytes int
returnErr error
closePipe bool
}
func (dumper *fakeDumper) Dump(
ctx context.Context,
opts pgdump.Options,
writer io.Writer,
) error {
if dumper.writeBytes > 0 {
data := make([]byte, dumper.writeBytes)
if _, err := writer.Write(data); err != nil {
return err
}
}
if dumper.closePipe {
if closer, ok := writer.(io.Closer); ok {
_ = closer.Close()
}
}
return dumper.returnErr
}
type fakeEncryptor struct {
readBytes int
returnErr error
}
func (encryptor *fakeEncryptor) Encrypt(
plaintext io.Reader,
recipients []crypto.RecipientPub,
sink io.Writer,
rand io.Reader,
) error {
if encryptor.readBytes > 0 {
buf := make([]byte, encryptor.readBytes)
if _, err := io.ReadFull(plaintext, buf); err != nil {
return err
}
}
return encryptor.returnErr
}
func countGoroutines() int {
return runtime.NumGoroutine()
}
func waitForGoroutinesStable(baseline int) bool {
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
if runtime.NumGoroutine() <= baseline {
return true
}
}
return false
}
func TestPipeline_DumpReturnsWithoutClosing(t *testing.T) {
baseline := countGoroutines()
sink := &fakeSink{}
dumper := &fakeDumper{
writeBytes: 4 * 1024,
returnErr: errPgDumpFailed,
closePipe: false,
}
encryptor := &fakeEncryptor{
readBytes: 4 * 1024,
}
runner := NewRunner(
WithDumper(dumper),
WithEncryptor(encryptor),
)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := runner.Run(
ctx,
pgdump.Options{Key: "backup.sql"},
make([]crypto.RecipientPub, 0),
sink,
nil,
)
if !errors.Is(err, errPgDumpFailed) {
t.Fatalf("expected errPgDumpFailed, got %v", err)
}
if !sink.transaction.aborted {
t.Fatalf("expected transaction to be aborted on error")
}
if waitForGoroutinesStable(baseline) {
return
}
t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
}
func TestPipeline_EncryptFailsFirst(t *testing.T) {
baseline := countGoroutines()
sink := &fakeSink{}
dumper := &fakeDumper{
writeBytes: 64 * 1024,
returnErr: nil,
closePipe: false,
}
encryptor := &fakeEncryptor{
readBytes: 1024,
returnErr: errEncryptFailed,
}
runner := NewRunner(
WithDumper(dumper),
WithEncryptor(encryptor),
)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := runner.Run(
ctx,
pgdump.Options{Key: "backup.sql"},
make([]crypto.RecipientPub, 0),
sink,
nil,
)
if !errors.Is(err, errEncryptFailed) {
t.Fatalf("expected errEncryptFailed, got %v", err)
}
if !sink.transaction.aborted {
t.Fatalf("expected transaction to be aborted on error")
}
if waitForGoroutinesStable(baseline) {
return
}
t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
}
func TestPipeline_SuccessfulRunCommits(t *testing.T) {
baseline := countGoroutines()
sink := &fakeSink{}
dumper := &fakeDumper{
writeBytes: 4 * 1024,
returnErr: nil,
closePipe: true,
}
encryptor := &fakeEncryptor{
readBytes: 4 * 1024,
}
runner := NewRunner(
WithDumper(dumper),
WithEncryptor(encryptor),
)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := runner.Run(
ctx,
pgdump.Options{Key: "backup.sql"},
make([]crypto.RecipientPub, 0),
sink,
nil,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !sink.transaction.committed {
t.Fatalf("expected transaction to be committed on success")
}
if waitForGoroutinesStable(baseline) {
return
}
t.Fatalf("goroutine leak detected: baseline %d, current %d", baseline, countGoroutines())
}