Skip to content

Commit

Permalink
Merge branch 'main' into prod-business
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-brennan2005 committed Jun 17, 2024
2 parents 9f82213 + b731b2e commit bc8c1a1
Show file tree
Hide file tree
Showing 224 changed files with 38,270 additions and 6,908 deletions.
1 change: 0 additions & 1 deletion backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
tests/
docs/
*.md
*.templ
5 changes: 3 additions & 2 deletions backend/Dockerfile.server
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ WORKDIR /app

COPY go.mod go.sum ./

COPY . ./
COPY . .

RUN go install github.com/a-h/templ/cmd/templ@latest
RUN templ generate

RUN CGO_ENABLED=0 GOOS=linux go build -v -o ./sac

EXPOSE 8080

CMD ["/app/sac"]
CMD ["/app/sac"]
4 changes: 4 additions & 0 deletions backend/background/jobs/welcome_sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"time"

"github.com/GenerateNU/sac/backend/background"
"github.com/GenerateNU/sac/backend/database/cache"

"github.com/GenerateNU/sac/backend/constants"
"github.com/GenerateNU/sac/backend/entities/models"
"gorm.io/gorm"
Expand Down Expand Up @@ -84,6 +86,8 @@ func (j *Jobs) WelcomeSender(ctx context.Context) background.JobFunc {
}

