happyDomain/internal/storage/kvtpl/check_evaluation.go
Pierre-Olivier Mercier 4ce33ade83 checkers: add storage interfaces, implementations, and tidy
Add the persistence layer for the checker system:
- Storage interfaces (CheckPlanStorage, CheckerOptionsStorage,
  CheckEvaluationStorage, ExecutionStorage, ObservationSnapshotStorage,
  SchedulerStateStorage) in the usecase/checker package
- KV-based implementations for LevelDB/Oracle NoSQL/InMemory backends
- Integrate checker storage into the main Storage interface
- Add tidy methods for checker entities (plans, configurations,
  evaluations, executions, snapshots, observation cache) and
  secondary index cleanup
2026-04-15 19:30:13 +07:00

316 lines
8.9 KiB
Go

// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"errors"
"fmt"
"log"
"sort"
"strings"
"git.happydns.org/happyDomain/model"
)
func (s *KVStorage) ListEvaluationsByPlan(planID happydns.Identifier) ([]*happydns.CheckEvaluation, error) {
prefix := fmt.Sprintf("chckeval-plan|%s|", planID.String())
iter := s.db.Search(prefix)
defer iter.Release()
var evals []*happydns.CheckEvaluation
for iter.Next() {
evalId, err := lastKeySegment(iter.Key())
if err != nil {
continue
}
eval, err := s.GetEvaluation(evalId)
if err != nil {
continue
}
evals = append(evals, eval)
}
return evals, nil
}
func (s *KVStorage) ListAllEvaluations() (happydns.Iterator[happydns.CheckEvaluation], error) {
iter := s.db.Search("chckeval|")
return NewKVIterator[happydns.CheckEvaluation](s.db, iter), nil
}
func (s *KVStorage) GetEvaluation(evalID happydns.Identifier) (*happydns.CheckEvaluation, error) {
eval := &happydns.CheckEvaluation{}
err := s.db.Get(fmt.Sprintf("chckeval|%s", evalID.String()), eval)
if errors.Is(err, happydns.ErrNotFound) {
return nil, happydns.ErrCheckEvaluationNotFound
}
return eval, err
}
func (s *KVStorage) GetLatestEvaluation(planID happydns.Identifier) (*happydns.CheckEvaluation, error) {
evals, err := s.ListEvaluationsByPlan(planID)
if err != nil {
return nil, err
}
if len(evals) == 0 {
return nil, happydns.ErrCheckEvaluationNotFound
}
latest := evals[0]
for _, e := range evals[1:] {
if e.EvaluatedAt.After(latest.EvaluatedAt) {
latest = e
}
}
return latest, nil
}
func (s *KVStorage) ListEvaluationsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.CheckEvaluation, error) {
prefix := fmt.Sprintf("chckeval-chkr|%s|%s|", checkerID, target.String())
iter := s.db.Search(prefix)
defer iter.Release()
var evals []*happydns.CheckEvaluation
for iter.Next() {
evalId, err := lastKeySegment(iter.Key())
if err != nil {
continue
}
eval, err := s.GetEvaluation(evalId)
if err != nil {
continue
}
evals = append(evals, eval)
}
// Sort by EvaluatedAt descending (most recent first).
sort.Slice(evals, func(i, j int) bool {
return evals[i].EvaluatedAt.After(evals[j].EvaluatedAt)
})
if limit > 0 && len(evals) > limit {
evals = evals[:limit]
}
return evals, nil
}
func (s *KVStorage) CreateEvaluation(eval *happydns.CheckEvaluation) error {
key, id, err := s.db.FindIdentifierKey("chckeval|")
if err != nil {
return err
}
eval.Id = id
// Store the primary record.
if err := s.db.Put(key, eval); err != nil {
return err
}
// Store secondary index by plan if applicable.
if eval.PlanID != nil {
indexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
if err := s.db.Put(indexKey, true); err != nil {
return err
}
}
// Store secondary index by checker+target.
checkerIndexKey := fmt.Sprintf("chckeval-chkr|%s|%s|%s", eval.CheckerID, eval.Target.String(), eval.Id.String())
if err := s.db.Put(checkerIndexKey, true); err != nil {
return err
}
return nil
}
func (s *KVStorage) DeleteEvaluation(evalID happydns.Identifier) error {
// Load first to find plan ID for index cleanup.
eval, err := s.GetEvaluation(evalID)
if err != nil {
return err
}
if eval.PlanID != nil {
indexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
if err := s.db.Delete(indexKey); err != nil {
log.Printf("DeleteEvaluation: failed to delete plan index %s: %v\n", indexKey, err)
}
}
// Clean up checker+target index.
checkerIndexKey := fmt.Sprintf("chckeval-chkr|%s|%s|%s", eval.CheckerID, eval.Target.String(), eval.Id.String())
if err := s.db.Delete(checkerIndexKey); err != nil {
log.Printf("DeleteEvaluation: failed to delete checker index %s: %v\n", checkerIndexKey, err)
}
return s.db.Delete(fmt.Sprintf("chckeval|%s", evalID.String()))
}
func (s *KVStorage) DeleteEvaluationsByChecker(checkerID string, target happydns.CheckTarget) error {
prefix := fmt.Sprintf("chckeval-chkr|%s|%s|", checkerID, target.String())
iter := s.db.Search(prefix)
defer iter.Release()
for iter.Next() {
evalId, err := lastKeySegment(iter.Key())
if err != nil {
continue
}
eval, err := s.GetEvaluation(evalId)
if err != nil {
// Primary record already gone; just clean up this index entry
// and attempt to clean up the plan index (best-effort scan).
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
s.deleteEvalPlanIndexByEvalID(evalId)
continue
}
// Delete plan index if applicable.
if eval.PlanID != nil {
planIndexKey := fmt.Sprintf("chckeval-plan|%s|%s", eval.PlanID.String(), eval.Id.String())
if err := s.db.Delete(planIndexKey); err != nil {
log.Printf("DeleteEvaluationsByChecker: failed to delete plan index %s: %v\n", planIndexKey, err)
}
}
// Delete primary record.
if err := s.db.Delete(fmt.Sprintf("chckeval|%s", eval.Id.String())); err != nil {
log.Printf("DeleteEvaluationsByChecker: failed to delete primary record %s: %v\n", eval.Id.String(), err)
}
// Delete this checker index entry.
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}
// deleteEvalPlanIndexByEvalID scans plan indexes to remove any entry for the
// given evaluation ID. Used when the primary record is already gone and we
// don't know which plan it belonged to.
func (s *KVStorage) deleteEvalPlanIndexByEvalID(evalId happydns.Identifier) {
suffix := "|" + evalId.String()
iter := s.db.Search("chckeval-plan|")
defer iter.Release()
for iter.Next() {
if strings.HasSuffix(iter.Key(), suffix) {
if err := s.db.Delete(iter.Key()); err != nil {
log.Printf("deleteEvalPlanIndexByEvalID: failed to delete %s: %v\n", iter.Key(), err)
}
}
}
}
func (s *KVStorage) TidyEvaluationIndexes() error {
// Tidy chckeval-plan|{planId}|{evalId} indexes.
planIter := s.db.Search("chckeval-plan|")
defer planIter.Release()
for planIter.Next() {
key := planIter.Key()
// Extract planId and evalId from "chckeval-plan|{planId}|{evalId}".
rest := strings.TrimPrefix(key, "chckeval-plan|")
parts := strings.SplitN(rest, "|", 2)
if len(parts) != 2 {
_ = s.db.Delete(key)
continue
}
planId, err := happydns.NewIdentifierFromString(parts[0])
if err != nil {
_ = s.db.Delete(key)
continue
}
evalId, err := happydns.NewIdentifierFromString(parts[1])
if err != nil {
_ = s.db.Delete(key)
continue
}
// Check plan exists.
if _, err := s.GetCheckPlan(planId); err != nil {
log.Printf("Deleting stale evaluation plan index (plan %s not found): %s\n", parts[0], key)
_ = s.db.Delete(key)
continue
}
// Check primary record exists.
if _, err := s.GetEvaluation(evalId); err != nil {
log.Printf("Deleting stale evaluation plan index (evaluation %s not found): %s\n", parts[1], key)
_ = s.db.Delete(key)
}
}
// Tidy chckeval-chkr|{checkerID}|{target}|{evalId} indexes.
chkrIter := s.db.Search("chckeval-chkr|")
defer chkrIter.Release()
for chkrIter.Next() {
key := chkrIter.Key()
// The evalId is the last segment after the last "|".
lastPipe := strings.LastIndex(key, "|")
if lastPipe < 0 {
_ = s.db.Delete(key)
continue
}
evalIdStr := key[lastPipe+1:]
evalId, err := happydns.NewIdentifierFromString(evalIdStr)
if err != nil {
_ = s.db.Delete(key)
continue
}
if _, err := s.GetEvaluation(evalId); err != nil {
log.Printf("Deleting stale evaluation checker index (evaluation %s not found): %s\n", evalIdStr, key)
_ = s.db.Delete(key)
}
}
return nil
}
func (s *KVStorage) ClearEvaluations() error {
// Delete secondary indexes (chckeval-plan|..., chckeval-chkr|...).
idxIter := s.db.Search("chckeval-")
defer idxIter.Release()
for idxIter.Next() {
if err := s.db.Delete(idxIter.Key()); err != nil {
return err
}
}
// Delete primary records (chckeval|...).
iter, err := s.ListAllEvaluations()
if err != nil {
return err
}
defer iter.Close()
for iter.Next() {
if err := s.db.Delete(iter.Key()); err != nil {
return err
}
}
return nil
}