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
7 changes: 6 additions & 1 deletion api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ DEFAULT_QUESTION_POINTS=1
# MAXIMUM TIME TO SHOW SCOREBOARD AFTER QUESTION COMPLETE
SCOREBOARD_MAX_DURATION=10

# Auto-terminate active quiz sessions running longer than this (hours, from start). Default 24.
ACTIVE_QUIZ_TTL_HOURS=24
# How often the background sweeper checks for expired sessions (minutes). Default 60.
ACTIVE_QUIZ_SWEEP_MINUTES=60

MIGRATION_DIR=database/migrations
# SQLITE_FILEPATH=database/jovvix.db

Expand Down Expand Up @@ -72,4 +77,4 @@ MAX_QUIZ_FILE_SIZE=3145728
BODY_LIMIT_MB=15 #MB

# Comma-separated emails allowed to create public quizzes (visible on the homepage).
PUBLIC_QUIZ_ADMIN_EMAILS=
PUBLIC_QUIZ_ADMIN_EMAILS=
24 changes: 24 additions & 0 deletions api/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"os"
"os/signal"
"syscall"
"time"

"go.uber.org/zap"

"github.com/Improwised/jovvix/api/config"
"github.com/Improwised/jovvix/api/database"
"github.com/Improwised/jovvix/api/models"
pMetrics "github.com/Improwised/jovvix/api/pkg/prometheus"
"github.com/Improwised/jovvix/api/routes"
fiber "github.com/gofiber/fiber/v2"
Expand Down Expand Up @@ -51,6 +53,28 @@ func GetAPICommandDef(cfg config.AppConfig, logger *zap.Logger) cobra.Command {
return err
}

// Background sweeper: every ACTIVE_QUIZ_SWEEP_MINUTES, auto-terminate any quiz
activeQuizModel := models.InitActiveQuizModel(db, logger)

ttl := time.Duration(cfg.Quiz.ActiveQuizTTLHours) * time.Hour //
if ttl <= 0 {
ttl = 24 * time.Hour
}
sweepInterval := time.Duration(cfg.Quiz.ActiveQuizSweepMinutes) * time.Minute
if sweepInterval <= 0 {
sweepInterval = 60 * time.Minute
}
go func() {
for {
if count, err := activeQuizModel.DeactivateExpired(ttl); err != nil {
logger.Error("active quiz sweeper failed", zap.Error(err))
} else if count > 0 {
logger.Info("active quiz sweeper: deactivated expired sessions", zap.Int64("count", count))
}
time.Sleep(sweepInterval)
}
}()

interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
Expand Down
12 changes: 7 additions & 5 deletions api/config/quiz.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package config
import "strings"

type QuizConfig struct {
QuestionTimeLimit string `envconfig:"QUESTION_TIME_LIMIT"`
DefaultQuestionPoints int16 `envconfig:"DEFAULT_QUESTION_POINTS"`
ScoreboardMaxDuration string `envconfig:"SCOREBOARD_MAX_DURATION"`
FileSize int64 `envconfig:"MAX_QUIZ_FILE_SIZE"`
PublicQuizAdminEmails []string `envconfig:"PUBLIC_QUIZ_ADMIN_EMAILS"`
QuestionTimeLimit string `envconfig:"QUESTION_TIME_LIMIT"`
DefaultQuestionPoints int16 `envconfig:"DEFAULT_QUESTION_POINTS"`
ScoreboardMaxDuration string `envconfig:"SCOREBOARD_MAX_DURATION"`
FileSize int64 `envconfig:"MAX_QUIZ_FILE_SIZE"`
PublicQuizAdminEmails []string `envconfig:"PUBLIC_QUIZ_ADMIN_EMAILS"`
ActiveQuizTTLHours int `envconfig:"ACTIVE_QUIZ_TTL_HOURS"`
ActiveQuizSweepMinutes int `envconfig:"ACTIVE_QUIZ_SWEEP_MINUTES"`
}

// IsPublicQuizAdmin reports whether the given email is allowed to publish public quizzes.
Expand Down
22 changes: 22 additions & 0 deletions api/models/active_quizzes.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,28 @@ func (model *ActiveQuizModel) Deactivate(id uuid.UUID) error {

}

func (model *ActiveQuizModel) DeactivateExpired(ttl time.Duration) (int64, error) {
ttlSeconds := int64(ttl.Seconds())

result, err := model.db.Update(ActiveQuizzesTable).Set(goqu.Record{
"invitation_code": nil,
"is_active": false,
"activated_to": goqu.L("now()"),
"current_question": nil,
"is_question_active": nil,
"updated_at": goqu.L("now()"),
}).Where(
goqu.I("is_active").Eq(true),
goqu.L("activated_from < now() - ? * interval '1 second'", ttlSeconds),
).Executor().Exec()

if err != nil {
return 0, err
}

return result.RowsAffected()
}

func (model *ActiveQuizModel) GetCurrentActiveQuestion(id uuid.UUID) (uuid.UUID, error) {
var currentQuestion uuid.UUID
found, err := model.db.Select("current_question").From(ActiveQuizzesTable).Where(goqu.I("id").Eq(id), goqu.I("is_question_active").Eq(true)).ScanVal(&currentQuestion)
Expand Down
Loading