Documentation
¶
Overview ¶
Package scheduler handles scheduled recommendation collection.
Index ¶
- func BackgroundCollectionAllowed(ctx context.Context) bool
- func WithBackgroundCollection(ctx context.Context) context.Context
- type CollectResult
- type ManagerInterface
- type STSClient
- type Scheduler
- func (s *Scheduler) CollectRecommendations(ctx context.Context, ownerToken string) (*CollectResult, error)
- func (s *Scheduler) GetRecommendationByID(ctx context.Context, id string) (rec *config.RecommendationRecord, hiddenBy []string, err error)
- func (s *Scheduler) IsLambda() bool
- func (s *Scheduler) ListRecommendations(ctx context.Context, filter config.RecommendationFilter) ([]config.RecommendationRecord, error)
- func (s *Scheduler) ListRecommendationsStrict(ctx context.Context, filter config.RecommendationFilter) ([]config.RecommendationRecord, error)
- type SchedulerConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BackgroundCollectionAllowed ¶
BackgroundCollectionAllowed reports whether ctx was marked by WithBackgroundCollection. Exported so callers' tests can assert which reads were allowed to kick a collection.
func WithBackgroundCollection ¶
WithBackgroundCollection returns a context that lets ListRecommendations kick its cold-start collection (taking the cluster-wide in-flight marker) or its stale-cache background refresh.
Both kicks collect for every enabled provider and every registered account, so only a caller entitled to every account may trigger them: a reader restricted to a subset of accounts must not cause cloud calls against accounts outside its scope, nor hold the marker that makes other users' POST /api/recommendations/refresh return 409 (issue #627, the read-path counterpart of #520). Without this marker ListRecommendations is a pure cache read, so a caller that forgets to set it fails closed.
Types ¶
type CollectResult ¶
type CollectResult struct {
FailedProviders map[string]string `json:"failed_providers,omitempty"`
SuccessfulProviders []string `json:"successful_providers,omitempty"`
Recommendations int `json:"recommendations"`
TotalSavings float64 `json:"total_savings"`
}
CollectResult holds the result of collecting recommendations.
SuccessfulProviders + FailedProviders track per-provider outcomes so the persistence layer (UpsertRecommendations) can scope the delete-stale clause to providers that actually ran, and the frontend banner can surface the specific failures.
type ManagerInterface ¶
type ManagerInterface interface {
ProcessScheduledPurchases(ctx context.Context) (*purchase.ProcessResult, error)
SendUpcomingPurchaseNotifications(ctx context.Context) (*purchase.NotificationResult, error)
// FireScheduledDelayedPurchases fires purchase_executions in status=scheduled
// whose scheduled_execution_at is in the past (Gmail-style pre-fire delay,
// issue #291 wave-2). Called on the "fire_scheduled_purchases" scheduler tick.
FireScheduledDelayedPurchases(ctx context.Context) (*purchase.FireResult, error)
}
ManagerInterface defines the purchase manager methods used by scheduler.
type STSClient ¶
type STSClient interface {
GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
}
STSClient is the minimal AWS STS surface the scheduler uses to discover the Lambda's own AWS account ID for the ambient-path account-tagging fix (issue #604). Mirrors purchase.STSClient deliberately — both packages need the same single call, and cross-package imports would tangle the purchase ↔ scheduler dependency.
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler handles scheduled tasks.
func NewScheduler ¶
func NewScheduler(cfg SchedulerConfig) *Scheduler
NewScheduler creates a new scheduler.
func (*Scheduler) CollectRecommendations ¶
func (s *Scheduler) CollectRecommendations(ctx context.Context, ownerToken string) (*CollectResult, error)
CollectRecommendations fetches recommendations from all configured cloud providers. Persists results to the recommendations cache so read handlers can serve from SQL instead of re-fetching live.
Bookkeeping: clears last_collection_started_at on exit (success or failure) so the frontend polling loop can detect completion, but only if ownerToken is non-empty and still matches last_collection_owner_id (issue #261 compare-and-clear guard). The scheduler is invoked either by the cron EventBridge rule or by an async self-invoke from the POST /api/recommendations/refresh handler. In the async case, MarkCollectionStarted has already set last_collection_started_at and returned the token that ownerToken carries here; kickColdStartCollection's background collect does too (issue #106), for the same reason. The cron case calls with an empty ownerToken since it never wins a marker of its own (it coordinates via the advisory lock instead, see HandleScheduledTask), and the clear is skipped entirely rather than clearing unconditionally, which previously let a cron run wipe a concurrent user-triggered run's marker. This deferred clear is the ONLY place that touches started_at: persistCollection's SetRecommendationsCollectionError call (below, on a provider failure) intentionally leaves started_at alone, so a tokenless run hitting a routine provider error cannot wipe another run's marker either.
func (*Scheduler) GetRecommendationByID ¶
func (s *Scheduler) GetRecommendationByID(ctx context.Context, id string) (rec *config.RecommendationRecord, hiddenBy []string, err error)
GetRecommendationByID fetches a single recommendation by its application- level ID (the id field in the stored payload), bypassing the account-override filter so deep-linked URLs to override-hidden recs still resolve.
Suppressions are still applied: a rec whose remaining count has been suppressed to zero is treated as gone (returns nil, nil, nil) so the caller can render a 404 rather than a "hidden" banner for actively-dismissed recs.
hiddenBy is non-nil (one or more reason strings) when the rec exists and passes the suppression check, but would be dropped by the account-override filter. Possible reasons: "enabled=false", "engine", "region", "resource_type". The caller renders a "hidden by your override" banner.
Returns (nil, nil, nil) when the rec is genuinely absent or fully suppressed.
func (*Scheduler) IsLambda ¶
IsLambda reports the Lambda mode this scheduler was constructed with (SchedulerConfig.IsLambda). It never re-reads the environment: the value is owned by whoever built the scheduler, so callers can assert that the mode they threaded in is the one that gates background goroutines (issue #110).
func (*Scheduler) ListRecommendations ¶
func (s *Scheduler) ListRecommendations(ctx context.Context, filter config.RecommendationFilter) ([]config.RecommendationRecord, error)
ListRecommendations reads cached recommendations from the store, applying the given filter as SQL WHERE clauses. Previously this did live cloud API calls on every request (2–10s per provider); now it's a pure DB read, typically under 100ms.
Order of operations:
- Read freshness.
- Cold-start (LastCollectedAt==nil): single-flight a background collect via kickColdStartCollection rather than blocking this read on it (issue #106) -- the frontend already drives a coordinated first collection on page load (triggerAutoRefreshIfStale -> POST /recommendations/refresh), so this is a best-effort kick for non-browser API callers only.
- Read from the cache (empty on a genuine cold start, until the kicked collection or the frontend's refresh populates it).
- Stale-while-revalidate: if the cache is older than cacheTTL AND we aren't on Lambda, kick off a background CollectRecommendations so the NEXT read sees fresh data. Lambda skips this (goroutines freeze between invocations); the scheduled cron is Lambda's refresh path.
Steps 2 and 4 run only when ctx carries WithBackgroundCollection, which the API sets for callers with unrestricted account scope. Both collect across every account, so a scoped reader gets a pure cache read (issue #627).
func (*Scheduler) ListRecommendationsStrict ¶
func (s *Scheduler) ListRecommendationsStrict(ctx context.Context, filter config.RecommendationFilter) ([]config.RecommendationRecord, error)
ListRecommendationsStrict is ListRecommendations for the money path: the same stored rows, suppressions and account overrides, but a suppression or override read failure is returned instead of logged. Over-showing is fine for a page; on a purchase it would re-buy what a prior step already bought. It has no cold-start or background-refresh side effects: an empty cache reads as empty and the caller retries on a later tick.
type SchedulerConfig ¶
type SchedulerConfig struct {
ConfigStore config.StoreInterface
PurchaseManager ManagerInterface
EmailSender email.SenderInterface
ProviderFactory provider.FactoryInterface
CredentialStore credentials.CredentialStore
OIDCSigner oidc.Signer
AssumeRoleSTS credentials.STSClient
STSClient STSClient
DashboardURL string
OIDCIssuerURL string
IsLambda bool
}
SchedulerConfig holds configuration for the scheduler.