diff --git a/sql/02_shortlinks.sql b/sql/02_shortlinks.sql new file mode 100644 index 0000000..92f0699 --- /dev/null +++ b/sql/02_shortlinks.sql @@ -0,0 +1,5 @@ +create table if not exists shortlinks ( + id text primary key, + url text, + expiration date +); \ No newline at end of file diff --git a/src/app.go b/src/app.go index 8f4fd1e..8831ec4 100644 --- a/src/app.go +++ b/src/app.go @@ -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() diff --git a/src/core/repos/shortlink_repo.go b/src/core/repos/shortlink_repo.go new file mode 100644 index 0000000..e8becbd --- /dev/null +++ b/src/core/repos/shortlink_repo.go @@ -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 +} diff --git a/src/core/repos/user_repo.go b/src/core/repos/user_repo.go index 72c436a..9406f71 100644 --- a/src/core/repos/user_repo.go +++ b/src/core/repos/user_repo.go @@ -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;` diff --git a/src/core/services/shortlink_service.go b/src/core/services/shortlink_service.go index ad487be..d5fb73e 100644 --- a/src/core/services/shortlink_service.go +++ b/src/core/services/shortlink_service.go @@ -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] + 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 + } + + s.cache.Set(id, url, cache.Expiration{ExpiresAt: expiration}) + + return id, nil } -func (s *shortlinkService) GetLink(id string) (string, error) { - val, ok := s.cache.Get(id) - if !ok { +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 } diff --git a/src/server/handlers/shortlink_handlers.go b/src/server/handlers/shortlink_handlers.go index 44e200e..1f417ac 100644 --- a/src/server/handlers/shortlink_handlers.go +++ b/src/server/handlers/shortlink_handlers.go @@ -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 diff --git a/src/server/middleware/request_log.go b/src/server/middleware/request_log.go index eb1b78f..2200328 100644 --- a/src/server/middleware/request_log.go +++ b/src/server/middleware/request_log.go @@ -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) diff --git a/src/server/middleware/tracing.go b/src/server/middleware/tracing.go index 9fac2c3..a739c3c 100644 --- a/src/server/middleware/tracing.go +++ b/src/server/middleware/tracing.go @@ -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()