boardcache

package
v0.0.77 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ItemKey

func ItemKey(repo string, number int) string

ItemKey returns the cache key for an issue item: "owner/repo#number". Exported so callers in other packages (e.g. engine) can construct keys without duplicating the format string.

Types

type CacheImpl

type CacheImpl struct {
	// contains filtered or unexported fields
}

CacheImpl is a goroutine-safe in-memory board cache. Webhook delta functions write to it; the poll loop reads from it via the ReadClient interface. Falls back to a fallback ReadClient on cache miss.

Internal ownership split:

  • store owns: items, shaToKey, itemIDToKey, prToKey, pendingCheckRuns
  • CacheImpl owns: paused, recentMissCache, projectID/Title/OwnerType, localDeltaAt

Locking invariant: mu guards CacheImpl-local fields only. Store has its own internal mutex. NEVER hold mu while calling any Store method — this prevents deadlock if Store observers call back into CacheImpl.

func NewCacheImpl

func NewCacheImpl(fallback ReadClient, store *itemstate.Store, logFn func(format string, args ...any)) *CacheImpl

NewCacheImpl creates an empty cache backed by fallback for misses. store must be the shared *itemstate.Store owned by the engine — passing nil panics.

func (*CacheImpl) ApplyCommentAdded

func (c *CacheImpl) ApplyCommentAdded(key string, comment gh.Comment)

ApplyCommentAdded appends comment to the cached comment list for the item identified by key. No-op when the key is not in the cache. Safe for concurrent use.

Note: LocalCommentAdded dedups by DatabaseID, mirroring IssueCommentCreated. If a webhook echo for the same comment arrives before the next Reconcile, the repeated LocalCommentAdded is a no-op rather than a duplicate append.

func (*CacheImpl) ApplyDelta

func (c *CacheImpl) ApplyDelta(eventType string, payload []byte)

ApplyDelta dispatches a webhook payload to the appropriate typed delta function. It is a no-op when the cache is paused (stream unhealthy).

func (*CacheImpl) ApplyIssueClosed

func (c *CacheImpl) ApplyIssueClosed(key string)

ApplyIssueClosed marks the item identified by key as closed in the store. No-op when the key is not in the cache. Safe for concurrent use.

func (*CacheImpl) ApplyLabelAdded

func (c *CacheImpl) ApplyLabelAdded(key, label string)

ApplyLabelAdded updates the cached label list for the item identified by key, appending label if not already present. No-op when the key is not in the cache. Safe for concurrent use.

func (*CacheImpl) ApplyLabelRemoved

func (c *CacheImpl) ApplyLabelRemoved(key, label string)

ApplyLabelRemoved updates the cached label list for the item identified by key, removing label if present. No-op when the key is not in the cache or label is absent. Safe for concurrent use.

func (*CacheImpl) ApplyStatusBatch

func (c *CacheImpl) ApplyStatusBatch(updates map[string]string)

ApplyStatusBatch updates Status for items identified by project-item node IDs. Entries whose itemID is not in the Store's itemIDToKey index are silently skipped. Safe for concurrent use.

func (*CacheImpl) BootstrapFromProbe

func (c *CacheImpl) BootstrapFromProbe(items []gh.BoardProbeItem, projectID string)

BootstrapFromProbe populates the cache from a ProbeProjectBoard result instead of a full FetchProjectBoard. This is the preferred cold-start path: probe costs ~250 nodes vs ~2350 for a full shallow fetch on a 47-item board.

Labels are absent from probe results; startup scans that rely on label data (runStartupTransientLabelScan, runStartupTerminalScan) will see empty label sets and silently become no-ops after this bootstrap path. This is an accepted trade-off: stale transient labels on closed terminal items will not be detected at startup (very low probability; requires a crash mid-Done-stage). Active items are deep-fetched on the first probe cycle, populating their labels normally.

Sets LinkedPR.Number from each probe item's LinkedPRNumber so the subsequent probe cycle does not see spurious linkage-drift on items that already have a PR.

Must be called before any engine mutations flow through the shared store.

func (*CacheImpl) FetchCheckRuns

func (c *CacheImpl) FetchCheckRuns(owner, repo, sha string) ([]gh.CheckRun, error)

FetchCheckRuns returns cached check runs for a SHA; falls back to GitHub on miss. Reads from Store.CheckRunsBySHA, which covers both pre-linkage (pendingCheckRuns) and post-linkage (LinkedPR.CheckRuns) runs. On total miss, fetches from GitHub and populates the Store via CheckRunCompleted mutations so subsequent calls are served from cache.

func (*CacheImpl) FetchCombinedStatus added in v0.0.76

func (c *CacheImpl) FetchCombinedStatus(owner, repo, ref string) ([]gh.CommitStatus, error)

FetchCombinedStatus always delegates to GitHub — classic commit statuses change without webhooks, same reasoning as FetchPRMergeableFields above.

func (*CacheImpl) FetchItemDetails

func (c *CacheImpl) FetchItemDetails(item *gh.ProjectItem) error

FetchItemDetails copies cached deep fields into the passed item pointer. Deep fields: Body, URL, Author, Assignees, BlockedBy, Comments, LinkedPRNumber, LinkedPRHeadSHA, LinkedPRReviewRequests, LinkedPRReviews, LinkedPRReviewThreadComments, LinkedPRResolvedThreadCount.

