Files
mcp/internal/masterdb/placements.go
Kyle Isom 3c0b55f9f8 Add master database with nodes, placements, and edge_routes
New internal/masterdb/ package for mcp-master cluster state. Separate
from the agent's registry because the schemas are fundamentally
different (cluster-wide placement vs node-local containers).

Tables: nodes, placements, edge_routes. Full CRUD with tests.
Follows the same Open/migrate pattern as internal/registry/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:26:04 -07:00

100 lines
2.8 KiB
Go

package masterdb
import (
"database/sql"
"fmt"
"time"
)
// Placement records which node hosts which service.
type Placement struct {
ServiceName string
Node string
Tier string
DeployedAt time.Time
}
// CreatePlacement inserts or replaces a placement record.
func CreatePlacement(db *sql.DB, serviceName, node, tier string) error {
_, err := db.Exec(`
INSERT INTO placements (service_name, node, tier, deployed_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT(service_name) DO UPDATE SET
node = excluded.node,
tier = excluded.tier,
deployed_at = datetime('now')
`, serviceName, node, tier)
if err != nil {
return fmt.Errorf("create placement %s: %w", serviceName, err)
}
return nil
}
// GetPlacement returns the placement for a service.
func GetPlacement(db *sql.DB, serviceName string) (*Placement, error) {
var p Placement
var deployedAt string
err := db.QueryRow(`
SELECT service_name, node, tier, deployed_at
FROM placements WHERE service_name = ?
`, serviceName).Scan(&p.ServiceName, &p.Node, &p.Tier, &deployedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("get placement %s: %w", serviceName, err)
}
p.DeployedAt, _ = time.Parse("2006-01-02 15:04:05", deployedAt)
return &p, nil
}
// ListPlacements returns all placements.
func ListPlacements(db *sql.DB) ([]*Placement, error) {
rows, err := db.Query(`SELECT service_name, node, tier, deployed_at FROM placements ORDER BY service_name`)
if err != nil {
return nil, fmt.Errorf("list placements: %w", err)
}
defer func() { _ = rows.Close() }()
var placements []*Placement
for rows.Next() {
var p Placement
var deployedAt string
if err := rows.Scan(&p.ServiceName, &p.Node, &p.Tier, &deployedAt); err != nil {
return nil, fmt.Errorf("scan placement: %w", err)
}
p.DeployedAt, _ = time.Parse("2006-01-02 15:04:05", deployedAt)
placements = append(placements, &p)
}
return placements, rows.Err()
}
// DeletePlacement removes a placement record.
func DeletePlacement(db *sql.DB, serviceName string) error {
_, err := db.Exec(`DELETE FROM placements WHERE service_name = ?`, serviceName)
if err != nil {
return fmt.Errorf("delete placement %s: %w", serviceName, err)
}
return nil
}
// CountPlacementsPerNode returns a map of node name → number of placed services.
func CountPlacementsPerNode(db *sql.DB) (map[string]int, error) {
rows, err := db.Query(`SELECT node, COUNT(*) FROM placements GROUP BY node`)
if err != nil {
return nil, fmt.Errorf("count placements: %w", err)
}
defer func() { _ = rows.Close() }()
counts := make(map[string]int)
for rows.Next() {
var node string
var count int
if err := rows.Scan(&node, &count); err != nil {
return nil, fmt.Errorf("scan count: %w", err)
}
counts[node] = count
}
return counts, rows.Err()
}