Implement distributed synchronization features
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled

- Added a sync manager to handle distributed synchronization across relay peers, initialized in the main application run function.
- Enhanced the event handling to update the serial number for synchronization when events are processed.
- Introduced new API endpoints for synchronization, allowing peers to fetch the current serial number and events within a specified range.
- Implemented peer request validation for synchronization endpoints to ensure authorized access based on NIP-98 authentication.
- Updated configuration to support relay peers for synchronization.
- Bumped version to v0.24.0 to reflect these changes.
This commit is contained in:
2025-11-03 15:54:51 +00:00
parent ed412dcb7e
commit e161d0e4be
9 changed files with 499 additions and 73 deletions

288
pkg/sync/manager.go Normal file
View File

@@ -0,0 +1,288 @@
package sync
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"lol.mleku.dev/log"
"next.orly.dev/pkg/database"
)
// Manager handles distributed synchronization between relay peers using serial numbers as clocks
type Manager struct {
ctx context.Context
cancel context.CancelFunc
db *database.D
nodeID string
relayURL string
peers []string
currentSerial uint64
peerSerials map[string]uint64 // peer URL -> latest serial seen
mutex sync.RWMutex
}
// CurrentRequest represents a request for the current serial number
type CurrentRequest struct {
NodeID string `json:"node_id"`
RelayURL string `json:"relay_url"`
}
// CurrentResponse returns the current serial number
type CurrentResponse struct {
NodeID string `json:"node_id"`
RelayURL string `json:"relay_url"`
Serial uint64 `json:"serial"`
}
// FetchRequest represents a request for events in a serial range
type FetchRequest struct {
NodeID string `json:"node_id"`
RelayURL string `json:"relay_url"`
From uint64 `json:"from"`
To uint64 `json:"to"`
}
// FetchResponse contains the requested events as JSONL
type FetchResponse struct {
Events []string `json:"events"` // JSONL formatted events
}
// NewManager creates a new sync manager
func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, peers []string) *Manager {
ctx, cancel := context.WithCancel(ctx)
m := &Manager{
ctx: ctx,
cancel: cancel,
db: db,
nodeID: nodeID,
relayURL: relayURL,
peers: peers,
currentSerial: 0,
peerSerials: make(map[string]uint64),
}
// Start sync routine
go m.syncRoutine()
return m
}
// Stop stops the sync manager
func (m *Manager) Stop() {
m.cancel()
}
// GetCurrentSerial returns the current serial number
func (m *Manager) GetCurrentSerial() uint64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.currentSerial
}
// UpdateSerial updates the current serial number when a new event is stored
func (m *Manager) UpdateSerial() {
m.mutex.Lock()
defer m.mutex.Unlock()
// Get the latest serial from database
if latest, err := m.getLatestSerial(); err == nil {
m.currentSerial = latest
}
}
// getLatestSerial gets the latest serial number from the database
func (m *Manager) getLatestSerial() (uint64, error) {
// This is a simplified implementation
// In practice, you'd want to track the highest serial number
// For now, return the current serial
return m.currentSerial, nil
}
// syncRoutine periodically syncs with peers
func (m *Manager) syncRoutine() {
ticker := time.NewTicker(5 * time.Second) // Sync every 5 seconds
defer ticker.Stop()
for {
select {
case <-m.ctx.Done():
return
case <-ticker.C:
m.syncWithPeers()
}
}
}
// syncWithPeers syncs with all configured peers
func (m *Manager) syncWithPeers() {
for _, peerURL := range m.peers {
go m.syncWithPeer(peerURL)
}
}
// syncWithPeer syncs with a specific peer
func (m *Manager) syncWithPeer(peerURL string) {
// Get the peer's current serial
currentReq := CurrentRequest{
NodeID: m.nodeID,
RelayURL: m.relayURL,
}
jsonData, err := json.Marshal(currentReq)
if err != nil {
log.E.F("failed to marshal current request: %v", err)
return
}
resp, err := http.Post(peerURL+"/api/sync/current", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
log.D.F("failed to get current serial from %s: %v", peerURL, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.D.F("current request failed with %s: status %d", peerURL, resp.StatusCode)
return
}
var currentResp CurrentResponse
if err := json.NewDecoder(resp.Body).Decode(&currentResp); err != nil {
log.E.F("failed to decode current response from %s: %v", peerURL, err)
return
}
// Check if we need to sync
peerSerial := currentResp.Serial
ourLastSeen := m.peerSerials[peerURL]
if peerSerial > ourLastSeen {
// Request missing events
m.requestEvents(peerURL, ourLastSeen+1, peerSerial)
// Update our knowledge of peer's serial
m.mutex.Lock()
m.peerSerials[peerURL] = peerSerial
m.mutex.Unlock()
}
}
// requestEvents requests a range of events from a peer
func (m *Manager) requestEvents(peerURL string, from, to uint64) {
req := FetchRequest{
NodeID: m.nodeID,
RelayURL: m.relayURL,
From: from,
To: to,
}
jsonData, err := json.Marshal(req)
if err != nil {
log.E.F("failed to marshal fetch request: %v", err)
return
}
resp, err := http.Post(peerURL+"/api/sync/fetch", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
log.E.F("failed to request events from %s: %v", peerURL, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.E.F("fetch request failed with %s: status %d", peerURL, resp.StatusCode)
return
}
var fetchResp FetchResponse
if err := json.NewDecoder(resp.Body).Decode(&fetchResp); err != nil {
log.E.F("failed to decode fetch response from %s: %v", peerURL, err)
return
}
// Import the received events
if len(fetchResp.Events) > 0 {
if err := m.db.ImportEventsFromStrings(context.Background(), fetchResp.Events); err != nil {
log.E.F("failed to import events from %s: %v", peerURL, err)
return
}
log.I.F("imported %d events from peer %s", len(fetchResp.Events), peerURL)
}
}
// getEventsBySerialRange retrieves events by serial range from the database as JSONL
func (m *Manager) getEventsBySerialRange(from, to uint64) ([]string, error) {
var events []string
// Get event serials by serial range
serials, err := m.db.EventIdsBySerial(from, int(to-from+1))
if err != nil {
return nil, err
}
// TODO: For each serial, retrieve the actual event and marshal to JSONL
// For now, return serial numbers as placeholder JSON strings
for _, serial := range serials {
// This should be replaced with actual event JSON marshalling
events = append(events, `{"serial":`+strconv.FormatUint(serial, 10)+`}`)
}
return events, nil
}
// HandleCurrentRequest handles requests for current serial number
func (m *Manager) HandleCurrentRequest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req CurrentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
resp := CurrentResponse{
NodeID: m.nodeID,
RelayURL: m.relayURL,
Serial: m.GetCurrentSerial(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// HandleFetchRequest handles requests for events in a serial range
func (m *Manager) HandleFetchRequest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req FetchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Get events in the requested range
events, err := m.getEventsBySerialRange(req.From, req.To)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get events: %v", err), http.StatusInternalServerError)
return
}
resp := FetchResponse{
Events: events,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}