| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "path/filepath" |
| 6 | "sync" |
| 7 | "sync/atomic" |
| 8 | |
| 9 | "reasonix/internal/attachment" |
| 10 | "reasonix/internal/sessioncontent" |
| 11 | ) |
| 12 | |
| 13 | var hostVariantCache = attachment.NewVariantCache(attachment.DefaultCacheBytes, attachment.DefaultTransforms) |
| 14 | |
| 15 | type controllerAttachmentState struct { |
| 16 | attachments atomic.Pointer[attachment.Service] |
| 17 | attachmentOwnerMu sync.Mutex |
| 18 | attachmentOwnerScope string |
| 19 | imageRoutesOnce sync.Once |
| 20 | imageRoutes map[string]ImageRequestRoute |
| 21 | imageRoutesErr error |
| 22 | } |
| 23 | |
| 24 | func (c *Controller) AttachmentOwnerIdentity() string { |
| 25 | ref, _ := c.SessionRef() |
| 26 | return ref.HostID + "\x00" + c.attachmentScope() |
| 27 | } |
| 28 | |
| 29 | // NewAttachmentOperationContext binds an operation to both caller and owner. |
| 30 | func (c *Controller) NewAttachmentOperationContext(parent context.Context) (context.Context, context.CancelFunc) { |
| 31 | ctx, cancel := context.WithCancel(parent) |
| 32 | stop := context.AfterFunc(c.attachmentContext(), cancel) |
| 33 | return ctx, func() { stop(); cancel() } |
| 34 | } |
| 35 | |
| 36 | func (c *Controller) attachmentContext() context.Context { |
| 37 | if c.goalDriverControl.ctx != nil { |
| 38 | return c.goalDriverControl.ctx |
| 39 | } |
| 40 | return context.Background() |
| 41 | } |
| 42 | |
| 43 | func (c *Controller) attachmentService() *attachment.Service { |
| 44 | if c == nil { |
| 45 | return nil |
| 46 | } |
| 47 | return c.attachments.Load() |
| 48 | } |
| 49 | |
| 50 | // Only the session publication owner changes this binding. Request goroutines |
| 51 | // retain their service snapshot; they never lazily choose another content root. |
| 52 | func (c *Controller) bindAttachmentService() { |
| 53 | c.attachmentOwnerMu.Lock() |
| 54 | defer c.attachmentOwnerMu.Unlock() |
| 55 | content := c.sessionContentStore() |
| 56 | scope := c.attachmentScope() |
| 57 | current := c.attachments.Load() |
| 58 | if current != nil && c.attachmentOwnerScope != scope { |
| 59 | current.Drafts().ReleaseScope(c.attachmentOwnerScope) |
| 60 | } |
| 61 | c.attachmentOwnerScope = scope |
| 62 | if current != nil && current.Store().Root() == content.Root() { |
| 63 | return |
| 64 | } |
| 65 | c.attachments.Store(attachment.NewService(content, hostVariantCache)) |
| 66 | } |
| 67 | |
| 68 | func (c *Controller) sessionContentStore() *sessioncontent.Store { |
| 69 | if c == nil { |
| 70 | return nil |
| 71 | } |
| 72 | if _, runtime, _ := c.v3Binding(); runtime != nil { |
| 73 | return runtime.Session().ContentStore() |
| 74 | } |
| 75 | if store := c.sessionEventStore(); store != nil { |
| 76 | if content := store.ContentStore(); content != nil { |
| 77 | return content |
| 78 | } |
| 79 | } |
| 80 | if c.sessionDir != "" { |
| 81 | return sessioncontent.New(filepath.Join(c.sessionDir, ".content-v1")) |
| 82 | } |
| 83 | if c.sessionPath != "" { |
| 84 | return sessioncontent.New(filepath.Join(filepath.Dir(c.sessionPath), ".content-v1")) |
| 85 | } |
| 86 | if c.workspaceRoot != "" { |
| 87 | return sessioncontent.New(filepath.Join(c.workspaceRoot, ".reasonix", "content-v1")) |
| 88 | } |
| 89 | return nil |
| 90 | } |
| 91 | |
| 92 | func (c *Controller) attachmentScope() string { |
| 93 | if c == nil { |
| 94 | return "" |
| 95 | } |
| 96 | if _, runtime, _ := c.v3Binding(); runtime != nil { |
| 97 | store := runtime.Session() |
| 98 | return store.ID() + ":" + store.StorageGeneration() |
| 99 | } |
| 100 | if store := c.sessionEventStore(); store != nil { |
| 101 | return store.ID() + ":" + store.StorageGeneration() |
| 102 | } |
| 103 | return c.sessionPath |
| 104 | } |
| 105 |