v0.90.0 — guest RAM resize (R-24) + fast-tick-until-convergence (R-28)
MinAgent coupling: felhom-controller v0.143.0 gates its guest-memory-resize UI on this agent (FeatureGuestMemoryResize, MinAgent 0.90.0). R-24 guest RAM resize (internal/localapi/guestmemory.go): self-scoped GET/POST /guest/memory. Agent enforces every bound FRESH per request (min 2048, max host_total-2048, shrink floor max(2048, usage+512)); applies via PVE SetConfig — live cgroup apply, no reboot (Phase-0 proven on the nested demo box). Verify-after-apply re-reads maxmem before claiming success. New narrow MemoryOps seam (GuestAPI untouched); Options.Memory nil -> 503. Memory only. R-28 fast-tick (internal/fasttick): while any desired-state item is unapplied - including the pre-tunnel window a hub poke can't reach - pulse the shared out-of-band trigger every 30s, self-disarm on convergence. Four cached sources (desired-gen==0, reconcile Planned-Pending>0, pbsdr waiting_secret only, wgtunnel desired-not-operational); LOUD pbsdr states + pending_signature excluded. Seams: reconcile.Engine.LastResult() + wgtunnel.Manager.TunnelConvergence() (cached, no per-tick exec). Guests-0/0: hypothesis REFUTED live (9201 IS a pool member; 0/0 was the pre-provision window; PoolAddVMID re-assert already covers restore-over-existing). No code change; the fast-tick mitigates the window. Tests + red-proofs (i floor guard, ii max guard, iii always-pulse) all restored green.
This commit is contained in:
@@ -108,6 +108,28 @@ type Manager struct {
|
||||
lastResolvedIP netip.Addr // last A-record we rendered into the conf; invalid → resolve on next apply
|
||||
resolveFailLogged bool // DNS-failure log throttle (reset on the next success)
|
||||
staleSameIPLogged bool // "stale but IP unchanged" log throttle (endpoint-down, not re-IP)
|
||||
|
||||
// conv is the CACHED tunnel-convergence snapshot (v0.90.0, R-28 fast-tick source), refreshed at
|
||||
// the end of every Apply (the tunnel's own cadence) so the fast-tick reads it WITHOUT execing
|
||||
// `wg`/`systemctl` per tick. wgBlockDesired = a wireguard block for THIS key is desired;
|
||||
// operational = registered (marker) AND the unit is active. desired && !operational is exactly
|
||||
// the poke-undeliverable window R-28 exists to close.
|
||||
conv convSnapshot
|
||||
}
|
||||
|
||||
// convSnapshot is the cheap, cached read the fast-tick consumes.
|
||||
type convSnapshot struct {
|
||||
wgBlockDesired bool
|
||||
operational bool
|
||||
}
|
||||
|
||||
// TunnelConvergence returns the cached (desired, operational) snapshot without any exec (fast-tick
|
||||
// source). Zero value (false, false) before the first Apply = "nothing desired yet" = converged for
|
||||
// this source (the never-fetched case is covered by the desired-generation source instead).
|
||||
func (m *Manager) TunnelConvergence() (desired, operational bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.conv.wgBlockDesired, m.conv.operational
|
||||
}
|
||||
|
||||
// NewManager builds a Manager. stateDir is the agent state dir (default /var/lib/felhom-agent —
|
||||
@@ -307,6 +329,20 @@ func (m *Manager) Apply(ctx context.Context, fetched bool, block *hub.WireWiregu
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Refresh the cached fast-tick convergence snapshot at the end of every Apply (this cadence),
|
||||
// so the fast-tick never execs. Runs under the lock, before the Unlock defer (LIFO), covering
|
||||
// every return path. desired = a wg block for this key is wanted; operational = registered + active.
|
||||
defer func() {
|
||||
desired := fetched && block != nil
|
||||
operational := false
|
||||
if desired {
|
||||
if mk := m.loadMarker(); mk != nil {
|
||||
operational = m.isActive(ctx)
|
||||
}
|
||||
}
|
||||
m.conv = convSnapshot{wgBlockDesired: desired, operational: operational}
|
||||
}()
|
||||
|
||||
keyExists := false
|
||||
if _, err := os.Stat(KeyFilePath(m.stateDir)); err == nil {
|
||||
keyExists = true
|
||||
|
||||
Reference in New Issue
Block a user