bugfix event
This commit is contained in:
Vendored
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-Bd95QA6X.js"></script>
|
<script type="module" crossorigin src="/assets/index-DJlbGMsa.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DtR_2V4s.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DtR_2V4s.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ interface ServiceInfo {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
status: string;
|
status: string;
|
||||||
lastHeartbeat: string;
|
lastHeartbeat: string;
|
||||||
role?: string; // "leader", "follower", or undefined (for non-meta-core services)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServicesResponse {
|
interface ServicesResponse {
|
||||||
@@ -56,11 +55,7 @@ function ServiceNav() {
|
|||||||
|
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
|
|
||||||
// Filter out follower meta-core instances (only show leader)
|
const sortedServices = [...services].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
const filteredServices = services.filter(
|
|
||||||
s => s.name !== 'meta-core' || s.role === 'leader'
|
|
||||||
);
|
|
||||||
const sortedServices = [...filteredServices].sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
if (sortedServices.length === 0) return null;
|
if (sortedServices.length === 0) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -400,7 +400,8 @@ func (s *Server) handleGetFileByCID(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file)
|
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleListServices handles GET /services
|
// handleListServices handles GET /services and GET /api/services
|
||||||
|
// Supports optional ?current=<service-name> query parameter
|
||||||
func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
|
||||||
services, err := s.discovery.DiscoverAll()
|
services, err := s.discovery.DiscoverAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -408,10 +409,18 @@ func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"services": services,
|
"services": services,
|
||||||
"count": len(services),
|
"count": len(services),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Add 'current' field if query parameter is provided
|
||||||
|
current := r.URL.Query().Get("current")
|
||||||
|
if current != "" {
|
||||||
|
response["current"] = current
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetService handles GET /services/{name}
|
// handleGetService handles GET /services/{name}
|
||||||
|
|||||||
+36
-1
@@ -90,7 +90,8 @@ func NewServer(
|
|||||||
// Create watchers poller
|
// Create watchers poller
|
||||||
if s.fileWatcher != nil {
|
if s.fileWatcher != nil {
|
||||||
s.watchersPoller = watchers.NewPoller(watchersManager, s.fileWatcher, s.watcherDispatcher)
|
s.watchersPoller = watchers.NewPoller(watchersManager, s.fileWatcher, s.watcherDispatcher)
|
||||||
s.watchersHandlers = watchers.NewHandlers(watchersManager, s.watchersPoller)
|
// Pass republish callback to handlers (handles nil metaPublisher case)
|
||||||
|
s.watchersHandlers = watchers.NewHandlers(watchersManager, s.watchersPoller, s.RepublishMetadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +174,9 @@ func (s *Server) setupRoutes() {
|
|||||||
s.router.HandleFunc("/file/cid", s.handleComputeFileCID).Methods("POST")
|
s.router.HandleFunc("/file/cid", s.handleComputeFileCID).Methods("POST")
|
||||||
|
|
||||||
// Service discovery
|
// Service discovery
|
||||||
|
// Both /services and /api/services are supported for consistency with other services
|
||||||
s.router.HandleFunc("/services", s.handleListServices).Methods("GET")
|
s.router.HandleFunc("/services", s.handleListServices).Methods("GET")
|
||||||
|
s.router.HandleFunc("/api/services", s.handleListServices).Methods("GET")
|
||||||
s.router.HandleFunc("/services/cleanup/stats", s.handleCleanupStats).Methods("GET")
|
s.router.HandleFunc("/services/cleanup/stats", s.handleCleanupStats).Methods("GET")
|
||||||
s.router.HandleFunc("/services/{name}", s.handleGetService).Methods("GET")
|
s.router.HandleFunc("/services/{name}", s.handleGetService).Methods("GET")
|
||||||
|
|
||||||
@@ -271,6 +274,16 @@ func (s *Server) Start() error {
|
|||||||
s.metaPublisher = events.NewMetaPublisher(s.storage.GetRedisClient())
|
s.metaPublisher = events.NewMetaPublisher(s.storage.GetRedisClient())
|
||||||
if err := s.metaPublisher.Start(); err != nil {
|
if err := s.metaPublisher.Start(); err != nil {
|
||||||
log.Printf("[API] Warning: failed to start meta publisher: %v", err)
|
log.Printf("[API] Warning: failed to start meta publisher: %v", err)
|
||||||
|
} else {
|
||||||
|
// Republish events for all existing metadata
|
||||||
|
go func() {
|
||||||
|
count, err := s.RepublishMetadata()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[API] Warning: failed to republish metadata on startup: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[API] Republished %d metadata events on startup", count)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +339,28 @@ func (s *Server) Stop() error {
|
|||||||
return s.server.Shutdown(ctx)
|
return s.server.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RepublishMetadata republishes all existing metadata to the meta:events stream
|
||||||
|
// This is called on startup and after reset-all
|
||||||
|
func (s *Server) RepublishMetadata() (int, error) {
|
||||||
|
if s.metaPublisher == nil {
|
||||||
|
return 0, fmt.Errorf("meta publisher not initialized")
|
||||||
|
}
|
||||||
|
if s.storage == nil || !s.storage.IsConnected() {
|
||||||
|
return 0, fmt.Errorf("storage not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the republish function that wraps storage methods
|
||||||
|
republishFunc := func() ([]string, func(string) (map[string]string, error), error) {
|
||||||
|
hashIDs, err := s.storage.GetAllHashIDs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return hashIDs, s.storage.GetMetadataFlat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.metaPublisher.RepublishAllMetadata(republishFunc)
|
||||||
|
}
|
||||||
|
|
||||||
// loggingMiddleware logs all requests
|
// loggingMiddleware logs all requests
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
func loggingMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -143,3 +143,63 @@ func (p *MetaPublisher) publishEvent(channel, operation string) {
|
|||||||
log.Printf("[MetaPublisher] Error publishing event: %v", err)
|
log.Printf("[MetaPublisher] Error publishing event: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// publishEventDirect publishes an event directly (not from keyspace notification)
|
||||||
|
func (p *MetaPublisher) publishEventDirect(operation, key string) {
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"type": operation,
|
||||||
|
"key": key,
|
||||||
|
"ts": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.client.XAdd(p.ctx, &redis.XAddArgs{
|
||||||
|
Stream: MetaEventsStream,
|
||||||
|
Values: fields,
|
||||||
|
}).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[MetaPublisher] Error publishing direct event: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepublishFunc is a function type for getting metadata that can be republished
|
||||||
|
type RepublishFunc func() (hashIDs []string, getMetadata func(hashID string) (map[string]string, error), err error)
|
||||||
|
|
||||||
|
// RepublishAllMetadata iterates over all existing file metadata and publishes events
|
||||||
|
// This is called on startup and can be triggered via API
|
||||||
|
func (p *MetaPublisher) RepublishAllMetadata(republishFunc RepublishFunc) (int, error) {
|
||||||
|
p.mu.RLock()
|
||||||
|
running := p.running
|
||||||
|
p.mu.RUnlock()
|
||||||
|
|
||||||
|
if !running {
|
||||||
|
return 0, fmt.Errorf("publisher not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all hash IDs and metadata getter
|
||||||
|
hashIDs, getMetadata, err := republishFunc()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get hash IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[MetaPublisher] Republishing events for %d files...", len(hashIDs))
|
||||||
|
|
||||||
|
publishedCount := 0
|
||||||
|
for _, hashID := range hashIDs {
|
||||||
|
// Get all properties for this file
|
||||||
|
metadata, err := getMetadata(hashID)
|
||||||
|
if err != nil || metadata == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish a set event for each property
|
||||||
|
for property := range metadata {
|
||||||
|
key := fmt.Sprintf("file:%s/%s", hashID, property)
|
||||||
|
p.publishEventDirect("set", key)
|
||||||
|
publishedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[MetaPublisher] Republished %d property events", publishedCount)
|
||||||
|
return publishedCount, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,28 @@ package watchers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RepublishCallback is a function type for republishing metadata events
|
||||||
|
type RepublishCallback func() (int, error)
|
||||||
|
|
||||||
// Handlers provides HTTP handlers for watchers management
|
// Handlers provides HTTP handlers for watchers management
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
manager *Manager
|
manager *Manager
|
||||||
poller *Poller
|
poller *Poller
|
||||||
|
republishCallback RepublishCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandlers creates new watchers handlers
|
// NewHandlers creates new watchers handlers
|
||||||
func NewHandlers(manager *Manager, poller *Poller) *Handlers {
|
func NewHandlers(manager *Manager, poller *Poller, republishCallback RepublishCallback) *Handlers {
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
manager: manager,
|
manager: manager,
|
||||||
poller: poller,
|
poller: poller,
|
||||||
|
republishCallback: republishCallback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,10 +215,22 @@ func (h *Handlers) handleResetAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Republish all metadata events after reset
|
||||||
|
republishedCount := 0
|
||||||
|
if h.republishCallback != nil {
|
||||||
|
count, err := h.republishCallback()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Handlers] Warning: failed to republish metadata after reset: %v", err)
|
||||||
|
} else {
|
||||||
|
republishedCount = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"message": "All watchers reset",
|
"message": "All watchers reset",
|
||||||
"totalFiles": totalCount,
|
"totalFiles": totalCount,
|
||||||
|
"republishedEvents": republishedCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user