Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ BLUEPRINT_DB_USERNAME=
BLUEPRINT_DB_PASSWORD=
BLUEPRINT_DB_SCHEMA=public
BLUEPRINT_DB_SSLMODE=disable
REDIS_URL=
5 changes: 5 additions & 0 deletions backend/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,10 @@ func main() {
}

<-done
if app.Cache != nil {
if err := app.Cache.Close(); err != nil {
slog.Error("cache close error", "error", err)
}
}
slog.Info("graceful shutdown complete")
}
2 changes: 2 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/jackc/pgx/v5 v5.10.0
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.27.1
github.com/redis/go-redis/v9 v9.20.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
Expand Down Expand Up @@ -100,6 +101,7 @@ require (
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.28.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
Expand Down Expand Up @@ -184,6 +188,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
Expand Down Expand Up @@ -230,6 +236,8 @@ github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand All @@ -246,6 +254,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
Expand Down
28 changes: 23 additions & 5 deletions backend/internal/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
_ "github.com/jackc/pgx/v5/stdlib"
_ "github.com/joho/godotenv/autoload"

"backend/internal/infrastructure/cache/redis"
"backend/internal/infrastructure/database/postgres"
"backend/internal/usecase"
"backend/pkg/logger"
)

Expand All @@ -31,15 +33,17 @@ const (
// Constructed once by Run and passed to the HTTP server.
type App struct {
DB *sql.DB
Cache usecase.CacheService // nil when REDIS_URL is not set
Config Config
Log *slog.Logger
}

// Config holds all validated configuration values read from environment variables.
type Config struct {
Port int
Env string
DB postgres.DBConfig
Port int
Env string
DB postgres.DBConfig
RedisURL string
}

// ConfigError is returned when required configuration is absent or invalid.
Expand Down Expand Up @@ -83,10 +87,23 @@ func Run(ctx context.Context) (*App, error) {
return nil, err
}

var cache usecase.CacheService
if cfg.RedisURL != "" {
c, err := redis.New(cfg.RedisURL)
if err != nil {
return nil, fmt.Errorf("bootstrap: redis: %w", err)
}
if err := probeWithRetry(probeCtx, "redis", c, log); err != nil {
return nil, err
}
cache = c
}

log.Info("bootstrap: all checks passed — ready to serve")

return &App{
DB: db,
Cache: cache,
Config: cfg,
Log: log,
}, nil
Expand All @@ -109,8 +126,9 @@ func loadConfig() Config {
}

return Config{
Port: port,
Env: os.Getenv("ENV"),
Port: port,
Env: os.Getenv("ENV"),
RedisURL: os.Getenv("REDIS_URL"),
DB: postgres.DBConfig{
Host: os.Getenv("BLUEPRINT_DB_HOST"),
Port: os.Getenv("BLUEPRINT_DB_PORT"),
Expand Down
64 changes: 64 additions & 0 deletions backend/internal/infrastructure/cache/redis/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package redis

import (
"context"
"errors"
"time"

"github.com/redis/go-redis/v9"

"backend/internal/usecase"
)

type cacheService struct {
client *redis.Client
}

// New creates a CacheService backed by Redis at the given URL.
// The caller is responsible for calling Close when done.
func New(redisURL string) (usecase.CacheService, error) {
opts, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
return &cacheService{client: redis.NewClient(opts)}, nil
}

func (c *cacheService) PingContext(ctx context.Context) error {
return c.client.Ping(ctx).Err()
}

func (c *cacheService) Get(ctx context.Context, key string) (string, bool, error) {
val, err := c.client.Get(ctx, key).Result()
if errors.Is(err, redis.Nil) {
return "", false, nil
}
if err != nil {
return "", false, err
}
return val, true, nil
}

func (c *cacheService) Set(ctx context.Context, key string, value string, ttl time.Duration) error {
return c.client.Set(ctx, key, value, ttl).Err()
}

func (c *cacheService) Delete(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}

func (c *cacheService) Exists(ctx context.Context, key string) (bool, error) {
n, err := c.client.Exists(ctx, key).Result()
if err != nil {
return false, err
}
return n > 0, nil
}

func (c *cacheService) SetNX(ctx context.Context, key string, value string, ttl time.Duration) (bool, error) {
return c.client.SetNX(ctx, key, value, ttl).Result()
}

func (c *cacheService) Close() error {
return c.client.Close()
}
18 changes: 18 additions & 0 deletions backend/internal/usecase/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package usecase

import (
"context"
"time"
)

// CacheService is the cache port that use cases depend on.
// Infrastructure implementations live in internal/infrastructure/cache/.
type CacheService interface {
PingContext(ctx context.Context) error
Get(ctx context.Context, key string) (string, bool, error)
Set(ctx context.Context, key string, value string, ttl time.Duration) error
Delete(ctx context.Context, key string) error
Exists(ctx context.Context, key string) (bool, error)
SetNX(ctx context.Context, key string, value string, ttl time.Duration) (bool, error)
Close() error
}
Loading