2024-01-10 06:12:10 +05:30
|
|
|
package mojang
|
|
|
|
|
|
|
|
import (
|
2024-02-07 06:06:18 +05:30
|
|
|
"context"
|
2024-01-10 06:12:10 +05:30
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/jellydator/ttlcache/v3"
|
2024-02-19 18:24:12 +05:30
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
"go.opentelemetry.io/otel/metric"
|
|
|
|
"go.uber.org/multierr"
|
2024-01-10 06:12:10 +05:30
|
|
|
)
|
|
|
|
|
2024-02-19 18:24:12 +05:30
|
|
|
type MojangApiTexturesProviderFunc func(ctx context.Context, uuid string, signed bool) (*ProfileResponse, error)
|
|
|
|
|
|
|
|
func NewMojangApiTexturesProvider(endpoint MojangApiTexturesProviderFunc) (*MojangApiTexturesProvider, error) {
|
|
|
|
metrics, err := newMojangApiTexturesProviderMetrics(otel.GetMeterProvider().Meter(ScopeName))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &MojangApiTexturesProvider{
|
|
|
|
MojangApiTexturesEndpoint: endpoint,
|
|
|
|
metrics: metrics,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2024-01-10 06:12:10 +05:30
|
|
|
type MojangApiTexturesProvider struct {
|
2024-02-19 18:24:12 +05:30
|
|
|
MojangApiTexturesEndpoint MojangApiTexturesProviderFunc
|
|
|
|
metrics *mojangApiTexturesProviderMetrics
|
2024-01-10 06:12:10 +05:30
|
|
|
}
|
|
|
|
|
2024-02-07 06:06:18 +05:30
|
|
|
func (p *MojangApiTexturesProvider) GetTextures(ctx context.Context, uuid string) (*ProfileResponse, error) {
|
2024-02-19 18:24:12 +05:30
|
|
|
p.metrics.Requests.Add(ctx, 1)
|
|
|
|
|
2024-02-13 06:38:42 +05:30
|
|
|
return p.MojangApiTexturesEndpoint(ctx, uuid, true)
|
2024-01-10 06:12:10 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
// Perfectly there should be an object with provider and cache implementation,
|
|
|
|
// but I decided not to introduce a layer and just implement cache in place.
|
|
|
|
type TexturesProviderWithInMemoryCache struct {
|
|
|
|
provider TexturesProvider
|
|
|
|
once sync.Once
|
2024-01-30 13:35:04 +05:30
|
|
|
cache *ttlcache.Cache[string, *ProfileResponse]
|
2024-02-19 18:24:12 +05:30
|
|
|
metrics *texturesProviderWithInMemoryCacheMetrics
|
2024-01-10 06:12:10 +05:30
|
|
|
}
|
|
|
|
|
2024-02-19 18:24:12 +05:30
|
|
|
func NewTexturesProviderWithInMemoryCache(provider TexturesProvider) (*TexturesProviderWithInMemoryCache, error) {
|
|
|
|
metrics, err := newTexturesProviderWithInMemoryCacheMetrics(otel.GetMeterProvider().Meter(ScopeName))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &TexturesProviderWithInMemoryCache{
|
2024-01-10 06:12:10 +05:30
|
|
|
provider: provider,
|
2024-01-30 13:35:04 +05:30
|
|
|
cache: ttlcache.New[string, *ProfileResponse](
|
|
|
|
ttlcache.WithDisableTouchOnHit[string, *ProfileResponse](),
|
2024-01-10 06:12:10 +05:30
|
|
|
// I'm aware of ttlcache.WithLoader(), but it doesn't allow to return an error
|
|
|
|
),
|
2024-02-19 18:24:12 +05:30
|
|
|
metrics: metrics,
|
|
|
|
}, nil
|
2024-01-10 06:12:10 +05:30
|
|
|
}
|
|
|
|
|
2024-02-07 06:06:18 +05:30
|
|
|
func (s *TexturesProviderWithInMemoryCache) GetTextures(ctx context.Context, uuid string) (*ProfileResponse, error) {
|
2024-01-10 06:12:10 +05:30
|
|
|
item := s.cache.Get(uuid)
|
|
|
|
// Don't check item.IsExpired() since Get function is already did this check
|
|
|
|
if item != nil {
|
2024-02-19 18:24:12 +05:30
|
|
|
s.metrics.Hits.Add(ctx, 1)
|
2024-01-10 06:12:10 +05:30
|
|
|
return item.Value(), nil
|
|
|
|
}
|
|
|
|
|
2024-02-19 18:24:12 +05:30
|
|
|
s.metrics.Misses.Add(ctx, 1)
|
|
|
|
|
2024-02-07 06:06:18 +05:30
|
|
|
result, err := s.provider.GetTextures(ctx, uuid)
|
2024-01-10 06:12:10 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
s.cache.Set(uuid, result, time.Minute)
|
|
|
|
// Call it only after first set so GC will work more often
|
|
|
|
s.startGcOnce()
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *TexturesProviderWithInMemoryCache) StopGC() {
|
|
|
|
// If you call the Stop() on a non-started GC, the process will hang trying to close the uninitialized channel
|
|
|
|
s.startGcOnce()
|
|
|
|
s.cache.Stop()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *TexturesProviderWithInMemoryCache) startGcOnce() {
|
|
|
|
s.once.Do(func() {
|
|
|
|
go s.cache.Start()
|
|
|
|
})
|
|
|
|
}
|
2024-02-19 18:24:12 +05:30
|
|
|
|
|
|
|
func newMojangApiTexturesProviderMetrics(meter metric.Meter) (*mojangApiTexturesProviderMetrics, error) {
|
|
|
|
m := &mojangApiTexturesProviderMetrics{}
|
|
|
|
var errors, err error
|
|
|
|
|
|
|
|
m.Requests, err = meter.Int64Counter(
|
2024-03-05 19:44:10 +05:30
|
|
|
"textures.request.sent",
|
|
|
|
metric.WithDescription("Number of textures requests sent to Mojang API"),
|
2024-02-19 18:24:12 +05:30
|
|
|
metric.WithUnit("1"),
|
|
|
|
)
|
|
|
|
errors = multierr.Append(errors, err)
|
|
|
|
|
|
|
|
return m, errors
|
|
|
|
}
|
|
|
|
|
|
|
|
type mojangApiTexturesProviderMetrics struct {
|
|
|
|
Requests metric.Int64Counter
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTexturesProviderWithInMemoryCacheMetrics(meter metric.Meter) (*texturesProviderWithInMemoryCacheMetrics, error) {
|
|
|
|
m := &texturesProviderWithInMemoryCacheMetrics{}
|
|
|
|
var errors, err error
|
|
|
|
|
|
|
|
m.Hits, err = meter.Int64Counter(
|
|
|
|
"textures.cache.hit",
|
2024-03-05 19:44:10 +05:30
|
|
|
metric.WithDescription("Number of Mojang textures found in the local cache"),
|
2024-02-19 18:24:12 +05:30
|
|
|
metric.WithUnit("1"),
|
|
|
|
)
|
|
|
|
errors = multierr.Append(errors, err)
|
|
|
|
|
|
|
|
m.Misses, err = meter.Int64Counter(
|
|
|
|
"textures.cache.miss",
|
2024-03-05 19:44:10 +05:30
|
|
|
metric.WithDescription("Number of Mojang textures missing from local cache"),
|
2024-02-19 18:24:12 +05:30
|
|
|
metric.WithUnit("1"),
|
|
|
|
)
|
|
|
|
errors = multierr.Append(errors, err)
|
|
|
|
|
|
|
|
return m, errors
|
|
|
|
}
|
|
|
|
|
|
|
|
type texturesProviderWithInMemoryCacheMetrics struct {
|
|
|
|
Hits metric.Int64Counter
|
|
|
|
Misses metric.Int64Counter
|
|
|
|
}
|