package pipeline import ( "context" "fmt" "net/http" "net/netip" "os" "strconv" "strings" "time" "evobgp/internal/asnresolve" "evobgp/internal/store" ) func asnCacheTTL() time.Duration { sec := 1800 if s := strings.TrimSpace(os.Getenv("EVOBGP_ASN_CACHE_TTL_SEC")); s != "" { if v, err := strconv.Atoi(s); err == nil && v > 0 { sec = v } } return time.Duration(sec) * time.Second } // asnHolderTTL is how long a holder name stays authoritative between refreshes; // holder text changes rarely, so it survives short prefix-cache TTLs. func asnHolderTTL() time.Duration { sec := 7 * 24 * 3600 if s := strings.TrimSpace(os.Getenv("EVOBGP_ASN_HOLDER_TTL_SEC")); s != "" { if v, err := strconv.Atoi(s); err == nil && v > 0 { sec = v } } return time.Duration(sec) * time.Second } // resolveASNForEntry fetches prefixes and holder with shared TTL cache (asn_prefix_cache). // The holder lookup starts concurrently with the prefix fetch (one RTT instead of two); // a holder failure is non-fatal — the previously cached holder name is kept. func resolveASNForEntry(ctx context.Context, st store.Backend, hc *http.Client, asn int64) ([]netip.Prefix, string, error) { ttl := asnCacheTTL() var prevHolder string if st != nil { if ent, ok, err := st.GetASNPrefixCache(asn); err == nil && ok && ent != nil { prevHolder = ent.Holder if time.Since(ent.FetchedAt) < ttl { return parseASNCachePrefixes(ent.Prefixes), ent.Holder, nil } } } holderCh := startASNHolderFetch(ctx, hc, st, asn, prevHolder) pfxs, err := asnresolve.AnnouncedPrefixes(ctx, hc, asn) if err != nil { holder := <-holderCh // drain to avoid leaking the goroutine's channel send _ = holder return nil, "", err } holder := <-holderCh if st != nil { strs := make([]string, len(pfxs)) for i, p := range pfxs { strs[i] = p.String() } if err := st.SetASNPrefixCache(asn, holder, strs); err != nil { return nil, "", fmt.Errorf("asn cache AS%d: %w", asn, err) } } return pfxs, holder, nil } // startASNHolderFetch launches the holder lookup concurrently. The returned buffered // channel always yields exactly one value, so callers may abandon it without leaking. func startASNHolderFetch(ctx context.Context, hc *http.Client, st store.Backend, asn int64, prevHolder string) <-chan string { ch := make(chan string, 1) go func() { if prevHolder != "" && holderStillFresh(st, asn, prevHolder) { ch <- prevHolder return } holder, err := asnresolve.ASHolderName(ctx, hc, asn) if err != nil || strings.TrimSpace(holder) == "" { ch <- prevHolder return } ch <- strings.TrimSpace(holder) }() return ch } // holderStillFresh reports whether the cached holder name is within its own (long) TTL. func holderStillFresh(st store.Backend, asn int64, prevHolder string) bool { if st == nil { return false } ent, ok, err := st.GetASNPrefixCache(asn) return err == nil && ok && ent != nil && ent.Holder == prevHolder && time.Since(ent.FetchedAt) < asnHolderTTL() } func parseASNCachePrefixes(raw []string) []netip.Prefix { out := make([]netip.Prefix, 0, len(raw)) for _, p := range raw { pfx, perr := netip.ParsePrefix(strings.TrimSpace(p)) if perr != nil { continue } out = append(out, pfx.Masked()) } return out } func parseCachedASNCachedPrefixes(raw []string) []netip.Prefix { out := make([]netip.Prefix, 0, len(raw)) for _, p := range raw { pfx, perr := netip.ParsePrefix(strings.TrimSpace(p)) if perr != nil { continue } out = append(out, pfx.Masked()) } return out }