Name each database target's archive for its webhook and target (closes #376)
check / check (push) Successful in 3m14s

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.

Renaming a webhook or a target renames its archive files under the
archive writer's lock, before the new name is saved, and back again if
the save fails. 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:
2026-10-02 05:10:15 +00:00
parent 7d360babed
commit d88948a1c3
21 changed files with 1277 additions and 663 deletions
+77 -11
View File
@@ -51,23 +51,67 @@ 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 fails with the given error.
type recordingArchives struct {
mu sync.Mutex
evicted []string
evictedTargets []string
renames []archiveRename
renameErr error
}
func (r *recordingEvictor) EvictWebhook(webhookID string) {
// errInjectedRename is the failure a test hands FailRenames.
var errInjectedRename = errors.New("injected rename failure")
// archiveRename is one recorded RenameArchive 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) RenameArchive(
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.renameErr
}
// FailRenames makes every later rename fail with err.
func (r *recordingArchives) FailRenames(err error) {
r.mu.Lock()
defer r.mu.Unlock()
r.renameErr = 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()
@@ -77,6 +121,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,
@@ -103,10 +169,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
},
middleware.New,