func (j *Jobs) dequeueWelcomeTask(tx *gorm.DB) (*models.WelcomeTask, error) {
tx = cache.SetUseCache(tx, true)

var task models.WelcomeTask
if err := tx.Raw("SELECT email, name, attempts FROM welcome_tasks FOR UPDATE SKIP LOCKED LIMIT 1").Scan(&task).Error; err != nil {
tx.Rollback()
Expand Down
33 changes: 15 additions & 18 deletions backend/config/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package config

import (
"fmt"
"runtime"

"github.com/GenerateNU/sac/backend/constants"
"github.com/redis/go-redis/v9"

m "github.com/garrettladley/mattress"
)
Expand All @@ -15,24 +19,17 @@ type RedisSettings struct {
// TLSConfig *TLSConfig
}

func (r RedisSettings) Username() string {
return r.username
}

func (r RedisSettings) Password() *m.Secret[string] {
return r.password
}

func (r RedisSettings) Host() string {
return r.host
}

func (r RedisSettings) Port() uint {
return r.port
}

func (r RedisSettings) DB() int {
return r.db
func (r *RedisSettings) Into() *redis.Client {
return redis.NewClient(&redis.Options{
Username: r.username,
Password: r.password.Expose(),
Addr: fmt.Sprintf("%s:%d", r.host, r.port),
DB: r.db,
PoolSize: 10 * runtime.GOMAXPROCS(0),
MaxActiveConns: constants.REDIS_MAX_OPEN_CONNECTIONS,
MaxIdleConns: constants.REDIS_MAX_IDLE_CONNECTIONS,
ContextTimeoutEnabled: true,
})
}

type intermediateRedisSettings struct {
Expand Down
24 changes: 24 additions & 0 deletions backend/config/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package config

import m "github.com/garrettladley/mattress"

type SessionSettings struct {
Redis RedisSettings
PassPhrase *m.Secret[string]
}

type intermediateSessionSettings struct {
PassPhrase string `env:"PASS_PHRASE"`
}

func (i *intermediateSessionSettings) into(redis RedisSettings) (*SessionSettings, error) {
passPhrase, err := m.NewSecret(i.PassPhrase)
if err != nil {
return nil, err
}

return &SessionSettings{
Redis: redis,
PassPhrase: passPhrase,
}, nil
}
22 changes: 21 additions & 1 deletion backend/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package config
type Settings struct {
Application ApplicationSettings
Database DatabaseSettings
RedisSession RedisSettings
DBCache RedisSettings
Session SessionSettings
RedisLimiter RedisSettings
SuperUser SuperUserSettings
Calendar CalendarSettings
Expand All @@ -21,7 +22,9 @@ type Integrations struct {
type intermediateSettings struct {
Application ApplicationSettings `envPrefix:"SAC_APPLICATION_"`
Database intermediateDatabaseSettings `envPrefix:"SAC_DB_"`
DBCache intermediateRedisSettings `envPrefix:"SAC_REDIS_DB_CACHE_"`
RedisSession intermediateRedisSettings `envPrefix:"SAC_REDIS_SESSION_"`
Session intermediateSessionSettings `envPrefix:"SAC_SESSION_"`
RedisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"`
SuperUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"`
AWS intermediateAWSSettings `envPrefix:"SAC_AWS_"`
Expand All @@ -38,6 +41,21 @@ func (i *intermediateSettings) into() (*Settings, error) {
return nil, err
}

dbCache, err := i.DBCache.into()
if err != nil {
return nil, err
}

redisSession, err := i.RedisSession.into()
if err != nil {
return nil, err
}

session, err := i.Session.into(*redisSession)
if err != nil {
return nil, err
}

redisLimiter, err := i.RedisLimiter.into()
if err != nil {
return nil, err
Expand Down Expand Up @@ -75,7 +93,9 @@ func (i *intermediateSettings) into() (*Settings, error) {

return &Settings{
Application: i.Application,
DBCache: *dbCache,
Database: *database,
Session: *session,
RedisLimiter: *redisLimiter,
SuperUser: *superUser,
Calendar: *calendar,
Expand Down
1 change: 1 addition & 0 deletions backend/constants/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ const (
MAX_IDLE_CONNECTIONS int = 10
MAX_OPEN_CONNECTIONS int = 100
DB_TIMEOUT time.Duration = 200 * time.Millisecond
DB_CACHE_TTL time.Duration = 60 * time.Second
)
3 changes: 3 additions & 0 deletions backend/database/cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Cache Acknowledgement

## Forked code from @go-gorm's [caches package](https://github.com/go-gorm/caches/tree/master) to fit into our internal project structure
199 changes: 199 additions & 0 deletions backend/database/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package cache

import (
"sync"
"time"

"gorm.io/gorm"
)

type Config struct {
Easer bool
Cacher Cacher
TTL time.Duration
}

type Caches struct {
callbacks map[queryType]func(db *gorm.DB)
Conf *Config

queue *sync.Map
}

func (c *Caches) Name() string {
return "gorm:caches"
}

func (c *Caches) Initialize(db *gorm.DB) error {
if c.Conf == nil {
c.Conf = &Config{
Easer: false,
Cacher: nil,
}
}

if c.Conf.Easer {
c.queue = &sync.Map{}
}

callbacks := make(map[queryType]func(db *gorm.DB), 4)
callbacks[uponQuery] = db.Callback().Query().Get("gorm:query")
callbacks[uponCreate] = db.Callback().Create().Get("gorm:query")
callbacks[uponUpdate] = db.Callback().Update().Get("gorm:query")
callbacks[uponDelete] = db.Callback().Delete().Get("gorm:query")
c.callbacks = callbacks

if err := db.Callback().Query().Replace("gorm:query", c.query); err != nil {
return err
}

if err := db.Callback().Create().Replace("gorm:query", c.getMutatorCb(uponCreate)); err != nil {
return err
}

if err := db.Callback().Update().Replace("gorm:query", c.getMutatorCb(uponUpdate)); err != nil {
return err
}

if err := db.Callback().Delete().Replace("gorm:query", c.getMutatorCb(uponDelete)); err != nil {
return err
}

return nil
}

func (c *Caches) query(db *gorm.DB) {
useCache, ok := db.Statement.Context.Value(useCacheKey).(bool)
if !ok {
useCache = false
}

cacheTTL, ok := db.Statement.Context.Value(cacheTTLKey).(time.Duration)
if !ok {
cacheTTL = c.Conf.TTL
}

if !useCache || (!c.Conf.Easer && c.Conf.Cacher == nil) {
c.callbacks[uponQuery](db)
return
}

if !c.Conf.Easer && c.Conf.Cacher == nil {
c.callbacks[uponQuery](db)
return
}

identifier := buildIdentifier(db)

if c.checkCache(db, identifier) {
return
}

c.ease(db, identifier)
if db.Error != nil {
return
}

c.storeInCache(db, identifier, cacheTTL)
if db.Error != nil {
return
}
}

// getMutatorCb returns a decorator which calls the Cacher's Invalidate method
func (c *Caches) getMutatorCb(typ queryType) func(db *gorm.DB) {
return func(db *gorm.DB) {
if c.Conf.Cacher != nil {
if err := c.Conf.Cacher.Invalidate(db.Statement.Context); err != nil {
_ = db.AddError(err)
}
}
if cb := c.callbacks[typ]; cb != nil { // By default, gorm has no callbacks associated with mutating behaviors
cb(db)
}
}
}

func (c *Caches) ease(db *gorm.DB, identifier string) {
if !c.Conf.Easer {
c.callbacks[uponQuery](db)
return
}

res := ease(&queryTask{
id: identifier,
db: db,
queryCb: c.callbacks[uponQuery],
}, c.queue).(*queryTask)

if db.Error != nil {
return
}

if res.db.Statement.Dest == db.Statement.Dest {
return
}

detachedQuery := &Query[any]{
Dest: db.Statement.Dest,
RowsAffected: db.Statement.RowsAffected,
}

easedQuery := &Query[any]{
Dest: res.db.Statement.Dest,
RowsAffected: res.db.Statement.RowsAffected,
}
if err := easedQuery.copyTo(detachedQuery); err != nil {
_ = db.AddError(err)
}

detachedQuery.replaceOn(db)
}

func (c *Caches) checkCache(db *gorm.DB, identifier string) bool {
if c.Conf.Cacher != nil {
res, err := c.Conf.Cacher.Get(db.Statement.Context, identifier, &Query[any]{
Dest: db.Statement.Dest,
RowsAffected: db.Statement.RowsAffected,
})
if err != nil {
_ = db.AddError(err)
}

if res != nil {
res.replaceOn(db)
return true
}
}
return false
}

func (c *Caches) storeInCache(db *gorm.DB, identifier string, ttl time.Duration) {
if c.Conf.Cacher != nil {
err := c.Conf.Cacher.Store(db.Statement.Context, identifier, &Query[any]{
Dest: db.Statement.Dest,
RowsAffected: db.Statement.RowsAffected,
},
ttl,
)
if err != nil {
_ = db.AddError(err)
}
}
}

type key byte

const (
useCacheKey key = iota
cacheTTLKey
)

type queryType byte

const (
uponQuery queryType = iota
uponCreate
uponUpdate
uponDelete
)
18 changes: 18 additions & 0 deletions backend/database/cache/cahcer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cache

import (
"context"
"time"
)

type Cacher interface {
// Get impl should check if a specific key exists in the cache and return its value
// look at Query.Marshal
Get(ctx context.Context, key string, q *Query[any]) (*Query[any], error)
// Store impl should store a cached representation of the val param
// look at Query.Unmarshal
Store(ctx context.Context, key string, val *Query[any], ttl time.Duration) error
// Invalidate impl should invalidate all cached values
// It will be called when INSERT / UPDATE / DELETE queries are sent to the DB
Invalidate(ctx context.Context) error
}
Loading

0 comments on commit bc8c1a1

Please sign in to comment.