Public source validation / validate (push) Failing after 3m8s
167 lines
4.3 KiB
Go
167 lines
4.3 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type SnapshotFunc func(context.Context) ([]Event, error)
|
|
|
|
// Event is one discovered change. SourceID and DedupKey together with OccurredAt
|
|
// form the deduplication identity that persistent stores rely on, so a repeated
|
|
// discovery pass re-emits the same event without creating a second row.
|
|
// EntityID and Severity are optional: they carry the inventory entity the change
|
|
// belongs to and how loud it is, and default to "no entity" and "info" so older
|
|
// producers keep working unchanged.
|
|
type Event struct {
|
|
SourceID, DedupKey, Type, Summary string
|
|
EntityID, Severity string
|
|
OccurredAt time.Time
|
|
}
|
|
type JobRun struct {
|
|
Key, Status, ErrorCode string
|
|
Attempts int
|
|
StartedAt, CompletedAt time.Time
|
|
}
|
|
|
|
type Store interface {
|
|
Claim(context.Context, string, time.Time) (bool, error)
|
|
Finish(context.Context, JobRun) error
|
|
Emit(context.Context, Event) (bool, error)
|
|
}
|
|
type AuditFunc func(context.Context, string, string) error
|
|
type AuthorizeFunc func(context.Context, string) bool
|
|
|
|
type Runner struct {
|
|
Store Store
|
|
MaxAttempts int
|
|
BaseRetry time.Duration
|
|
Sleep func(context.Context, time.Duration) error
|
|
}
|
|
|
|
func (r Runner) Run(ctx context.Context, jobKey string, discover SnapshotFunc) error {
|
|
if r.Store == nil || discover == nil || jobKey == "" {
|
|
return errors.New("discovery runner requires store, key, and discover function")
|
|
}
|
|
max := r.MaxAttempts
|
|
if max == 0 {
|
|
max = 3
|
|
}
|
|
if max < 1 || max > 5 {
|
|
return errors.New("discovery attempts must be between 1 and 5")
|
|
}
|
|
base := r.BaseRetry
|
|
if base == 0 {
|
|
base = 100 * time.Millisecond
|
|
}
|
|
sleeper := r.Sleep
|
|
if sleeper == nil {
|
|
sleeper = func(ctx context.Context, d time.Duration) error {
|
|
timer := time.NewTimer(d)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-timer.C:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
claimed, err := r.Store.Claim(ctx, jobKey, time.Now().UTC())
|
|
if err != nil {
|
|
return fmt.Errorf("claim discovery job: %w", err)
|
|
}
|
|
if !claimed {
|
|
return nil
|
|
}
|
|
run := JobRun{Key: jobKey, Status: "running", StartedAt: time.Now().UTC()}
|
|
var last error
|
|
for attempt := 1; attempt <= max; attempt++ {
|
|
run.Attempts = attempt
|
|
events, runErr := discover(ctx)
|
|
if runErr == nil {
|
|
for _, event := range events {
|
|
if _, err := r.Store.Emit(ctx, event); err != nil {
|
|
runErr = fmt.Errorf("emit discovery event: %w", err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if runErr == nil {
|
|
run.Status = "succeeded"
|
|
run.CompletedAt = time.Now().UTC()
|
|
if err := r.Store.Finish(ctx, run); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
last = runErr
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
if attempt < max {
|
|
if err := sleeper(ctx, base*time.Duration(1<<(attempt-1))); err != nil {
|
|
last = err
|
|
break
|
|
}
|
|
}
|
|
}
|
|
run.Status = "failed"
|
|
run.ErrorCode = "DISCOVERY_FAILED"
|
|
run.CompletedAt = time.Now().UTC()
|
|
if err := r.Store.Finish(ctx, run); err != nil {
|
|
return err
|
|
}
|
|
return last
|
|
}
|
|
|
|
func (r Runner) RunManual(ctx context.Context, actor string, authorize AuthorizeFunc, audit AuditFunc, jobKey string, discover SnapshotFunc) error {
|
|
if authorize == nil || !authorize(ctx, actor) {
|
|
return errors.New("manual discovery is unauthorized")
|
|
}
|
|
if audit != nil {
|
|
if err := audit(ctx, actor, "discovery.manual.run"); err != nil {
|
|
return fmt.Errorf("audit manual discovery: %w", err)
|
|
}
|
|
}
|
|
return r.Run(ctx, jobKey, discover)
|
|
}
|
|
|
|
type MemoryStore struct {
|
|
mu sync.Mutex
|
|
claimed map[string]bool
|
|
Runs []JobRun
|
|
Events map[string]Event
|
|
}
|
|
|
|
func NewMemoryStore() *MemoryStore {
|
|
return &MemoryStore{claimed: make(map[string]bool), Events: make(map[string]Event)}
|
|
}
|
|
func (s *MemoryStore) Claim(_ context.Context, key string, _ time.Time) (bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.claimed[key] {
|
|
return false, nil
|
|
}
|
|
s.claimed[key] = true
|
|
return true, nil
|
|
}
|
|
func (s *MemoryStore) Finish(_ context.Context, run JobRun) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.Runs = append(s.Runs, run)
|
|
return nil
|
|
}
|
|
func (s *MemoryStore) Emit(_ context.Context, event Event) (bool, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if _, ok := s.Events[event.SourceID+"\x00"+event.DedupKey]; ok {
|
|
return false, nil
|
|
}
|
|
s.Events[event.SourceID+"\x00"+event.DedupKey] = event
|
|
return true, nil
|
|
}
|