Replaces #559, which conflicted. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user