|
|
@@ -4,6 +4,8 @@ import (
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
+ "io"
|
|
|
+ "net/http"
|
|
|
"sort"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
@@ -14,12 +16,14 @@ import (
|
|
|
|
|
|
// KVServer wraps Raft to provide a distributed key-value store
|
|
|
type KVServer struct {
|
|
|
- Raft *Raft
|
|
|
- DB *db.Engine
|
|
|
- CLI *CLI
|
|
|
- stopCh chan struct{}
|
|
|
- wg sync.WaitGroup
|
|
|
- stopOnce sync.Once
|
|
|
+ Raft *Raft
|
|
|
+ DB *db.Engine
|
|
|
+ CLI *CLI
|
|
|
+ Watcher *WebHookWatcher
|
|
|
+ httpServer *http.Server
|
|
|
+ stopCh chan struct{}
|
|
|
+ wg sync.WaitGroup
|
|
|
+ stopOnce sync.Once
|
|
|
// leavingNodes tracks nodes that are currently being removed
|
|
|
// to prevent auto-rejoin/discovery logic from interfering
|
|
|
leavingNodes sync.Map
|
|
|
@@ -71,6 +75,10 @@ func NewKVServer(config *Config) (*KVServer, error) {
|
|
|
applyCh := make(chan ApplyMsg, 1000) // Increase buffer for async processing
|
|
|
transport := NewTCPTransport(config.ListenAddr, 10, config.Logger)
|
|
|
|
|
|
+ // Initialize WebHookWatcher
|
|
|
+ // 5 workers, 3 retries
|
|
|
+ watcher := NewWebHookWatcher(5, 3, config.Logger)
|
|
|
+
|
|
|
r, err := NewRaft(config, transport, applyCh)
|
|
|
if err != nil {
|
|
|
engine.Close()
|
|
|
@@ -78,10 +86,11 @@ func NewKVServer(config *Config) (*KVServer, error) {
|
|
|
}
|
|
|
|
|
|
s := &KVServer{
|
|
|
- Raft: r,
|
|
|
- DB: engine,
|
|
|
- CLI: nil,
|
|
|
- stopCh: stopCh,
|
|
|
+ Raft: r,
|
|
|
+ DB: engine,
|
|
|
+ CLI: nil,
|
|
|
+ Watcher: watcher,
|
|
|
+ stopCh: stopCh,
|
|
|
}
|
|
|
|
|
|
// Initialize CLI
|
|
|
@@ -102,6 +111,14 @@ func (s *KVServer) Start() error {
|
|
|
if s.Raft.config.EnableCLI {
|
|
|
go s.CLI.Start()
|
|
|
}
|
|
|
+
|
|
|
+ // Start HTTP Server if configured
|
|
|
+ if s.Raft.config.HTTPAddr != "" {
|
|
|
+ if err := s.startHTTPServer(s.Raft.config.HTTPAddr); err != nil {
|
|
|
+ s.Raft.config.Logger.Warn("Failed to start HTTP server: %v", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
return s.Raft.Start()
|
|
|
}
|
|
|
|
|
|
@@ -113,6 +130,14 @@ func (s *KVServer) Stop() error {
|
|
|
close(s.stopCh)
|
|
|
s.wg.Wait()
|
|
|
}
|
|
|
+ // Stop Watcher
|
|
|
+ if s.Watcher != nil {
|
|
|
+ s.Watcher.Stop()
|
|
|
+ }
|
|
|
+ // Stop HTTP Server
|
|
|
+ if s.httpServer != nil {
|
|
|
+ s.httpServer.Close()
|
|
|
+ }
|
|
|
// Stop Raft first
|
|
|
if errRaft := s.Raft.Stop(); errRaft != nil {
|
|
|
err = errRaft
|
|
|
@@ -151,8 +176,14 @@ func (s *KVServer) runApplyLoop(applyCh chan ApplyMsg) {
|
|
|
switch cmd.Type {
|
|
|
case KVSet:
|
|
|
err = s.DB.Set(cmd.Key, cmd.Value, msg.CommandIndex)
|
|
|
+ if err == nil {
|
|
|
+ s.Watcher.Notify(cmd.Key, cmd.Value, KVSet)
|
|
|
+ }
|
|
|
case KVDel:
|
|
|
err = s.DB.Delete(cmd.Key, msg.CommandIndex)
|
|
|
+ if err == nil {
|
|
|
+ s.Watcher.Notify(cmd.Key, "", KVDel)
|
|
|
+ }
|
|
|
default:
|
|
|
s.Raft.config.Logger.Error("Unknown command type: %d", cmd.Type)
|
|
|
}
|
|
|
@@ -342,6 +373,16 @@ func (s *KVServer) GetDBSize() int64 {
|
|
|
return s.DB.GetDBSize()
|
|
|
}
|
|
|
|
|
|
+// WatchURL registers a webhook url for a key
|
|
|
+func (s *KVServer) WatchURL(key, url string) {
|
|
|
+ s.Watcher.Subscribe(key, url)
|
|
|
+}
|
|
|
+
|
|
|
+// UnwatchURL removes a webhook url for a key
|
|
|
+func (s *KVServer) UnwatchURL(key, url string) {
|
|
|
+ s.Watcher.Unsubscribe(key, url)
|
|
|
+}
|
|
|
+
|
|
|
// WatchAll registers a watcher for all keys
|
|
|
func (s *KVServer) WatchAll(handler WatchHandler) {
|
|
|
// s.FSM.WatchAll(handler)
|
|
|
@@ -505,3 +546,116 @@ func (s *KVServer) checkConnections() {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// startHTTPServer starts the HTTP API server
|
|
|
+func (s *KVServer) startHTTPServer(addr string) error {
|
|
|
+ mux := http.NewServeMux()
|
|
|
+
|
|
|
+ // KV API
|
|
|
+ mux.HandleFunc("/kv", func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ switch r.Method {
|
|
|
+ case http.MethodGet:
|
|
|
+ key := r.URL.Query().Get("key")
|
|
|
+ if key == "" {
|
|
|
+ http.Error(w, "missing key", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ val, found, err := s.GetLinear(key)
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if !found {
|
|
|
+ http.Error(w, "not found", http.StatusNotFound)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ w.Write([]byte(val))
|
|
|
+
|
|
|
+ case http.MethodPost:
|
|
|
+ body, _ := io.ReadAll(r.Body)
|
|
|
+ var req struct {
|
|
|
+ Key string `json:"key"`
|
|
|
+ Value string `json:"value"`
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(body, &req); err != nil {
|
|
|
+ http.Error(w, "invalid json", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := s.Set(req.Key, req.Value); err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+
|
|
|
+ case http.MethodDelete:
|
|
|
+ key := r.URL.Query().Get("key")
|
|
|
+ if key == "" {
|
|
|
+ http.Error(w, "missing key", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err := s.Del(key); err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+
|
|
|
+ default:
|
|
|
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Watcher API
|
|
|
+ mux.HandleFunc("/watch", func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodPost {
|
|
|
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ body, _ := io.ReadAll(r.Body)
|
|
|
+ var req struct {
|
|
|
+ Key string `json:"key"`
|
|
|
+ URL string `json:"url"`
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(body, &req); err != nil {
|
|
|
+ http.Error(w, "invalid json", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if req.Key == "" || req.URL == "" {
|
|
|
+ http.Error(w, "missing key or url", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.WatchURL(req.Key, req.URL)
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+ })
|
|
|
+
|
|
|
+ mux.HandleFunc("/unwatch", func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ if r.Method != http.MethodPost {
|
|
|
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ body, _ := io.ReadAll(r.Body)
|
|
|
+ var req struct {
|
|
|
+ Key string `json:"key"`
|
|
|
+ URL string `json:"url"`
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(body, &req); err != nil {
|
|
|
+ http.Error(w, "invalid json", http.StatusBadRequest)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ s.UnwatchURL(req.Key, req.URL)
|
|
|
+ w.WriteHeader(http.StatusOK)
|
|
|
+ })
|
|
|
+
|
|
|
+ s.httpServer = &http.Server{
|
|
|
+ Addr: addr,
|
|
|
+ Handler: mux,
|
|
|
+ }
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ s.Raft.config.Logger.Info("HTTP API server listening on %s", addr)
|
|
|
+ if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
+ s.Raft.config.Logger.Error("HTTP server failed: %v", err)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|