package ingest
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"craftdesign.group/quarry/internal/events"
)
// Forget erases a deleted repository's pipeline state: its publisher
// cursor and publish record, and every node's durable consumer on the
// store event stream with that consumer's fold mark, this node's mark
// included even before it has a consumer. A repository added again
// under the same name, onto an index that was deleted with the old one,
// must republish its census; a cursor that outlived the index would
// resume past it (quarry#294).
//
// All of it is keyed by repository name, and one name may continue
// another's: "seen.indexer-foo-bar" is foo-bar's mark, and so is
// "seen.indexer-foo-<node hash>" when a repository is literally named
// that. Another node's consumer is therefore recognised by the subjects
// it reads, never by its name.
func Forget(ev *events.Server, repo, node string) error {
if ev == nil || repo == "" {
return nil
}
kv, err := ev.Cursors()
if err != nil {
return fmt.Errorf("ingest: forget %s: %w", repo, err)
}
consumers, err := storeConsumers(ev, repo)
if err != nil {
return fmt.Errorf("ingest: forget %s: %w", repo, err)
}
doomed := []string{repo, "pub." + repo, "seen." + durableName(repo, ""), "seen." + durableName(repo, node)}
for _, name := range consumers {
doomed = append(doomed, "seen."+name)
}
for _, k := range doomed {
if err := kv.Delete(k); err != nil && !errors.Is(err, nats.ErrKeyNotFound) {
return fmt.Errorf("ingest: forget %s: %w", repo, err)
}
}
for _, name := range consumers {
if err := ev.JS().DeleteConsumer(events.StreamName, name); err != nil && !errors.Is(err, nats.ErrConsumerNotFound) {
return fmt.Errorf("ingest: forget %s: %w", repo, err)
}
}
return nil
}
// storeConsumers names the pipelines' durable consumers that read
// repo's store events, one per node that has folded it.
func storeConsumers(ev *events.Server, repo string) ([]string, error) {
js, err := jetstream.New(ev.Conn())
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := js.Stream(ctx, events.StreamName)
if err != nil {
if errors.Is(err, jetstream.ErrStreamNotFound) {
return nil, nil
}
return nil, err
}
filter := (&Pipeline{code: repo}).subject(">")
var names []string
list := stream.ListConsumers(ctx)
for c := range list.Info() {
if strings.HasPrefix(c.Name, "indexer-") && c.Config.FilterSubject == filter {
names = append(names, c.Name)
}
}
return names, list.Err()
}