Jelajahi Sumber

Add per-router command queue (server-side)

- routerQueues[routerID]: stores commands when offline
- If router offline → status: queued, queue_depth returned
- On reconnect → auto-flush all queued commands
- Idempotency still works for queued commands

Flow:
  1. Send command while router offline
  2. Server queues it
  3. Router reconnects
  4. Server flushes queue automatically
Luis Rosales 2 bulan lalu
induk
melakukan
7c85b33f4e
1 mengubah file dengan 37 tambahan dan 11 penghapusan
  1. 37 11
      server/main.go

+ 37 - 11
server/main.go

@@ -78,6 +78,9 @@ type Router struct {
 
 var routers = make(map[string]*Router)
 
+// Per-router command queue (for offline routers - auto-flush on reconnect)
+var routerQueues = make(map[string][]RouterCommand)
+
 // Pending commands awaiting results (command_id -> result channel)
 var pendingCommands = make(map[string]chan CommandResult)
 const commandTimeout = 30 * time.Second
@@ -187,7 +190,21 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
 		Connected:  true,
 	}
 
-	log.Printf("Router connected: %s", routerID)
+	log.Printf("Router connected: %s (flushing queue)", routerID)
+
+	// Flush queued commands for this router
+	if queuedCmds, ok := routerQueues[routerID]; ok && len(queuedCmds) > 0 {
+		log.Printf("Flushing %d queued commands to %s", len(queuedCmds), routerID)
+		for _, cmd := range queuedCmds {
+			resultChan := make(chan CommandResult, 1)
+			cmd.SentAt = time.Now()
+			pendingCommands[cmd.ID] = resultChan
+			publishCommand(cmd)
+			log.Printf("Queued cmd sent: %s", cmd.Command)
+			// Fire and forget - waiter will handle result
+		}
+		delete(routerQueues, routerID)
+	}
 
 	// Message loop
 	for {
@@ -303,10 +320,6 @@ func handleCommand(w http.ResponseWriter, r *http.Request) {
 	}
 
 	router := routers[routerID]
-	if router == nil || !router.Connected {
-		http.Error(w, "Router not connected", http.StatusServiceUnavailable)
-		return
-	}
 
 	// ──────────────────────────────────────────────────────────
 	// IDEMPOTENCY CHECK - Dedup before publishing!
@@ -314,7 +327,6 @@ func handleCommand(w http.ResponseWriter, r *http.Request) {
 	// If client provides an idempotency key, reuse it
 	xecID := cmd.ID
 	if xecID == "" {
-		// New command ID acts as idempotency key
 		xecID = uuid.New().String()
 	}
 
@@ -329,18 +341,32 @@ func handleCommand(w http.ResponseWriter, r *http.Request) {
 			})
 			return
 		}
-		// Old entry, allow retry
 		delete(executedCommands, xecID)
 	}
 
-
-	// Create result channel
-	resultChan := make(chan CommandResult, 1)
+	// ──────────────────────────────────────────────────────────
+	// Queue vs Live delivery
+	// ──────────────────────────────────────────────────────────
 	cmd.ID = xecID
 	cmd.RouterID = routerID
 	cmd.SentAt = time.Now()
 
-	// Store pending command
+	if router == nil || !router.Connected {
+		// Router offline - QUEUE it!
+		routerQueues[routerID] = append(routerQueues[routerID], cmd)
+		log.Printf("Queued command for %s: %s (queue depth: %d)", routerID, cmd.Command, len(routerQueues[routerID]))
+		json.NewEncoder(w).Encode(map[string]interface{}{
+			"command_id": cmd.ID,
+			"status":     "queued",
+			"queued_for": routerID,
+			"queue_depth": len(routerQueues[routerID]),
+			"message":   "Router offline, command queued",
+		})
+		return
+	}
+
+	// Router online - send directly
+	resultChan := make(chan CommandResult, 1)
 	pendingCommands[cmd.ID] = resultChan
 
 	if err := publishCommand(cmd); err != nil {