Name each database target's archive for its webhook and target (closes #376)
check / check (push) Successful in 3m14s
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:
@@ -559,6 +559,7 @@ func (h *Handlers) applyWebhookEdit(
|
||||
return
|
||||
}
|
||||
|
||||
oldName := webhook.Name
|
||||
webhook.Name = name
|
||||
webhook.Description = r.PostFormValue("description")
|
||||
|
||||
@@ -581,8 +582,24 @@ func (h *Handlers) applyWebhookEdit(
|
||||
|
||||
webhook.RetentionDays = retentionDays
|
||||
|
||||
err := h.db.DB().Save(webhook).Error
|
||||
// The archive files are renamed before the new name is saved
|
||||
// (see delivery.Engine.RenameArchive). If either step fails,
|
||||
// they go back to the name that is still stored.
|
||||
err := h.renameWebhookArchives(webhook.ID, webhook.Name)
|
||||
if err == nil {
|
||||
err = h.db.DB().Save(webhook).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
restoreErr := h.renameWebhookArchives(webhook.ID, oldName)
|
||||
if restoreErr != nil {
|
||||
h.log.Error(
|
||||
"failed to rename archives back",
|
||||
"webhook_id", webhook.ID,
|
||||
"error", restoreErr,
|
||||
)
|
||||
}
|
||||
|
||||
h.serverError(w, "failed to update webhook", err)
|
||||
|
||||
return
|
||||
@@ -717,11 +734,11 @@ func (h *Handlers) commitWebhookDeletion(
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// evictArchiveWriter asks the delivery engine to drop its
|
||||
// cached archive writer for a webhook, closing the archive file
|
||||
// handle.
|
||||
// evictArchiveWriter asks the delivery engine to drop the cached
|
||||
// archive writers of a webhook's database targets, closing their
|
||||
// archive file handles.
|
||||
//
|
||||
// The archive database file is NOT deleted. Unlike the event
|
||||
// The archive database files are NOT deleted. Unlike the event
|
||||
// database — which is per-webhook working storage and is
|
||||
// hard-deleted with the webhook — an archive is explicitly
|
||||
// long-term storage that an operator may want to keep or move
|
||||
@@ -729,50 +746,57 @@ func (h *Handlers) commitWebhookDeletion(
|
||||
// deleting a webhook would be a surprising and unrecoverable
|
||||
// data loss, so the file is left for the operator to handle.
|
||||
func (h *Handlers) evictArchiveWriter(webhookID string) {
|
||||
if h.evictor == nil {
|
||||
if h.archives == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.evictor.EvictWebhook(webhookID)
|
||||
h.archives.EvictWebhook(webhookID)
|
||||
}
|
||||
|
||||
// evictArchiveWriterIfUnused releases a webhook's archive
|
||||
// writer once the webhook has no database target left to feed
|
||||
// it.
|
||||
//
|
||||
// It is called after any child resource of a webhook is
|
||||
// deleted, and is correct without knowing which kind was: it
|
||||
// evicts only when no database target remains, so deleting one
|
||||
// of several database targets — or deleting an unrelated
|
||||
// target type — leaves a still-needed writer alone. When no
|
||||
// database target ever existed there is no writer and eviction
|
||||
// is a no-op. Soft-deleted targets are excluded by GORM's
|
||||
// default scope, so the row just deleted is not counted.
|
||||
func (h *Handlers) evictArchiveWriterIfUnused(webhookID string) {
|
||||
var remaining int64
|
||||
// evictTargetArchiveWriter is evictArchiveWriter for one deleted
|
||||
// target, and leaves its archive file on disk for the same reason.
|
||||
// A target that is not a database target has no writer, and
|
||||
// evicting it does nothing.
|
||||
func (h *Handlers) evictTargetArchiveWriter(targetID string) {
|
||||
if h.archives == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.archives.EvictTarget(targetID)
|
||||
}
|
||||
|
||||
// renameWebhookArchives renames the archive file of every database
|
||||
// target of a webhook for the webhook name webhookName, keeping
|
||||
// each target's own name. It stops at the first failure.
|
||||
func (h *Handlers) renameWebhookArchives(
|
||||
webhookID, webhookName string,
|
||||
) error {
|
||||
if h.archives == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var targets []database.Target
|
||||
|
||||
err := h.db.DB().
|
||||
Model(&database.Target{}).
|
||||
Where(
|
||||
"webhook_id = ? AND type = ?",
|
||||
webhookID, database.TargetTypeDatabase,
|
||||
).
|
||||
Count(&remaining).Error
|
||||
Find(&targets).Error
|
||||
if err != nil {
|
||||
h.log.Error(
|
||||
"failed to count remaining database targets",
|
||||
"webhook_id", webhookID,
|
||||
"error", err,
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range targets {
|
||||
err = h.archives.RenameArchive(
|
||||
targets[i].ID, webhookName, targets[i].Name,
|
||||
)
|
||||
|
||||
return
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if remaining > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
h.evictArchiveWriter(webhookID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ownedWebhook resolves the request's sourceID parameter to a
|
||||
@@ -1649,27 +1673,26 @@ func (h *Handlers) HandleEntrypointDelete() http.HandlerFunc {
|
||||
)
|
||||
}
|
||||
|
||||
// HandleTargetDelete handles deleting a target. Deleting the
|
||||
// last database target of a webhook leaves its archive writer
|
||||
// with nothing to write, so the writer is evicted and its
|
||||
// handle closed; the archive file is left on disk.
|
||||
// HandleTargetDelete handles deleting a target. A deleted
|
||||
// database target's archive writer is evicted and its handle
|
||||
// closed; the archive file is left on disk.
|
||||
func (h *Handlers) HandleTargetDelete() http.HandlerFunc {
|
||||
return h.deleteChildResource(
|
||||
"targetID", &database.Target{},
|
||||
"failed to delete target",
|
||||
h.evictArchiveWriterIfUnused,
|
||||
h.evictTargetArchiveWriter,
|
||||
)
|
||||
}
|
||||
|
||||
// deleteChildResource returns a handler that deletes a child
|
||||
// resource (entrypoint or target) belonging to a webhook. The
|
||||
// optional afterDelete hook runs with the webhook's id once the
|
||||
// delete has succeeded, before the redirect.
|
||||
// optional afterDelete hook runs with the child's id once the
|
||||
// delete has removed it, before the redirect.
|
||||
func (h *Handlers) deleteChildResource(
|
||||
idParam string,
|
||||
model any,
|
||||
errMsg string,
|
||||
afterDelete func(webhookID string),
|
||||
afterDelete func(childID string),
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := h.getUserID(r)
|
||||
@@ -1709,8 +1732,10 @@ func (h *Handlers) deleteChildResource(
|
||||
return
|
||||
}
|
||||
|
||||
if afterDelete != nil {
|
||||
afterDelete(webhook.ID)
|
||||
// Only for a row this webhook really had: the id came from
|
||||
// the URL and may name another webhook's child.
|
||||
if afterDelete != nil && result.RowsAffected > 0 {
|
||||
afterDelete(childID)
|
||||
}
|
||||
|
||||
http.Redirect(
|
||||
|
||||
Reference in New Issue
Block a user