Add cluster replication features and membership management

- Introduced a new `ClusterManager` to handle cluster membership events and facilitate event replication across relay peers.
- Implemented HTTP endpoints for retrieving the latest serial and fetching events within a specified range.
- Enhanced event handling to process cluster membership events (Kind 39108) and update relay lists accordingly.
- Updated configuration to support cluster administrators and their management capabilities.
- Added comprehensive tests to validate the new cluster replication functionalities.
- Documented the cluster replication protocol in a new specification file.
- Bumped version to reflect these changes.
This commit is contained in:
2025-11-03 19:02:20 +00:00
parent e56bf76257
commit b1f1334e39
7 changed files with 884 additions and 1072 deletions

View File

@@ -52,6 +52,7 @@ type C struct {
RelayAddresses []string `env:"ORLY_RELAY_ADDRESSES" usage:"comma-separated list of websocket addresses for this relay (e.g., wss://relay.example.com,wss://backup.example.com)"`
RelayPeers []string `env:"ORLY_RELAY_PEERS" usage:"comma-separated list of peer relay URLs for distributed synchronization (e.g., https://peer1.example.com,https://peer2.example.com)"`
RelayGroupAdmins []string `env:"ORLY_RELAY_GROUP_ADMINS" usage:"comma-separated list of npubs authorized to publish relay group configuration events"`
ClusterAdmins []string `env:"ORLY_CLUSTER_ADMINS" usage:"comma-separated list of npubs authorized to manage cluster membership"`
FollowListFrequency time.Duration `env:"ORLY_FOLLOW_LIST_FREQUENCY" usage:"how often to fetch admin follow lists (default: 1h)" default:"1h"`
// Blossom blob storage service level settings

View File

@@ -467,6 +467,13 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
}
}
// Handle cluster membership events (Kind 39108)
if env.E.Kind == 39108 && l.clusterManager != nil {
if err := l.clusterManager.HandleMembershipEvent(env.E); err != nil {
log.W.F("invalid cluster membership event %s: %v", hex.Enc(env.E.ID), err)
}
}
// Update serial for distributed synchronization
if l.syncManager != nil {
l.syncManager.UpdateSerial()

View File

@@ -152,6 +152,23 @@ func Run(
}
}
// Initialize cluster manager for cluster replication
var clusterAdminNpubs []string
if len(cfg.ClusterAdmins) > 0 {
clusterAdminNpubs = cfg.ClusterAdmins
} else {
// Default to regular admins if no cluster admins specified
for _, admin := range cfg.Admins {
clusterAdminNpubs = append(clusterAdminNpubs, admin)
}
}
if len(clusterAdminNpubs) > 0 {
l.clusterManager = dsync.NewClusterManager(ctx, db, clusterAdminNpubs)
l.clusterManager.Start()
log.I.F("cluster replication manager initialized with %d admin npubs", len(clusterAdminNpubs))
}
// Initialize the user interface
l.UserInterface()

View File

@@ -53,6 +53,7 @@ type Server struct {
spiderManager *spider.Spider
syncManager *dsync.Manager
relayGroupMgr *dsync.RelayGroupManager
clusterManager *dsync.ClusterManager
blossomServer *blossom.Server
}
@@ -259,6 +260,13 @@ func (s *Server) UserInterface() {
s.mux.HandleFunc("/blossom/", s.blossomHandler)
log.Printf("Blossom blob storage API enabled at /blossom")
}
// Cluster replication API endpoints
if s.clusterManager != nil {
s.mux.HandleFunc("/cluster/latest", s.clusterManager.HandleLatestSerial)
s.mux.HandleFunc("/cluster/events", s.clusterManager.HandleEventsRange)
log.Printf("Cluster replication API enabled at /cluster")
}
}
// handleFavicon serves orly-favicon.png as favicon.ico