This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

replica / cmd / main.go
4.6 kB 169 lines
1package main 2 3import ( 4 "context" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "os" 9 "os/signal" 10 "syscall" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/earthboundkid/versioninfo/v2" 15 "github.com/prometheus/client_golang/prometheus/promhttp" 16 "github.com/urfave/cli/v3" 17 "golang.org/x/sync/errgroup" 18 "tangled.org/pds.dad/replica/internal" 19) 20 21func main() { 22 if err := run(os.Args); err != nil { 23 slog.Error("exiting process", "error", err) 24 os.Exit(-1) 25 } 26} 27 28func run(args []string) error { 29 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 30 defer cancel() 31 32 app := &cli.Command{ 33 Name: "replica", 34 Usage: "PDS repo replica tool", 35 Version: versioninfo.Short(), 36 37 Commands: []*cli.Command{ 38 { 39 Name: "run", 40 Usage: "start the replica service", 41 Flags: []cli.Flag{ 42 &cli.StringFlag{ 43 Name: "pds", 44 Usage: "PDS URL to replicate", 45 Sources: cli.EnvVars("REPLICA_PDS"), 46 }, 47 &cli.StringFlag{ 48 Name: "db-url", 49 Usage: "sqlite database location", 50 Value: "./pds-replica.db", 51 Sources: cli.EnvVars("REPLICA_DATABASE_URL"), 52 }, 53 &cli.StringFlag{ 54 Name: "data-dir", 55 Usage: "directory to store repo CAR backups", 56 Value: "./data", 57 Sources: cli.EnvVars("REPLICA_DATA_DIR"), 58 }, 59 &cli.DurationFlag{ 60 Name: "scrub-interval", 61 Usage: "how often to verify on-disk CARs against the DB state", 62 Value: 24 * time.Hour, 63 Sources: cli.EnvVars("REPLICA_SCRUB_INTERVAL"), 64 }, 65&cli.DurationFlag{ 66 Name: "deleted-retention", 67 Usage: "how long to keep backups of deleted accounts before purging", 68 Value: 720 * time.Hour, 69 Sources: cli.EnvVars("REPLICA_DELETED_RETENTION"), 70 }, 71 &cli.StringFlag{ 72 Name: "metrics-listen", 73 Usage: "address for the Prometheus /metrics and pprof HTTP server (disabled if empty)", 74 Sources: cli.EnvVars("REPLICA_METRICS_LISTEN"), 75 }, 76 }, 77 Action: runReplica, 78 }, 79 }, 80 } 81 return app.Run(ctx, args) 82} 83 84func runReplica(ctx context.Context, cmd *cli.Command) error { 85 internal.SetupLogger() 86 pdsURL := cmd.String("pds") 87 if pdsURL == "" { 88 return cli.Exit("pds flag is required", 1) 89 } 90 dbURL := cmd.String("db-url") 91 dataDir := cmd.String("data-dir") 92 93 slog.Info("starting replica", "pds", pdsURL, "db", dbURL, "dataDir", dataDir) 94 95 db, err := internal.SetupDatabase(dbURL) 96 if err != nil { 97 return fmt.Errorf("failed to setup database: %w", err) 98 } 99 100 store, err := internal.NewCarStore(dataDir) 101 if err != nil { 102 return fmt.Errorf("failed to setup CAR store: %w", err) 103 } 104 105 // replay any root-patch journals left by a crash before anything reads 106 // or appends to the CARs 107 if err := store.RecoverAllJournals(); err != nil { 108 return fmt.Errorf("failed to recover root journals: %w", err) 109 } 110 111 logger := slog.Default() 112 idDir := identity.DefaultDirectory() 113 repoLocks := internal.NewKeyedMutex() 114 115 crawler := internal.NewCrawler(logger, db, pdsURL) 116 resyncer := internal.NewResyncer(logger, db, store, idDir, repoLocks, pdsURL, 4) 117 firehose := internal.NewFirehose(logger, db, store, idDir, repoLocks, pdsURL) 118 scrubber := internal.NewScrubber(logger, db, store, repoLocks, cmd.Duration("scrub-interval"), cmd.Duration("deleted-retention")) 119 120 g, ctx := errgroup.WithContext(ctx) 121 g.Go(func() error { 122 crawler.Run(ctx) 123 return nil 124 }) 125 g.Go(func() error { 126 resyncer.Run(ctx) 127 return nil 128 }) 129 g.Go(func() error { 130 return firehose.Run(ctx) 131 }) 132 g.Go(func() error { 133 scrubber.Run(ctx) 134 return nil 135 }) 136 // always run the gauges poller so per-state counts and disk bytes stay 137 // live for anyone scraping /metrics later 138 g.Go(func() error { 139 internal.RunMetricsCollector(ctx, db, dataDir) 140 return nil 141 }) 142 if metricsAddr := cmd.String("metrics-listen"); metricsAddr != "" { 143 mux := http.NewServeMux() 144 mux.Handle("/metrics", promhttp.Handler()) 145 mux.HandleFunc("/_health", func(w http.ResponseWriter, _ *http.Request) { 146 w.WriteHeader(http.StatusOK) 147 }) 148 g.Go(func() error { 149 logger.Info("starting metrics server", "addr", metricsAddr) 150 srv := &http.Server{ 151 Addr: metricsAddr, 152 Handler: mux, 153 ReadHeaderTimeout: 10 * time.Second, 154 } 155 go func() { 156 <-ctx.Done() 157 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 158 defer cancel() 159 _ = srv.Shutdown(shutdownCtx) 160 }() 161 if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 162 return fmt.Errorf("metrics server failed: %w", err) 163 } 164 return nil 165 }) 166 } 167 168 return g.Wait() 169}