返回 DeepSeek-Reasonix
variant.go
根目录 / internal / attachment / variant.go
1 package attachment
2
3 import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "image"
10 "image/jpeg"
11 "image/png"
12 "sync"
13
14 xdraw "golang.org/x/image/draw"
15 "reasonix/internal/sessioncontent"
16 )
17
18 type variantKey struct {
19 digest string
20 version int
21 width int
22 height int
23 format string
24 quality int
25 }
26
27 type cacheEntry struct {
28 key variantKey
29 size int64
30 result Variant
31 witness sessioncontent.ObjectWitness
32 }
33
34 // VariantCache is a process-local LRU of request encodings. It never stores
35 // or deletes persistent originals.
36 type VariantCache struct {
37 mu sync.Mutex
38 maxBytes int64
39 used int64
40 entries map[variantKey]*cacheEntry
41 order []*cacheEntry
42 transforms chan struct{}
43 inflight map[variantKey]*sharedTransform
44 }
45
46 type sharedTransform struct {
47 waiters int
48 cancel context.CancelFunc
49 done chan struct{}
50 result Variant
51 err error
52 witness sessioncontent.ObjectWitness
53 }
54
55 func NewVariantCache(maxBytes int64, transforms int) *VariantCache {
56 if maxBytes <= 0 {
57 maxBytes = DefaultCacheBytes
58 }
59 if transforms <= 0 {
60 transforms = DefaultTransforms
61 }
62 return &VariantCache{
63 maxBytes: maxBytes,
64 entries: map[variantKey]*cacheEntry{},
65 transforms: make(chan struct{}, transforms),
66 inflight: map[variantKey]*sharedTransform{},
67 }
68 }
69
70 func (s *Service) PrepareVariant(ctx context.Context, ref AttachmentRef, policyVersion int) (Variant, error) {
71 if s == nil || s.cache == nil {
72 return Variant{}, Error{Code: CodeUnreadable, Message: "attachment variant cache unavailable"}
73 }
74 key, err := variantCacheKeyFromRef(ref, policyVersion)
75 if err != nil {
76 return Variant{}, err
77 }
78 witness, err := s.store.Probe(ctx, ref.Content)
79 if err != nil {
80 return Variant{}, err
81 }
82 if hit, cachedWitness, ok := s.cache.getWithWitness(key); ok {
83 if cachedWitness.Same(witness) {
84 if err := verifyVariant(hit, key); err == nil {
85 return cloneVariant(hit), nil
86 }
87 }
88 s.cache.remove(key)
89 }
90 return s.cache.shareWithWitness(ctx, key, witness, func(workCtx context.Context) (Variant, error) {
91 raw, err := s.ReadVerified(workCtx, ref)
92 if err != nil {
93 return Variant{}, err
94 }
95 verifiedKey, err := variantCacheKey(ref, raw, policyVersion)
96 if err != nil {
97 return Variant{}, err
98 }
99 if verifiedKey != key {
100 return Variant{}, Error{Code: CodeChanged, Name: ref.DisplayName, Message: "attachment metadata changed during validation"}
101 }
102 return encodeVariant(workCtx, ref, raw, policyVersion)
103 })
104 }
105
106 func variantCacheKeyFromRef(ref AttachmentRef, policyVersion int) (variantKey, error) {
107 if policyVersion == 0 {
108 policyVersion = VariantPolicyV1
109 }
110 if policyVersion != VariantPolicyV1 {
111 return variantKey{}, Error{Code: CodeUnsupported, Name: ref.DisplayName, Message: "unsupported image transform policy"}
112 }
113 if err := ref.Validate(); err != nil {
114 return variantKey{}, err
115 }
116 w, h := scaledDims(ref.Width, ref.Height, VariantMaxDim)
117 format, quality := "original", 0
118 if w != ref.Width || h != ref.Height {
119 if ref.MIME() == "image/jpeg" {
120 format, quality = "jpeg", VariantJPEGQuality
121 } else {
122 format = "png"
123 }
124 }
125 return variantKey{digest: ref.Content.Digest, version: policyVersion, width: w, height: h, format: format, quality: quality}, nil
126 }
127
128 func (c *VariantCache) Prepare(ctx context.Context, ref AttachmentRef, raw []byte, policyVersion int) (Variant, error) {
129 if policyVersion == 0 {
130 policyVersion = VariantPolicyV1
131 }
132 if policyVersion != VariantPolicyV1 {
133 return Variant{}, Error{Code: CodeUnsupported, Name: ref.DisplayName, Message: "unsupported image transform policy"}
134 }
135 if err := ctx.Err(); err != nil {
136 return Variant{}, canceledError(err)
137 }
138 key, err := variantCacheKey(ref, raw, policyVersion)
139 if err != nil {
140 return Variant{}, err
141 }
142 if hit, ok := c.get(key); ok {
143 if err := verifyVariant(hit, key); err != nil {
144 c.remove(key)
145 } else {
146 return cloneVariant(hit), nil
147 }
148 }
149 return c.share(ctx, key, func(workCtx context.Context) (Variant, error) {
150 return encodeVariant(workCtx, ref, raw, policyVersion)
151 })
152 }
153
154 func variantCacheKey(ref AttachmentRef, raw []byte, policyVersion int) (variantKey, error) {
155 cfg, _, err := image.DecodeConfig(bytes.NewReader(raw))
156 if err != nil {
157 return variantKey{}, Error{Code: CodeCorrupt, Name: ref.DisplayName, Message: defaultDetail(CodeCorrupt), Cause: err}
158 }
159 w, h := scaledDims(cfg.Width, cfg.Height, VariantMaxDim)
160 format, quality := "original", 0
161 if w != cfg.Width || h != cfg.Height {
162 if ref.MIME() == "image/jpeg" {
163 format, quality = "jpeg", VariantJPEGQuality
164 } else {
165 format, quality = "png", 0
166 }
167 }
168 return variantKey{digest: ref.Content.Digest, version: policyVersion, width: w, height: h, format: format, quality: quality}, nil
169 }
170
171 func encodeVariant(ctx context.Context, ref AttachmentRef, raw []byte, policyVersion int) (Variant, error) {
172 if err := ctx.Err(); err != nil {
173 return Variant{}, canceledError(err)
174 }
175 cfg, _, err := image.DecodeConfig(bytes.NewReader(raw))
176 if err != nil {
177 return Variant{}, Error{Code: CodeCorrupt, Name: ref.DisplayName, Message: defaultDetail(CodeCorrupt), Cause: err}
178 }
179 w, h := scaledDims(cfg.Width, cfg.Height, VariantMaxDim)
180 if w == cfg.Width && h == cfg.Height {
181 return variantFromOriginal(ref, raw, cfg.Width, cfg.Height, policyVersion), nil
182 }
183 if err := ctx.Err(); err != nil {
184 return Variant{}, canceledError(err)
185 }
186 src, _, err := image.Decode(bytes.NewReader(raw))
187 if err != nil {
188 return Variant{}, Error{Code: CodeCorrupt, Name: ref.DisplayName, Message: defaultDetail(CodeCorrupt), Cause: err}
189 }
190 if err := ctx.Err(); err != nil {
191 return Variant{}, canceledError(err)
192 }
193 dst := image.NewRGBA(image.Rect(0, 0, w, h))
194 xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil)
195 if err := ctx.Err(); err != nil {
196 return Variant{}, canceledError(err)
197 }
198 var buf bytes.Buffer
199 mime := "image/png"
200 if ref.MIME() == "image/jpeg" && !hasAlpha(src) {
201 if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: VariantJPEGQuality}); err != nil {
202 return Variant{}, Error{Code: CodeCorrupt, Name: ref.DisplayName, Message: "could not encode the image variant", Cause: err}
203 }
204 mime = "image/jpeg"
205 } else if err := png.Encode(&buf, dst); err != nil {
206 return Variant{}, Error{Code: CodeCorrupt, Name: ref.DisplayName, Message: "could not encode the image variant", Cause: err}
207 }
208 if err := ctx.Err(); err != nil {
209 return Variant{}, canceledError(err)
210 }
211 out := buf.Bytes()
212 sum := sha256.Sum256(out)
213 return Variant{
214 Bytes: append([]byte(nil), out...),
215 MIME: mime,
216 Width: w,
217 Height: h,
218 SourceWidth: cfg.Width,
219 SourceHeight: cfg.Height,
220 PolicyVersion: policyVersion,
221 Digest: hex.EncodeToString(sum[:]),
222 }, nil
223 }
224
225 func variantFromOriginal(ref AttachmentRef, raw []byte, width, height, policyVersion int) Variant {
226 sum := sha256.Sum256(raw)
227 return Variant{
228 Bytes: append([]byte(nil), raw...),
229 MIME: ref.MIME(),
230 Width: width,
231 Height: height,
232 SourceWidth: width,
233 SourceHeight: height,
234 PolicyVersion: policyVersion,
235 Digest: hex.EncodeToString(sum[:]),
236 }
237 }
238
239 func scaledDims(w, h, limit int) (int, int) {
240 if w <= 0 || h <= 0 || limit <= 0 || (w <= limit && h <= limit) {
241 return maxInt(w, 1), maxInt(h, 1)
242 }
243 if w >= h {
244 return limit, maxInt(h*limit/w, 1)
245 }
246 return maxInt(w*limit/h, 1), limit
247 }
248
249 func maxInt(a, b int) int {
250 if a > b {
251 return a
252 }
253 return b
254 }
255
256 func (c *VariantCache) share(ctx context.Context, key variantKey, work func(context.Context) (Variant, error)) (Variant, error) {
257 return c.shareWithWitness(ctx, key, sessioncontent.ObjectWitness{}, work)
258 }
259
260 func (c *VariantCache) shareWithWitness(ctx context.Context, key variantKey, witness sessioncontent.ObjectWitness, work func(context.Context) (Variant, error)) (Variant, error) {
261 if err := ctx.Err(); err != nil {
262 return Variant{}, canceledError(err)
263 }
264 c.mu.Lock()
265 if hit, ok := c.entries[key]; ok {
266 c.touchLocked(hit)
267 result := cloneVariant(hit.result)
268 c.mu.Unlock()
269 return result, nil
270 }
271 shared, ok := c.inflight[key]
272 if ok && witness.Valid() && shared.witness.Valid() && !shared.witness.Same(witness) {
273 delete(c.inflight, key)
274 shared.cancel()
275 shared, ok = nil, false
276 }
277 if !ok {
278 workCtx, cancel := context.WithCancel(context.Background())
279 shared = &sharedTransform{cancel: cancel, done: make(chan struct{}), witness: witness}
280 c.inflight[key] = shared
281 go func() {
282 select {
283 case c.transforms <- struct{}{}:
284 case <-workCtx.Done():
285 c.finish(key, shared, Variant{}, witness, canceledError(workCtx.Err()))
286 return
287 }
288 if err := workCtx.Err(); err != nil {
289 <-c.transforms
290 c.finish(key, shared, Variant{}, witness, canceledError(err))
291 return
292 }
293 result, err := work(workCtx)
294 <-c.transforms
295 if err == nil {
296 if pubErr := workCtx.Err(); pubErr != nil {
297 err = canceledError(pubErr)
298 result = Variant{}
299 }
300 }
301 c.finish(key, shared, result, witness, err)
302 }()
303 }
304 shared.waiters++
305 c.mu.Unlock()
306 defer c.leave(key, shared)
307
308 select {
309 case <-ctx.Done():
310 return Variant{}, canceledError(ctx.Err())
311 case <-shared.done:
312 if err := ctx.Err(); err != nil {
313 return Variant{}, canceledError(err)
314 }
315 if shared.err != nil {
316 return Variant{}, shared.err
317 }
318 return cloneVariant(shared.result), nil
319 }
320 }
321
322 func (c *VariantCache) finish(key variantKey, shared *sharedTransform, result Variant, witness sessioncontent.ObjectWitness, err error) {
323 c.mu.Lock()
324 if err == nil && shared.waiters > 0 && c.inflight[key] == shared {
325 c.putLocked(key, result, witness)
326 }
327 shared.result = result
328 shared.err = err
329 close(shared.done)
330 if c.inflight[key] == shared {
331 delete(c.inflight, key)
332 }
333 c.mu.Unlock()
334 }
335
336 func (c *VariantCache) leave(key variantKey, shared *sharedTransform) {
337 c.mu.Lock()
338 defer c.mu.Unlock()
339 shared.waiters--
340 if shared.waiters <= 0 {
341 if c.inflight[key] == shared {
342 delete(c.inflight, key)
343 }
344 shared.cancel()
345 }
346 }
347
348 func (c *VariantCache) get(key variantKey) (Variant, bool) {
349 result, _, ok := c.getWithWitness(key)
350 return result, ok
351 }
352
353 func (c *VariantCache) getWithWitness(key variantKey) (Variant, sessioncontent.ObjectWitness, bool) {
354 c.mu.Lock()
355 defer c.mu.Unlock()
356 entry, ok := c.entries[key]
357 if !ok {
358 return Variant{}, sessioncontent.ObjectWitness{}, false
359 }
360 c.touchLocked(entry)
361 return cloneVariant(entry.result), entry.witness, true
362 }
363
364 func (c *VariantCache) putLocked(key variantKey, result Variant, witness sessioncontent.ObjectWitness) {
365 if _, ok := c.entries[key]; ok {
366 return
367 }
368 entry := &cacheEntry{key: key, size: int64(len(result.Bytes)), result: cloneVariant(result), witness: witness}
369 c.entries[key] = entry
370 c.order = append(c.order, entry)
371 c.used += entry.size
372 c.evictLocked()
373 }
374
375 func (c *VariantCache) remove(key variantKey) {
376 c.mu.Lock()
377 defer c.mu.Unlock()
378 entry, ok := c.entries[key]
379 if !ok {
380 return
381 }
382 delete(c.entries, key)
383 c.used -= entry.size
384 for i, item := range c.order {
385 if item == entry {
386 c.order = append(c.order[:i], c.order[i+1:]...)
387 break
388 }
389 }
390 }
391
392 func (c *VariantCache) touchLocked(entry *cacheEntry) {
393 for i, item := range c.order {
394 if item == entry {
395 c.order = append(append(c.order[:i], c.order[i+1:]...), entry)
396 return
397 }
398 }
399 }
400
401 func (c *VariantCache) evictLocked() {
402 for c.used > c.maxBytes && len(c.order) > 0 {
403 oldest := c.order[0]
404 c.order = c.order[1:]
405 delete(c.entries, oldest.key)
406 c.used -= oldest.size
407 }
408 }
409
410 func (c *VariantCache) Used() int64 {
411 c.mu.Lock()
412 defer c.mu.Unlock()
413 return c.used
414 }
415
416 func cloneVariant(in Variant) Variant {
417 out := in
418 if len(in.Bytes) > 0 {
419 out.Bytes = append([]byte(nil), in.Bytes...)
420 }
421 return out
422 }
423
424 func verifyVariant(in Variant, key variantKey) error {
425 if int64(len(in.Bytes)) == 0 || in.Digest == "" {
426 return fmt.Errorf("empty variant")
427 }
428 sum := sha256.Sum256(in.Bytes)
429 if hex.EncodeToString(sum[:]) != in.Digest {
430 return fmt.Errorf("variant digest mismatch")
431 }
432 if in.PolicyVersion != key.version || in.Width != key.width || in.Height != key.height {
433 return fmt.Errorf("variant identity mismatch")
434 }
435 return nil
436 }
437
437 lines GO