save shortlink in postgres, improve traces

This commit is contained in:
Sergey Chubaryan 2024-09-03 01:33:58 +03:00
parent e9ce51b2ca
commit ceac105645
8 changed files with 162 additions and 22 deletions

5
sql/02_shortlinks.sql Normal file
View File

@ -0,0 +1,5 @@
create table if not exists shortlinks (
id text primary key,
url text,
expiration date
);

View File

@ -117,8 +117,8 @@ func (a *App) Run(p RunParams) {
traceSdk.WithSampler(traceSdk.AlwaysSample()),
traceSdk.WithBatcher(
tracerExporter,
traceSdk.WithMaxQueueSize(4096),
traceSdk.WithMaxExportBatchSize(1024),
traceSdk.WithMaxQueueSize(8192),
traceSdk.WithMaxExportBatchSize(2048),
),
)
tracer = tracerProvider.Tracer("backend")
@ -137,6 +137,7 @@ func (a *App) Run(p RunParams) {
userRepo = repos.NewUserRepo(sqlDb, tracer)
emailRepo = repos.NewEmailRepo()
actionTokenRepo = repos.NewActionTokenRepo(sqlDb)
shortlinkRepo = repos.NewShortlinkRepo(sqlDb, tracer)
userCache = cache.NewCacheInmemSharded[models.UserDTO](cache.ShardingTypeInteger)
jwtCache = cache.NewCacheInmemSharded[string](cache.ShardingTypeJWT)
@ -176,8 +177,12 @@ func (a *App) Run(p RunParams) {
shortlinkService = services.NewShortlinkSevice(
services.NewShortlinkServiceParams{
Cache: linksCache,
Repo: shortlinkRepo,
},
)
// TODO: Run cleanup routine
// go shortlinkService.ShortlinkRoutine(ctx)
}
clientNotifier := client_notifier.NewBasicNotifier()

View File

@ -0,0 +1,95 @@
package repos
import (
"backend/src/integrations"
"context"
"database/sql"
"errors"
"time"
"go.opentelemetry.io/otel/trace"
)
type ShortlinkDTO struct {
Id string
Url string
Expiration time.Time
}
type ShortlinkRepo interface {
AddShortlink(ctx context.Context, dto ShortlinkDTO) error
GetShortlink(ctx context.Context, id string) (*ShortlinkDTO, error)
DeleteExpiredShortlinks(ctx context.Context, limit int) (int, error)
}
func NewShortlinkRepo(db integrations.SqlDB, tracer trace.Tracer) ShortlinkRepo {
return &shortlinkRepo{db, tracer}
}
type shortlinkRepo struct {
db integrations.SqlDB
tracer trace.Tracer
}
func (u *shortlinkRepo) AddShortlink(ctx context.Context, dto ShortlinkDTO) error {
_, span := u.tracer.Start(ctx, "postgres::AddShortlink")
defer span.End()
query := `insert into shortlinks (id, url, expiration) values ($1, $2, $3);`
_, err := u.db.ExecContext(ctx, query, dto.Id, dto.Url, dto.Expiration)
return err
}
func (u *shortlinkRepo) GetShortlink(ctx context.Context, id string) (*ShortlinkDTO, error) {
_, span := u.tracer.Start(ctx, "postgres::GetShortlink")
defer span.End()
query := `select url, expiration from shortlinks where id = $1;`
row := u.db.QueryRowContext(ctx, query, id)
if err := row.Err(); err != nil {
return nil, err
}
dto := &ShortlinkDTO{Id: id}
err := row.Scan(&dto.Url, &dto.Expiration)
if err == nil {
return dto, nil
}
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
func (u *shortlinkRepo) DeleteExpiredShortlinks(ctx context.Context, limit int) (int, error) {
_, span := u.tracer.Start(ctx, "postgres::CheckExpiredShortlinks")
defer span.End()
query := `
select count(*) from (
delete from shortlinks
where id in (
select id
from shortlinks
where current_date > expiration
limit $1
)
returning *
);`
row := u.db.QueryRowContext(ctx, query, limit)
if err := row.Err(); err != nil {
return 0, err
}
count := 0
err := row.Scan(&count)
if err == nil {
return count, nil
}
if errors.Is(err, sql.ErrNoRows) {
return 0, nil
}
return 0, err
}

View File

@ -34,7 +34,7 @@ type userRepo struct {
}
func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models.UserDTO, error) {
_, span := u.tracer.Start(ctx, "postgres")
_, span := u.tracer.Start(ctx, "postgres::CreateUser")
defer span.End()
query := `insert into users (email, secret, name) values ($1, $2, $3) returning id;`
@ -54,7 +54,7 @@ func (u *userRepo) CreateUser(ctx context.Context, dto models.UserDTO) (*models.
}
func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.UserUpdateDTO) error {
_, span := u.tracer.Start(ctx, "postgres")
_, span := u.tracer.Start(ctx, "postgres::UpdateUser")
defer span.End()
query := `update users set secret=$1, name=$2 where id = $3;`
@ -67,7 +67,7 @@ func (u *userRepo) UpdateUser(ctx context.Context, userId string, dto models.Use
}
func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO, error) {
_, span := u.tracer.Start(ctx, "postgres")
_, span := u.tracer.Start(ctx, "postgres::GetUserById")
defer span.End()
query := `select id, email, secret, name from users where id = $1;`
@ -86,7 +86,7 @@ func (u *userRepo) GetUserById(ctx context.Context, id string) (*models.UserDTO,
}
func (u *userRepo) GetUserByEmail(ctx context.Context, login string) (*models.UserDTO, error) {
_, span := u.tracer.Start(ctx, "postgres")
_, span := u.tracer.Start(ctx, "postgres::GetUserByEmail")
defer span.End()
query := `select id, email, secret, name from users where email = $1;`

View File

@ -3,46 +3,75 @@ package services
import (
"backend/src/cache"
"backend/src/charsets"
"backend/src/core/repos"
"context"
"fmt"
"math/rand"
"time"
)
type ShortlinkService interface {
CreateLink(in string) (string, error)
GetLink(id string) (string, error)
CreateShortlink(ctx context.Context, url string) (string, error)
GetShortlink(ctx context.Context, id string) (string, error)
ShortlinkRoutine(ctx context.Context) error
}
type NewShortlinkServiceParams struct {
Endpoint string
Cache cache.Cache[string, string]
Repo repos.ShortlinkRepo
}
func NewShortlinkSevice(params NewShortlinkServiceParams) ShortlinkService {
return &shortlinkService{
cache: params.Cache,
repo: params.Repo,
}
}
type shortlinkService struct {
cache cache.Cache[string, string]
repo repos.ShortlinkRepo
}
func (s *shortlinkService) CreateLink(in string) (string, error) {
func (s *shortlinkService) CreateShortlink(ctx context.Context, url string) (string, error) {
charset := charsets.GetCharset(charsets.CharsetTypeAll)
src := rand.NewSource(time.Now().UnixMicro())
randGen := rand.New(src)
str := charset.RandomString(randGen, 10)
id := charset.RandomString(randGen, 10)
s.cache.Set(str, in, cache.Expiration{Ttl: 7 * 24 * time.Hour})
return str, nil
expiration := time.Now().Add(7 * 24 * time.Hour)
dto := repos.ShortlinkDTO{
Id: id,
Url: url,
Expiration: expiration,
}
if err := s.repo.AddShortlink(ctx, dto); err != nil {
return "", err
}
func (s *shortlinkService) GetLink(id string) (string, error) {
val, ok := s.cache.Get(id)
if !ok {
s.cache.Set(id, url, cache.Expiration{ExpiresAt: expiration})
return id, nil
}
func (s *shortlinkService) GetShortlink(ctx context.Context, id string) (string, error) {
if link, ok := s.cache.Get(id); ok {
return link, nil
}
link, err := s.repo.GetShortlink(ctx, id)
if err != nil {
return "", err
}
if link == nil {
return "", fmt.Errorf("link does not exist or expired")
}
return val, nil
return link.Url, nil
}
func (s *shortlinkService) ShortlinkRoutine(ctx context.Context) error {
return nil
}

View File

@ -28,14 +28,14 @@ func NewShortlinkCreateHandler(shortlinkService services.ShortlinkService) gin.H
}
u.Scheme = "https"
linkId, err := shortlinkService.CreateLink(u.String())
linkId, err := shortlinkService.CreateShortlink(ctx, u.String())
if err != nil {
ctx.Data(500, "plain/text", []byte(err.Error()))
return
}
resultBody, err := json.Marshal(shortlinkCreateOutput{
Link: "https:/nucrea.ru/s/" + linkId,
Link: "https://nucrea.ru/s/" + linkId,
})
if err != nil {
ctx.AbortWithError(500, err)
@ -50,7 +50,7 @@ func NewShortlinkResolveHandler(shortlinkService services.ShortlinkService) gin.
return func(ctx *gin.Context) {
linkId := ctx.Param("linkId")
linkUrl, err := shortlinkService.GetLink(linkId)
linkUrl, err := shortlinkService.GetShortlink(ctx, linkId)
if err != nil {
ctx.AbortWithError(500, err)
return

View File

@ -19,6 +19,7 @@ func NewRequestLogMiddleware(logger log.Logger, tracer trace.Tracer, prometheus
if requestId == "" {
requestId = uuid.New().String()
}
c.Header("X-Request-Id", requestId)
log.SetCtxRequestId(c, requestId)

View File

@ -1,6 +1,8 @@
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
@ -18,9 +20,12 @@ func NewTracingMiddleware(tracer trace.Tracer) gin.HandlerFunc {
ctx := prop.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header))
ctx, span := tracer.Start(ctx, c.Request.URL.Path)
ctx, span := tracer.Start(ctx, fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path))
defer span.End()
traceId := span.SpanContext().TraceID()
c.Header("X-Trace-Id", traceId.String())
c.Request = c.Request.WithContext(ctx)
c.Next()