check / check (push) Waiting to run
Replaces #559, which conflicted. Model: opus-5-5
281 lines
7.2 KiB
Go
281 lines
7.2 KiB
Go
package archive
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// ErrExportArchiveWriterEvicted exposes the sentinel returned by
|
|
// an evicted archive writer. It carries the Err prefix rather
|
|
// than this file's usual Export one because it is a sentinel
|
|
// error.
|
|
var ErrExportArchiveWriterEvicted = errArchiveWriterEvicted
|
|
|
|
// ExportArchivedEvent aliases the archive row type so black-box
|
|
// tests can construct and read archive rows.
|
|
type ExportArchivedEvent = archivedEvent
|
|
|
|
// ExportArchiveWriter wraps an archiveWriter so black-box tests
|
|
// can exercise the archive file mechanics.
|
|
type ExportArchiveWriter struct {
|
|
w *archiveWriter
|
|
}
|
|
|
|
// NewExportArchiveWriter builds an archive writer for tests,
|
|
// optionally overriding the reopen debounce (a non-positive
|
|
// debounce keeps the production default).
|
|
func NewExportArchiveWriter(
|
|
path string, log *slog.Logger, debounce time.Duration,
|
|
) *ExportArchiveWriter {
|
|
w := newArchiveWriter(path, log)
|
|
if debounce > 0 {
|
|
w.debounce = debounce
|
|
}
|
|
|
|
return &ExportArchiveWriter{w: w}
|
|
}
|
|
|
|
// Write archives a row through the writer, into the file named
|
|
// without a period.
|
|
func (e *ExportArchiveWriter) Write(
|
|
row ExportArchivedEvent, expiry time.Duration,
|
|
) error {
|
|
return e.w.write(row, expiry, "")
|
|
}
|
|
|
|
// WritePeriod archives a row through the writer, into the file for
|
|
// period.
|
|
func (e *ExportArchiveWriter) WritePeriod(
|
|
row ExportArchivedEvent, expiry time.Duration, period string,
|
|
) error {
|
|
return e.w.write(row, expiry, period)
|
|
}
|
|
|
|
// Open opens the archive file, pruning when expiry is positive.
|
|
func (e *ExportArchiveWriter) Open(expiry time.Duration) error {
|
|
return e.w.open(e.w.path, expiry)
|
|
}
|
|
|
|
// Reopen closes and reopens the archive file.
|
|
func (e *ExportArchiveWriter) Reopen(
|
|
expiry time.Duration,
|
|
) error {
|
|
return e.w.reopen(e.w.path, expiry)
|
|
}
|
|
|
|
// SetNow replaces the clock the writer measures its reopen
|
|
// debounce on.
|
|
func (e *ExportArchiveWriter) SetNow(now func() time.Time) {
|
|
e.w.now = now
|
|
}
|
|
|
|
// Reopens reports how many times the file has been opened.
|
|
func (e *ExportArchiveWriter) Reopens() int {
|
|
return e.w.reopens
|
|
}
|
|
|
|
// DB returns the writer's current open handle for row
|
|
// inspection in tests.
|
|
func (e *ExportArchiveWriter) DB() *gorm.DB {
|
|
return e.w.db
|
|
}
|
|
|
|
// Path returns the archive file the writer owns.
|
|
func (e *ExportArchiveWriter) Path() string {
|
|
return e.w.path
|
|
}
|
|
|
|
// OpenExisting opens the archive without permitting creation,
|
|
// the way the idle sweep does.
|
|
func (e *ExportArchiveWriter) OpenExisting(
|
|
expiry time.Duration,
|
|
) error {
|
|
return e.w.openMode(e.w.path, archiveModeExisting, expiry)
|
|
}
|
|
|
|
// SweepExpired runs an idle sweep of the archive.
|
|
func (e *ExportArchiveWriter) SweepExpired(
|
|
expiry time.Duration,
|
|
) error {
|
|
return e.w.sweepExpired(expiry)
|
|
}
|
|
|
|
// Evict marks the writer evicted and closes its handle, exactly
|
|
// as leaving the registry does.
|
|
func (e *ExportArchiveWriter) Evict() {
|
|
e.w.evict()
|
|
}
|
|
|
|
// Rename gives the archive file a new name in the same directory,
|
|
// as a rename of the webhook or target does.
|
|
func (e *ExportArchiveWriter) Rename(name string) error {
|
|
return e.w.rename(name)
|
|
}
|
|
|
|
// HandleOpen reports whether the writer currently holds an open
|
|
// archive handle.
|
|
func (e *ExportArchiveWriter) HandleOpen() bool {
|
|
e.w.mu.Lock()
|
|
defer e.w.mu.Unlock()
|
|
|
|
return e.w.db != nil
|
|
}
|
|
|
|
// Same reports whether both wrappers refer to the very same
|
|
// underlying archive writer, so a test can prove a registry entry
|
|
// is the writer it was handed rather than a replacement.
|
|
func (e *ExportArchiveWriter) Same(
|
|
other *ExportArchiveWriter,
|
|
) bool {
|
|
return other != nil && e.w == other.w
|
|
}
|
|
|
|
// ExportArchiveWriterFor returns the archive writer the registry
|
|
// currently caches for a database target, or nil when none is
|
|
// cached. It never creates one, so a test can hold a reference to
|
|
// the very writer an eviction is about to detach.
|
|
func (ws *Writers) ExportArchiveWriterFor(
|
|
targetID string,
|
|
) *ExportArchiveWriter {
|
|
ws.mu.Lock()
|
|
defer ws.mu.Unlock()
|
|
|
|
w, ok := ws.writers[targetID]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return &ExportArchiveWriter{w: w}
|
|
}
|
|
|
|
// ExportHasArchiveWriter reports whether the registry currently
|
|
// caches an archive writer for a target.
|
|
func (ws *Writers) ExportHasArchiveWriter(
|
|
targetID string,
|
|
) bool {
|
|
ws.mu.Lock()
|
|
defer ws.mu.Unlock()
|
|
|
|
_, ok := ws.writers[targetID]
|
|
|
|
return ok
|
|
}
|
|
|
|
// ExportArchiveHandleOpen reports whether the cached archive
|
|
// writer for a target holds an open database handle. It
|
|
// returns false when no writer is cached.
|
|
func (ws *Writers) ExportArchiveHandleOpen(
|
|
targetID string,
|
|
) bool {
|
|
ws.mu.Lock()
|
|
w, ok := ws.writers[targetID]
|
|
ws.mu.Unlock()
|
|
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
return w.db != nil
|
|
}
|
|
|
|
// ExportEnsureArchiveWriter creates (if needed) and returns the
|
|
// archive file path of the cached writer for a target, so a
|
|
// test can prime the registry the way a delivery would.
|
|
func (ws *Writers) ExportEnsureArchiveWriter(
|
|
targetID string,
|
|
) (string, error) {
|
|
w, err := ws.writerFor(targetID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return w.path, nil
|
|
}
|
|
|
|
// ExportSweepWriterFor takes a target's registry writer exactly
|
|
// as the idle sweep does, reporting whether the sweep had to
|
|
// create the entry. It lets a test drive the registry through the
|
|
// sweep's own entry point instead of choreographing goroutines.
|
|
func (ws *Writers) ExportSweepWriterFor(
|
|
targetID string,
|
|
) (*ExportArchiveWriter, bool, error) {
|
|
w, created, err := ws.sweepWriterFor(targetID)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return &ExportArchiveWriter{w: w}, created, nil
|
|
}
|
|
|
|
// ExportReleaseSweepWriter releases a sweep-created registry entry
|
|
// exactly as a finished sweep does.
|
|
func (ws *Writers) ExportReleaseSweepWriter(
|
|
targetID string, w *ExportArchiveWriter,
|
|
) {
|
|
ws.releaseSweepWriter(targetID, w.w)
|
|
}
|
|
|
|
// NewTestSweeper builds a Sweeper backed by the given main
|
|
// database and archive writers, without the fx lifecycle.
|
|
// Intended for tests.
|
|
func NewTestSweeper(
|
|
db *database.Database,
|
|
writers *Writers,
|
|
log *slog.Logger,
|
|
) *Sweeper {
|
|
return &Sweeper{
|
|
db: db,
|
|
writers: writers,
|
|
log: log,
|
|
interval: time.Hour,
|
|
}
|
|
}
|
|
|
|
// ExportSweep runs a single archive sweep synchronously for
|
|
// tests.
|
|
func (s *Sweeper) ExportSweep(ctx context.Context) {
|
|
s.sweep(ctx)
|
|
}
|
|
|
|
// ExportStart starts the sweeper's background loop for tests.
|
|
func (s *Sweeper) ExportStart() {
|
|
s.start()
|
|
}
|
|
|
|
// ExportRegisterHooks registers the sweeper's real fx lifecycle
|
|
// hooks on a lifecycle supplied by a test, so a test can drive
|
|
// the exact OnStart/OnStop functions the application runs and
|
|
// hand OnStart the kind of context fx actually supplies.
|
|
func (s *Sweeper) ExportRegisterHooks(lc fx.Lifecycle) {
|
|
s.registerHooks(lc)
|
|
}
|
|
|
|
// ExportStop stops the sweeper's background loop for tests.
|
|
func (s *Sweeper) ExportStop(ctx context.Context) error {
|
|
return s.stop(ctx)
|
|
}
|
|
|
|
// ExportWedgeLoop adds a goroutine to the sweeper's WaitGroup
|
|
// that never observes cancellation and returns only when release
|
|
// is closed. It stands in for a prune stuck on a locked archive.
|
|
func (s *Sweeper) ExportWedgeLoop(
|
|
release <-chan struct{},
|
|
) {
|
|
s.wg.Go(func() {
|
|
<-release
|
|
})
|
|
}
|
|
|
|
// ExportSetInterval overrides the sweep interval for tests.
|
|
func (s *Sweeper) ExportSetInterval(d time.Duration) {
|
|
s.interval = d
|
|
}
|