···1010 "log/slog"
1111 "net/http"
1212 "strings"
1313+ "sync"
1314 "time"
14151516 "atcr.io/pkg/appview/db"
···1819 "atcr.io/pkg/auth/oauth"
1920)
20212222+// captainCheckTTL is how long a successful captain XRPC fetch suppresses
2323+// further fetches for the same hold within this worker process.
2424+const captainCheckTTL = 1 * time.Hour
2525+2126// BackfillWorker uses com.atproto.sync.listReposByCollection to backfill historical data
2227type BackfillWorker struct {
2328 db *sql.DB
···2631 defaultHoldDID string // Default hold DID from AppView config (e.g., "did:web:hold01.atcr.io")
2732 testMode bool // If true, suppress warnings for external holds
2833 refresher *oauth.Refresher // OAuth refresher for PDS writes (optional, can be nil)
3434+3535+ // captainChecked tracks the last time we successfully fetched each hold's
3636+ // captain record from XRPC. This is the freshness gate for queryCaptainRecord
3737+ // — we can't rely on the DB row's updated_at because UpsertCaptainRecord
3838+ // intentionally skips writes (and thus the timestamp bump) when the captain
3939+ // data hasn't changed, to avoid round-tripping no-op writes to remote libsql.
4040+ captainCheckedMu sync.Mutex
4141+ captainChecked map[string]time.Time
2942}
30433144// BackfillState tracks backfill progress
···5568 defaultHoldDID: defaultHoldDID,
5669 testMode: testMode,
5770 refresher: refresher,
7171+ captainChecked: make(map[string]time.Time),
5872 }, nil
5973}
6074···478492 return nil
479493}
480494481481-// queryCaptainRecord queries a hold's captain record and caches it in the database
495495+// queryCaptainRecord queries a hold's captain record and caches it in the database.
496496+//
497497+// Freshness is tracked in an in-memory map keyed by hold DID rather than the
498498+// row's updated_at column. UpsertCaptainRecord deliberately skips no-op writes
499499+// to avoid round-tripping unchanged data to remote libsql, which means
500500+// updated_at is "last time data changed" not "last time we checked" — using it
501501+// as a freshness gate would re-fetch on every call once the row aged past TTL.
482502func (b *BackfillWorker) queryCaptainRecord(ctx context.Context, holdDID string) error {
483483- // Check if we already have it cached (skip if recently updated)
484484- existing, err := db.GetCaptainRecord(b.db, holdDID)
485485- if err == nil && existing != nil {
486486- // If cached within last hour, skip refresh
487487- if time.Since(existing.UpdatedAt) < 1*time.Hour {
488488- return nil
489489- }
503503+ b.captainCheckedMu.Lock()
504504+ last, ok := b.captainChecked[holdDID]
505505+ b.captainCheckedMu.Unlock()
506506+ if ok && time.Since(last) < captainCheckTTL {
507507+ return nil
490508 }
491509492510 // Resolve hold DID to URL
···530548 if err := db.UpsertCaptainRecord(b.db, &captainRecord); err != nil {
531549 return fmt.Errorf("failed to cache captain record: %w", err)
532550 }
551551+552552+ b.captainCheckedMu.Lock()
553553+ b.captainChecked[holdDID] = time.Now()
554554+ b.captainCheckedMu.Unlock()
533555534556 slog.Info("Backfill cached captain record for hold", "hold_did", holdDID, "owner_did", captainRecord.OwnerDID)
535557 return nil
+6
pkg/s3/types.go
···223223 // with trailers, which causes XAmzContentSHA256Mismatch errors on
224224 // S3-compatible services that don't support this.
225225 o.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
226226+ // Mirror on the response side. Default is WhenSupported, which logs a
227227+ // WARN on every GetObject whose response lacks x-amz-checksum-* —
228228+ // i.e. every read against Storj/R2/MinIO/Backblaze. The bytes are fine,
229229+ // the warning is just SDK chatter; switch to WhenRequired so the SDK
230230+ // only validates when the server actually returned a checksum.
231231+ o.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired
226232 })
227233228234 var s3PathPrefix string