Initial release: Go Jdenticon library v0.1.0
- Core library with SVG and PNG generation - CLI tool with generate and batch commands - Cross-platform path handling for Windows compatibility - Comprehensive test suite with integration tests
This commit is contained in:
103
internal/engine/singleflight.go
Normal file
103
internal/engine/singleflight.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ungluedlabs/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)
|
||||
}
|
||||
Reference in New Issue
Block a user