2022-01-09 05:27:18 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-11-26 05:04:40 +01:00
|
|
|
"context"
|
2022-01-09 05:27:18 +01:00
|
|
|
"fmt"
|
2022-04-07 03:18:46 +02:00
|
|
|
"log"
|
2022-04-08 05:59:40 +02:00
|
|
|
"os"
|
2022-04-09 08:47:13 +02:00
|
|
|
"time"
|
2022-01-09 05:27:18 +01:00
|
|
|
|
2023-09-17 21:53:55 +02:00
|
|
|
"github.com/ddworken/hishtory/backend/server/internal/database"
|
|
|
|
"github.com/ddworken/hishtory/backend/server/internal/release"
|
|
|
|
"github.com/ddworken/hishtory/backend/server/internal/server"
|
2024-08-11 21:19:41 +02:00
|
|
|
|
|
|
|
"github.com/DataDog/datadog-go/statsd"
|
2022-03-30 06:56:28 +02:00
|
|
|
_ "github.com/lib/pq"
|
|
|
|
"gorm.io/gorm"
|
2022-12-24 02:17:44 +01:00
|
|
|
"gorm.io/gorm/logger"
|
2022-03-30 06:56:28 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-09-12 15:26:20 +02:00
|
|
|
PostgresDb = "postgresql://postgres:%s@postgres:5432/hishtory?sslmode=disable"
|
|
|
|
StatsdSocket = "unix:///var/run/datadog/dsd.socket"
|
2022-01-09 06:59:28 +01:00
|
|
|
)
|
2022-01-09 05:27:18 +01:00
|
|
|
|
2024-08-11 21:19:41 +02:00
|
|
|
// Filled in via ldflags with the latest released version as of the server getting built
|
|
|
|
var ReleaseVersion string
|
2022-04-03 07:27:20 +02:00
|
|
|
|
2022-04-08 05:59:40 +02:00
|
|
|
func isTestEnvironment() bool {
|
2022-11-17 05:54:47 +01:00
|
|
|
return os.Getenv("HISHTORY_TEST") != ""
|
2022-04-08 05:59:40 +02:00
|
|
|
}
|
|
|
|
|
2022-11-26 05:04:40 +01:00
|
|
|
func isProductionEnvironment() bool {
|
|
|
|
return os.Getenv("HISHTORY_ENV") == "prod"
|
2022-11-17 05:58:19 +01:00
|
|
|
}
|
|
|
|
|
2023-11-19 17:48:46 +01:00
|
|
|
func getLoggerConfig() logger.Interface {
|
|
|
|
// The same as the default logger, except with a higher SlowThreshold
|
|
|
|
return logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{
|
|
|
|
SlowThreshold: 1000 * time.Millisecond,
|
|
|
|
LogLevel: logger.Info,
|
|
|
|
IgnoreRecordNotFoundError: false,
|
|
|
|
Colorful: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-06 17:21:29 +02:00
|
|
|
func OpenDB() (*database.DB, error) {
|
2022-04-08 05:59:40 +02:00
|
|
|
if isTestEnvironment() {
|
2023-11-19 17:48:46 +01:00
|
|
|
db, err := database.OpenSQLite("file::memory:?_journal_mode=WAL&cache=shared", &gorm.Config{Logger: getLoggerConfig()})
|
2022-04-06 08:31:24 +02:00
|
|
|
if err != nil {
|
2023-09-05 21:08:55 +02:00
|
|
|
return nil, fmt.Errorf("failed to connect to the DB: %w", err)
|
2022-04-06 08:31:24 +02:00
|
|
|
}
|
2023-09-06 17:21:29 +02:00
|
|
|
underlyingDb, err := db.DB.DB()
|
2023-02-14 05:50:33 +01:00
|
|
|
if err != nil {
|
2023-09-05 21:08:55 +02:00
|
|
|
return nil, fmt.Errorf("failed to access underlying DB: %w", err)
|
2023-02-14 05:50:33 +01:00
|
|
|
}
|
|
|
|
underlyingDb.SetMaxOpenConns(1)
|
2022-04-28 19:56:59 +02:00
|
|
|
db.Exec("PRAGMA journal_mode = WAL")
|
2023-09-12 17:30:43 +02:00
|
|
|
err = db.AddDatabaseTables()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create underlying DB tables: %w", err)
|
|
|
|
}
|
2022-04-06 08:31:24 +02:00
|
|
|
return db, nil
|
2022-03-30 06:56:28 +02:00
|
|
|
}
|
|
|
|
|
2022-11-14 17:35:05 +01:00
|
|
|
var sqliteDb string
|
|
|
|
if os.Getenv("HISHTORY_SQLITE_DB") != "" {
|
|
|
|
sqliteDb = os.Getenv("HISHTORY_SQLITE_DB")
|
|
|
|
}
|
|
|
|
|
2023-11-19 17:48:46 +01:00
|
|
|
config := gorm.Config{Logger: getLoggerConfig()}
|
2023-09-06 17:21:29 +02:00
|
|
|
|
2023-09-16 02:07:25 +02:00
|
|
|
fmt.Println("Connecting to DB")
|
2023-09-06 17:21:29 +02:00
|
|
|
var db *database.DB
|
2022-11-14 17:35:05 +01:00
|
|
|
if sqliteDb != "" {
|
2022-12-12 05:31:50 +01:00
|
|
|
var err error
|
2023-09-06 17:21:29 +02:00
|
|
|
db, err = database.OpenSQLite(sqliteDb, &config)
|
2022-12-12 05:31:50 +01:00
|
|
|
if err != nil {
|
2023-09-05 21:08:55 +02:00
|
|
|
return nil, fmt.Errorf("failed to connect to the DB: %w", err)
|
2022-12-12 05:31:50 +01:00
|
|
|
}
|
2022-11-14 17:35:05 +01:00
|
|
|
} else {
|
2023-09-06 17:21:29 +02:00
|
|
|
var err error
|
2022-11-14 17:35:05 +01:00
|
|
|
postgresDb := fmt.Sprintf(PostgresDb, os.Getenv("POSTGRESQL_PASSWORD"))
|
|
|
|
if os.Getenv("HISHTORY_POSTGRES_DB") != "" {
|
|
|
|
postgresDb = os.Getenv("HISHTORY_POSTGRES_DB")
|
|
|
|
}
|
2023-09-06 17:21:29 +02:00
|
|
|
|
|
|
|
db, err = database.OpenPostgres(postgresDb, &config)
|
2022-12-12 05:31:50 +01:00
|
|
|
if err != nil {
|
2023-09-05 21:08:55 +02:00
|
|
|
return nil, fmt.Errorf("failed to connect to the DB: %w", err)
|
2022-12-12 05:31:50 +01:00
|
|
|
}
|
2022-10-15 18:21:10 +02:00
|
|
|
}
|
2023-09-16 02:51:14 +02:00
|
|
|
if !isProductionEnvironment() {
|
2023-12-10 19:31:38 +01:00
|
|
|
fmt.Println("AutoMigrating DB tables")
|
2023-09-16 02:51:14 +02:00
|
|
|
err := db.AddDatabaseTables()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create underlying DB tables: %w", err)
|
|
|
|
}
|
2023-11-12 12:21:01 +01:00
|
|
|
err = db.CreateIndices()
|
|
|
|
if err != nil {
|
2024-09-07 02:08:08 +02:00
|
|
|
return nil, fmt.Errorf("failed to create indices: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if os.Getenv("HISHTORY_COMPOSE_TEST") != "" {
|
|
|
|
// Run an extra round of migrations to test the migration code path to prevent issues like #241
|
|
|
|
fmt.Println("AutoMigrating DB tables a second time for test coverage")
|
|
|
|
err := db.AddDatabaseTables()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create underlying DB tables: %w", err)
|
|
|
|
}
|
|
|
|
err = db.CreateIndices()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create indices: %w", err)
|
2023-11-12 12:21:01 +01:00
|
|
|
}
|
2023-09-23 04:11:43 +02:00
|
|
|
}
|
2022-12-12 05:31:50 +01:00
|
|
|
return db, nil
|
|
|
|
}
|
2022-11-14 17:35:05 +01:00
|
|
|
|
2024-08-11 21:19:41 +02:00
|
|
|
var (
|
|
|
|
LAST_USER_STATS_RUN = time.Unix(0, 0)
|
|
|
|
LAST_DEEP_CLEAN = time.Unix(0, 0)
|
|
|
|
)
|
2023-09-30 03:21:23 +02:00
|
|
|
|
2023-09-12 15:48:48 +02:00
|
|
|
func cron(ctx context.Context, db *database.DB, stats *statsd.Client) error {
|
2023-09-30 03:21:23 +02:00
|
|
|
// Determine the latest released version of hishtory to serve via the /api/v1/download
|
|
|
|
// endpoint for hishtory updates.
|
2023-09-12 16:09:38 +02:00
|
|
|
if err := release.UpdateReleaseVersion(); err != nil {
|
2023-09-12 15:48:48 +02:00
|
|
|
return fmt.Errorf("updateReleaseVersion: %w", err)
|
2022-04-17 01:28:53 +02:00
|
|
|
}
|
2023-09-12 15:48:48 +02:00
|
|
|
|
2023-09-30 03:21:23 +02:00
|
|
|
// Clean the DB to remove entries that have already been read
|
2023-09-12 15:48:48 +02:00
|
|
|
if err := db.Clean(ctx); err != nil {
|
|
|
|
return fmt.Errorf("db.Clean: %w", err)
|
2022-04-17 01:28:53 +02:00
|
|
|
}
|
2023-09-30 03:21:23 +02:00
|
|
|
|
|
|
|
// Flush out datadog statsd
|
2023-09-12 15:48:48 +02:00
|
|
|
if stats != nil {
|
|
|
|
if err := stats.Flush(); err != nil {
|
|
|
|
return fmt.Errorf("stats.Flush: %w", err)
|
2022-12-24 02:17:44 +01:00
|
|
|
}
|
|
|
|
}
|
2023-09-30 03:21:23 +02:00
|
|
|
|
2023-11-02 02:06:16 +01:00
|
|
|
// Run a deep clean less often to cover some more edge cases that hurt DB performance
|
2024-08-19 06:48:53 +02:00
|
|
|
if time.Since(LAST_DEEP_CLEAN) > 24*3*time.Hour {
|
2023-11-02 02:06:16 +01:00
|
|
|
LAST_DEEP_CLEAN = time.Now()
|
2024-08-19 06:48:53 +02:00
|
|
|
if isProductionEnvironment() {
|
|
|
|
if err := db.DeepClean(ctx); err != nil {
|
|
|
|
return fmt.Errorf("db.DeepClean: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !isProductionEnvironment() && !isTestEnvironment() {
|
|
|
|
if err := db.SelfHostedDeepClean(ctx); err != nil {
|
|
|
|
return fmt.Errorf("db.SelfHostedDeepClean: %w", err)
|
|
|
|
}
|
2023-11-02 02:06:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-30 03:21:23 +02:00
|
|
|
// Collect and store metrics on active users so we can track trends over time. This doesn't
|
2023-11-02 02:06:16 +01:00
|
|
|
// have to be run as often, so only run it periodically.
|
|
|
|
if time.Since(LAST_USER_STATS_RUN) > 12*time.Hour {
|
|
|
|
LAST_USER_STATS_RUN = time.Now()
|
2023-09-30 03:21:23 +02:00
|
|
|
if err := db.GenerateAndStoreActiveUserStats(ctx); err != nil {
|
|
|
|
return fmt.Errorf("db.GenerateAndStoreActiveUserStats: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-17 01:28:53 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-09-14 06:47:06 +02:00
|
|
|
func runBackgroundJobs(ctx context.Context, srv *server.Server, db *database.DB, stats *statsd.Client) {
|
2022-04-28 19:56:59 +02:00
|
|
|
time.Sleep(5 * time.Second)
|
2022-04-09 08:47:13 +02:00
|
|
|
for {
|
2023-09-14 06:47:06 +02:00
|
|
|
err := cron(ctx, db, stats)
|
2022-04-09 21:57:58 +02:00
|
|
|
if err != nil {
|
2023-11-19 15:36:33 +01:00
|
|
|
fmt.Printf("Cron failure: %v", err)
|
2022-04-16 20:37:43 +02:00
|
|
|
}
|
2023-09-12 16:09:38 +02:00
|
|
|
srv.UpdateReleaseVersion(release.Version, release.BuildUpdateInfo(release.Version))
|
2022-04-09 08:47:13 +02:00
|
|
|
time.Sleep(10 * time.Minute)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-12 16:09:38 +02:00
|
|
|
func InitDB() *database.DB {
|
2023-09-16 02:07:25 +02:00
|
|
|
fmt.Println("Opening DB")
|
2023-09-14 06:47:06 +02:00
|
|
|
db, err := OpenDB()
|
2022-01-09 05:27:18 +01:00
|
|
|
if err != nil {
|
2023-09-08 16:57:44 +02:00
|
|
|
panic(fmt.Errorf("OpenDB: %w", err))
|
2022-04-03 07:27:20 +02:00
|
|
|
}
|
2023-09-06 17:21:29 +02:00
|
|
|
|
2023-09-16 02:07:25 +02:00
|
|
|
fmt.Println("Pinging DB to confirm liveness")
|
2023-09-14 06:47:06 +02:00
|
|
|
if err := db.Ping(); err != nil {
|
2023-09-06 17:21:29 +02:00
|
|
|
panic(fmt.Errorf("ping: %w", err))
|
2022-04-03 07:27:20 +02:00
|
|
|
}
|
2022-11-27 03:33:54 +01:00
|
|
|
if isProductionEnvironment() {
|
2023-09-14 06:47:06 +02:00
|
|
|
if err := db.SetMaxIdleConns(10); err != nil {
|
2023-09-08 16:57:44 +02:00
|
|
|
panic(fmt.Errorf("failed to set max idle conns: %w", err))
|
|
|
|
}
|
2022-11-27 03:33:54 +01:00
|
|
|
}
|
2022-11-27 07:40:43 +01:00
|
|
|
if isTestEnvironment() {
|
2023-09-14 06:47:06 +02:00
|
|
|
if err := db.SetMaxIdleConns(1); err != nil {
|
2023-09-08 16:57:44 +02:00
|
|
|
panic(fmt.Errorf("failed to set max idle conns: %w", err))
|
|
|
|
}
|
2022-11-27 07:40:43 +01:00
|
|
|
}
|
2023-09-16 02:07:25 +02:00
|
|
|
fmt.Println("Done initializing DB")
|
2023-09-14 06:47:06 +02:00
|
|
|
return db
|
2022-04-22 07:02:28 +02:00
|
|
|
}
|
|
|
|
|
2023-09-12 15:26:20 +02:00
|
|
|
func main() {
|
2023-09-14 06:47:06 +02:00
|
|
|
// Startup check:
|
|
|
|
release.Version = ReleaseVersion
|
|
|
|
if release.Version == "UNKNOWN" && !isTestEnvironment() {
|
|
|
|
panic("server.go was built without a ReleaseVersion!")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create DB and stats
|
|
|
|
db := InitDB()
|
|
|
|
stats, err := statsd.New(StatsdSocket)
|
2022-12-18 06:27:00 +01:00
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Failed to start DataDog statsd: %v\n", err)
|
|
|
|
}
|
|
|
|
|
2023-09-12 15:48:48 +02:00
|
|
|
srv := server.NewServer(
|
2023-09-14 06:47:06 +02:00
|
|
|
db,
|
|
|
|
server.WithStatsd(stats),
|
2023-09-12 16:09:38 +02:00
|
|
|
server.WithReleaseVersion(release.Version),
|
2023-09-12 15:48:48 +02:00
|
|
|
server.IsTestEnvironment(isTestEnvironment()),
|
|
|
|
server.IsProductionEnvironment(isProductionEnvironment()),
|
|
|
|
server.WithCron(cron),
|
2023-09-12 16:09:38 +02:00
|
|
|
server.WithUpdateInfo(release.BuildUpdateInfo(release.Version)),
|
2023-09-24 23:17:15 +02:00
|
|
|
server.TrackUsageData(true),
|
2023-09-12 15:48:48 +02:00
|
|
|
)
|
2022-11-26 05:04:40 +01:00
|
|
|
|
2023-09-14 06:47:06 +02:00
|
|
|
go runBackgroundJobs(context.Background(), srv, db, stats)
|
2023-09-12 16:09:38 +02:00
|
|
|
|
2023-12-24 02:22:26 +01:00
|
|
|
port := os.Getenv("HISHTORY_SERVER_PORT")
|
|
|
|
if port == "" {
|
|
|
|
port = "8080"
|
|
|
|
}
|
|
|
|
if err := srv.Run(context.Background(), ":"+port); err != nil {
|
2023-09-12 15:26:20 +02:00
|
|
|
panic(err)
|
2022-05-02 04:37:26 +02:00
|
|
|
}
|
2022-01-09 05:27:18 +01:00
|
|
|
}
|