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>
This commit is contained in:
99
internal/masterdb/placements.go
Normal file
99
internal/masterdb/placements.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user