scheduler

package
v0.0.0-...-8caed39 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: OSL-3.0 Imports: 31 Imported by: 0

Documentation

Overview

Package scheduler handles scheduled recommendation collection.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BackgroundCollectionAllowed

func BackgroundCollectionAllowed(ctx context.Context) bool

BackgroundCollectionAllowed reports whether ctx was marked by WithBackgroundCollection. Exported so callers' tests can assert which reads were allowed to kick a collection.

func WithBackgroundCollection

func WithBackgroundCollection(ctx context.Context) context.Context

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

func (s *Scheduler) IsLambda() bool

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:

  1. Read freshness.
  2. 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.
  3. Read from the cache (empty on a genuine cold start, until the kicked collection or the frontend's refresh populates it).
  4. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL