mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-26 04:04:15 +00:00
6a7ddb8 moved RefreshHoldTiers under a lease with the comment "one-shot; the
lease is released when it returns". It never returns: past the startup retries
it sits on a 30-minute ticker forever. Three consequences, all observed on a
two-instance run against one database:
The billing-tiers lease is never released on a clean stop, so it survives to
its TTL and the replacement instance waits a full minute for a worker the old
one is no longer running. Worse, the goroutine stays in the shutdown WaitGroup,
so "Timed out waiting for leased workers to stop" now fires on EVERY clean
shutdown. The other four leases released in 12ms and the warning fired anyway.
A warning that is always present cannot report the case it exists for, which is
the jetstream lease genuinely failing to release.
And the lease was the wrong tool regardless. The commit justified it as
"RefreshHoldTiers writes tier state derived from Stripe" that instances would
race on. It writes holdTierCache, a per-process map, from read-only ListTiers
calls; there is no shared state anywhere in the path. Electing one refresher
means every other instance keeps an empty cache forever, so
aggregateHoldFeatures reports "no hold data" on all but one — a regression that
only appears at the scale the lease was added to support. This is the hold
health worker's situation exactly, and that one was deliberately left unleased
in the same commit.
So it runs on every instance again, with a context. The retry backoff was
time.Sleep for up to 45s total against an unreachable hold; it and the ticker
now select on ctx.Done, and ListTiers gets the context instead of
context.Background. LeaseBillingTiers is gone rather than left as a dead name.
Verified: with the backoff restored to time.Sleep the new test fails on its own
2s deadline rather than hanging, which is how a shutdown regression here should
present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rmjvU2gSRL9wFnmqVsWaF
98 lines
3.2 KiB
Go
98 lines
3.2 KiB
Go
//go:build !billing
|
|
|
|
package billing
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"net/http"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Manager is a no-op billing manager when billing is not compiled in.
|
|
type Manager struct {
|
|
captainChecker CaptainChecker
|
|
}
|
|
|
|
// New creates a no-op billing manager.
|
|
func New(_ *Config, _ *atcrypto.PrivateKeyP256, _ string, _ []string, _ string, _ *sql.DB) *Manager {
|
|
return &Manager{}
|
|
}
|
|
|
|
// SetCaptainChecker sets a callback that checks if a user is a hold captain.
|
|
func (m *Manager) SetCaptainChecker(fn CaptainChecker) {
|
|
m.captainChecker = fn
|
|
}
|
|
|
|
// SetActiveHoldChecker is a no-op when billing is not compiled in.
|
|
func (m *Manager) SetActiveHoldChecker(_ ActiveHoldChecker) {}
|
|
|
|
// WebhookConfigured returns false when billing is not compiled in.
|
|
func (m *Manager) WebhookConfigured() bool { return false }
|
|
|
|
// Enabled returns false when billing is not compiled in.
|
|
func (m *Manager) Enabled() bool { return false }
|
|
|
|
// GetWebhookLimits returns default limits when billing is not compiled in.
|
|
// Hold captains get unlimited webhooks with all triggers.
|
|
func (m *Manager) GetWebhookLimits(userDID string) (int, bool) {
|
|
if m.captainChecker != nil && userDID != "" && m.captainChecker(userDID) {
|
|
return -1, true
|
|
}
|
|
return 1, false
|
|
}
|
|
|
|
// HasAIAdvisor returns whether a user has access to the AI Image Advisor.
|
|
// Hold captains always have access. Default is false when billing is not compiled in.
|
|
func (m *Manager) HasAIAdvisor(userDID string) bool {
|
|
if m.captainChecker != nil && userDID != "" && m.captainChecker(userDID) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetSubscriptionInfo returns an error when billing is not compiled in.
|
|
func (m *Manager) GetSubscriptionInfo(_ string) (*SubscriptionInfo, error) {
|
|
return nil, ErrBillingDisabled
|
|
}
|
|
|
|
// HasActiveSubscription returns false when billing is not compiled in.
|
|
func (m *Manager) HasActiveSubscription(_ string) bool { return false }
|
|
|
|
// CreateCheckoutSession returns an error when billing is not compiled in.
|
|
func (m *Manager) CreateCheckoutSession(_ *http.Request, _, _ string, _ *CheckoutSessionRequest) (*CheckoutSessionResponse, error) {
|
|
return nil, ErrBillingDisabled
|
|
}
|
|
|
|
// GetBillingPortalURL returns an error when billing is not compiled in.
|
|
func (m *Manager) GetBillingPortalURL(_ string, _ string) (*BillingPortalResponse, error) {
|
|
return nil, ErrBillingDisabled
|
|
}
|
|
|
|
// HandleWebhook returns an error when billing is not compiled in.
|
|
func (m *Manager) HandleWebhook(_ *http.Request) error {
|
|
return ErrBillingDisabled
|
|
}
|
|
|
|
// GetSupporterBadge returns empty string when billing is not compiled in.
|
|
// Hold captains get a "Captain" badge.
|
|
func (m *Manager) GetSupporterBadge(userDID string) string {
|
|
if m.captainChecker != nil && userDID != "" && m.captainChecker(userDID) {
|
|
return "Captain"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetFirstTierWithAllTriggers returns empty string when billing is not compiled in.
|
|
func (m *Manager) GetFirstTierWithAllTriggers() string {
|
|
return ""
|
|
}
|
|
|
|
// RegisterRoutes is a no-op when billing is not compiled in.
|
|
func (m *Manager) RegisterRoutes(_ chi.Router) {}
|
|
|
|
// RefreshHoldTiers is a no-op when billing is not compiled in.
|
|
func (m *Manager) RefreshHoldTiers(context.Context) {}
|