···1010 "log/slog"
1111 "net/http"
1212 "strings"
1313- "sync"
1413 "time"
15141615 "atcr.io/pkg/appview/readme"
···2221// ManifestStore implements distribution.ManifestService
2322// It stores manifests in ATProto as records
2423type ManifestStore struct {
2525- ctx *RegistryContext // Context with user/hold info
2626- mu sync.RWMutex // Protects lastFetchedHoldDID
2727- lastFetchedHoldDID string // Hold DID from most recently fetched manifest (for pull)
2828- blobStore distribution.BlobStore // Blob store for fetching config during push
2424+ ctx *RegistryContext // Context with user/hold info
2525+ blobStore distribution.BlobStore // Blob store for fetching config during push
2926}
30273128// NewManifestStore creates a new ATProto-backed manifest store
···6562 if err := json.Unmarshal(record.Value, &manifestRecord); err != nil {
6663 return nil, fmt.Errorf("failed to unmarshal manifest record: %w", err)
6764 }
6868-6969- // Store the hold DID for subsequent blob requests during pull
7070- // Prefer HoldDID (new format) with fallback to HoldEndpoint (legacy URL format)
7171- // The routing repository will cache this for concurrent blob fetches
7272- s.mu.Lock()
7373- if manifestRecord.HoldDID != "" {
7474- // New format: DID reference (preferred)
7575- s.lastFetchedHoldDID = manifestRecord.HoldDID
7676- } else if manifestRecord.HoldEndpoint != "" {
7777- // Legacy format: URL reference - convert to DID
7878- s.lastFetchedHoldDID = atproto.ResolveHoldDIDFromURL(manifestRecord.HoldEndpoint)
7979- }
8080- s.mu.Unlock()
81658266 var ociManifest []byte
8367
+10-26
pkg/appview/storage/routing_repository.go
···77import (
88 "context"
99 "log/slog"
1010- "sync"
11101211 "github.com/distribution/distribution/v3"
1312)
14131514// RoutingRepository routes manifests to ATProto and blobs to external hold service
1615// The registry (AppView) is stateless and NEVER stores blobs locally
1616+// NOTE: A fresh instance is created per-request (see middleware/registry.go)
1717+// so no mutex is needed - each request has its own instance
1718type RoutingRepository struct {
1819 distribution.Repository
1920 Ctx *RegistryContext // All context and services (exported for token updates)
2020- mu sync.Mutex // Protects manifestStore and blobStore
2121- manifestStore *ManifestStore // Cached manifest store instance
2222- blobStore *ProxyBlobStore // Cached blob store instance
2121+ manifestStore *ManifestStore // Manifest store instance (lazy-initialized)
2222+ blobStore *ProxyBlobStore // Blob store instance (lazy-initialized)
2323}
24242525// NewRoutingRepository creates a new routing repository
···32323333// Manifests returns the ATProto-backed manifest service
3434func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
3535- r.mu.Lock()
3636- // Create or return cached manifest store
3535+ // Lazy-initialize manifest store (no mutex needed - one instance per request)
3736 if r.manifestStore == nil {
3837 // Ensure blob store is created first (needed for label extraction during push)
3939- // Release lock while calling Blobs to avoid deadlock
4040- r.mu.Unlock()
4138 blobStore := r.Blobs(ctx)
4242- r.mu.Lock()
4343-4444- // Double-check after reacquiring lock (another goroutine might have set it)
4545- if r.manifestStore == nil {
4646- r.manifestStore = NewManifestStore(r.Ctx, blobStore)
4747- }
3939+ r.manifestStore = NewManifestStore(r.Ctx, blobStore)
4840 }
4949- manifestStore := r.manifestStore
5050- r.mu.Unlock()
5151-5252- return manifestStore, nil
4141+ return r.manifestStore, nil
5342}
54435544// Blobs returns a proxy blob store that routes to external hold service
5645// The registry (AppView) NEVER stores blobs locally - all blobs go through hold service
5746func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore {
5858- r.mu.Lock()
5959- // Return cached blob store if available
4747+ // Return cached blob store if available (no mutex needed - one instance per request)
6048 if r.blobStore != nil {
6161- blobStore := r.blobStore
6262- r.mu.Unlock()
6349 slog.Debug("Returning cached blob store", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository)
6464- return blobStore
5050+ return r.blobStore
6551 }
66526753 // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation
···1038910490 // Create and cache proxy blob store
10591 r.blobStore = NewProxyBlobStore(r.Ctx)
106106- blobStore := r.blobStore
107107- r.mu.Unlock()
108108- return blobStore
9292+ return r.blobStore
10993}
1109411195// Tags returns the tag service