feat: P1-3 add heartbeat keep-alive goroutine
- add heartbeat.go with heartbeatIntervalSecs var, SetHeartbeatInterval, runHeartbeat (no build tag, CGO-free) - add heartbeat_test.go with 5 tests covering interval validation and goroutine lifecycle - add StartHeartbeat to clib.go (CGO impl calling xcecli_heartbeat) - remove SetHeartbeatInterval stub from stub.go (now real code in heartbeat.go) - remove TestSetHeartbeatInterval_StubNoOp from stub_funcs_test.go - call SetHeartbeatInterval in OnClusterUpdated to track backend-pushed interval - call backend.StartHeartbeat in cmd/main.go after backend registration
This commit is contained in:
parent
bc3103a6d5
commit
ffc5b20cdb
7 changed files with 125 additions and 12 deletions
|
|
@ -64,6 +64,7 @@ import "C"
|
||||||
import (
|
import (
|
||||||
"apiserver/models"
|
"apiserver/models"
|
||||||
logger "apiserver/utils"
|
logger "apiserver/utils"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
@ -153,6 +154,14 @@ func Cleanup() {
|
||||||
C.idgen_ctx_fini(&gIDCtx)
|
C.idgen_ctx_fini(&gIDCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartHeartbeat launches a goroutine that sends periodic xcecli_heartbeat
|
||||||
|
// keep-alive messages to the controller. Stops when ctx is cancelled.
|
||||||
|
func StartHeartbeat(ctx context.Context) {
|
||||||
|
go runHeartbeat(ctx, func(secs int) {
|
||||||
|
C.xcecli_heartbeat(gCtx.ctx, C.int(secs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// go_push_handler is called by the C library on every incoming request.
|
// go_push_handler is called by the C library on every incoming request.
|
||||||
// It must be exported so the C trampoline can call it from a C thread.
|
// It must be exported so the C trampoline can call it from a C thread.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ func (h *DefaultHandler) OnClusterUpdated(incoming *models.ClusterInfo) {
|
||||||
existing.Vcpus = incoming.Vcpus
|
existing.Vcpus = incoming.Vcpus
|
||||||
existing.InactiveHeartbeatCount = incoming.InactiveHeartbeatCount
|
existing.InactiveHeartbeatCount = incoming.InactiveHeartbeatCount
|
||||||
store.Clusters.Put(existing.ClusterName, existing)
|
store.Clusters.Put(existing.ClusterName, existing)
|
||||||
|
SetHeartbeatInterval(incoming.HeartbeatInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *DefaultHandler) OnResourceUpdated(res *models.ResourceDefinitionInfo) {
|
func (h *DefaultHandler) OnResourceUpdated(res *models.ResourceDefinitionInfo) {
|
||||||
|
|
|
||||||
33
src/frontend/apiserver/backend/heartbeat.go
Normal file
33
src/frontend/apiserver/backend/heartbeat.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// heartbeatIntervalSecs is the current heartbeat period in seconds.
|
||||||
|
// Updated atomically by SetHeartbeatInterval; read atomically by runHeartbeat.
|
||||||
|
var heartbeatIntervalSecs int32 = 30
|
||||||
|
|
||||||
|
// SetHeartbeatInterval updates the heartbeat period.
|
||||||
|
// Values <= 0 are ignored to prevent disabling the keep-alive.
|
||||||
|
func SetHeartbeatInterval(secs int) {
|
||||||
|
if secs > 0 {
|
||||||
|
atomic.StoreInt32(&heartbeatIntervalSecs, int32(secs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runHeartbeat loops until ctx is cancelled, calling fn(intervalSecs) on each tick.
|
||||||
|
// Extracted from clib.go for testability without CGO.
|
||||||
|
func runHeartbeat(ctx context.Context, fn func(int)) {
|
||||||
|
for {
|
||||||
|
interval := int(atomic.LoadInt32(&heartbeatIntervalSecs))
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Duration(interval) * time.Second):
|
||||||
|
fn(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/frontend/apiserver/backend/heartbeat_test.go
Normal file
80
src/frontend/apiserver/backend/heartbeat_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetHeartbeatInterval_ValidValue(t *testing.T) {
|
||||||
|
atomic.StoreInt32(&heartbeatIntervalSecs, 30)
|
||||||
|
SetHeartbeatInterval(60)
|
||||||
|
if got := atomic.LoadInt32(&heartbeatIntervalSecs); got != 60 {
|
||||||
|
t.Errorf("heartbeatIntervalSecs = %d, want 60", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetHeartbeatInterval_ZeroIgnored(t *testing.T) {
|
||||||
|
atomic.StoreInt32(&heartbeatIntervalSecs, 30)
|
||||||
|
SetHeartbeatInterval(0)
|
||||||
|
if got := atomic.LoadInt32(&heartbeatIntervalSecs); got != 30 {
|
||||||
|
t.Errorf("heartbeatIntervalSecs = %d, want 30 (zero should be ignored)", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetHeartbeatInterval_NegativeIgnored(t *testing.T) {
|
||||||
|
atomic.StoreInt32(&heartbeatIntervalSecs, 30)
|
||||||
|
SetHeartbeatInterval(-1)
|
||||||
|
if got := atomic.LoadInt32(&heartbeatIntervalSecs); got != 30 {
|
||||||
|
t.Errorf("heartbeatIntervalSecs = %d, want 30 (negative should be ignored)", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHeartbeat_CallsFunction(t *testing.T) {
|
||||||
|
atomic.StoreInt32(&heartbeatIntervalSecs, 1)
|
||||||
|
|
||||||
|
var callCount int64
|
||||||
|
fn := func(_ int) {
|
||||||
|
atomic.AddInt64(&callCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go runHeartbeat(ctx, fn)
|
||||||
|
|
||||||
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if got := atomic.LoadInt64(&callCount); got < 1 {
|
||||||
|
t.Errorf("fn was called %d times, want at least 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunHeartbeat_CancelsOnContext(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // already cancelled
|
||||||
|
|
||||||
|
var callCount int64
|
||||||
|
fn := func(_ int) {
|
||||||
|
atomic.AddInt64(&callCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
runHeartbeat(ctx, fn)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// returned quickly as expected
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Error("runHeartbeat did not return promptly after context cancellation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := atomic.LoadInt64(&callCount); got != 0 {
|
||||||
|
t.Errorf("fn was called %d times, want 0 for already-cancelled context", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,9 +36,6 @@ func ControlHost(hostname string, action int) error { return errNoCGO }
|
||||||
// StartHeartbeat stub — real implementation in clib.go (requires CGO).
|
// StartHeartbeat stub — real implementation in clib.go (requires CGO).
|
||||||
func StartHeartbeat(_ context.Context) {}
|
func StartHeartbeat(_ context.Context) {}
|
||||||
|
|
||||||
// SetHeartbeatInterval stub — real implementation in clib.go (requires CGO).
|
|
||||||
func SetHeartbeatInterval(_ int) {}
|
|
||||||
|
|
||||||
// EvalSelectForHost stub — real implementation in clib.go (requires CGO).
|
// EvalSelectForHost stub — real implementation in clib.go (requires CGO).
|
||||||
func EvalSelectForHost(_, _ string) error { return errNoCGO }
|
func EvalSelectForHost(_, _ string) error { return errNoCGO }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,6 @@ func TestStartHeartbeat_StubNoOp(t *testing.T) {
|
||||||
StartHeartbeat(context.Background())
|
StartHeartbeat(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSetHeartbeatInterval_StubNoOp verifies the no-CGO stub for
|
|
||||||
// SetHeartbeatInterval is a no-op that does not panic.
|
|
||||||
func TestSetHeartbeatInterval_StubNoOp(t *testing.T) {
|
|
||||||
// Should not panic and completes immediately
|
|
||||||
SetHeartbeatInterval(30)
|
|
||||||
SetHeartbeatInterval(0)
|
|
||||||
SetHeartbeatInterval(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestEvalSelectForHost_StubErrNoCGO verifies the no-CGO stub for
|
// TestEvalSelectForHost_StubErrNoCGO verifies the no-CGO stub for
|
||||||
// EvalSelectForHost returns errNoCGO, blocking callers from using
|
// EvalSelectForHost returns errNoCGO, blocking callers from using
|
||||||
// select expressions without the real CGO backend.
|
// select expressions without the real CGO backend.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"apiserver/store"
|
"apiserver/store"
|
||||||
"apiserver/utils"
|
"apiserver/utils"
|
||||||
logger "apiserver/utils"
|
logger "apiserver/utils"
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -71,6 +72,7 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
logger.Info("********** Backend connection registered!")
|
logger.Info("********** Backend connection registered!")
|
||||||
|
backend.StartHeartbeat(context.Background())
|
||||||
} else {
|
} else {
|
||||||
logger.Info("********** Backend not configured — running in stub mode")
|
logger.Info("********** Backend not configured — running in stub mode")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue