mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 16:26:56 +00:00
The Jetstream consumer, backfill, labeler subscriber, cleanup sweep and billing tier refresh all started unconditionally in every process. That is correct for one instance and wrong for two. The consumer is the case with teeth. StatsCache is per-process in-memory state, and the aggregate it produces is written to repository_stats as an absolute value rather than an increment, so two consumers each hold a partial view of the holds and each write their partial sum as though it were the whole truth, overwriting one another indefinitely. The webhook dispatcher hangs off the same processor, so a second consumer also doubles every delivery. Each now runs under a named lease, so exactly one instance runs it and a replacement takes over when that instance goes away. The health worker is deliberately not leased: it refreshes a cache each instance needs locally, so running it everywhere is correct. Two structural changes came with it. The cleanup loop moved out of InitializeDatabase, where it was a bare goroutine with no way to reach the lease manager, into RunPeriodicCleanup called from the server. And backfill's startup run and periodic schedule became one leased worker instead of two goroutines on context.Background(), so shutdown actually stops a backfill in flight rather than letting it run on against a closing database. With interval=0 that worker holds its lease instead of returning, since releasing would let another instance acquire and run its own startup backfill, turning "once" into "once per instance". Verified with two instances against one database: exactly one acquired, the other contended without starting a worker; SIGTERM handed over in 13ms via the release, SIGKILL handed over in ~12s via TTL expiry. That first number only holds because of Manager.Go and Manager.Wait, which this commit adds. The first cut used `go m.Run(...)` and cancelled the worker context during shutdown without waiting, so the process exited before the release landed and the lease survived to its TTL — a rolling deploy would have paused indexing for a minute rather than a second. Nothing in the unit tests caught it; the two-instance run did. TestWaitBlocksUntilLeaseReleased covers it now. leases.enabled defaults to true. A single instance is unaffected, since it always wins its own leases, while an operator who scales out without reading the docs still gets correct behavior instead of silent stats corruption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
8.0 KiB
YAML
173 lines
8.0 KiB
YAML
# ATCR AppView Configuration
|
|
# Generated with defaults — edit as needed.
|
|
|
|
# Configuration format version.
|
|
version: "0.1"
|
|
# Log level: debug, info, warn, error.
|
|
log_level: info
|
|
# Remote log shipping settings.
|
|
log_shipper:
|
|
# Log shipping backend: "victoria", "opensearch", or "loki". Empty disables shipping.
|
|
backend: ""
|
|
# Remote log service endpoint, e.g. "http://victorialogs:9428".
|
|
url: ""
|
|
# Number of log entries to buffer before flushing to the remote service.
|
|
batch_size: 100
|
|
# Maximum time between flushes, even if batch is not full.
|
|
flush_interval: 5s
|
|
# Basic auth username for the log service (optional).
|
|
username: ""
|
|
# Basic auth password for the log service (optional).
|
|
password: ""
|
|
# HTTP server and identity settings.
|
|
server:
|
|
# Listen address, e.g. ":5000" or "127.0.0.1:5000".
|
|
addr: :5000
|
|
# Public-facing URL for OAuth callbacks and JWT realm. Auto-detected if empty.
|
|
base_url: ""
|
|
# Allows HTTP (not HTTPS) for DID resolution and uses transition:generic OAuth scope.
|
|
test_mode: false
|
|
# Display name shown on OAuth authorization screens.
|
|
client_name: AT Container Registry
|
|
# Short name used in page titles and browser tabs.
|
|
client_short_name: ATCR
|
|
# Separate domains for OCI registry API (e.g. ["buoy.cr"]). First is primary. Browser visits redirect to BaseURL.
|
|
registry_domains: [127.0.0.1:5000, atcr.io]
|
|
# DIDs of holds this appview manages billing for (REQUIRED). The first entry is the default blob-storage hold. Tier updates are pushed to these holds.
|
|
managed_holds:
|
|
- did:web:172.28.0.3%3A8080
|
|
# Web UI settings.
|
|
ui:
|
|
# SQLite/libSQL database for OAuth sessions, stars, pull counts, and device approvals.
|
|
database_path: /var/lib/atcr/ui.db
|
|
# Visual theme name (e.g. "seamark"). Empty uses default atcr.io branding.
|
|
theme: "seamark"
|
|
# libSQL sync URL (libsql://...). Works with Turso cloud or self-hosted libsql-server. Leave empty for local-only SQLite.
|
|
libsql_sync_url: ""
|
|
# Auth token for libSQL sync. Required if libsql_sync_url is set.
|
|
libsql_auth_token: ""
|
|
# How often to sync with remote libSQL server. Default: 60s.
|
|
libsql_sync_interval: 1m0s
|
|
# Source code URL displayed in the footer "Source" link. Defaults to the upstream ATCR project.
|
|
source_url: https://tangled.org/evan.jarrett.net/at-container-registry
|
|
# Health check and cache settings.
|
|
health:
|
|
# How long to cache hold health check results.
|
|
cache_ttl: 15m0s
|
|
# How often to refresh hold health checks.
|
|
check_interval: 15m0s
|
|
# Leader election for background workers. Required when running more than one AppView instance.
|
|
leases:
|
|
# Elect a single instance to run background workers. Required when running more than one AppView instance.
|
|
enabled: true
|
|
# How long a lease survives without renewal before another instance may take it. Must exceed any plausible clock skew between instances.
|
|
ttl: 1m0s
|
|
# How often the holder renews its lease, and how often waiting instances retry. Must be well under ttl.
|
|
renew_interval: 20s
|
|
# ATProto Jetstream event stream settings.
|
|
jetstream:
|
|
# Jetstream WebSocket endpoints, tried in order on failure.
|
|
urls:
|
|
- wss://jetstream2.us-west.bsky.network/subscribe
|
|
- wss://jetstream1.us-west.bsky.network/subscribe
|
|
- wss://jetstream2.us-east.bsky.network/subscribe
|
|
- wss://jetstream1.us-east.bsky.network/subscribe
|
|
# Sync existing records from PDS on startup.
|
|
backfill_enabled: true
|
|
# How often to re-run backfill to catch missed events. Set to 0 to only backfill on startup.
|
|
backfill_interval: 24h0m0s
|
|
# Endpoints used for backfill. MUST support com.atproto.sync.listReposByCollection. Tried in order on failure.
|
|
relay_endpoints:
|
|
- https://relay1.us-east.bsky.network
|
|
- https://relay1.us-west.bsky.network
|
|
# JWT authentication settings.
|
|
auth:
|
|
# X.509 certificate matching the JWT signing key (auto-generated on each boot from the JWT key in the database).
|
|
cert_path: /var/lib/atcr/auth/private-key.crt
|
|
# Credential helper download settings.
|
|
credential_helper:
|
|
# Tangled repository URL for credential helper downloads.
|
|
tangled_repo: ""
|
|
# Legal page customization for self-hosted instances.
|
|
legal:
|
|
# Organization name for Terms of Service and Privacy Policy. Defaults to server.client_name.
|
|
company_name: ""
|
|
# Governing law jurisdiction for legal terms.
|
|
jurisdiction: ""
|
|
# AI-powered image advisor settings.
|
|
ai:
|
|
# Anthropic API key for AI Image Advisor. Also reads CLAUDE_API_KEY env var as fallback.
|
|
api_key: ""
|
|
# ATProto labeler for content moderation (DMCA takedowns).
|
|
labeler:
|
|
# DID of the ATProto labeler (did:plc:... or did:web:...). Empty disables label filtering.
|
|
did: ""
|
|
# Stripe billing integration (requires -tags billing build).
|
|
billing:
|
|
# Stripe secret key. Can also be set via STRIPE_SECRET_KEY env var (takes precedence). Billing is enabled automatically when set.
|
|
stripe_secret_key: ""
|
|
# Stripe webhook signing secret. Can also be set via STRIPE_WEBHOOK_SECRET env var (takes precedence).
|
|
webhook_secret: ""
|
|
# ISO 4217 currency code (e.g. "usd").
|
|
currency: usd
|
|
# Redirect URL after successful checkout. Use {base_url} placeholder.
|
|
success_url: '{base_url}/settings/billing'
|
|
# Redirect URL after cancelled checkout. Use {base_url} placeholder.
|
|
cancel_url: '{base_url}/settings/billing'
|
|
# Subscription tiers ordered by rank (lowest to highest).
|
|
tiers:
|
|
- # Tier name. Position in list determines rank (0-based).
|
|
name: Free
|
|
# Short description shown on the plan card.
|
|
description: Get started with basic storage
|
|
# List of features included in this tier.
|
|
features: []
|
|
# Stripe price ID for monthly billing. Empty = free tier.
|
|
stripe_price_monthly: ""
|
|
# Stripe price ID for yearly billing.
|
|
stripe_price_yearly: ""
|
|
# Maximum webhooks for this tier (-1 = unlimited).
|
|
max_webhooks: 1
|
|
# Allow all webhook trigger types (not just first-scan).
|
|
webhook_all_triggers: false
|
|
# Enable AI Image Advisor for this tier.
|
|
ai_advisor: false
|
|
# Show supporter badge on user profiles for subscribers at this tier.
|
|
supporter_badge: false
|
|
- # Tier name. Position in list determines rank (0-based).
|
|
name: Supporter
|
|
# Short description shown on the plan card.
|
|
description: Get started with basic storage
|
|
# List of features included in this tier.
|
|
features: []
|
|
# Stripe price ID for monthly billing. Empty = free tier.
|
|
stripe_price_monthly: ""
|
|
# Stripe price ID for yearly billing.
|
|
stripe_price_yearly: "price_1SmK1mRROAC4bYmSwhTQ7RY9"
|
|
# Maximum webhooks for this tier (-1 = unlimited).
|
|
max_webhooks: 1
|
|
# Allow all webhook trigger types (not just first-scan).
|
|
webhook_all_triggers: true
|
|
# Enable AI Image Advisor for this tier.
|
|
ai_advisor: true
|
|
# Show supporter badge on user profiles for subscribers at this tier.
|
|
supporter_badge: true
|
|
- # Tier name. Position in list determines rank (0-based).
|
|
name: Pro
|
|
# Short description shown on the plan card.
|
|
description: More storage with scan-on-push
|
|
# List of features included in this tier.
|
|
features: []
|
|
# Stripe price ID for monthly billing. Empty = free tier.
|
|
stripe_price_monthly: "price_1SmK4QRROAC4bYmSxpr35HUl"
|
|
# Stripe price ID for yearly billing.
|
|
stripe_price_yearly: "price_1SmJuLRROAC4bYmSUgVCwZWo"
|
|
# Maximum webhooks for this tier (-1 = unlimited).
|
|
max_webhooks: 10
|
|
# Allow all webhook trigger types (not just first-scan).
|
|
webhook_all_triggers: true
|
|
# Enable AI Image Advisor for this tier.
|
|
ai_advisor: true
|
|
# Show supporter badge on user profiles for subscribers at this tier.
|
|
supporter_badge: true
|