mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 11:33:04 +00:00
342 lines
9.7 KiB
Go
342 lines
9.7 KiB
Go
package namespace
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/client"
|
|
"github.com/DeBrosOfficial/network/pkg/rqlite"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// NamespacePortAllocator manages the reserved port range (10000-10099) for namespace services.
|
|
// Each namespace instance on a node gets a block of 5 consecutive ports.
|
|
type NamespacePortAllocator struct {
|
|
db rqlite.Client
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewNamespacePortAllocator creates a new port allocator
|
|
func NewNamespacePortAllocator(db rqlite.Client, logger *zap.Logger) *NamespacePortAllocator {
|
|
return &NamespacePortAllocator{
|
|
db: db,
|
|
logger: logger.With(zap.String("component", "namespace-port-allocator")),
|
|
}
|
|
}
|
|
|
|
// AllocatePortBlock finds and allocates the next available 5-port block on a node.
|
|
// Returns an error if the node is at capacity (20 namespace instances).
|
|
func (npa *NamespacePortAllocator) AllocatePortBlock(ctx context.Context, nodeID, namespaceClusterID string) (*PortBlock, error) {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
// Check if allocation already exists for this namespace on this node
|
|
existingBlock, err := npa.GetPortBlock(ctx, namespaceClusterID, nodeID)
|
|
if err == nil && existingBlock != nil {
|
|
npa.logger.Debug("Port block already allocated",
|
|
zap.String("node_id", nodeID),
|
|
zap.String("namespace_cluster_id", namespaceClusterID),
|
|
zap.Int("port_start", existingBlock.PortStart),
|
|
)
|
|
return existingBlock, nil
|
|
}
|
|
|
|
// Retry logic for handling concurrent allocation conflicts
|
|
maxRetries := 10
|
|
retryDelay := 100 * time.Millisecond
|
|
|
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
|
block, err := npa.tryAllocatePortBlock(internalCtx, nodeID, namespaceClusterID)
|
|
if err == nil {
|
|
npa.logger.Info("Port block allocated successfully",
|
|
zap.String("node_id", nodeID),
|
|
zap.String("namespace_cluster_id", namespaceClusterID),
|
|
zap.Int("port_start", block.PortStart),
|
|
zap.Int("attempt", attempt+1),
|
|
)
|
|
return block, nil
|
|
}
|
|
|
|
// If it's a conflict error, retry with exponential backoff
|
|
if isConflictError(err) {
|
|
npa.logger.Debug("Port allocation conflict, retrying",
|
|
zap.String("node_id", nodeID),
|
|
zap.String("namespace_cluster_id", namespaceClusterID),
|
|
zap.Int("attempt", attempt+1),
|
|
zap.Error(err),
|
|
)
|
|
time.Sleep(retryDelay)
|
|
retryDelay *= 2
|
|
continue
|
|
}
|
|
|
|
// Other errors are non-retryable
|
|
return nil, err
|
|
}
|
|
|
|
return nil, &ClusterError{
|
|
Message: fmt.Sprintf("failed to allocate port block after %d retries", maxRetries),
|
|
}
|
|
}
|
|
|
|
// tryAllocatePortBlock attempts to allocate a port block (single attempt)
|
|
func (npa *NamespacePortAllocator) tryAllocatePortBlock(ctx context.Context, nodeID, namespaceClusterID string) (*PortBlock, error) {
|
|
// Query all allocated port blocks on this node
|
|
type portRow struct {
|
|
PortStart int `db:"port_start"`
|
|
}
|
|
|
|
var allocatedBlocks []portRow
|
|
query := `SELECT port_start FROM namespace_port_allocations WHERE node_id = ? ORDER BY port_start ASC`
|
|
err := npa.db.Query(ctx, &allocatedBlocks, query, nodeID)
|
|
if err != nil {
|
|
return nil, &ClusterError{
|
|
Message: "failed to query allocated ports",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
// Build map of allocated block starts
|
|
allocatedStarts := make(map[int]bool)
|
|
for _, row := range allocatedBlocks {
|
|
allocatedStarts[row.PortStart] = true
|
|
}
|
|
|
|
// Check node capacity
|
|
if len(allocatedBlocks) >= MaxNamespacesPerNode {
|
|
return nil, ErrNodeAtCapacity
|
|
}
|
|
|
|
// Find first available port block
|
|
portStart := -1
|
|
for start := NamespacePortRangeStart; start <= NamespacePortRangeEnd-PortsPerNamespace+1; start += PortsPerNamespace {
|
|
if !allocatedStarts[start] {
|
|
portStart = start
|
|
break
|
|
}
|
|
}
|
|
|
|
if portStart < 0 {
|
|
return nil, ErrNoPortsAvailable
|
|
}
|
|
|
|
// Create port block
|
|
block := &PortBlock{
|
|
ID: uuid.New().String(),
|
|
NodeID: nodeID,
|
|
NamespaceClusterID: namespaceClusterID,
|
|
PortStart: portStart,
|
|
PortEnd: portStart + PortsPerNamespace - 1,
|
|
RQLiteHTTPPort: portStart + 0,
|
|
RQLiteRaftPort: portStart + 1,
|
|
OlricHTTPPort: portStart + 2,
|
|
OlricMemberlistPort: portStart + 3,
|
|
GatewayHTTPPort: portStart + 4,
|
|
AllocatedAt: time.Now(),
|
|
}
|
|
|
|
// Attempt to insert allocation record
|
|
insertQuery := `
|
|
INSERT INTO namespace_port_allocations (
|
|
id, node_id, namespace_cluster_id, port_start, port_end,
|
|
rqlite_http_port, rqlite_raft_port, olric_http_port, olric_memberlist_port, gateway_http_port,
|
|
allocated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
_, err = npa.db.Exec(ctx, insertQuery,
|
|
block.ID,
|
|
block.NodeID,
|
|
block.NamespaceClusterID,
|
|
block.PortStart,
|
|
block.PortEnd,
|
|
block.RQLiteHTTPPort,
|
|
block.RQLiteRaftPort,
|
|
block.OlricHTTPPort,
|
|
block.OlricMemberlistPort,
|
|
block.GatewayHTTPPort,
|
|
block.AllocatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, &ClusterError{
|
|
Message: "failed to insert port allocation",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
return block, nil
|
|
}
|
|
|
|
// DeallocatePortBlock releases a port block when a namespace is deprovisioned
|
|
func (npa *NamespacePortAllocator) DeallocatePortBlock(ctx context.Context, namespaceClusterID, nodeID string) error {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
query := `DELETE FROM namespace_port_allocations WHERE namespace_cluster_id = ? AND node_id = ?`
|
|
_, err := npa.db.Exec(internalCtx, query, namespaceClusterID, nodeID)
|
|
if err != nil {
|
|
return &ClusterError{
|
|
Message: "failed to deallocate port block",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
npa.logger.Info("Port block deallocated",
|
|
zap.String("namespace_cluster_id", namespaceClusterID),
|
|
zap.String("node_id", nodeID),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeallocateAllPortBlocks releases all port blocks for a namespace cluster
|
|
func (npa *NamespacePortAllocator) DeallocateAllPortBlocks(ctx context.Context, namespaceClusterID string) error {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
query := `DELETE FROM namespace_port_allocations WHERE namespace_cluster_id = ?`
|
|
_, err := npa.db.Exec(internalCtx, query, namespaceClusterID)
|
|
if err != nil {
|
|
return &ClusterError{
|
|
Message: "failed to deallocate all port blocks",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
npa.logger.Info("All port blocks deallocated",
|
|
zap.String("namespace_cluster_id", namespaceClusterID),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPortBlock retrieves the port block for a namespace on a specific node
|
|
func (npa *NamespacePortAllocator) GetPortBlock(ctx context.Context, namespaceClusterID, nodeID string) (*PortBlock, error) {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
var blocks []PortBlock
|
|
query := `
|
|
SELECT id, node_id, namespace_cluster_id, port_start, port_end,
|
|
rqlite_http_port, rqlite_raft_port, olric_http_port, olric_memberlist_port, gateway_http_port,
|
|
allocated_at
|
|
FROM namespace_port_allocations
|
|
WHERE namespace_cluster_id = ? AND node_id = ?
|
|
LIMIT 1
|
|
`
|
|
err := npa.db.Query(internalCtx, &blocks, query, namespaceClusterID, nodeID)
|
|
if err != nil {
|
|
return nil, &ClusterError{
|
|
Message: "failed to query port block",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
if len(blocks) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &blocks[0], nil
|
|
}
|
|
|
|
// GetAllPortBlocks retrieves all port blocks for a namespace cluster
|
|
func (npa *NamespacePortAllocator) GetAllPortBlocks(ctx context.Context, namespaceClusterID string) ([]PortBlock, error) {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
var blocks []PortBlock
|
|
query := `
|
|
SELECT id, node_id, namespace_cluster_id, port_start, port_end,
|
|
rqlite_http_port, rqlite_raft_port, olric_http_port, olric_memberlist_port, gateway_http_port,
|
|
allocated_at
|
|
FROM namespace_port_allocations
|
|
WHERE namespace_cluster_id = ?
|
|
ORDER BY port_start ASC
|
|
`
|
|
err := npa.db.Query(internalCtx, &blocks, query, namespaceClusterID)
|
|
if err != nil {
|
|
return nil, &ClusterError{
|
|
Message: "failed to query port blocks",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
return blocks, nil
|
|
}
|
|
|
|
// GetNodeCapacity returns how many more namespace instances a node can host
|
|
func (npa *NamespacePortAllocator) GetNodeCapacity(ctx context.Context, nodeID string) (int, error) {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
type countResult struct {
|
|
Count int `db:"count"`
|
|
}
|
|
|
|
var results []countResult
|
|
query := `SELECT COUNT(*) as count FROM namespace_port_allocations WHERE node_id = ?`
|
|
err := npa.db.Query(internalCtx, &results, query, nodeID)
|
|
if err != nil {
|
|
return 0, &ClusterError{
|
|
Message: "failed to count allocated port blocks",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
return MaxNamespacesPerNode, nil
|
|
}
|
|
|
|
allocated := results[0].Count
|
|
available := MaxNamespacesPerNode - allocated
|
|
|
|
if available < 0 {
|
|
available = 0
|
|
}
|
|
|
|
return available, nil
|
|
}
|
|
|
|
// GetNodeAllocationCount returns the number of namespace instances on a node
|
|
func (npa *NamespacePortAllocator) GetNodeAllocationCount(ctx context.Context, nodeID string) (int, error) {
|
|
internalCtx := client.WithInternalAuth(ctx)
|
|
|
|
type countResult struct {
|
|
Count int `db:"count"`
|
|
}
|
|
|
|
var results []countResult
|
|
query := `SELECT COUNT(*) as count FROM namespace_port_allocations WHERE node_id = ?`
|
|
err := npa.db.Query(internalCtx, &results, query, nodeID)
|
|
if err != nil {
|
|
return 0, &ClusterError{
|
|
Message: "failed to count allocated port blocks",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
return results[0].Count, nil
|
|
}
|
|
|
|
// isConflictError checks if an error is due to a constraint violation
|
|
func isConflictError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
errStr := err.Error()
|
|
return contains(errStr, "UNIQUE") || contains(errStr, "constraint") || contains(errStr, "conflict")
|
|
}
|
|
|
|
// contains checks if a string contains a substring (case-insensitive)
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && findSubstring(s, substr))
|
|
}
|
|
|
|
func findSubstring(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|