Files
silo-server/internal/taskmanager/tasks/cleanup_client_diagnostics.go
Quick104andClaude Fable 5 a6348b3dc5 fix(diagnostics): address PR #445 review findings
- bundle: reject tar entry names that differ from their trimmed form instead
  of normalizing padded names into the allowlist
- repo: reserve expected bytes on receiving rows and count receiving+ready in
  the per-user byte quota so concurrent/multi-node uploads can't overshoot
- contract: require the crash object for event report types and keep it absent
  for manual; add contract tests
- settings/service: seed diagnostics.server_instance_id atomically via
  insert-if-absent and adopt the winning value across nodes
- bundle/service: capture the embedded manifest.json during ValidateBundle and
  reject reports whose embedded manifest disagrees with the part-1 manifest
  (minus archive); add tests
- admin: delete the DB row before the blob on DeleteReport; log bucket/key when
  the blob delete fails instead of leaving a visible report with a missing bundle
- bundle: reject PAX/GNU tar formats and extension records that smuggle bytes
  past validation; add a PAX-archive rejection test
- migration: add CHECK constraints for state, report_type, and platform
- docs: add text/jsonc language identifiers to the two unfenced code blocks
- cleanup: log-and-continue per report and aggregate errors so one poisoned
  report no longer blocks the whole run; update tests
- tasks: give diagnostics its own cleanup interval key instead of reusing the
  opslog key, and bound the startup settings lookup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e3QjbPo96ed9Mn2qRiUkh
2026-07-21 10:03:11 -04:00

74 lines
2.3 KiB
Go

package tasks
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/Silo-Server/silo-server/internal/diagnostics"
"github.com/Silo-Server/silo-server/internal/taskmanager"
)
// ClientDiagnosticsCleanupTask prunes expired client diagnostics and reconciles
// object-store leftovers from interrupted uploads.
type ClientDiagnosticsCleanupTask struct {
repo diagnostics.CleanupRepository
settings diagnostics.SettingsStore
store diagnostics.ObjectStore
logger *slog.Logger
}
func NewClientDiagnosticsCleanupTask(
repo diagnostics.CleanupRepository,
settings diagnostics.SettingsStore,
store diagnostics.ObjectStore,
) *ClientDiagnosticsCleanupTask {
return &ClientDiagnosticsCleanupTask{
repo: repo,
settings: settings,
store: store,
logger: slog.Default(),
}
}
func (t *ClientDiagnosticsCleanupTask) Key() string { return "cleanup_client_diagnostics" }
func (t *ClientDiagnosticsCleanupTask) Name() string {
return "Cleanup Client Diagnostics"
}
func (t *ClientDiagnosticsCleanupTask) Description() string {
return "Prunes expired client diagnostic reports and orphaned bundles"
}
func (t *ClientDiagnosticsCleanupTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategorySystem
}
func (t *ClientDiagnosticsCleanupTask) IsHidden() bool { return false }
func (t *ClientDiagnosticsCleanupTask) DefaultTriggers() []taskmanager.TriggerConfig {
// DefaultTriggers runs on the startup path, so bound the settings read rather
// than letting a slow store stall trigger setup before the default applies.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return []taskmanager.TriggerConfig{
{Type: taskmanager.TriggerTypeStartup},
{
Type: taskmanager.TriggerTypeInterval,
IntervalMs: int64(diagnostics.LoadCleanupInterval(ctx, t.settings) / time.Millisecond),
},
}
}
func (t *ClientDiagnosticsCleanupTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
progress.Report(0, "Pruning client diagnostic reports")
result, err := diagnostics.CleanupOnce(ctx, t.repo, t.settings, t.store, t.logger)
if err != nil {
return err
}
progress.Report(100, fmt.Sprintf(
"Pruned %d client diagnostic reports and %d orphaned bundles",
result.ReportsDeleted(),
result.OrphanObjectsDeleted,
))
return nil
}