chrly/internal/mojang/provider.go

174 lines
4.2 KiB
Go

package mojang
import (
"context"
"errors"
"regexp"
"strings"
"github.com/brunomvsouza/singleflight"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
)
const ScopeName = "ely.by/chrly/internal/mojang"
var InvalidUsername = errors.New("the username passed doesn't meet Mojang's requirements")
// https://help.minecraft.net/hc/en-us/articles/4408950195341#h_01GE5JX1Z0CZ833A7S54Y195KV
var allowedUsernamesRegex = regexp.MustCompile(`(?i)^[0-9a-z_]{3,16}$`)
type UuidsProvider interface {
GetUuid(ctx context.Context, username string) (*ProfileInfo, error)
}
type TexturesProvider interface {
GetTextures(ctx context.Context, uuid string) (*ProfileResponse, error)
}
func NewMojangTexturesProvider(
uuidsProvider UuidsProvider,
texturesProvider TexturesProvider,
) (*MojangTexturesProvider, error) {
meter, err := newProviderMetrics(otel.GetMeterProvider().Meter(ScopeName))
if err != nil {
return nil, err
}
return &MojangTexturesProvider{
UuidsProvider: uuidsProvider,
TexturesProvider: texturesProvider,
metrics: meter,
}, nil
}
type MojangTexturesProvider struct {
UuidsProvider
TexturesProvider
metrics *providerMetrics
group singleflight.Group[string, *ProfileResponse]
}
func (p *MojangTexturesProvider) GetForUsername(ctx context.Context, username string) (*ProfileResponse, error) {
if !allowedUsernamesRegex.MatchString(username) {
return nil, InvalidUsername
}
username = strings.ToLower(username)
result, err, shared := p.group.Do(username, func() (*ProfileResponse, error) {
var profile *ProfileInfo
var textures *ProfileResponse
var err error
defer p.recordMetrics(ctx, profile, textures, err)
profile, err = p.UuidsProvider.GetUuid(ctx, username)
if err != nil {
return nil, err
}
if profile == nil {
return nil, nil
}
textures, err = p.TexturesProvider.GetTextures(ctx, profile.Id)
return textures, err
})
if shared {
p.metrics.Shared.Add(ctx, 1)
}
return result, err
}
func (p *MojangTexturesProvider) recordMetrics(ctx context.Context, profile *ProfileInfo, textures *ProfileResponse, err error) {
if err != nil {
p.metrics.Failed.Add(ctx, 1)
return
}
if profile == nil {
p.metrics.UsernameMissed.Add(ctx, 1)
p.metrics.TextureMissed.Add(ctx, 1)
return
}
p.metrics.UsernameFound.Add(ctx, 1)
if textures != nil {
p.metrics.TextureFound.Add(ctx, 1)
} else {
p.metrics.TextureMissed.Add(ctx, 1)
}
}
type NilProvider struct {
}
func (*NilProvider) GetForUsername(ctx context.Context, username string) (*ProfileResponse, error) {
return nil, nil
}
func newProviderMetrics(meter metric.Meter) (*providerMetrics, error) {
m := &providerMetrics{}
var errors, err error
m.UsernameFound, err = meter.Int64Counter(
"provider.username_found",
metric.WithDescription("Number of queries for which username was found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.UsernameMissed, err = meter.Int64Counter(
"provider.username_missed",
metric.WithDescription("Number of queries for which username was not found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.TextureFound, err = meter.Int64Counter(
"provider.textures_found",
metric.WithDescription("Number of queries for which textures were successfully found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.TextureMissed, err = meter.Int64Counter(
"provider.textures_missed",
metric.WithDescription("Number of queries for which no textures were found"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.Failed, err = meter.Int64Counter(
"provider.failed",
metric.WithDescription("Number of requests that ended in an error"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
m.Shared, err = meter.Int64Counter(
"provider.singleflight.shared",
metric.WithDescription("Number of requests that are already being processed in another thread"),
metric.WithUnit("1"),
)
errors = multierr.Append(errors, err)
return m, errors
}
type providerMetrics struct {
UsernameFound metric.Int64Counter
UsernameMissed metric.Int64Counter
TextureFound metric.Int64Counter
TextureMissed metric.Int64Counter
Failed metric.Int64Counter
Shared metric.Int64Counter
}