package service import ( "context" "database/sql" "errors" "fmt" "strings" "time" "mal/internal/db" "mal/internal/domain" "mal/internal/observability" ) func (s *EpisodeService) providerID(ctx context.Context, anime domain.Anime, provider domain.EpisodeAvailabilityProvider, titles []string) (string, error) { providerID, found, err := s.cachedProviderID(ctx, anime, provider) if found || err != nil { return providerID, err } providerID, err = provider.ResolveEpisodeProviderID(ctx, anime.MalID, titles) if err != nil { s.cacheProviderIDFailure(ctx, anime, provider, err) return "", err } s.cacheProviderIDSuccess(ctx, anime, provider, providerID) observability.Info( "episodes_provider_id_resolved", "episodes", "", map[string]any{ "anime_id": anime.MalID, "provider": provider.Name(), "provider_id": providerID, }, ) return providerID, nil } func (s *EpisodeService) cachedProviderID(ctx context.Context, anime domain.Anime, provider domain.EpisodeAvailabilityProvider) (string, bool, error) { row, err := s.queries.GetEpisodeProviderMapping(ctx, db.GetEpisodeProviderMappingParams{ AnimeID: int64(anime.MalID), Provider: provider.Name(), }) if err != nil { s.metrics.ObserveCache("episode_provider_mapping", "miss") if errors.Is(err, sql.ErrNoRows) { return "", false, nil } observability.Warn( "episodes_provider_id_cache_read_failed", "episodes", "", map[string]any{ "anime_id": anime.MalID, "provider": provider.Name(), }, err, ) return "", false, nil } if row.FailedUntil.Valid && row.FailedUntil.Time.After(s.clock.Now()) { s.metrics.ObserveCache("episode_provider_mapping", "hit") return "", true, fmt.Errorf("cached provider mapping failure active until %s: %s", row.FailedUntil.Time.Format(time.RFC3339), row.LastError) } if strings.TrimSpace(row.ProviderShowID) == "" { s.metrics.ObserveCache("episode_provider_mapping", "miss") return "", false, nil } s.metrics.ObserveCache("episode_provider_mapping", "hit") observability.Info( "episodes_provider_id_cache_hit", "episodes", "", map[string]any{ "anime_id": anime.MalID, "provider": provider.Name(), "provider_id": row.ProviderShowID, }, ) return row.ProviderShowID, true, nil } func (s *EpisodeService) cacheProviderIDFailure(ctx context.Context, anime domain.Anime, provider domain.EpisodeAvailabilityProvider, resolveErr error) { _ = s.queries.UpsertEpisodeProviderMapping(ctx, db.UpsertEpisodeProviderMappingParams{ AnimeID: int64(anime.MalID), Provider: provider.Name(), ProviderShowID: "", FailedUntil: sql.NullTime{Time: s.clock.Now().Add(time.Hour), Valid: true}, LastError: truncate(resolveErr.Error(), 400), }) } func (s *EpisodeService) cacheProviderIDSuccess(ctx context.Context, anime domain.Anime, provider domain.EpisodeAvailabilityProvider, providerID string) { err := s.queries.UpsertEpisodeProviderMapping(ctx, db.UpsertEpisodeProviderMappingParams{ AnimeID: int64(anime.MalID), Provider: provider.Name(), ProviderShowID: providerID, FailedUntil: sql.NullTime{}, LastError: "", }) if err == nil { return } observability.Warn( "episodes_provider_id_cache_write_failed", "episodes", "", map[string]any{ "anime_id": anime.MalID, "provider": provider.Name(), }, err, ) }