mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-08-31 08:57:14 +02:00
Replace the free-form service_type string with a ServiceType enum (ccms, ccmc, ccb, cces, ccsa, ccnc, ccem). The short code is the canonical value stored in the DB, the config-tree directory name and a NATS discovery subject token; Description() gives the human-readable name. Add a second scope alongside per-node agents: ScopeInfra covers cluster-independent monitoring infrastructure services, which are identified by (hostname, service_type) and carry an empty cluster. InfraRegistry is the ScopeInfra sibling of Registry and shares the same table, state machine and config-revision handshake. It deliberately exposes no StartSweep: MarkStale ages rows by last_heartbeat regardless of scope, so a single sweep goroutine per process covers both scopes. On the repository side ServiceDB gains a Scope column and the two queries the discovery publisher and infra enumeration need, ListActive() and ListByScope(). ResetConnection() now also resets the fleet repository singleton so tests get a fresh handle. Migration 13 is edited in place rather than superseded by a new migration: the service table is unreleased, so no existing deployment has it yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
5.1 KiB
Go
161 lines
5.1 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package fleet
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
|
)
|
|
|
|
// Scope values distinguish the two kinds of fleet member persisted in the
|
|
// shared "service" table:
|
|
//
|
|
// - ScopeCluster: per-node agents, identified by (cluster, hostname,
|
|
// service_type). This is the default and what Registry issues.
|
|
// - ScopeInfra: cluster-independent monitoring infrastructure services
|
|
// (metric stores, collectors, gateways). They are not tied to a single
|
|
// cluster, so their cluster column is empty and identity is
|
|
// (hostname, service_type). InfraRegistry issues these.
|
|
const (
|
|
ScopeCluster = "cluster"
|
|
ScopeInfra = "infra"
|
|
)
|
|
|
|
// InfraRegistrationRequest is what a cluster-independent monitoring
|
|
// infrastructure service posts to the REST registration endpoint. Unlike
|
|
// RegistrationRequest it has no Cluster field: these services span clusters.
|
|
type InfraRegistrationRequest struct {
|
|
Hostname string
|
|
ServiceType ServiceType
|
|
MetaData map[string]string
|
|
}
|
|
|
|
// InfraRegistry is the business-logic layer for cluster-independent monitoring
|
|
// infrastructure services. It is the ScopeInfra sibling of Registry and shares
|
|
// the same FleetRepository and "service" table; identity issuance, the
|
|
// pending/active/stale/deregistered state machine and the config-revision
|
|
// handshake are identical. The only differences are that registration takes no
|
|
// cluster and discovery is by scope rather than by cluster.
|
|
//
|
|
// Staleness: MarkStale (driven by StartSweep) ages any service by its
|
|
// last_heartbeat regardless of scope, so infra rows are already covered by the
|
|
// sweep. Do NOT start a second sweep goroutine here — exactly one StartSweep
|
|
// across the whole process is sufficient. InfraRegistry therefore deliberately
|
|
// exposes no StartSweep.
|
|
type InfraRegistry struct {
|
|
repo *repository.FleetRepository
|
|
}
|
|
|
|
// NewInfraRegistry returns an InfraRegistry backed by the singleton
|
|
// FleetRepository.
|
|
func NewInfraRegistry() *InfraRegistry {
|
|
return &InfraRegistry{
|
|
repo: repository.GetFleetRepository(),
|
|
}
|
|
}
|
|
|
|
// Register upserts a cluster-independent service's identity by
|
|
// (hostname, service_type) and returns a freshly issued instance_id plus the
|
|
// config_revision currently on record (0 for a never-before-seen service, so
|
|
// the caller knows to pull its initial config).
|
|
func (r *InfraRegistry) Register(req InfraRegistrationRequest) (*Registration, error) {
|
|
if req.Hostname == "" {
|
|
return nil, errors.New("fleet: hostname is required")
|
|
}
|
|
if !req.ServiceType.Valid() {
|
|
return nil, fmt.Errorf("fleet: unknown service_type %q", req.ServiceType)
|
|
}
|
|
|
|
instanceID, err := generateInstanceID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metaJSON, err := marshalMeta(req.MetaData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
svc := &repository.ServiceDB{
|
|
Cluster: "",
|
|
Hostname: req.Hostname,
|
|
ServiceType: string(req.ServiceType),
|
|
InstanceID: instanceID,
|
|
Scope: ScopeInfra,
|
|
RegisteredAt: time.Now().Unix(),
|
|
MetaData: metaJSON,
|
|
}
|
|
|
|
id, err := r.repo.RegisterService(svc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stored, err := r.repo.GetByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cclog.Infof("fleet: registered infra %s/%s as instance '%s'", req.Hostname, req.ServiceType, instanceID)
|
|
return &Registration{InstanceID: instanceID, ConfigRevision: stored.ConfigRevision}, nil
|
|
}
|
|
|
|
// Heartbeat refreshes liveness for an already-registered infra instance. It is
|
|
// a no-op for unknown or deregistered instance IDs — see the package doc for
|
|
// why that must hold when this is reachable over NATS.
|
|
func (r *InfraRegistry) Heartbeat(instanceID string, at time.Time) error {
|
|
affected, err := r.repo.Heartbeat(instanceID, at.Unix())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected == 0 {
|
|
return ErrUnknownInstance
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Deregister marks an infra instance as deregistered. Idempotent.
|
|
func (r *InfraRegistry) Deregister(instanceID string) error {
|
|
return r.repo.Deregister(instanceID)
|
|
}
|
|
|
|
// AckConfig records that instanceID has pulled configRevision. Called by the
|
|
// REST config-pull handler after it serves the config payload.
|
|
func (r *InfraRegistry) AckConfig(instanceID string, configRevision int64) error {
|
|
return r.repo.SetConfigRevision(instanceID, configRevision)
|
|
}
|
|
|
|
// Get returns a single infra service by instance_id.
|
|
func (r *InfraRegistry) Get(instanceID string) (*Service, error) {
|
|
svc, err := r.repo.GetByInstanceID(instanceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return toService(svc)
|
|
}
|
|
|
|
// List returns all registered cluster-independent infrastructure services.
|
|
func (r *InfraRegistry) List() ([]*Service, error) {
|
|
rows, err := r.repo.ListByScope(ScopeInfra)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
services := make([]*Service, 0, len(rows))
|
|
for _, row := range rows {
|
|
svc, err := toService(row)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services = append(services, svc)
|
|
}
|
|
return services, nil
|
|
}
|