Name each database target's archive for its webhook and target (closes #376)
check / check (push) Successful in 3m13s
check / check (push) Successful in 3m13s
Each database target now has its own archive file, archive-WEBHOOKNAME-TARGETNAME-TARGETID.db, instead of one archive-WEBHOOKID.db per webhook. delivery.ArchiveFileName builds the name: each name is lowercased, keeps ASCII letters and digits, turns every other run of characters into one dash, and is cut to 40 characters. A change of webhook or target name renames its archive files under the archive writer's lock, before the new name is saved, and back again if the save fails. A rename never replaces a file, and one that fails part way moves back what it moved. Deleting a target evicts only that target's writer. Archive files are never deleted, and nothing looks for files under the old name. Model: opus-5-5
This commit is contained in:
@@ -3,6 +3,7 @@ package handlers_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -52,23 +53,77 @@ func (n *recordingNotifier) Tasks() []delivery.Task {
|
||||
return out
|
||||
}
|
||||
|
||||
// recordingEvictor is a delivery.WebhookEvictor that records
|
||||
// the webhook ids it was asked to evict, so a test can prove
|
||||
// that a deletion path reached the delivery engine.
|
||||
type recordingEvictor struct {
|
||||
mu sync.Mutex
|
||||
evicted []string
|
||||
// recordingArchives is a delivery.Archives that records what it
|
||||
// was asked to do, so a test can prove that a deletion or rename
|
||||
// path reached the delivery engine. After FailRenames, every
|
||||
// rename of that target fails with the given error.
|
||||
type recordingArchives struct {
|
||||
mu sync.Mutex
|
||||
evicted []string
|
||||
evictedTargets []string
|
||||
renames []archiveRename
|
||||
renameErrs map[string]error
|
||||
}
|
||||
|
||||
func (r *recordingEvictor) EvictWebhook(webhookID string) {
|
||||
// errInjectedRename is the failure a test hands FailRenames.
|
||||
var errInjectedRename = errors.New("injected rename failure")
|
||||
|
||||
// errNameTaken is what the delivery engine returns when a file
|
||||
// already has an archive's new name, here archive-taken.db.
|
||||
var errNameTaken = fmt.Errorf(
|
||||
"%w: archive-taken.db", delivery.ErrArchiveNameTaken,
|
||||
)
|
||||
|
||||
// archiveRename is one recorded Rename call.
|
||||
type archiveRename struct {
|
||||
TargetID string
|
||||
WebhookName string
|
||||
TargetName string
|
||||
}
|
||||
|
||||
func (r *recordingArchives) EvictWebhook(webhookID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.evicted = append(r.evicted, webhookID)
|
||||
}
|
||||
|
||||
func (r *recordingArchives) EvictTarget(targetID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.evictedTargets = append(r.evictedTargets, targetID)
|
||||
}
|
||||
|
||||
func (r *recordingArchives) Rename(
|
||||
targetID, webhookName, targetName string,
|
||||
) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.renames = append(r.renames, archiveRename{
|
||||
TargetID: targetID,
|
||||
WebhookName: webhookName,
|
||||
TargetName: targetName,
|
||||
})
|
||||
|
||||
return r.renameErrs[targetID]
|
||||
}
|
||||
|
||||
// FailRenames makes every later rename of targetID fail with err.
|
||||
func (r *recordingArchives) FailRenames(targetID string, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.renameErrs == nil {
|
||||
r.renameErrs = map[string]error{}
|
||||
}
|
||||
|
||||
r.renameErrs[targetID] = err
|
||||
}
|
||||
|
||||
// Evicted returns a copy of the recorded webhook ids.
|
||||
func (r *recordingEvictor) Evicted() []string {
|
||||
func (r *recordingArchives) Evicted() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -78,6 +133,28 @@ func (r *recordingEvictor) Evicted() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// EvictedTargets returns a copy of the recorded target ids.
|
||||
func (r *recordingArchives) EvictedTargets() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
out := make([]string, len(r.evictedTargets))
|
||||
copy(out, r.evictedTargets)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Renames returns a copy of the recorded renames.
|
||||
func (r *recordingArchives) Renames() []archiveRename {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
out := make([]archiveRename, len(r.renames))
|
||||
copy(out, r.renames)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func newTestApp(
|
||||
t *testing.T,
|
||||
targets ...any,
|
||||
@@ -108,10 +185,10 @@ func newTestApp(
|
||||
func(n *recordingNotifier) delivery.Notifier {
|
||||
return n
|
||||
},
|
||||
func() *recordingEvictor {
|
||||
return &recordingEvictor{}
|
||||
func() *recordingArchives {
|
||||
return &recordingArchives{}
|
||||
},
|
||||
func(r *recordingEvictor) delivery.WebhookEvictor {
|
||||
func(r *recordingArchives) delivery.Archives {
|
||||
return r
|
||||
},
|
||||
metrics.NewRegistry,
|
||||
|
||||
Reference in New Issue
Block a user