From 35aa19e2b2e7fbaaba1d6001b4000333fe51cdfb Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sat, 2 May 2026 11:11:01 -0500 Subject: [PATCH] fix captain record check ttl --- pkg/appview/jetstream/backfill.go | 38 ++++++++++++++++++++++++------- pkg/s3/types.go | 6 +++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/pkg/appview/jetstream/backfill.go b/pkg/appview/jetstream/backfill.go index 63791d1..868ec8d 100644 --- a/pkg/appview/jetstream/backfill.go +++ b/pkg/appview/jetstream/backfill.go @@ -10,6 +10,7 @@ import ( "log/slog" "net/http" "strings" + "sync" "time" "atcr.io/pkg/appview/db" @@ -18,6 +19,10 @@ import ( "atcr.io/pkg/auth/oauth" ) +// captainCheckTTL is how long a successful captain XRPC fetch suppresses +// further fetches for the same hold within this worker process. +const captainCheckTTL = 1 * time.Hour + // BackfillWorker uses com.atproto.sync.listReposByCollection to backfill historical data type BackfillWorker struct { db *sql.DB @@ -26,6 +31,14 @@ type BackfillWorker struct { defaultHoldDID string // Default hold DID from AppView config (e.g., "did:web:hold01.atcr.io") testMode bool // If true, suppress warnings for external holds refresher *oauth.Refresher // OAuth refresher for PDS writes (optional, can be nil) + + // captainChecked tracks the last time we successfully fetched each hold's + // captain record from XRPC. This is the freshness gate for queryCaptainRecord + // — we can't rely on the DB row's updated_at because UpsertCaptainRecord + // intentionally skips writes (and thus the timestamp bump) when the captain + // data hasn't changed, to avoid round-tripping no-op writes to remote libsql. + captainCheckedMu sync.Mutex + captainChecked map[string]time.Time } // BackfillState tracks backfill progress @@ -55,6 +68,7 @@ func NewBackfillWorker(database *sql.DB, relayEndpoints []string, defaultHoldDID defaultHoldDID: defaultHoldDID, testMode: testMode, refresher: refresher, + captainChecked: make(map[string]time.Time), }, nil } @@ -478,15 +492,19 @@ func (b *BackfillWorker) queryCaptainRecordWrapper(ctx context.Context, holdDID return nil } -// queryCaptainRecord queries a hold's captain record and caches it in the database +// queryCaptainRecord queries a hold's captain record and caches it in the database. +// +// Freshness is tracked in an in-memory map keyed by hold DID rather than the +// row's updated_at column. UpsertCaptainRecord deliberately skips no-op writes +// to avoid round-tripping unchanged data to remote libsql, which means +// updated_at is "last time data changed" not "last time we checked" — using it +// as a freshness gate would re-fetch on every call once the row aged past TTL. func (b *BackfillWorker) queryCaptainRecord(ctx context.Context, holdDID string) error { - // Check if we already have it cached (skip if recently updated) - existing, err := db.GetCaptainRecord(b.db, holdDID) - if err == nil && existing != nil { - // If cached within last hour, skip refresh - if time.Since(existing.UpdatedAt) < 1*time.Hour { - return nil - } + b.captainCheckedMu.Lock() + last, ok := b.captainChecked[holdDID] + b.captainCheckedMu.Unlock() + if ok && time.Since(last) < captainCheckTTL { + return nil } // Resolve hold DID to URL @@ -531,6 +549,10 @@ func (b *BackfillWorker) queryCaptainRecord(ctx context.Context, holdDID string) return fmt.Errorf("failed to cache captain record: %w", err) } + b.captainCheckedMu.Lock() + b.captainChecked[holdDID] = time.Now() + b.captainCheckedMu.Unlock() + slog.Info("Backfill cached captain record for hold", "hold_did", holdDID, "owner_did", captainRecord.OwnerDID) return nil } diff --git a/pkg/s3/types.go b/pkg/s3/types.go index 8914ddd..39809e3 100644 --- a/pkg/s3/types.go +++ b/pkg/s3/types.go @@ -223,6 +223,12 @@ func NewS3Service(params map[string]any) (*S3Service, error) { // with trailers, which causes XAmzContentSHA256Mismatch errors on // S3-compatible services that don't support this. o.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired + // Mirror on the response side. Default is WhenSupported, which logs a + // WARN on every GetObject whose response lacks x-amz-checksum-* — + // i.e. every read against Storj/R2/MinIO/Backblaze. The bytes are fine, + // the warning is just SDK chatter; switch to WhenRequired so the SDK + // only validates when the server actually returned a checksum. + o.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired }) var s3PathPrefix string