main.go raw
1 // orly-acl is a standalone gRPC ACL server for the ORLY relay.
2 // It wraps the ACL implementations and exposes them via gRPC.
3 package main
4
5 import (
6 "context"
7 "net"
8 "os"
9 "os/signal"
10 "syscall"
11 "time"
12
13 "google.golang.org/grpc"
14 "google.golang.org/grpc/reflection"
15 "next.orly.dev/pkg/lol"
16 "next.orly.dev/pkg/lol/chk"
17 "next.orly.dev/pkg/lol/log"
18
19 "next.orly.dev/app/config"
20 "next.orly.dev/pkg/acl"
21 "next.orly.dev/pkg/database"
22 databasegrpc "next.orly.dev/pkg/database/grpc"
23 orlyaclv1 "next.orly.dev/pkg/proto/orlyacl/v1"
24 )
25
26 func main() {
27 cfg := loadConfig()
28
29 // Set log level
30 lol.SetLogLevel(cfg.LogLevel)
31 log.I.F("orly-acl starting with log level: %s, mode: %s", cfg.LogLevel, cfg.ACLMode)
32
33 ctx, cancel := context.WithCancel(context.Background())
34 defer cancel()
35
36 // Initialize database (direct Badger or gRPC client)
37 var db database.Database
38 var err error
39
40 if cfg.DBType == "grpc" {
41 // Use gRPC database client
42 log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer)
43 dbClient, err := databasegrpc.New(ctx, &databasegrpc.ClientConfig{
44 ServerAddress: cfg.GRPCDBServer,
45 ConnectTimeout: 30 * time.Second,
46 })
47 if chk.E(err) {
48 log.E.F("failed to connect to gRPC database: %v", err)
49 os.Exit(1)
50 }
51 db = dbClient
52 } else {
53 // Use direct Badger database
54 dbCfg := &database.DatabaseConfig{
55 DataDir: cfg.DataDir,
56 LogLevel: cfg.LogLevel,
57 BlockCacheMB: cfg.BlockCacheMB,
58 IndexCacheMB: cfg.IndexCacheMB,
59 QueryCacheSizeMB: cfg.QueryCacheSizeMB,
60 QueryCacheMaxAge: cfg.QueryCacheMaxAge,
61 QueryCacheDisabled: cfg.QueryCacheDisabled,
62 SerialCachePubkeys: cfg.SerialCachePubkeys,
63 SerialCacheEventIds: cfg.SerialCacheEventIds,
64 ZSTDLevel: cfg.ZSTDLevel,
65 }
66
67 log.I.F("initializing Badger database at %s", cfg.DataDir)
68 db, err = database.NewWithConfig(ctx, cancel, dbCfg)
69 if chk.E(err) {
70 log.E.F("failed to initialize database: %v", err)
71 os.Exit(1)
72 }
73 }
74
75 // Wait for database to be ready
76 log.I.F("waiting for database to be ready...")
77 <-db.Ready()
78 log.I.F("database ready")
79
80 // Create gRPC server EARLY so launcher can detect it's alive
81 grpcServer := grpc.NewServer(
82 grpc.MaxRecvMsgSize(16<<20), // 16MB
83 grpc.MaxSendMsgSize(16<<20), // 16MB
84 )
85
86 // Register ACL service (will be configured below)
87 service := NewACLService(cfg, db)
88 orlyaclv1.RegisterACLServiceServer(grpcServer, service)
89
90 // Register reflection for debugging with grpcurl
91 reflection.Register(grpcServer)
92
93 // Start listening immediately so launcher knows we're alive
94 lis, err := net.Listen("tcp", cfg.Listen)
95 if chk.E(err) {
96 log.E.F("failed to listen on %s: %v", cfg.Listen, err)
97 os.Exit(1)
98 }
99 log.I.F("gRPC ACL server listening on %s", cfg.Listen)
100
101 // Start serving in background while we configure
102 go func() {
103 if err := grpcServer.Serve(lis); err != nil {
104 log.E.F("gRPC server error: %v", err)
105 }
106 }()
107
108 // Create app config for ACL configuration
109 appCfg := &config.C{
110 Owners: cfg.GetOwners(),
111 Admins: cfg.GetAdmins(),
112 BootstrapRelays: cfg.GetBootstrapRelays(),
113 RelayAddresses: cfg.GetRelayAddresses(),
114 FollowListFrequency: cfg.FollowListFrequency,
115 FollowsThrottleEnabled: cfg.FollowsThrottleEnabled,
116 FollowsThrottlePerEvent: cfg.FollowsThrottlePerEvent,
117 FollowsThrottleMaxDelay: cfg.FollowsThrottleMaxDelay,
118 }
119
120 // Set ACL mode first
121 acl.Registry.SetMode(cfg.ACLMode)
122
123 // Mark service as ready IMMEDIATELY so relay can start
124 // Configure runs in background and loads follow lists asynchronously
125 service.SetReady(true)
126 log.I.F("ACL service ready (follow lists loading in background)")
127
128 // Run Configure in background (may take time to load follow lists)
129 go func() {
130 if err := acl.Registry.Configure(appCfg, db, ctx); chk.E(err) {
131 log.E.F("failed to configure ACL: %v", err)
132 // Don't exit - service can still function with limited ACL
133 }
134 log.I.F("ACL configuration complete")
135 // Start the syncer goroutine for background operations
136 acl.Registry.Syncer()
137 log.I.F("ACL syncer started for mode: %s", cfg.ACLMode)
138 }()
139
140 // Handle graceful shutdown - block until signal received
141 sigs := make(chan os.Signal, 1)
142 signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)
143 sig := <-sigs
144 log.I.F("received signal %v, shutting down...", sig)
145
146 // Cancel context to stop all operations
147 cancel()
148
149 // Gracefully stop gRPC server with timeout
150 stopped := make(chan struct{})
151 go func() {
152 grpcServer.GracefulStop()
153 close(stopped)
154 }()
155
156 select {
157 case <-stopped:
158 log.I.F("gRPC server stopped gracefully")
159 case <-time.After(5 * time.Second):
160 log.W.F("gRPC graceful stop timed out, forcing stop")
161 grpcServer.Stop()
162 }
163
164 // Sync and close database (only for direct Badger)
165 if cfg.DBType != "grpc" {
166 log.I.F("syncing database...")
167 if err := db.Sync(); chk.E(err) {
168 log.W.F("failed to sync database: %v", err)
169 }
170 log.I.F("closing database...")
171 if err := db.Close(); chk.E(err) {
172 log.W.F("failed to close database: %v", err)
173 }
174 }
175 log.I.F("shutdown complete")
176 }
177