feat: bootstrap supply intelligence baseline
This commit is contained in:
150
internal/control/module.go
Normal file
150
internal/control/module.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ModuleState represents the lifecycle state of a module
|
||||
type ModuleState string
|
||||
|
||||
const (
|
||||
ModuleStateActive ModuleState = "active"
|
||||
ModuleStateClosing ModuleState = "closing"
|
||||
ModuleStateClosed ModuleState = "closed"
|
||||
)
|
||||
|
||||
// ModuleGate controls the enable/disable/close lifecycle of a module
|
||||
type ModuleGate struct {
|
||||
mu sync.RWMutex
|
||||
enabled bool
|
||||
state ModuleState
|
||||
closedAt *time.Time
|
||||
}
|
||||
|
||||
func NewModuleGate(enabled bool) *ModuleGate {
|
||||
return &ModuleGate{enabled: enabled, state: ModuleStateActive}
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the module is accepting new tasks
|
||||
func (g *ModuleGate) IsEnabled() bool {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
return g.enabled && g.state == ModuleStateActive
|
||||
}
|
||||
|
||||
// Close signals the module to stop accepting new tasks
|
||||
func (g *ModuleGate) Close() {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if g.state == ModuleStateActive {
|
||||
g.state = ModuleStateClosing
|
||||
now := time.Now().UTC()
|
||||
g.closedAt = &now
|
||||
}
|
||||
}
|
||||
|
||||
// MarkClosed marks the module as fully closed (no in-flight tasks)
|
||||
func (g *ModuleGate) MarkClosed() {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.state = ModuleStateClosed
|
||||
g.enabled = false
|
||||
}
|
||||
|
||||
// State returns the current module state
|
||||
func (g *ModuleGate) State() ModuleState {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
return g.state
|
||||
}
|
||||
|
||||
// ModuleController manages all module gates
|
||||
type ModuleController struct {
|
||||
probes *ModuleGate
|
||||
discovery *ModuleGate
|
||||
admission *ModuleGate
|
||||
publish *ModuleGate
|
||||
}
|
||||
|
||||
func NewModuleController(enabled bool) *ModuleController {
|
||||
return &ModuleController{
|
||||
probes: NewModuleGate(enabled),
|
||||
discovery: NewModuleGate(enabled),
|
||||
admission: NewModuleGate(enabled),
|
||||
publish: NewModuleGate(enabled),
|
||||
}
|
||||
}
|
||||
|
||||
// ShutdownInitiate closes all modules (stop accepting new tasks)
|
||||
func (c *ModuleController) ShutdownInitiate() {
|
||||
c.probes.Close()
|
||||
c.discovery.Close()
|
||||
c.admission.Close()
|
||||
c.publish.Close()
|
||||
}
|
||||
|
||||
// ShutdownComplete marks all modules as fully closed
|
||||
func (c *ModuleController) ShutdownComplete() {
|
||||
c.probes.MarkClosed()
|
||||
c.discovery.MarkClosed()
|
||||
c.admission.MarkClosed()
|
||||
c.publish.MarkClosed()
|
||||
}
|
||||
|
||||
// IsInflight returns true if any module still has in-flight tasks
|
||||
func (c *ModuleController) IsInflight() bool {
|
||||
return c.probes.State() == ModuleStateClosing ||
|
||||
c.discovery.State() == ModuleStateClosing ||
|
||||
c.admission.State() == ModuleStateClosing ||
|
||||
c.publish.State() == ModuleStateClosing
|
||||
}
|
||||
|
||||
// GetModuleState returns the state of a specific module
|
||||
func (c *ModuleController) GetModuleState(name string) ModuleState {
|
||||
switch name {
|
||||
case "probes":
|
||||
return c.probes.State()
|
||||
case "discovery":
|
||||
return c.discovery.State()
|
||||
case "admission":
|
||||
return c.admission.State()
|
||||
case "publish":
|
||||
return c.publish.State()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Status returns a snapshot of all module states
|
||||
type ModuleStatus struct {
|
||||
Probes ModuleState `json:"probes"`
|
||||
Discovery ModuleState `json:"discovery"`
|
||||
Admission ModuleState `json:"admission"`
|
||||
Publish ModuleState `json:"publish"`
|
||||
}
|
||||
|
||||
func (c *ModuleController) Status() ModuleStatus {
|
||||
return ModuleStatus{
|
||||
Probes: c.probes.State(),
|
||||
Discovery: c.discovery.State(),
|
||||
Admission: c.admission.State(),
|
||||
Publish: c.publish.State(),
|
||||
}
|
||||
}
|
||||
|
||||
// RejectIfNotEnabled returns an error if the module is not enabled
|
||||
func (g *ModuleGate) RejectIfNotEnabled(moduleName string) error {
|
||||
if !g.IsEnabled() {
|
||||
return ErrModuleClosed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrModuleClosed = &ModuleClosedError{}
|
||||
|
||||
type ModuleClosedError struct{}
|
||||
|
||||
func (e *ModuleClosedError) Error() string {
|
||||
return "module is not accepting new tasks"
|
||||
}
|
||||
124
internal/control/module_test.go
Normal file
124
internal/control/module_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestModuleGate_IsEnabled(t *testing.T) {
|
||||
g := NewModuleGate(true)
|
||||
if !g.IsEnabled() {
|
||||
t.Fatal("expected enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleGate_IsDisabled(t *testing.T) {
|
||||
g := NewModuleGate(false)
|
||||
if g.IsEnabled() {
|
||||
t.Fatal("expected disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleGate_Close(t *testing.T) {
|
||||
g := NewModuleGate(true)
|
||||
g.Close()
|
||||
if g.State() != ModuleStateClosing {
|
||||
t.Fatalf("expected closing, got: %s", g.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleGate_MarkClosed(t *testing.T) {
|
||||
g := NewModuleGate(true)
|
||||
g.Close()
|
||||
g.MarkClosed()
|
||||
if g.State() != ModuleStateClosed {
|
||||
t.Fatalf("expected closed, got: %s", g.State())
|
||||
}
|
||||
if g.IsEnabled() {
|
||||
t.Fatal("expected not enabled after closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleGate_RejectIfNotEnabled(t *testing.T) {
|
||||
g := NewModuleGate(true)
|
||||
err := g.RejectIfNotEnabled("test")
|
||||
if err != nil {
|
||||
t.Fatal("expected no error when enabled")
|
||||
}
|
||||
|
||||
g.Close()
|
||||
err = g.RejectIfNotEnabled("test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when closing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleController_ShutdownInitiate(t *testing.T) {
|
||||
c := NewModuleController(true)
|
||||
c.ShutdownInitiate()
|
||||
|
||||
if c.probes.State() != ModuleStateClosing {
|
||||
t.Fatalf("probes should be closing, got: %s", c.probes.State())
|
||||
}
|
||||
if c.discovery.State() != ModuleStateClosing {
|
||||
t.Fatalf("discovery should be closing, got: %s", c.discovery.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleController_ShutdownComplete(t *testing.T) {
|
||||
c := NewModuleController(true)
|
||||
c.ShutdownInitiate()
|
||||
c.ShutdownComplete()
|
||||
|
||||
if c.probes.State() != ModuleStateClosed {
|
||||
t.Fatalf("probes should be closed, got: %s", c.probes.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleController_IsInflight(t *testing.T) {
|
||||
c := NewModuleController(true)
|
||||
c.ShutdownInitiate()
|
||||
|
||||
if !c.IsInflight() {
|
||||
t.Fatal("expected in-flight during closing")
|
||||
}
|
||||
|
||||
c.ShutdownComplete()
|
||||
|
||||
if c.IsInflight() {
|
||||
t.Fatal("expected not in-flight after closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleController_GetModuleState(t *testing.T) {
|
||||
c := NewModuleController(true)
|
||||
|
||||
if c.GetModuleState("probes") != ModuleStateActive {
|
||||
t.Fatalf("expected active, got: %s", c.GetModuleState("probes"))
|
||||
}
|
||||
if c.GetModuleState("unknown") != "" {
|
||||
t.Fatalf("expected empty for unknown module")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleController_Status(t *testing.T) {
|
||||
c := NewModuleController(true)
|
||||
status := c.Status()
|
||||
|
||||
if status.Probes != ModuleStateActive {
|
||||
t.Fatalf("expected active, got: %s", status.Probes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleGate_ClosedAt(t *testing.T) {
|
||||
g := NewModuleGate(true)
|
||||
g.Close()
|
||||
|
||||
if g.State() != ModuleStateClosing {
|
||||
t.Fatal("expected closing state")
|
||||
}
|
||||
|
||||
// closedAt should be set when entering closing state
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
_ = g.closedAt // not nil when closing
|
||||
}
|
||||
Reference in New Issue
Block a user