main.go raw

   1  // orly-db-badger is a standalone gRPC database server using the Badger backend.
   2  package main
   3  
   4  import (
   5  	"context"
   6  	"os"
   7  	"path/filepath"
   8  	"time"
   9  
  10  	"go-simpler.org/env"
  11  	"next.orly.dev/pkg/lol"
  12  	"next.orly.dev/pkg/lol/chk"
  13  	"next.orly.dev/pkg/lol/log"
  14  
  15  	"next.orly.dev/pkg/database"
  16  	"next.orly.dev/pkg/database/server"
  17  )
  18  
  19  // Config holds the database server configuration.
  20  type Config struct {
  21  	// Listen is the gRPC server listen address
  22  	Listen string `env:"ORLY_DB_LISTEN" default:"127.0.0.1:50051" usage:"gRPC server listen address"`
  23  
  24  	// DataDir is the database data directory
  25  	DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory"`
  26  
  27  	// LogLevel is the logging level
  28  	LogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"`
  29  
  30  	// Badger configuration
  31  	BlockCacheMB int `env:"ORLY_DB_BLOCK_CACHE_MB" default:"1024" usage:"block cache size in MB"`
  32  	IndexCacheMB int `env:"ORLY_DB_INDEX_CACHE_MB" default:"512" usage:"index cache size in MB"`
  33  	ZSTDLevel    int `env:"ORLY_DB_ZSTD_LEVEL" default:"3" usage:"ZSTD compression level (1-19)"`
  34  
  35  	// Query cache configuration
  36  	QueryCacheSizeMB   int           `env:"ORLY_DB_QUERY_CACHE_SIZE_MB" default:"256" usage:"query cache size in MB"`
  37  	QueryCacheMaxAge   time.Duration `env:"ORLY_DB_QUERY_CACHE_MAX_AGE" default:"5m" usage:"query cache max age"`
  38  	QueryCacheDisabled bool          `env:"ORLY_DB_QUERY_CACHE_DISABLED" default:"false" usage:"disable query cache"`
  39  
  40  	// Serial cache configuration
  41  	SerialCachePubkeys  int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"serial cache pubkeys capacity"`
  42  	SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"serial cache event IDs capacity"`
  43  
  44  	// gRPC server configuration
  45  	StreamBatchSize int `env:"ORLY_DB_STREAM_BATCH_SIZE" default:"100" usage:"events per stream batch"`
  46  }
  47  
  48  func main() {
  49  	cfg := loadConfig()
  50  
  51  	// Set log level
  52  	lol.SetLogLevel(cfg.LogLevel)
  53  	log.I.F("orly-db-badger starting with log level: %s", cfg.LogLevel)
  54  
  55  	ctx, cancel := context.WithCancel(context.Background())
  56  	defer cancel()
  57  
  58  	// Create database configuration
  59  	dbCfg := &database.DatabaseConfig{
  60  		DataDir:             cfg.DataDir,
  61  		LogLevel:            cfg.LogLevel,
  62  		BlockCacheMB:        cfg.BlockCacheMB,
  63  		IndexCacheMB:        cfg.IndexCacheMB,
  64  		QueryCacheSizeMB:    cfg.QueryCacheSizeMB,
  65  		QueryCacheMaxAge:    cfg.QueryCacheMaxAge,
  66  		QueryCacheDisabled:  cfg.QueryCacheDisabled,
  67  		SerialCachePubkeys:  cfg.SerialCachePubkeys,
  68  		SerialCacheEventIds: cfg.SerialCacheEventIds,
  69  		ZSTDLevel:           cfg.ZSTDLevel,
  70  	}
  71  
  72  	// Initialize Badger database
  73  	log.I.F("initializing Badger database at %s", cfg.DataDir)
  74  	db, err := database.NewWithConfig(ctx, cancel, dbCfg)
  75  	if chk.E(err) {
  76  		log.E.F("failed to initialize database: %v", err)
  77  		os.Exit(1)
  78  	}
  79  
  80  	// Wait for database to be ready
  81  	log.I.F("waiting for database to be ready...")
  82  	<-db.Ready()
  83  	log.I.F("database ready")
  84  
  85  	// Reconcile any orphaned blob files (files on disk without metadata)
  86  	if reconciled, err := db.ReconcileBlobMetadata(); err != nil {
  87  		log.W.F("blob metadata reconciliation failed: %v", err)
  88  	} else if reconciled > 0 {
  89  		log.I.F("reconciled %d blob metadata entries", reconciled)
  90  	}
  91  
  92  	// Create and start gRPC server
  93  	serverCfg := &server.Config{
  94  		Listen:          cfg.Listen,
  95  		LogLevel:        cfg.LogLevel,
  96  		StreamBatchSize: cfg.StreamBatchSize,
  97  	}
  98  
  99  	srv := server.New(db, serverCfg)
 100  	if err := srv.ListenAndServe(ctx, cancel); err != nil {
 101  		log.E.F("gRPC server error: %v", err)
 102  	}
 103  }
 104  
 105  func loadConfig() *Config {
 106  	cfg := &Config{}
 107  	if err := env.Load(cfg, nil); chk.E(err) {
 108  		log.E.F("failed to load config: %v", err)
 109  		os.Exit(1)
 110  	}
 111  
 112  	// Set default data directory if not specified
 113  	if cfg.DataDir == "" {
 114  		home, err := os.UserHomeDir()
 115  		if chk.E(err) {
 116  			log.E.F("failed to get home directory: %v", err)
 117  			os.Exit(1)
 118  		}
 119  		cfg.DataDir = filepath.Join(home, ".local", "share", "ORLY")
 120  	}
 121  
 122  	// Ensure data directory exists
 123  	if err := os.MkdirAll(cfg.DataDir, 0700); chk.E(err) {
 124  		log.E.F("failed to create data directory %s: %v", cfg.DataDir, err)
 125  		os.Exit(1)
 126  	}
 127  
 128  	return cfg
 129  }
 130