Cache freshness contract: the cache is treated as authoritative only while the fresh board's pi.UpdatedAt has not advanced past LastSeenSourceUpdatedAt (the pi.UpdatedAt observed at the moment of the last successful deep fetch). When the board's updatedAt is newer, the cache is stale and we fall through to a real GraphQL fetch. pi.UpdatedAt is computed by FetchProjectBoard as max(issue.updatedAt, projectItem.updatedAt, linkedPR.updatedAt), so PR-side changes (new reviews, comments, draft toggles) bump it. Webhooks remain a pure optimization that mutate the cache between polls; in their absence (or when they're unhealthy), the board's updatedAt correctly forces a re-fetch.

Falls back to GitHub on cache miss or when the cache is stale, and populates the cache with the result.

func (*CacheImpl) FetchLabels

func (c *CacheImpl) FetchLabels(owner, repo string, issueNumber int) ([]string, error)

FetchLabels returns the cached label list for an issue; falls back to GitHub on miss.

func (*CacheImpl) FetchLinkedPR

func (c *CacheImpl) FetchLinkedPR(owner, repo string, issueNumber int) (*gh.PRDetails, error)

FetchLinkedPR returns cached PR details for an issue; falls back to GitHub on miss or once the cached record exceeds linkedPRCacheTTL (#1303).

func (*CacheImpl) FetchPRClosingIssues

func (c *CacheImpl) FetchPRClosingIssues(owner, repo string, prNumber int) ([]int, error)

FetchPRClosingIssues always delegates to GitHub — used by the auto-heal path in delta handlers.

func (*CacheImpl) FetchPRMergeable

func (c *CacheImpl) FetchPRMergeable(owner, repo string, prNumber int) (*bool, error)

FetchPRMergeable always delegates to GitHub — mergeability changes without webhooks.

func (*CacheImpl) FetchPRMergeableFields added in v0.0.70

func (c *CacheImpl) FetchPRMergeableFields(owner, repo string, prNumber int) (*bool, string, error)

FetchPRMergeableFields always delegates to GitHub — mergeability changes without webhooks.

func (*CacheImpl) FetchPRMergeableState

func (c *CacheImpl) FetchPRMergeableState(owner, repo string, prNumber int) (string, error)

FetchPRMergeableState always delegates to GitHub — mergeability changes without webhooks.

func (*CacheImpl) FetchPRMerged added in v0.0.70

func (c *CacheImpl) FetchPRMerged(owner, repo string, prNumber int) (bool, error)

FetchPRMerged always delegates to GitHub — the authoritative merged flag must be fresh (the cache/list-endpoint copy lags right after a merge).

func (*CacheImpl) FetchPRReviewDecision added in v0.0.76

func (c *CacheImpl) FetchPRReviewDecision(owner, repo string, prNumber int) (string, error)

FetchPRReviewDecision always delegates to GitHub, no caching — same rationale as FetchPRReviews.

func (*CacheImpl) FetchPRReviewRequests added in v0.0.75

func (c *CacheImpl) FetchPRReviewRequests(owner, repo string, prNumber int) ([]gh.ReviewRequest, error)

FetchPRReviewRequests always delegates to GitHub, no caching — same rationale as FetchPRReviews.

func (*CacheImpl) FetchPRReviews added in v0.0.75

func (c *CacheImpl) FetchPRReviews(owner, repo string, prNumber int) ([]gh.PRReview, error)

FetchPRReviews always delegates to GitHub, no caching — review state is highly time-sensitive and is only consulted while a base:<branch> item's review gate is actively open.

func (*CacheImpl) FetchPRsForSHA

func (c *CacheImpl) FetchPRsForSHA(owner, repo, sha string) ([]int, error)

FetchPRsForSHA always delegates to GitHub — used by the auto-heal path in applyCheckRunCompleted.

func (*CacheImpl) FetchProjectBoard

func (c *CacheImpl) FetchProjectBoard(owner, repo string, projectNum int, ownerType string) (*gh.ProjectBoard, error)

FetchProjectBoard returns a *gh.ProjectBoard reconstructed from the Store. Falls back to GitHub when the cache has not been bootstrapped or is paused.

func (*CacheImpl) FetchProjectItem

func (c *CacheImpl) FetchProjectItem(owner, repo string, issueNumber int) (*gh.ProjectItem, error)

FetchProjectItem always delegates to GitHub — used by ensureIssueInStore for the fallback fetch path.

func (*CacheImpl) FetchStatusField

func (c *CacheImpl) FetchStatusField(projectID string) (*gh.StatusField, error)

FetchStatusField always delegates to GitHub — project metadata, not board-item state.

func (*CacheImpl) GetItemID

func (c *CacheImpl) GetItemID(key string) (string, bool)

GetItemID returns the project-item node ID (PVTI_...) for the given cache key. Returns ("", false) when the key is not present or has no ItemID.

func (*CacheImpl) IsBootstrapped

func (c *CacheImpl) IsBootstrapped() bool

IsBootstrapped returns true when the Store contains at least one cached item. Uses HasItems (O(1), non-allocating) rather than All to avoid a full snapshot allocation that would otherwise duplicate the one inside FetchProjectBoard. Called outside c.mu per the "NEVER hold mu while calling Store" invariant.

func (*CacheImpl) IsItemCacheFresh

func (c *CacheImpl) IsItemCacheFresh(repo string, number int, sourceUpdatedAt time.Time) bool

IsItemCacheFresh returns true when the cache has deep-fetched details for the given item AND those details are not stale relative to the supplied sourceUpdatedAt (typically pi.UpdatedAt from a fresh board read). When the cache is stale, FetchItemDetails will fall through to a GraphQL deep-fetch. Called outside c.mu.

func (*CacheImpl) IsItemDeepFetched

func (c *CacheImpl) IsItemDeepFetched(repo string, number int) bool

IsItemDeepFetched returns true when the cache holds deep-fetched details for the given item (LastDeepFetchAt is non-zero). Called outside c.mu.

Note: this only reports whether a deep fetch ever happened; it does not check freshness. Callers that need to know whether the cache is currently authoritative should use IsItemCacheFresh.

func (*CacheImpl) IsPaused

func (c *CacheImpl) IsPaused() bool

IsPaused returns true when delta application is paused.

func (*CacheImpl) LightReconcile

func (c *CacheImpl) LightReconcile(owner, repo string, projectNum int, ownerType string) (driftCount int, driftedKeys []string, freshBoard *gh.ProjectBoard, err error)

LightReconcile fetches a fresh shallow board snapshot from GitHub and compares it against the current cache state on two fields: status and updatedAt. Label count is intentionally excluded: the board query returns at most 30 labels (shallow), while the cache may hold the full deep-fetched set; comparing counts would produce persistent false-positive drift for issues with >30 labels. Label mutations are captured by updatedAt, so the two-field check is sufficient.

Note: LightReconcile still calls FetchProjectBoard (full shallow with labels). The per-poll probe path (engine/poll.go runProbeAndDeepFetch) is the primary cost-reduction mechanism; LightReconcile fires at most 20×/hour in webhook mode only, making it a lower-priority optimization target.

Returns the number of drifted items, their keys, and the fresh board (to avoid a double-fetch when the caller passes it to Reconcile on drift).

On network error the method returns a non-nil err, nil freshBoard, and 0 drift. The caller should log a warning and make no health state change.

LightReconcile must not hold c.mu during the FetchProjectBoard call, following the "NEVER hold mu while calling Store" invariant and avoiding slow I/O under lock.

func (*CacheImpl) Pause

func (c *CacheImpl) Pause()

Pause stops delta application (called on WebhookStreamUnhealthy transition). Observers are called after the lock is released, and only on an actual false→true transition to avoid spamming observers on repeated Pause calls.

func (*CacheImpl) ProjectID

func (c *CacheImpl) ProjectID() string

ProjectID returns the project node ID stored from the last Bootstrap/Reconcile call. Returns "" when the cache has not yet been bootstrapped.

func (*CacheImpl) RateLimitStats

func (c *CacheImpl) RateLimitStats() (rest, graphql gh.RateLimitStats)

RateLimitStats always delegates to GitHub.

func (*CacheImpl) Reconcile

func (c *CacheImpl) Reconcile(board *gh.ProjectBoard)

Reconcile replaces shallow board state from a fresh board fetch. Preserves deep fields (Comments, LinkedPRReviews, etc.) for items that have already been deep-fetched. Logs the drift count when items differ. Shallow drift in linkage (LinkedPRNumber) invalidates deep cache for the affected key, forcing a fresh FetchItemDetails on next access.

func (*CacheImpl) RecordPRLinkage

func (c *CacheImpl) RecordPRLinkage(fullRepo string, prNumber, issueNumber int)

RecordPRLinkage records an authoritative PR→issue mapping in the Store index. fullRepo must be in "owner/repo" format. Called by the engine immediately after CreateDraftPR succeeds, so all subsequent webhooks for this PR resolve to the correct issue without consulting the regex. No-op when the mapping is already present (avoids clobbering real webhook data) or when the issue is not yet in the Store (cold-cache bootstrap not yet complete).

func (*CacheImpl) RefreshCheckRunsLive added in v0.0.76

func (c *CacheImpl) RefreshCheckRunsLive(owner, repo, sha string) error

RefreshCheckRunsLive unconditionally fetches check runs for sha from GitHub, bypassing FetchCheckRuns's cache-trust check entirely, and applies the result into the Store exactly as FetchCheckRuns's own miss path does — so a subsequent FetchCheckRuns call for the same sha is served from this fresh data.

#1303: this exists to close a narrow but real gap in FetchCheckRuns's general cache-trust contract (deliberately left unchanged — see its doc comment): a cached PENDING classification is served from cache indefinitely, since only a would-be-FAILED classification forces a live refetch (#958 leg 3). On a webhook-less deployment nothing else ever supersedes a stale cached PENDING snapshot with a check_run event, so settleAwaitingCIScan (the one caller for which an open-ended stale-Pending read is consequential — it can silently claim the item forever via checkMergeabilityGate's PRMergeUnsettled branch) calls this once per fabrik:awaiting-ci item per poll to prime the store with genuinely fresh data before the Phase 1 handler chain runs, rather than trusting whatever FetchCheckRuns' own cache-trust check would otherwise decide.

func (*CacheImpl) RegisterItemID

func (c *CacheImpl) RegisterItemID(key, itemID string)

RegisterItemID sets the project-item node ID for an existing cache entry that was added without one (e.g., via issues.opened before projects_v2_item.created). No-op when key is not in the Store or itemID is empty. Safe for concurrent use.

func (*CacheImpl) RemoveItem added in v0.0.75

func (c *CacheImpl) RemoveItem(itemID string)

RemoveItem removes the cached item identified by itemID (the GraphQL project-item node ID), mirroring the webhook delta path's "deleted"/"archived" handling (boardcache/delta.go). Used by the archive-done settle scan (engine/archive_done_settle.go) to write through a successful ArchiveProjectItem call so the cache stays consistent with GitHub's view without waiting for a webhook echo or the next Reconcile. No-op (not an error) when itemID is unknown — safe to call redundantly. Safe for concurrent use.

func (*CacheImpl) Resume

func (c *CacheImpl) Resume()

Resume re-enables delta application (called after reconciliation on stream recovery). Observers are called after the lock is released, and only on an actual true→false transition to avoid spamming observers on repeated Resume calls.

func (*CacheImpl) SetMatchEchoFn

func (c *CacheImpl) SetMatchEchoFn(fn func(eventType, action, key string))

SetMatchEchoFn injects the MatchEcho function from the webhook manager.

func (*CacheImpl) Subscribe

func (c *CacheImpl) Subscribe(o itemstate.Observer) func()

Subscribe registers an observer on the underlying Store. The returned func unsubscribes the observer. Safe to call from any goroutine.

func (*CacheImpl) SubscribePause

func (c *CacheImpl) SubscribePause(fn func(bool)) func()

SubscribePause registers a function that is called after every Pause/Resume transition. The function receives true when the cache is paused and false when resumed. Observers run outside c.mu, so calling other CacheImpl methods from an observer is safe. Calling Pause or Resume re-entrantly from within an observer is semantically wrong (double-fire, inconsistent state) and must be avoided. The returned func unsubscribes the observer.

func (*CacheImpl) UpdateItemStatus

func (c *CacheImpl) UpdateItemStatus(key, newStatus string)

UpdateItemStatus updates the Status field for the item identified by key. No-op when the key is not in the cache. Safe for concurrent use.

type GitHubAdapter

type GitHubAdapter struct {
	// contains filtered or unexported fields
}

GitHubAdapter wraps a ReadClient with pass-through implementations. Used as the fallback inside CacheImpl (cache miss → forward to GitHub) and directly in NewWithDeps test wiring (engine tests bypass CacheImpl).

func NewGitHubAdapter

func NewGitHubAdapter(client ReadClient) *GitHubAdapter

NewGitHubAdapter wraps any ReadClient as a pass-through GitHubAdapter.

func (*GitHubAdapter) FetchCheckRuns

func (a *GitHubAdapter) FetchCheckRuns(owner, repo, sha string) ([]gh.CheckRun, error)

func (*GitHubAdapter) FetchCombinedStatus added in v0.0.76

func (a *GitHubAdapter) FetchCombinedStatus(owner, repo, ref string) ([]gh.CommitStatus, error)

func (*GitHubAdapter) FetchItemDetails

func (a *GitHubAdapter) FetchItemDetails(item *gh.ProjectItem) error

func (*GitHubAdapter) FetchLabels

func (a *GitHubAdapter) FetchLabels(owner, repo string, issueNumber int) ([]string, error)

func (*GitHubAdapter) FetchLinkedPR

func (a *GitHubAdapter) FetchLinkedPR(owner, repo string, issueNumber int) (*gh.PRDetails, error)

func (*GitHubAdapter) FetchPRClosingIssues

func (a *GitHubAdapter) FetchPRClosingIssues(owner, repo string, prNumber int) ([]int, error)

func (*GitHubAdapter) FetchPRMergeable

func (a *GitHubAdapter) FetchPRMergeable(owner, repo string, prNumber int) (*bool, error)

func (*GitHubAdapter) FetchPRMergeableFields added in v0.0.70

func (a *GitHubAdapter) FetchPRMergeableFields(owner, repo string, prNumber int) (*bool, string, error)

func (*GitHubAdapter) FetchPRMergeableState

func (a *GitHubAdapter) FetchPRMergeableState(owner, repo string, prNumber int) (string, error)

func (*GitHubAdapter) FetchPRMerged added in v0.0.70

func (a *GitHubAdapter) FetchPRMerged(owner, repo string, prNumber int) (bool, error)

func (*GitHubAdapter) FetchPRReviewDecision added in v0.0.76

func (a *GitHubAdapter) FetchPRReviewDecision(owner, repo string, prNumber int) (string, error)

func (*GitHubAdapter) FetchPRReviewRequests added in v0.0.75

func (a *GitHubAdapter) FetchPRReviewRequests(owner, repo string, prNumber int) ([]gh.ReviewRequest, error)

func (*GitHubAdapter) FetchPRReviews added in v0.0.75

func (a *GitHubAdapter) FetchPRReviews(owner, repo string, prNumber int) ([]gh.PRReview, error)

func (*GitHubAdapter) FetchPRsForSHA

func (a *GitHubAdapter) FetchPRsForSHA(owner, repo, sha string) ([]int, error)

func (*GitHubAdapter) FetchProjectBoard

func (a *GitHubAdapter) FetchProjectBoard(owner, repo string, projectNum int, ownerType string) (*gh.ProjectBoard, error)

func (*GitHubAdapter) FetchProjectItem

func (a *GitHubAdapter) FetchProjectItem(owner, repo string, issueNumber int) (*gh.ProjectItem, error)

func (*GitHubAdapter) FetchStatusField

func (a *GitHubAdapter) FetchStatusField(projectID string) (*gh.StatusField, error)

func (*GitHubAdapter) RateLimitStats

func (a *GitHubAdapter) RateLimitStats() (rest, graphql gh.RateLimitStats)

type ReadClient

type ReadClient interface {
	FetchProjectBoard(owner, repo string, projectNum int, ownerType string) (*gh.ProjectBoard, error)
	FetchItemDetails(item *gh.ProjectItem) error
	FetchCheckRuns(owner, repo, sha string) ([]gh.CheckRun, error)
	FetchCombinedStatus(owner, repo, ref string) ([]gh.CommitStatus, error)
	FetchLinkedPR(owner, repo string, issueNumber int) (*gh.PRDetails, error)
	FetchPRMergeableFields(owner, repo string, prNumber int) (mergeable *bool, mergeableState string, err error)
	FetchPRMergeable(owner, repo string, prNumber int) (*bool, error)
	FetchPRMerged(owner, repo string, prNumber int) (bool, error)
	FetchPRMergeableState(owner, repo string, prNumber int) (string, error)
	FetchLabels(owner, repo string, issueNumber int) ([]string, error)
	FetchStatusField(projectID string) (*gh.StatusField, error)
	FetchPRClosingIssues(owner, repo string, prNumber int) ([]int, error)
	FetchPRReviews(owner, repo string, prNumber int) ([]gh.PRReview, error)
	FetchPRReviewRequests(owner, repo string, prNumber int) ([]gh.ReviewRequest, error)
	FetchPRReviewDecision(owner, repo string, prNumber int) (string, error)
	FetchPRsForSHA(owner, repo, sha string) ([]int, error)
	FetchProjectItem(owner, repo string, issueNumber int) (*gh.ProjectItem, error)
	RateLimitStats() (rest, graphql gh.RateLimitStats)
}

ReadClient is the subset of engine.GitHubClient covering read-only board/item/PR/check-run state. engine.GitHubClient is a strict superset; the concrete gh.Client satisfies this interface.

Jump to

Keyboard shortcuts

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