Files
go-jdenticon/internal/engine/singleflight.go
Kevin McIntyre f1544ef49c
Some checks failed
CI / Test (Go 1.24.x, ubuntu-latest) (push) Successful in 1m53s
CI / Code Quality (push) Failing after 26s
CI / Security Scan (push) Failing after 11s
CI / Test Coverage (push) Successful in 1m13s
CI / Benchmarks (push) Failing after 10m22s
CI / Build CLI (push) Failing after 8s
Benchmarks / Run Benchmarks (push) Failing after 10m13s
Release / Test (push) Successful in 55s
Release / Build (amd64, darwin, ) (push) Failing after 12s
Release / Build (amd64, linux, ) (push) Failing after 6s
Release / Build (amd64, windows, .exe) (push) Failing after 12s
Release / Build (arm64, darwin, ) (push) Failing after 12s
Release / Build (arm64, linux, ) (push) Failing after 12s
Release / Release (push) Has been skipped
CI / Test (Go 1.24.x, macos-latest) (push) Has been cancelled
CI / Test (Go 1.24.x, windows-latest) (push) Has been cancelled
chore: update module path to gitea.dockr.co/kev/go-jdenticon
Move hosting from GitHub to private Gitea instance.
2026-02-10 10:07:57 -05:00

104 lines
2.7 KiB
Go

package engine
import (
"context"
"fmt"
"gitea.dockr.co/kev/go-jdenticon/internal/util"
)
// Generate creates an identicon with the specified hash and size
// This method includes caching and singleflight to prevent duplicate work
func (g *Generator) Generate(ctx context.Context, hash string, size float64) (*Icon, error) {
// Basic validation
if hash == "" {
return nil, fmt.Errorf("jdenticon: engine: generation failed: hash cannot be empty")
}
if size <= 0 {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid size: %f", size)
}
// Check icon size limits
if g.maxIconSize > 0 && int(size) > g.maxIconSize {
return nil, fmt.Errorf("jdenticon: engine: generation failed: icon size %d exceeds maximum allowed size %d", int(size), g.maxIconSize)
}
// Validate hash format
if !util.IsValidHash(hash) {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid hash format: %s", hash)
}
// Check for context cancellation before proceeding
if err := ctx.Err(); err != nil {
return nil, err
}
// Generate cache key
key := g.cacheKey(hash, size)
// Check cache first (with read lock)
g.mu.RLock()
if cached, ok := g.cache.Get(key); ok {
g.mu.RUnlock()
g.metrics.recordHit()
return cached, nil
}
g.mu.RUnlock()
// Use singleflight to prevent multiple concurrent generations for the same key
result, err, _ := g.sf.Do(key, func() (interface{}, error) {
// Check cache again inside singleflight (another goroutine might have populated it)
g.mu.RLock()
if cached, ok := g.cache.Get(key); ok {
g.mu.RUnlock()
g.metrics.recordHit()
return cached, nil
}
g.mu.RUnlock()
// Generate the icon
icon, err := g.generateIcon(ctx, hash, size)
if err != nil {
return nil, err
}
// Store in cache (with write lock)
g.mu.Lock()
g.cache.Add(key, icon)
g.mu.Unlock()
g.metrics.recordMiss()
return icon, nil
})
if err != nil {
return nil, err
}
return result.(*Icon), nil
}
// GenerateWithoutCache creates an identicon without using cache
// This method is useful for testing or when caching is not desired
func (g *Generator) GenerateWithoutCache(ctx context.Context, hash string, size float64) (*Icon, error) {
// Basic validation
if hash == "" {
return nil, fmt.Errorf("jdenticon: engine: generation failed: hash cannot be empty")
}
if size <= 0 {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid size: %f", size)
}
// Validate hash format
if !util.IsValidHash(hash) {
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid hash format: %s", hash)
}
// Check for context cancellation
if err := ctx.Err(); err != nil {
return nil, err
}
return g.generateIcon(ctx, hash, size)
}