Files
kandev/apps/backend/internal/task/archivecascade/context.go
T
Clément Fiere 01b0dedd6c fix: harden task archive lifecycle recovery (#3651)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-09-14 16:34:25 +01:00

90 lines
2.8 KiB
Go

package archivecascade
import (
"context"
"errors"
"fmt"
"time"
)
// ErrSizeExceeded identifies a cascade that exceeds a bounded v1 dimension.
var ErrSizeExceeded = errors.New("archive_size_exceeded")
// SizeExceededError reports the rejected cascade dimension and observed bound.
type SizeExceededError struct {
Dimension string
Limit int
Submitted int
}
func (e *SizeExceededError) Error() string {
return fmt.Sprintf("%s: %s limit %d submitted %d", ErrSizeExceeded, e.Dimension, e.Limit, e.Submitted)
}
func (e *SizeExceededError) Unwrap() error {
return ErrSizeExceeded
}
// ErrCrossWorkspaceDescendant identifies a cascade containing a task outside
// the root task's workspace.
var ErrCrossWorkspaceDescendant = errors.New("cross_workspace_descendant")
// CrossWorkspaceDescendantError reports the rejected task and workspace IDs.
type CrossWorkspaceDescendantError struct {
TaskID string
WorkspaceID string
ExpectedWorkspaceID string
}
func (e *CrossWorkspaceDescendantError) Error() string {
return fmt.Sprintf("%s: task %s belongs to workspace %s, expected %s",
ErrCrossWorkspaceDescendant, e.TaskID, e.WorkspaceID, e.ExpectedWorkspaceID)
}
func (e *CrossWorkspaceDescendantError) Unwrap() error {
return ErrCrossWorkspaceDescendant
}
// ErrInvalidStructure identifies a cascade with a cycle or duplicate member.
var ErrInvalidStructure = errors.New("archive_cascade_invalid_structure")
// StructureError reports the invalid relationship and repeated task ID.
type StructureError struct {
Kind string
TaskID string
}
func (e *StructureError) Error() string {
return fmt.Sprintf("%s: %s at task %s", ErrInvalidStructure, e.Kind, e.TaskID)
}
func (e *StructureError) Unwrap() error {
return ErrInvalidStructure
}
const TaskArchiveTimeout = 2 * time.Minute
// ArchiveDeadline captures the absolute archive deadline at operation entry.
func ArchiveDeadline(ctx context.Context) time.Time {
deadline := time.Now().Add(TaskArchiveTimeout)
if callerDeadline, ok := ctx.Deadline(); ok && callerDeadline.Before(deadline) {
deadline = callerDeadline
}
return deadline
}
// ContinuationContext preserves the caller deadline while detaching committed
// archive work from cancellation caused by stopping the caller's execution.
func ContinuationContext(ctx context.Context) (context.Context, context.CancelFunc) {
return ContinuationContextUntil(ctx, ArchiveDeadline(ctx))
}
// ContinuationContextUntil detaches cancellation while retaining the captured
// absolute deadline for archive work and compensation.
func ContinuationContextUntil(ctx context.Context, deadline time.Time) (context.Context, context.CancelFunc) {
if callerDeadline, ok := ctx.Deadline(); ok && callerDeadline.Before(deadline) {
deadline = callerDeadline
}
return context.WithDeadline(context.WithoutCancel(ctx), deadline)
}