package wire import ( "sort" "sync" "time" proto_socket "git.toki-labs.com/toki/proto-socket/go" "google.golang.org/protobuf/proto" iop "iop/proto/gen/iop" ) const recentNodeEventCapacity = 128 const recentCommandCapacity = 128 const commandEventCapacity = 64 // EdgeConnectionState captures the Control Plane's internal view of a single // Edge wire connection. type EdgeConnectionState struct { EdgeID string EdgeName string Version string Capabilities []string Protocol string Connected bool LastSeen time.Time DisconnectInfo proto_socket.DisconnectInfo // token identifies the specific connection that produced this state. It is an // internal registry detail (not an external API contract) used to ignore a // stale connection's late disconnect after the same edge_id reconnected. token uint64 } // EdgeNodeEventRecord is the Control Plane's copied recent view of one node // lifecycle event as received from a specific Edge connection. type EdgeNodeEventRecord struct { EdgeID string Event *iop.EdgeNodeEvent ReceivedAt time.Time } // EdgeCommandRecord is the Control Plane's bounded audit view of one command // dispatched to an Edge: the request summary, the latest result, and a bounded // list of relayed lifecycle events. It is a control/audit view; durable command // history is intentionally outside this in-memory baseline. type EdgeCommandRecord struct { EdgeID string CommandID string Operation string TargetSelector string Status string Summary string Error string CreatedAt time.Time UpdatedAt time.Time Events []EdgeCommandEventRecord } // EdgeCommandEventRecord is the Control Plane's copied view of one command // lifecycle event relayed from an Edge during command execution. type EdgeCommandEventRecord struct { Phase string Summary string OccurredAt time.Time ReceivedAt time.Time } // EdgeRegistry stores Edge connection state keyed by edge_id behind a mutex. type EdgeRegistry struct { mu sync.Mutex edges map[string]*EdgeConnectionState nextToken uint64 recentNodeEvents []EdgeNodeEventRecord commands []*EdgeCommandRecord } // NewEdgeRegistry returns an empty Edge connection registry. func NewEdgeRegistry() *EdgeRegistry { return &EdgeRegistry{edges: make(map[string]*EdgeConnectionState)} } // MarkConnected records or refreshes an Edge as connected from its hello and // returns the connection token to hand back on disconnect. A reconnection // supersedes any earlier connection for the same edge_id. func (r *EdgeRegistry) MarkConnected(req *iop.EdgeHelloRequest, at time.Time) uint64 { r.mu.Lock() defer r.mu.Unlock() r.nextToken++ token := r.nextToken r.edges[req.GetEdgeId()] = &EdgeConnectionState{ EdgeID: req.GetEdgeId(), EdgeName: req.GetEdgeName(), Version: req.GetVersion(), Capabilities: append([]string(nil), req.GetCapabilities()...), Protocol: Protocol, Connected: true, LastSeen: at, token: token, } return token } // MarkDisconnected flips a tracked Edge to disconnected and records why, but // only when token still matches the current connection. A stale token from a // superseded connection is ignored so a reconnection is not cleared. It returns // true only when the current connection was flipped, so callers (e.g. the // EdgeServer active client map) can apply the same stale-safe cleanup. func (r *EdgeRegistry) MarkDisconnected(edgeID string, token uint64, at time.Time, info proto_socket.DisconnectInfo) bool { r.mu.Lock() defer r.mu.Unlock() state, ok := r.edges[edgeID] if !ok || state.token != token { return false } state.Connected = false state.LastSeen = at state.DisconnectInfo = info return true } // Snapshot returns a copy of the tracked state for an Edge. func (r *EdgeRegistry) Snapshot(edgeID string) (EdgeConnectionState, bool) { r.mu.Lock() defer r.mu.Unlock() state, ok := r.edges[edgeID] if !ok { return EdgeConnectionState{}, false } snapshot := *state snapshot.Capabilities = append([]string(nil), state.Capabilities...) return snapshot, true } // Snapshots returns a stable, copied view of all tracked Edge states. func (r *EdgeRegistry) Snapshots() []EdgeConnectionState { r.mu.Lock() defer r.mu.Unlock() ids := make([]string, 0, len(r.edges)) for id := range r.edges { ids = append(ids, id) } sort.Strings(ids) snapshots := make([]EdgeConnectionState, 0, len(ids)) for _, id := range ids { state := r.edges[id] snapshot := *state snapshot.Capabilities = append([]string(nil), state.Capabilities...) snapshots = append(snapshots, snapshot) } return snapshots } // RecordNodeEvent stores a copied EdgeNodeEvent under the identified Edge. It // is intended for tests and non-connection seed paths that do not have an // active connection token. func (r *EdgeRegistry) RecordNodeEvent(edgeID string, event *iop.EdgeNodeEvent, receivedAt time.Time) { r.recordNodeEvent(edgeID, 0, false, event, receivedAt) } // RecordNodeEventFromConnection stores a copied EdgeNodeEvent only when the // supplied token still identifies the current connection for edgeID. It returns // false for stale superseded connections. func (r *EdgeRegistry) RecordNodeEventFromConnection(edgeID string, token uint64, event *iop.EdgeNodeEvent, receivedAt time.Time) bool { return r.recordNodeEvent(edgeID, token, true, event, receivedAt) } // recordNodeEvent appends to the bounded recent buffer. The recent buffer is // global to the in-memory registry; durable event history is intentionally // outside this baseline. func (r *EdgeRegistry) recordNodeEvent(edgeID string, token uint64, requireToken bool, event *iop.EdgeNodeEvent, receivedAt time.Time) bool { if edgeID == "" || event == nil { return false } if receivedAt.IsZero() { receivedAt = time.Now() } copied := cloneEdgeNodeEvent(event) if copied.GetTimestamp() == 0 { copied.Timestamp = receivedAt.UnixNano() } r.mu.Lock() defer r.mu.Unlock() if requireToken { state, ok := r.edges[edgeID] if !ok || state.token != token { return false } } if len(r.recentNodeEvents) >= recentNodeEventCapacity { r.recentNodeEvents = append(r.recentNodeEvents[:0], r.recentNodeEvents[1:]...) } r.recentNodeEvents = append(r.recentNodeEvents, EdgeNodeEventRecord{ EdgeID: edgeID, Event: copied, ReceivedAt: receivedAt, }) return true } // NodeEvents returns copied recent node lifecycle events for an Edge. When // nodeID is non-empty, only events for that node are returned. func (r *EdgeRegistry) NodeEvents(edgeID, nodeID string) []EdgeNodeEventRecord { if edgeID == "" { return nil } r.mu.Lock() defer r.mu.Unlock() out := make([]EdgeNodeEventRecord, 0, len(r.recentNodeEvents)) for _, record := range r.recentNodeEvents { if record.EdgeID != edgeID { continue } if nodeID != "" && record.Event.GetNodeId() != nodeID { continue } out = append(out, cloneEdgeNodeEventRecord(record)) } if len(out) == 0 { return nil } return out } // RecordCommandRequest stores or refreshes a pending command audit record for a // command the Control Plane is dispatching to an Edge. It is bounded; the oldest // record is evicted past capacity. func (r *EdgeRegistry) RecordCommandRequest(edgeID string, req *iop.EdgeCommandRequest, at time.Time) { if edgeID == "" || req == nil || req.GetCommandId() == "" { return } if at.IsZero() { at = time.Now() } r.mu.Lock() defer r.mu.Unlock() if rec := r.findCommandLocked(edgeID, req.GetCommandId()); rec != nil { rec.Operation = req.GetOperation() rec.TargetSelector = req.GetTargetSelector() rec.UpdatedAt = at return } rec := r.appendCommandLocked(edgeID, req.GetCommandId(), at) rec.Operation = req.GetOperation() rec.TargetSelector = req.GetTargetSelector() rec.Status = "pending" } // RecordCommandResult updates the audit record for a command with the Edge's // response status/summary. A missing record is created so a result is never // silently dropped. func (r *EdgeRegistry) RecordCommandResult(edgeID string, resp *iop.EdgeCommandResponse, at time.Time) { if edgeID == "" || resp == nil || resp.GetCommandId() == "" { return } if at.IsZero() { at = time.Now() } r.mu.Lock() defer r.mu.Unlock() rec := r.findCommandLocked(edgeID, resp.GetCommandId()) if rec == nil { rec = r.appendCommandLocked(edgeID, resp.GetCommandId(), at) } status := resp.GetStatus() if status == "" { status = "ok" } rec.Status = status rec.Summary = resp.GetSummary() rec.Error = resp.GetError() rec.UpdatedAt = at } // RecordCommandEventFromConnection appends a bounded command lifecycle event to // the matching command audit record, but only when token still identifies the // current connection for edgeID. It returns false for stale superseded // connections. func (r *EdgeRegistry) RecordCommandEventFromConnection(edgeID string, token uint64, event *iop.EdgeCommandEvent, receivedAt time.Time) bool { return r.recordCommandEvent(edgeID, token, true, event, receivedAt) } // RecordCommandEvent stores a command lifecycle event without a connection // token. It is intended for tests and non-connection seed paths. func (r *EdgeRegistry) RecordCommandEvent(edgeID string, event *iop.EdgeCommandEvent, receivedAt time.Time) { r.recordCommandEvent(edgeID, 0, false, event, receivedAt) } func (r *EdgeRegistry) recordCommandEvent(edgeID string, token uint64, requireToken bool, event *iop.EdgeCommandEvent, receivedAt time.Time) bool { if edgeID == "" || event == nil || event.GetCommandId() == "" { return false } if receivedAt.IsZero() { receivedAt = time.Now() } occurredAt := receivedAt if event.GetOccurredAt() != 0 { occurredAt = time.Unix(0, event.GetOccurredAt()) } r.mu.Lock() defer r.mu.Unlock() if requireToken { state, ok := r.edges[edgeID] if !ok || state.token != token { return false } } rec := r.findCommandLocked(edgeID, event.GetCommandId()) if rec == nil { rec = r.appendCommandLocked(edgeID, event.GetCommandId(), receivedAt) rec.Status = "running" } if len(rec.Events) >= commandEventCapacity { rec.Events = append(rec.Events[:0], rec.Events[1:]...) } rec.Events = append(rec.Events, EdgeCommandEventRecord{ Phase: event.GetPhase(), Summary: event.GetSummary(), OccurredAt: occurredAt, ReceivedAt: receivedAt, }) rec.UpdatedAt = receivedAt return true } // Commands returns copied command audit records for an Edge, oldest first. func (r *EdgeRegistry) Commands(edgeID string) []EdgeCommandRecord { if edgeID == "" { return nil } r.mu.Lock() defer r.mu.Unlock() out := make([]EdgeCommandRecord, 0, len(r.commands)) for _, rec := range r.commands { if rec.EdgeID != edgeID { continue } out = append(out, cloneEdgeCommandRecord(rec)) } if len(out) == 0 { return nil } return out } // Command returns a single copied command audit record. func (r *EdgeRegistry) Command(edgeID, commandID string) (EdgeCommandRecord, bool) { r.mu.Lock() defer r.mu.Unlock() rec := r.findCommandLocked(edgeID, commandID) if rec == nil { return EdgeCommandRecord{}, false } return cloneEdgeCommandRecord(rec), true } // findCommandLocked returns the stored command record for an edge_id/command_id // pair. Callers must hold r.mu. func (r *EdgeRegistry) findCommandLocked(edgeID, commandID string) *EdgeCommandRecord { for _, rec := range r.commands { if rec.EdgeID == edgeID && rec.CommandID == commandID { return rec } } return nil } // appendCommandLocked appends a new bounded command record. Callers must hold // r.mu. func (r *EdgeRegistry) appendCommandLocked(edgeID, commandID string, at time.Time) *EdgeCommandRecord { if len(r.commands) >= recentCommandCapacity { r.commands = append(r.commands[:0], r.commands[1:]...) } rec := &EdgeCommandRecord{ EdgeID: edgeID, CommandID: commandID, CreatedAt: at, UpdatedAt: at, } r.commands = append(r.commands, rec) return rec } func cloneEdgeCommandRecord(rec *EdgeCommandRecord) EdgeCommandRecord { clone := *rec clone.Events = append([]EdgeCommandEventRecord(nil), rec.Events...) return clone } // Len returns the number of tracked Edge connections. func (r *EdgeRegistry) Len() int { r.mu.Lock() defer r.mu.Unlock() return len(r.edges) } func cloneEdgeNodeEventRecord(record EdgeNodeEventRecord) EdgeNodeEventRecord { return EdgeNodeEventRecord{ EdgeID: record.EdgeID, Event: cloneEdgeNodeEvent(record.Event), ReceivedAt: record.ReceivedAt, } } func cloneEdgeNodeEvent(event *iop.EdgeNodeEvent) *iop.EdgeNodeEvent { if event == nil { return nil } copied, ok := proto.Clone(event).(*iop.EdgeNodeEvent) if !ok { return &iop.EdgeNodeEvent{} } return copied }