From d01c6c6a0cd7f732447e831820acf35e6f048a51 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 10 Feb 2026 08:11:08 +0100 Subject: [PATCH 01/11] Fix plog.Logger.WithPrefix not returning a clone of the Logger So calling plog.Logger.WithPrefix() to prepare a new logger to assign would change the original logger, which is inconsistent with Attr(). --- util/plog/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/util/plog/main.go b/util/plog/main.go index 055032b7c..bee99dd2f 100644 --- a/util/plog/main.go +++ b/util/plog/main.go @@ -42,8 +42,9 @@ func NewLogger(logger zerolog.Logger) *Logger { } func (t *Logger) WithPrefix(prefix string) *Logger { - t.prefix = prefix - return t + n := NewLogger(t.logger) + n.prefix = prefix + return n } func (t *Logger) Prefix() string { From a2a06ac0fb268b6897319a67eb1a82d49c8c7e20 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Thu, 12 Feb 2026 18:37:55 +0100 Subject: [PATCH 02/11] Avoid NodeConfigUpdated pubs when no data actually changed ClusterConfigUpdated triggered a NodeConfigUpdated in case the node config defaults to cluster config values. Detect when the node config data did change and skip publication. --- daemon/nmon/main.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/daemon/nmon/main.go b/daemon/nmon/main.go index bda29e819..cba65a545 100644 --- a/daemon/nmon/main.go +++ b/daemon/nmon/main.go @@ -782,12 +782,16 @@ func (t *Manager) loadConfig() error { } func (t *Manager) loadConfigAndPublish() error { + prevNodeConfig := t.nodeConfig + if err := t.loadConfig(); err != nil { return err } - node.ConfigData.Set(t.localhost, t.nodeConfig.DeepCopy()) - t.publisher.Pub(&msgbus.NodeConfigUpdated{Node: t.localhost, Value: t.nodeConfig}, t.labelLocalhost) + if prevNodeConfig != t.nodeConfig { + node.ConfigData.Set(t.localhost, t.nodeConfig.DeepCopy()) + t.publisher.Pub(&msgbus.NodeConfigUpdated{Node: t.localhost, Value: t.nodeConfig}, t.labelLocalhost) + } if stats := node.StatsData.GetByNode(t.localhost); stats != nil && stats.MemTotalMB != 0 { t.updateIsOverloaded(*stats) From dc1266b0ce8e7c3189ac5a3740334d986edda8ee Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Mon, 16 Feb 2026 13:58:40 +0100 Subject: [PATCH 03/11] Add a (*plog.Logger).AddPrefix() method To replace this ugly pattern: prefix := log.Prefix() + "foo: " log := log.WithPrefix(prefix) With simpler: log := log.AddPrefix("foo: ") --- util/plog/main.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/util/plog/main.go b/util/plog/main.go index bee99dd2f..2ec6d9936 100644 --- a/util/plog/main.go +++ b/util/plog/main.go @@ -41,6 +41,12 @@ func NewLogger(logger zerolog.Logger) *Logger { } } +func (t *Logger) AddPrefix(prefix string) *Logger { + n := NewLogger(t.logger) + n.prefix = t.prefix + prefix + return n +} + func (t *Logger) WithPrefix(prefix string) *Logger { n := NewLogger(t.logger) n.prefix = prefix From f97b0e95a6a9705f1010648cc8aac20468fe45e9 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Mon, 16 Feb 2026 14:02:08 +0100 Subject: [PATCH 04/11] Add a log prefix formatter to the schedule.Entry type This prefix is hard to get right, so implement here and optimise the strings manipulation for performance. --- core/schedule/main.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/schedule/main.go b/core/schedule/main.go index 46c14653e..932c3282f 100644 --- a/core/schedule/main.go +++ b/core/schedule/main.go @@ -1,6 +1,7 @@ package schedule import ( + "strings" "time" "github.com/opensvc/om3/v3/core/naming" @@ -129,6 +130,25 @@ func (t Entry) GetNext() (time.Time, time.Duration, error) { return sc.Next(usched.NextWithLast(t.LastRunAt)) } +func (t Entry) LogPrefix() string { + var s strings.Builder + + if t.Path.IsZero() { + s.WriteString("node: ") + } else { + s.WriteString(t.Path.String()) + s.WriteString(": ") + } + + if rid := t.RID(); rid != "DEFAULT" { + s.WriteString(rid) + s.WriteString(": ") + } + s.WriteString(t.Action) + s.WriteString(": ") + return s.String() +} + func (t Entry) RID() string { k := key.Parse(t.Key) return k.Section From 253afebeb8d9f9f913374cc35837c8f4687511c3 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Mon, 16 Feb 2026 14:06:45 +0100 Subject: [PATCH 05/11] Better scheduler logs * remove undue gommon import * remove log entries Attributes that are not index * make sure we log the reason for unschedule and reschedule * remove eventJobRun, no need to make this async * better log prefix --- daemon/scheduler/jobs.go | 2 +- daemon/scheduler/main.go | 362 +++++++++++++++++++++++---------------- 2 files changed, 211 insertions(+), 153 deletions(-) diff --git a/daemon/scheduler/jobs.go b/daemon/scheduler/jobs.go index 23fc855e9..d9ed4dd58 100644 --- a/daemon/scheduler/jobs.go +++ b/daemon/scheduler/jobs.go @@ -80,7 +80,7 @@ func (o *T) action(e schedule.Entry) error { if err := cmd.Run(); err != nil { duration := time.Now().Sub(startTime) o.publisher.Pub(&msgbus.ExecFailed{Command: cmd.String(), Duration: duration, ErrS: err.Error(), Node: o.localhost, Origin: "scheduler", SessionID: sid}, labels...) - o.log.Attr("cmd", cmd.String()).Errorf("%s: %s", cmd, err) + o.log.Errorf("%s: %s", cmd, err) return err } duration := time.Now().Sub(startTime) diff --git a/daemon/scheduler/main.go b/daemon/scheduler/main.go index cff6635c8..240de6b23 100644 --- a/daemon/scheduler/main.go +++ b/daemon/scheduler/main.go @@ -10,8 +10,6 @@ import ( "sync" "time" - "github.com/labstack/gommon/log" - "github.com/opensvc/om3/v3/core/driver" "github.com/opensvc/om3/v3/core/instance" "github.com/opensvc/om3/v3/core/kwoption" @@ -43,8 +41,8 @@ type ( databus *daemondata.T publisher pubsub.Publisher - events chan any jobs Jobs + events chan any enabled bool provisioned map[naming.Path]bool failover map[naming.Path]bool @@ -73,18 +71,22 @@ type ( // // This map is updated from the InstanceStatusUpdated events // received via ObjectStatusUpdated.SrcEv for the local node. - reqSatisfied boolMap + reqSatisfied errMap } Schedules map[naming.Path]map[string]schedule.Entry - Jobs map[string]Job - Job struct { + // Job is a schedule entries with alarms set. + Job struct { CreatedAt time.Time LastRunAt time.Time schedule schedule.Entry cancel []func() } + + // Jobs is a map of Job + Jobs map[string]Job + eventJobAlarm struct { schedule schedule.Entry } @@ -92,13 +94,9 @@ type ( schedule schedule.Entry end time.Time } - eventJobRun struct { - schedule schedule.Entry - begin time.Time - } timeMap map[string]time.Time - boolMap map[string]bool + errMap map[string]error ) var ( @@ -111,7 +109,7 @@ var ( } ) -func (t Schedules) DelByPath(path naming.Path) { +func (t Schedules) Del(path naming.Path) { delete(t, path) } @@ -154,7 +152,7 @@ func New(subQS pubsub.QueueSizer, opts ...funcopt.O) *T { provisioned: make(map[naming.Path]bool), subQS: subQS, lastRunOnAllPeers: make(timeMap), - reqSatisfied: make(boolMap), + reqSatisfied: make(errMap), status: daemonsubsystem.Scheduler{ Status: daemonsubsystem.Status{CreatedAt: time.Now(), ID: "scheduler"}, @@ -172,6 +170,16 @@ func newJobId(e schedule.Entry) string { return fmt.Sprintf("%s:%s", e.Path, e.Key) } +func (t Jobs) PathIds(path naming.Path) []string { + var l []string + for id, job := range t { + if job.schedule.Path == path { + l = append(l, id) + } + } + return l +} + func (t Jobs) Table(path naming.Path) schedule.Table { table := make(schedule.Table, 0) for _, job := range t { @@ -217,8 +225,7 @@ func (t Jobs) Done(e schedule.Entry) Job { return job } -func (t Jobs) Del(e schedule.Entry) { - jobId := newJobId(e) +func (t Jobs) DelId(jobId string) { job, ok := t[jobId] if !ok { return @@ -227,6 +234,11 @@ func (t Jobs) Del(e schedule.Entry) { delete(t, jobId) } +func (t Jobs) Del(e schedule.Entry) { + jobId := newJobId(e) + t.DelId(jobId) +} + func (t Jobs) DelPath(p naming.Path) { for _, e := range t { if e.schedule.Path != p { @@ -293,7 +305,7 @@ func (t *T) createJob(e schedule.Entry) { return } if e.Require != "" { - if isSatisfied, ok := t.reqSatisfied.Get(e.Path, e.Key); !ok || !isSatisfied { + if satisfied, ok := t.reqSatisfied.Get(e.Path, e.Key); !ok || satisfied != nil { return } } @@ -312,7 +324,7 @@ func (t *T) createJob(e schedule.Entry) { now := time.Now() // keep before GetNext call next, _, err := e.GetNext() if err != nil { - logger.Attr("definition", e.Schedule).Warnf("failed to find a next date: %s", err) + logger.Warnf("failed to find a next date: %s", err) t.jobs.Del(e) return } @@ -335,20 +347,8 @@ func (t *T) createJob(e schedule.Entry) { } func (t *T) jobLogger(e schedule.Entry) *plog.Logger { - logger := naming.LogWithPath(t.log, e.Path).Attr("action", e.Action).Attr("key", e.Key) - var obj string - if e.Path.IsZero() { - obj = "node" - } else { - obj = e.Path.String() - } - var prefix string - if rid := e.RID(); rid != "DEFAULT" { - prefix = fmt.Sprintf("%s%s: %s: %s: ", t.log.Prefix(), obj, rid, e.Action) - } else { - prefix = fmt.Sprintf("%s%s: %s: ", t.log.Prefix(), obj, e.Action) - } - return logger.WithPrefix(prefix) + logger := naming.LogWithPath(t.log, e.Path) + return logger.AddPrefix(e.LogPrefix()) } func (t *T) isFailover(path naming.Path) bool { @@ -365,31 +365,31 @@ func (t *T) onJobAlarm(c eventJobAlarm) { logger := t.jobLogger(c.schedule) e, ok := t.schedules.Get(c.schedule.Path, c.schedule.Key) if !ok { - logger.Infof("aborted, schedule is gone") + logger.Infof("abort (schedule deleted)") return } if e.RequireCollector && !t.isCollectorJoinable { - logger.Infof("aborted, the collector is not joinable") + logger.Infof("abort (collector not joinable)") return } if !e.Path.IsZero() { if e.RequireProvisioned && !t.isProvisioned(e.Path) { - logger.Infof("%s: aborted, the object is no longer provisioned", e.RID()) + logger.Infof("abort (no longer provisioned)") return } - if isSatisfied, ok := t.reqSatisfied.Get(e.Path, e.Key); ok { - if !isSatisfied { - log.Infof("%s: aborted, requirements no longer met", e.RID()) + if satisfied, ok := t.reqSatisfied.Get(e.Path, e.Key); ok { + if satisfied != nil { + logger.Tracef("abort (requirements no longer met)") return } } else if e.Require != "" { - log.Infof("%s: aborted, requirements not yet evaluated", e.RID()) + logger.Infof("abort (requirements not yet evaluated)") return } } if tm := t.peerInstanceLastRun(e); c.schedule.LastRunAt.Before(tm) { - logger.Infof("aborted, job ran on peer at %s", tm) + logger.Infof("abort (job ran on peer at %s)", tm) t.recreateJobFrom(e, tm) return } @@ -404,26 +404,34 @@ func (t *T) onJobAlarm(c eventJobAlarm) { logger.Warnf("%s", err) return } else if n >= e.MaxParallel { - logger.Infof("aborted, %d/%d jobs already running", n, e.MaxParallel) + logger.Infof("abort (%d/%d jobs already running)", n, e.MaxParallel) return } + + // Update the by-node last run cache + t.lastRunOnAllPeers.Set(e.Path, e.Key, e.NextRunAt) + + // Update the job last run date + jobId := newJobId(e) + job, ok := t.jobs[jobId] + if ok { + job.LastRunAt = c.schedule.NextRunAt + t.jobs[jobId] = job + } + go func() { - t.events <- eventJobRun{ - schedule: e, - begin: c.schedule.NextRunAt, - } if err := t.action(e); err != nil { - logger.Errorf("on exec %s: %s", e.Key, err) + logger.Errorf("on exec: %s", err) } else { // remember last success, for users benefit if err := e.SetLastSuccess(c.schedule.NextRunAt); err != nil { - logger.Errorf("on update last success %s: %s", e.Key, err) + logger.Errorf("on update last success: %s", err) } } // remember last run, to not run the job too soon after a daemon restart if err := e.SetLastRun(c.schedule.NextRunAt); err != nil { - logger.Errorf("on update last run %s: %s", e.Key, err) + logger.Errorf("on update last run: %s", err) } t.events <- eventJobDone{ @@ -437,10 +445,9 @@ func (t *T) runningCount(e schedule.Entry) (int, error) { if e.RunDir == "" { return -1, nil } - logger := t.jobLogger(e) dir := runfiles.Dir{ Path: e.RunDir, - Log: logger, + Log: t.jobLogger(e), } n, err := dir.Count() if err != nil { @@ -474,7 +481,6 @@ func (t *T) Stop() error { func (t *T) startSubscriptions() *pubsub.Subscription { sub := pubsub.SubFromContext(t.ctx, "daemon.scheduler", t.subQS) labelLocalhost := pubsub.Label{"node", t.localhost} - sub.AddFilter(&msgbus.InstanceConfigUpdated{}, labelLocalhost) sub.AddFilter(&msgbus.InstanceStatusDeleted{}, labelLocalhost) sub.AddFilter(&msgbus.ObjectStatusDeleted{}, labelLocalhost) sub.AddFilter(&msgbus.ObjectStatusUpdated{}, labelLocalhost) @@ -520,8 +526,6 @@ func (t *T) loop() { select { case ev := <-sub.C: switch c := ev.(type) { - case *msgbus.InstanceConfigUpdated: - t.onInstanceConfigUpdated(c) case *msgbus.InstanceStatusDeleted: t.onInstanceStatusDeleted(c) case *msgbus.NodeMonitorUpdated: @@ -541,8 +545,6 @@ func (t *T) loop() { t.onJobAlarm(c) case eventJobDone: t.onJobDone(c) - case eventJobRun: - t.onJobRun(c) default: t.log.Errorf("received an unsupported event: %#v", c) } @@ -553,17 +555,6 @@ func (t *T) loop() { } } -func (t *T) onJobRun(c eventJobRun) { - t.lastRunOnAllPeers.Set(c.schedule.Path, c.schedule.Key, c.schedule.NextRunAt) - jobId := newJobId(c.schedule) - job, ok := t.jobs[jobId] - if !ok { - return - } - job.LastRunAt = c.begin - t.jobs[jobId] = job -} - func (t *T) onJobDone(c eventJobDone) { job := t.jobs.Done(c.schedule) t.recreateJobFrom(c.schedule, job.LastRunAt) @@ -581,7 +572,7 @@ func (t *T) recreateJobFrom(prev schedule.Entry, lastRunAt time.Time) { } func (t *T) onInstanceStatusDeleted(c *msgbus.InstanceStatusDeleted) { - t.loggerWithPath(c.Path).Infof("%s: unschedule all jobs (instance deleted)", c.Path) + t.loggerWithPath(c.Path).Infof("unschedule all jobs (instance deleted)") t.unschedule(c.Path) } @@ -612,35 +603,32 @@ func (t *T) onLocalInstanceStatusUpdated(c *msgbus.InstanceStatusUpdated) bool { return nil } - log := t.loggerWithPath(c.Path) changed := false for _, e := range schedules { if e.Require == "" { continue } + log := t.jobLogger(e) reqs := resourcereqs.New(e.Require) for requiredRID, requiredStatusList := range reqs.Requirements() { - err := checkReq(requiredRID, requiredStatusList) + satisfied := checkReq(requiredRID, requiredStatusList) currentlySatisfied, ok := t.reqSatisfied.Get(c.Path, e.Key) - if err != nil { + t.reqSatisfied.Set(c.Path, e.Key, satisfied) + if satisfied != nil { if !ok { - t.reqSatisfied.Set(c.Path, e.Key, false) - log.Infof("%s: %s: requirement unsatisfied: %s", c.Path, e.RID(), err) + log.Tracef("requirement unsatisfied: %s", satisfied) changed = true - } else if currentlySatisfied { - t.reqSatisfied.Set(c.Path, e.Key, false) - log.Infof("%s: %s: requirement no longer satisfied: %s", c.Path, e.RID(), err) + } else if currentlySatisfied == nil { + log.Tracef("requirement no longer satisfied: %s", satisfied) changed = true } } else { if !ok { - t.reqSatisfied.Set(c.Path, e.Key, true) - log.Infof("%s: %s: requirement satisfied", c.Path, e.RID()) + log.Tracef("requirement satisfied: %s", e.Require) changed = true - } else if !currentlySatisfied { - t.reqSatisfied.Set(c.Path, e.Key, true) - log.Infof("%s: %s: requirement now satisfied", c.Path, e.RID()) + } else if currentlySatisfied != nil { + log.Tracef("requirement now satisfied: %s", e.Require) changed = true } } @@ -654,8 +642,9 @@ func (t *T) onPeerInstanceStatusUpdated(c *msgbus.InstanceStatusUpdated) bool { // we don't have a local instance return false } - log := t.loggerWithPath(c.Path) + pathLog := t.loggerWithPath(c.Path) for rid, r := range c.Value.Resources { + log := pathLog.AddPrefix(rid + ": ") resourceId, err := resourceid.Parse(rid) if err != nil { continue @@ -668,7 +657,7 @@ func (t *T) onPeerInstanceStatusUpdated(c *msgbus.InstanceStatusUpdated) bool { } if _, ok := t.lastRunOnAllPeers.GetWithRID(c.Path, rid); !ok { if tm, nodename, err := t.readLastRunOnFile(c.Path, rid); err == nil { - log.Infof("%s: %s: initialize last run at %s on %s", c.Path, rid, tm, nodename) + log.Infof("initialize last run at %s on %s", tm, nodename) t.lastRunOnAllPeers.SetWithRID(c.Path, rid, tm) } } @@ -691,13 +680,13 @@ func (t *T) onPeerInstanceStatusUpdated(c *msgbus.InstanceStatusUpdated) bool { continue } if err := t.updateLastRunOnFile(c.Path, rid, c.Node, lastRunAtOnPeer); err != nil { - log.Warnf("%s: %s: write last run on file: %s", c.Path, rid, err) + log.Warnf("write last run on file: %s", err) } - cachedLastRunAtOnPeer, ok := t.lastRunOnAllPeers.GetWithRID(c.Path, rid) + lastestRunAtOnPeer, ok := t.lastRunOnAllPeers.GetWithRID(c.Path, rid) - if !ok || lastRunAtOnPeer.After(cachedLastRunAtOnPeer) { - log.Tracef("%s: %s: last run on peer %s at %s", c.Path, rid, c.Node, lastRunAtOnPeer) + if !ok || lastRunAtOnPeer.After(lastestRunAtOnPeer) { + log.Tracef("last run on peer %s at %s", c.Node, lastRunAtOnPeer) t.lastRunOnAllPeers.SetWithRID(c.Path, rid, lastRunAtOnPeer) } } @@ -769,12 +758,7 @@ func (t *T) onDaemonCollectorUpdated(c *msgbus.DaemonCollectorUpdated) { t.isCollectorJoinable = false if previousIsCollectorJoinable { t.log.Infof("disable jobs requiring a joinable collector") - for key, job := range t.jobs { - if job.schedule.RequireCollector { - job.Cancel() - delete(t.jobs, key) - } - } + t.unscheduleRequireCollector() t.scheduleAll() } } @@ -791,39 +775,69 @@ func (t *T) onObjectStatusUpdated(c *msgbus.ObjectStatusUpdated) { if c.Value.ActorStatus == nil { return } - changed := false - if srcEv, ok := c.SrcEv.(*msgbus.InstanceStatusUpdated); ok { + var changed bool + switch srcEv := c.SrcEv.(type) { + case *msgbus.InstanceStatusUpdated: if t.onInstanceStatusUpdated(srcEv) { changed = true } + case *msgbus.InstanceConfigUpdated: + if t.onInstanceConfigUpdated(srcEv) { + changed = true + } } if c.Value.Provisioned == provisioned.Undef { delete(t.provisioned, c.Path) return } - t.failover[c.Path] = c.Value.Topology == topology.Failover - isProvisioned := c.Value.Provisioned.IsOneOf(provisioned.True, provisioned.NotApplicable) - wasProvisioned, ok := t.provisioned[c.Path] - t.provisioned[c.Path] = isProvisioned - if !ok || (isProvisioned != wasProvisioned) || changed { + if t.updateFailover(c.Path, c.Value.Topology) { + changed = true + } + if t.updateProvisioned(c.Path, c.Value.Provisioned) { + changed = true + } + if changed { t.scheduleObject(c.Path) } } +func (t *T) updateFailover(path naming.Path, state topology.T) bool { + wasFailover, ok := t.failover[path] + isFailover := state == topology.Failover + t.failover[path] = isFailover + if !ok || isFailover != wasFailover { + return true + } + return false +} + +func (t *T) updateProvisioned(path naming.Path, state provisioned.T) bool { + isProvisioned := state.IsOneOf(provisioned.True, provisioned.NotApplicable) + wasProvisioned, ok := t.provisioned[path] + t.provisioned[path] = isProvisioned + if !ok || isProvisioned != wasProvisioned { + return true + } + return false +} + func (t *T) loggerWithPath(path naming.Path) *plog.Logger { - return naming.LogWithPath(t.log, path) + return naming.LogWithPath(t.log, path).AddPrefix(fmt.Sprintf("%s: ", path)) } -func (t *T) onInstanceConfigUpdated(c *msgbus.InstanceConfigUpdated) { +func (t *T) onInstanceConfigUpdated(c *msgbus.InstanceConfigUpdated) bool { if c.Value.ActorConfig == nil { - return + return false + } + if c.Node != t.localhost { + return false } switch { case t.enabled: - t.loggerWithPath(c.Path).Infof("%s: update schedules", c.Path) - t.unschedule(c.Path) - t.scheduleObject(c.Path) + t.loggerWithPath(c.Path).Tracef("update schedules") + return true } + return false } func (t *T) onNodeConfigUpdated(c *msgbus.NodeConfigUpdated) { @@ -840,8 +854,7 @@ func (t *T) onNodeConfigUpdated(c *msgbus.NodeConfigUpdated) { } switch { case t.enabled: - t.log.Infof("node: update schedules") - t.unschedule(naming.Path{}) + t.log.Tracef("node: update schedules") t.scheduleNode() } } @@ -885,14 +898,6 @@ func (t *T) scheduleAll() { t.scheduleNode() } -func (t *T) schedule(p naming.Path) { - if p.IsZero() { - t.scheduleNode() - } else { - t.scheduleObject(p) - } -} - func (t *T) scheduleNode() { if !t.enabled { return @@ -904,20 +909,19 @@ func (t *T) scheduleNode() { } path := naming.Path{} - table := o.Schedules() - defer t.updateExposedSchedules(path) - for _, e := range table { + for _, e := range o.Schedules() { t.schedules.Add(path, e) t.createJob(e) } + + t.updateExposedSchedules(path) } func (t *T) scheduleObject(path naming.Path) { if !t.enabled { return } - log := t.loggerWithPath(path).WithPrefix(t.log.Prefix() + path.String() + ": ") instanceConfig := instance.ConfigData.GetByPathAndNode(path, t.localhost) if instanceConfig == nil || instanceConfig.ActorConfig == nil || instanceConfig.Schedules == nil || len(instanceConfig.Schedules) == 0 { @@ -925,63 +929,108 @@ func (t *T) scheduleObject(path naming.Path) { return } - defer t.updateExposedSchedules(path) - isProvisioned, hasProvisioned := t.provisioned[path] + jobIds := make(map[string]any) - for _, scheduleConfig := range instanceConfig.Schedules { + scheduleOne := func(scheduleConfig schedule.Config) { e := schedule.Entry{ Node: t.localhost, Path: path, Config: scheduleConfig, } + + // Remember we saw that job id, so we can purge unconfigured jobs after this loop + jobIds[newJobId(e)] = nil + + log := t.jobLogger(e) + prevSchedule, hasSchedule := t.schedules.Get(path, e.Key) + hasJob := t.jobs.Has(e) t.schedules.Add(path, e) - if e.Schedule == "" || e.Schedule == "@0" { - if t.jobs.Has(e) { - log.Infof("%s: unschedule %s (schedule is now @0)", e.RID(), e.Action) - t.jobs.Del(e) + var validated []string + var skipped string + + if hasSchedule && (scheduleConfig.Schedule != prevSchedule.Config.Schedule) { + if hasJob { + if e.Schedule == "" { + log.Infof("unschedule (schedule is now @0)") + t.jobs.Del(e) + return + } else { + // At the end of this func, if we did not recreate the job, + // log it as unscheduled + defer func() { + if !t.jobs.Has(e) { + log.Infof("unschedule on config change, skip reschedule (%s)", skipped) + } + }() + t.jobs.Del(e) + } } - continue } + validated = append(validated, e.Schedule) + if e.RequireProvisioned { if !hasProvisioned { - log.Infof("%s: skip schedule %s (instance provisioned state is still unknown)", e.RID(), e.Action) - t.jobs.Del(e) - continue + if hasJob { + log.Infof("unschedule (instance provisioned state is still unknown)") + t.jobs.Del(e) + } else { + skipped = "instance provisioned state is still unknown" + } + return } if !isProvisioned { - if t.jobs.Has(e) { - log.Infof("%s: unschedule %s (instance no longer provisionned)", e.RID(), e.Action) + if hasJob { + log.Infof("unschedule (instance no longer provisionned)") t.jobs.Del(e) } else { - log.Infof("%s: skip schedule %s (instance not provisioned)", e.RID(), e.Action) + skipped = "instance no longer provisionned" } - continue + return } + validated = append(validated, "provisioned") } - if isSatisfied, ok := t.reqSatisfied.Get(path, e.Key); ok { - if !isSatisfied { - if t.jobs.Has(e) { - log.Infof("%s: unschedule %s (requirements no longer met)", e.RID(), e.Action) + if satisfied, ok := t.reqSatisfied.Get(path, e.Key); ok { + if satisfied != nil { + if hasJob { + log.Infof("unschedule (%s)", satisfied) t.jobs.Del(e) } else { - log.Infof("%s: skip schedule %s (requirements not met)", e.RID(), e.Action) + skipped = satisfied.Error() } - continue + return } - + validated = append(validated, fmt.Sprintf("require %s is satisfied", e.Require)) } else if e.Require != "" { - if t.jobs.Has(e) { - log.Infof("%s: unschedule %s (requirements not yet evaluated)", e.RID(), e.Action) + if hasJob { + log.Infof("unschedule (require %s not yet evaluated)", e.Require) t.jobs.Del(e) } else { - log.Infof("%s: skip schedule %s (requirements not yet evaluated)", e.RID(), e.Action) + skipped = fmt.Sprintf("require %s not yet evaluated", e.Require) } - continue + return } t.schedules.Add(path, e) - t.createJob(e) + if !t.jobs.Has(e) { + log.Infof("schedule (%s)", strings.Join(validated, ", ")) + t.createJob(e) + } } + + for _, scheduleConfig := range instanceConfig.Schedules { + scheduleOne(scheduleConfig) + } + + // Purge unconfigured jobs + for _, id := range t.jobs.PathIds(path) { + if _, ok := jobIds[id]; !ok { + job := t.jobs[id] + t.jobLogger(job.schedule).Infof("unschedule (no longer configured)") + t.jobs.DelId(id) + } + } + + t.updateExposedSchedules(path) } func (t *T) updateExposedSchedules(path naming.Path) { @@ -993,9 +1042,18 @@ func (t *T) updateExposedSchedules(path naming.Path) { schedule.TableData.Set(path, &table) } +func (t *T) unscheduleRequireCollector() { + for key, job := range t.jobs { + if job.schedule.RequireCollector { + job.Cancel() + delete(t.jobs, key) + } + } +} + func (t *T) unschedule(path naming.Path) { t.reqSatisfied.UnsetPath(path) - t.schedules.DelByPath(path) + t.schedules.Del(path) t.jobs.DelPath(path) } @@ -1005,27 +1063,27 @@ func (t *T) publishUpdate() { t.publisher.Pub(&msgbus.DaemonSchedulerUpdated{Node: t.localhost, Value: *t.status.DeepCopy()}, pubsub.Label{"node", t.localhost}) } -func (t boolMap) key(path naming.Path, s string) string { +func (t errMap) key(path naming.Path, s string) string { return fmt.Sprintf("%s:%s", path.String(), s) } -func (t boolMap) Get(path naming.Path, s string) (bool, bool) { +func (t errMap) Get(path naming.Path, s string) (error, bool) { id := t.key(path, s) v, ok := t[id] return v, ok } -func (t boolMap) Set(path naming.Path, s string, v bool) { +func (t errMap) Set(path naming.Path, s string, err error) { id := t.key(path, s) - t[id] = v + t[id] = err } -func (t boolMap) Unset(path naming.Path, s string) { +func (t errMap) Unset(path naming.Path, s string) { id := t.key(path, s) delete(t, id) } -func (t boolMap) UnsetPath(path naming.Path) { +func (t errMap) UnsetPath(path naming.Path) { prefix := t.key(path, "") for k, _ := range t { if strings.HasPrefix(k, prefix) { From bd575ffc9115063a0f4122dec9b9d7b42fc04776 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 17 Feb 2026 09:21:44 +0100 Subject: [PATCH 06/11] Add the schedule table to node.Config So the scheduler is notified of schedules changes. Also fix a undue reschedule message on daemon start for tasks requiring a joinable collector. --- core/node/config.go | 64 +++++++++++++----- core/object/node_print_schedule.go | 4 -- daemon/daemondata/data_init.go | 2 + daemon/msgbus/node_config.go | 2 +- daemon/nmon/main.go | 2 +- daemon/nmon/main_cmd.go | 6 ++ daemon/scheduler/main.go | 100 ++++++++++++++++++++++++++--- 7 files changed, 149 insertions(+), 31 deletions(-) diff --git a/core/node/config.go b/core/node/config.go index 705692de4..08fa52601 100644 --- a/core/node/config.go +++ b/core/node/config.go @@ -1,25 +1,59 @@ package node -import "time" +import ( + "time" + + "github.com/opensvc/om3/v3/core/schedule" +) type ( Config struct { - Env string `json:"env"` - MaintenanceGracePeriod time.Duration `json:"maintenance_grace_period"` - MaxParallel int `json:"max_parallel"` - MaxKeySize int64 `json:"max_key_size"` - MinAvailMemPct int `json:"min_avail_mem_pct"` - MinAvailSwapPct int `json:"min_avail_swap_pct"` - ReadyPeriod time.Duration `json:"ready_period"` - RejoinGracePeriod time.Duration `json:"rejoin_grace_period"` - SplitAction string `json:"split_action"` - SSHKey string `json:"sshkey"` - PRKey string `json:"prkey"` + Env string `json:"env"` + MaintenanceGracePeriod time.Duration `json:"maintenance_grace_period"` + MaxParallel int `json:"max_parallel"` + MaxKeySize int64 `json:"max_key_size"` + MinAvailMemPct int `json:"min_avail_mem_pct"` + MinAvailSwapPct int `json:"min_avail_swap_pct"` + ReadyPeriod time.Duration `json:"ready_period"` + RejoinGracePeriod time.Duration `json:"rejoin_grace_period"` + Schedules []schedule.Config `json:"schedules"` + SplitAction string `json:"split_action"` + SSHKey string `json:"sshkey"` + PRKey string `json:"prkey"` } ) -func (t *Config) DeepCopy() *Config { - var data Config = *t - return &data +func (cfg *Config) DeepCopy() *Config { + newCfg := *cfg + newCfg.Schedules = append([]schedule.Config{}, cfg.Schedules...) + return &newCfg + +} + +func (c Config) Equals(other Config) bool { + if c.Env != other.Env || + c.MaintenanceGracePeriod != other.MaintenanceGracePeriod || + c.MaxParallel != other.MaxParallel || + c.MaxKeySize != other.MaxKeySize || + c.MinAvailMemPct != other.MinAvailMemPct || + c.MinAvailSwapPct != other.MinAvailSwapPct || + c.ReadyPeriod != other.ReadyPeriod || + c.RejoinGracePeriod != other.RejoinGracePeriod || + c.SplitAction != other.SplitAction || + c.SSHKey != other.SSHKey || + c.PRKey != other.PRKey { + return false + } + + // Compare Schedules slice + if len(c.Schedules) != len(other.Schedules) { + return false + } + for i := range c.Schedules { + if c.Schedules[i] != other.Schedules[i] { + return false + } + } + return true } diff --git a/core/object/node_print_schedule.go b/core/object/node_print_schedule.go index 0a9456fb0..e0f089438 100644 --- a/core/object/node_print_schedule.go +++ b/core/object/node_print_schedule.go @@ -6,7 +6,6 @@ import ( "time" "github.com/opensvc/om3/v3/core/driver" - "github.com/opensvc/om3/v3/core/resource" "github.com/opensvc/om3/v3/core/resourceid" "github.com/opensvc/om3/v3/core/schedule" "github.com/opensvc/om3/v3/util/file" @@ -63,9 +62,6 @@ func (t *Node) Schedules() schedule.Table { t.newScheduleEntry("sysreport", "sysreport", "", "sysreport_push"), t.newScheduleEntry("dequeue_actions", "dequeue_actions", "", "dequeue_actions_push"), ) - type scheduleOptioner interface { - ScheduleOptions() resource.ScheduleOptions - } for _, s := range t.config.SectionStrings() { rid, err := resourceid.Parse(s) if err != nil { diff --git a/daemon/daemondata/data_init.go b/daemon/daemondata/data_init.go index 0dae3c9cb..7d390b7af 100644 --- a/daemon/daemondata/data_init.go +++ b/daemon/daemondata/data_init.go @@ -11,6 +11,7 @@ import ( "github.com/opensvc/om3/v3/core/node" "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/core/rawconfig" + "github.com/opensvc/om3/v3/core/schedule" "github.com/opensvc/om3/v3/daemon/daemonsubsystem" "github.com/opensvc/om3/v3/daemon/msgbus" "github.com/opensvc/om3/v3/util/file" @@ -85,6 +86,7 @@ func newNodeData(localNode string) node.Node { MaxParallel: object.DefaultNodeMaxParallel, MinAvailMemPct: 0, MinAvailSwapPct: 0, + Schedules: make([]schedule.Config, 0), }, Instance: map[string]instance.Instance{}, Monitor: node.Monitor{ diff --git a/daemon/msgbus/node_config.go b/daemon/msgbus/node_config.go index 5c37f1488..e91958288 100644 --- a/daemon/msgbus/node_config.go +++ b/daemon/msgbus/node_config.go @@ -6,7 +6,7 @@ import "github.com/opensvc/om3/v3/util/pubsub" func (data *ClusterData) onNodeConfigUpdated(m *NodeConfigUpdated) { newConfig := m.Value v := data.Cluster.Node[m.Node] - if v.Config == newConfig { + if v.Config.Equals(newConfig) { return } v.Config = m.Value diff --git a/daemon/nmon/main.go b/daemon/nmon/main.go index cba65a545..728357d4a 100644 --- a/daemon/nmon/main.go +++ b/daemon/nmon/main.go @@ -788,7 +788,7 @@ func (t *Manager) loadConfigAndPublish() error { return err } - if prevNodeConfig != t.nodeConfig { + if !prevNodeConfig.Equals(t.nodeConfig) { node.ConfigData.Set(t.localhost, t.nodeConfig.DeepCopy()) t.publisher.Pub(&msgbus.NodeConfigUpdated{Node: t.localhost, Value: t.nodeConfig}, t.labelLocalhost) } diff --git a/daemon/nmon/main_cmd.go b/daemon/nmon/main_cmd.go index 5fde0ccc2..46c70a80c 100644 --- a/daemon/nmon/main_cmd.go +++ b/daemon/nmon/main_cmd.go @@ -11,6 +11,7 @@ import ( "github.com/opensvc/om3/v3/core/clusternode" "github.com/opensvc/om3/v3/core/node" + "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/core/rawconfig" "github.com/opensvc/om3/v3/daemon/msgbus" "github.com/opensvc/om3/v3/util/errcontext" @@ -108,6 +109,11 @@ func (t *Manager) getNodeConfig() node.Config { cfg.MaxParallel = MinMaxParallel } + node, _ := object.NewNode(object.WithVolatile(true)) + for _, e := range node.Schedules() { + cfg.Schedules = append(cfg.Schedules, e.Config) + } + return cfg } diff --git a/daemon/scheduler/main.go b/daemon/scheduler/main.go index 240de6b23..a727818bc 100644 --- a/daemon/scheduler/main.go +++ b/daemon/scheduler/main.go @@ -15,7 +15,6 @@ import ( "github.com/opensvc/om3/v3/core/kwoption" "github.com/opensvc/om3/v3/core/naming" "github.com/opensvc/om3/v3/core/node" - "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/core/provisioned" "github.com/opensvc/om3/v3/core/resourceid" "github.com/opensvc/om3/v3/core/resourcereqs" @@ -834,7 +833,7 @@ func (t *T) onInstanceConfigUpdated(c *msgbus.InstanceConfigUpdated) bool { } switch { case t.enabled: - t.loggerWithPath(c.Path).Tracef("update schedules") + t.loggerWithPath(c.Path).Tracef("update schedules on config change") return true } return false @@ -854,7 +853,7 @@ func (t *T) onNodeConfigUpdated(c *msgbus.NodeConfigUpdated) { } switch { case t.enabled: - t.log.Tracef("node: update schedules") + t.log.Tracef("node: update schedules on config change") t.scheduleNode() } } @@ -902,17 +901,83 @@ func (t *T) scheduleNode() { if !t.enabled { return } - o, err := object.NewNode() - if err != nil { - t.log.Errorf("node: %s", err) + nodeConfig := node.ConfigData.GetByNode(t.localhost) + if nodeConfig == nil || nodeConfig.Schedules == nil || len(nodeConfig.Schedules) == 0 { return } path := naming.Path{} + jobIds := make(map[string]any) + + scheduleOne := func(scheduleConfig schedule.Config) { + e := schedule.Entry{ + Node: t.localhost, + Config: scheduleConfig, + } - for _, e := range o.Schedules() { + // Remember we saw that job id, so we can purge unconfigured jobs after this loop + jobIds[newJobId(e)] = nil + + log := t.jobLogger(e) + prevSchedule, hasSchedule := t.schedules.Get(path, e.Key) + hasJob := t.jobs.Has(e) t.schedules.Add(path, e) - t.createJob(e) + var validated []string + var skipped string + + if hasSchedule && (scheduleConfig.Schedule != prevSchedule.Config.Schedule) { + if hasJob { + if e.Schedule == "" || e.Schedule == "@0" { + log.Infof("unschedule (schedule is now @0)") + t.jobs.Del(e) + return + } else { + // At the end of this func, if we did not recreate the job, + // log it as unscheduled + defer func() { + if !t.jobs.Has(e) { + log.Infof("unschedule on config change, skip reschedule (%s)", skipped) + } + }() + t.jobs.Del(e) + } + } else { + if e.Schedule == "" || e.Schedule == "@0" { + return + } + } + } else if e.Schedule == "" || e.Schedule == "@0" { + return + } + validated = append(validated, e.Schedule) + + if e.RequireCollector && !t.isCollectorJoinable { + if hasJob { + log.Infof("unschedule (collector unjoignable)") + t.jobs.Del(e) + } else { + skipped = "collector unjoignable" + } + return + } + + if !t.jobs.Has(e) { + log.Infof("schedule (%s)", strings.Join(validated, ", ")) + t.createJob(e) + } + } + + for _, scheduleConfig := range nodeConfig.Schedules { + scheduleOne(scheduleConfig) + } + + // Purge unconfigured jobs + for _, id := range t.jobs.PathIds(path) { + if _, ok := jobIds[id]; !ok { + job := t.jobs[id] + t.jobLogger(job.schedule).Infof("unschedule (no longer configured)") + t.jobs.DelId(id) + } } t.updateExposedSchedules(path) @@ -951,7 +1016,7 @@ func (t *T) scheduleObject(path naming.Path) { if hasSchedule && (scheduleConfig.Schedule != prevSchedule.Config.Schedule) { if hasJob { - if e.Schedule == "" { + if e.Schedule == "" || e.Schedule == "@0" { log.Infof("unschedule (schedule is now @0)") t.jobs.Del(e) return @@ -965,10 +1030,26 @@ func (t *T) scheduleObject(path naming.Path) { }() t.jobs.Del(e) } + } else { + if e.Schedule == "" || e.Schedule == "@0" { + return + } } + } else if e.Schedule == "" || e.Schedule == "@0" { + return } validated = append(validated, e.Schedule) + if e.RequireCollector && !t.isCollectorJoinable { + if hasJob { + log.Infof("unschedule (collector unjoignable)") + t.jobs.Del(e) + } else { + skipped = "collector unjoignable" + } + return + } + if e.RequireProvisioned { if !hasProvisioned { if hasJob { @@ -1010,7 +1091,6 @@ func (t *T) scheduleObject(path naming.Path) { } return } - t.schedules.Add(path, e) if !t.jobs.Has(e) { log.Infof("schedule (%s)", strings.Join(validated, ", ")) t.createJob(e) From bc6f328bcca9df3e14d8417dedfc113cfb299190 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 17 Feb 2026 14:25:20 +0100 Subject: [PATCH 07/11] Switch the "pool '%s' status loaded" message to debug --- daemon/nmon/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/nmon/main.go b/daemon/nmon/main.go index 728357d4a..5d23b0864 100644 --- a/daemon/nmon/main.go +++ b/daemon/nmon/main.go @@ -854,7 +854,7 @@ func (t *Manager) loadPools() { t.log.Warnf("loading pool '%s' status: %s", poolName, ctx.Err()) return } - t.log.Infof("pool '%s' status loaded", poolName) + t.log.Debugf("pool '%s' status loaded", poolName) renewedMu.Lock() renewed[poolName] = nil From 0d909ec4c0861b16a5011d9563179e942ddc8d00 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 17 Feb 2026 14:39:52 +0100 Subject: [PATCH 08/11] Log api requests at the debug or trace levels --- daemon/daemonapi/lib_middleware.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/daemon/daemonapi/lib_middleware.go b/daemon/daemonapi/lib_middleware.go index 4f0625b42..ef6853f75 100644 --- a/daemon/daemonapi/lib_middleware.go +++ b/daemon/daemonapi/lib_middleware.go @@ -32,11 +32,13 @@ var ( // logRequestLevelPerPath defines logRequestMiddleWare log level per path. // The default value is LevelInfo - logRequestLevelPerPath = map[string]zerolog.Level{ - "/metrics": zerolog.DebugLevel, - "/api/openapi": zerolog.DebugLevel, - "/api/docs/*": zerolog.DebugLevel, + logRequestLevelPerPrefix = map[string]zerolog.Level{ + "/metrics": zerolog.TraceLevel, + "/api/openapi": zerolog.TraceLevel, + "/api/docs": zerolog.TraceLevel, "/api/relay/message": zerolog.DebugLevel, + "/api": zerolog.DebugLevel, + // "/api/instance/path/:namespace/:kind/:name/status": zerolog.InfoLevel, } rateLimitDeniedTotal = promauto.NewCounterVec( @@ -228,9 +230,15 @@ func LogUserMiddleware(parent context.Context) echo.MiddlewareFunc { func LogRequestMiddleWare(parent context.Context) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - level := zerolog.InfoLevel - if l, ok := logRequestLevelPerPath[c.Path()]; ok { - level = l + level := zerolog.DebugLevel + if lvl, ok := logRequestLevelPerPrefix[c.Path()]; ok { + level = lvl + } else { + for prefix, lvl := range logRequestLevelPerPrefix { + if strings.HasPrefix(c.Path(), prefix+"/") { + level = lvl + } + } } if level != zerolog.NoLevel { userDesc := userFromContext(c).GetUserName() From f67e16def0140bd75563b617c3b56166d02bad6a Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Tue, 17 Feb 2026 18:16:31 +0100 Subject: [PATCH 09/11] Simplify the schedule.Config struct Replace LastRunFile and LastSuccessFile fields by a single StatefileKey and 2 type functions: * schedule.Entry.LastRunFile() * schedule.Entry.LastSuccessFile() Use pointer receivers for all schedule.Entry functions. Add omitempty to the Require field. --- core/naming/path.go | 10 +- core/object/actor_print_schedule.go | 27 +-- core/object/node_print_schedule.go | 24 +- core/omcmd/node_schedule_list.go | 3 +- core/omcmd/object_schedule_list.go | 3 +- core/schedule/main.go | 39 ++- daemon/api/api.yaml | 16 +- daemon/api/codegen_server_gen.go | 276 +++++++++++----------- daemon/api/codegen_type_gen.go | 6 +- daemon/api/unstructured.go | 3 +- daemon/daemonapi/get_instance_schedule.go | 3 +- daemon/daemonapi/get_node_schedule.go | 3 +- 12 files changed, 197 insertions(+), 216 deletions(-) diff --git a/core/naming/path.go b/core/naming/path.go index 10f8923d2..eb4d66b92 100644 --- a/core/naming/path.go +++ b/core/naming/path.go @@ -405,13 +405,17 @@ func (t Paths) Merge(other Paths) Paths { // variable persistent data is stored as files. func (t Path) VarDir() string { var s string + if t.IsZero() { + return filepath.Join(rawconfig.Paths.Var, "node") + } + switch t.Namespace { case "", NsRoot: - s = fmt.Sprintf("%s/%s/%s", rawconfig.Paths.Var, t.Kind, t.Name) + s = filepath.Join(rawconfig.Paths.Var, t.Kind.String(), t.Name) default: - s = fmt.Sprintf("%s/%s", rawconfig.Paths.VarNs, t) + s = filepath.Join(rawconfig.Paths.VarNs, t.String()) } - return filepath.FromSlash(s) + return s } // TmpDir returns the directory on the local filesystem where the object diff --git a/core/object/actor_print_schedule.go b/core/object/actor_print_schedule.go index b211d3bbc..22450a8af 100644 --- a/core/object/actor_print_schedule.go +++ b/core/object/actor_print_schedule.go @@ -1,14 +1,10 @@ package object import ( - "path/filepath" - "time" - "github.com/opensvc/om3/v3/core/kwoption" "github.com/opensvc/om3/v3/core/naming" "github.com/opensvc/om3/v3/core/resource" "github.com/opensvc/om3/v3/core/schedule" - "github.com/opensvc/om3/v3/util/file" "github.com/opensvc/om3/v3/util/hostname" "github.com/opensvc/om3/v3/util/key" ) @@ -21,16 +17,7 @@ func (t *actor) lastRunFile(action, rid, desc string) string { if rid != "" { base = base + "_" + rid } - return filepath.Join(t.VarDir(), "scheduler", base) -} - -func (t *actor) lastSuccessFile(action, rid, base string) string { - return filepath.Join(t.lastRunFile(action, rid, base) + ".success") -} - -func (t *actor) loadLast(action, rid, base string) time.Time { - fpath := t.lastRunFile(action, rid, base) - return file.ModTime(fpath) + return base } func (t *actor) newScheduleEntry(action, keyStr, rid, base string, reqCol, reqProv bool) schedule.Entry { @@ -39,21 +26,21 @@ func (t *actor) newScheduleEntry(action, keyStr, rid, base string, reqCol, reqPr if err != nil { panic(err) } - return schedule.Entry{ + entry := schedule.Entry{ Config: schedule.Config{ Action: action, Key: k.String(), - LastRunFile: t.lastRunFile(action, rid, base), - LastSuccessFile: t.lastSuccessFile(action, rid, base), MaxParallel: 1, RequireCollector: reqCol, RequireProvisioned: reqProv, Schedule: def, + StatefileKey: t.lastRunFile(action, rid, base), }, - LastRunAt: t.loadLast(action, rid, base), - Node: hostname.Hostname(), - Path: t.path, + Node: hostname.Hostname(), + Path: t.path, } + entry.LastRunAt = entry.LoadLast() + return entry } func (t *actor) Schedules() schedule.Table { diff --git a/core/object/node_print_schedule.go b/core/object/node_print_schedule.go index e0f089438..ac48a7594 100644 --- a/core/object/node_print_schedule.go +++ b/core/object/node_print_schedule.go @@ -2,13 +2,10 @@ package object import ( "fmt" - "path/filepath" - "time" "github.com/opensvc/om3/v3/core/driver" "github.com/opensvc/om3/v3/core/resourceid" "github.com/opensvc/om3/v3/core/schedule" - "github.com/opensvc/om3/v3/util/file" "github.com/opensvc/om3/v3/util/hostname" "github.com/opensvc/om3/v3/util/key" ) @@ -18,16 +15,7 @@ func (t *Node) lastRunFile(action, rid, base string) string { if rid != "" { base = base + "_" + rid } - return filepath.Join(t.VarDir(), "scheduler", base) -} - -func (t *Node) lastSuccessFile(action, rid, base string) string { - return filepath.Join(t.lastRunFile(action, rid, base) + ".success") -} - -func (t *Node) loadLast(action, rid, base string) time.Time { - fpath := t.lastRunFile(action, rid, base) - return file.ModTime(fpath) + return base } func (t *Node) newScheduleEntry(action, section, rid, base string) schedule.Entry { @@ -36,19 +24,19 @@ func (t *Node) newScheduleEntry(action, section, rid, base string) schedule.Entr if err != nil { panic(err) } - return schedule.Entry{ + entry := schedule.Entry{ Config: schedule.Config{ Action: action, Key: k.String(), - LastRunFile: t.lastRunFile(action, rid, base), - LastSuccessFile: t.lastSuccessFile(action, rid, base), MaxParallel: 1, RequireCollector: true, Schedule: def, + StatefileKey: t.lastRunFile(action, rid, base), }, - LastRunAt: t.loadLast(action, rid, base), - Node: hostname.Hostname(), + Node: hostname.Hostname(), } + entry.LastRunAt = entry.LoadLast() + return entry } func (t *Node) Schedules() schedule.Table { diff --git a/core/omcmd/node_schedule_list.go b/core/omcmd/node_schedule_list.go index dad772ca8..129400cc0 100644 --- a/core/omcmd/node_schedule_list.go +++ b/core/omcmd/node_schedule_list.go @@ -62,13 +62,12 @@ func (t *CmdNodeScheduleList) extractLocal() (api.ScheduleItems, error) { Action: e.Action, Key: e.Key, LastRunAt: e.LastRunAt, - LastRunFile: e.LastRunFile, - LastSuccessFile: e.LastSuccessFile, MaxParallel: e.MaxParallel, NextRunAt: e.NextRunAt, RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, + StatefileKey: e.StatefileKey, }, } items = append(items, item) diff --git a/core/omcmd/object_schedule_list.go b/core/omcmd/object_schedule_list.go index 4a4175667..bc0037bd5 100644 --- a/core/omcmd/object_schedule_list.go +++ b/core/omcmd/object_schedule_list.go @@ -73,14 +73,13 @@ func (t *CmdObjectScheduleList) extractLocal(selector string) (api.ScheduleList, Action: e.Action, Key: e.Key, LastRunAt: e.LastRunAt, - LastRunFile: e.LastRunFile, - LastSuccessFile: e.LastSuccessFile, MaxParallel: e.MaxParallel, NextRunAt: e.NextRunAt, Require: e.Require, RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, + StatefileKey: e.StatefileKey, }, } data.Items = append(data.Items, item) diff --git a/core/schedule/main.go b/core/schedule/main.go index 932c3282f..ef02ccce1 100644 --- a/core/schedule/main.go +++ b/core/schedule/main.go @@ -1,6 +1,7 @@ package schedule import ( + "path/filepath" "strings" "time" @@ -17,12 +18,14 @@ type ( Action string `json:"action"` Schedule string `json:"schedule"` Key string `json:"key"` - LastRunFile string `json:"last_run_file"` - LastSuccessFile string `json:"last_success_file"` MaxParallel int `json:"max_parallel"` - Require string `json:"require"` + Require string `json:"require,omitempty"` RequireCollector bool `json:"require_collector"` RequireProvisioned bool `json:"require_provisioned"` + + // StatefileKey is used in the last run filename and last run success formatters. + // Defaults to Action if empty. + StatefileKey string `json:"statefile_key,omitempty"` } Entry struct { @@ -107,13 +110,12 @@ func (t Table) DeepCopy() *Table { Config: Config{ Action: x.Action, Key: x.Key, - LastRunFile: x.LastRunFile, - LastSuccessFile: x.LastSuccessFile, MaxParallel: x.MaxParallel, Require: x.Require, RequireCollector: x.RequireCollector, RequireProvisioned: x.RequireProvisioned, Schedule: x.Schedule, + StatefileKey: x.StatefileKey, }, LastRunAt: x.LastRunAt, NextRunAt: x.NextRunAt, @@ -149,19 +151,32 @@ func (t Entry) LogPrefix() string { return s.String() } -func (t Entry) RID() string { +func (t *Entry) RID() string { k := key.Parse(t.Key) return k.Section } -func (t Entry) SetLastSuccess(tm time.Time) error { - return file.Touch(t.LastSuccessFile, tm) +func (t *Entry) SetLastSuccess(tm time.Time) error { + return file.Touch(t.LastSuccessFile(), tm) +} + +func (t *Entry) SetLastRun(tm time.Time) error { + return file.Touch(t.LastRunFile(), tm) +} + +func (t *Entry) GetLastRun() time.Time { + return file.ModTime(t.LastRunFile()) +} + +func (t *Entry) LastRunFile() string { + return filepath.Join(t.Path.VarDir(), "scheduler", t.StatefileKey) } -func (t Entry) SetLastRun(tm time.Time) error { - return file.Touch(t.LastRunFile, tm) +func (t *Entry) LastSuccessFile() string { + return t.LastRunFile() + ".success" } -func (t Entry) GetLastRun() time.Time { - return file.ModTime(t.LastRunFile) +func (t *Entry) LoadLast() time.Time { + fpath := t.LastRunFile() + return file.ModTime(fpath) } diff --git a/daemon/api/api.yaml b/daemon/api/api.yaml index 0e8e6ad19..bd8b3e1e8 100644 --- a/daemon/api/api.yaml +++ b/daemon/api/api.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: opensvc agent api - version: 4.1.3 + version: 4.1.4 paths: /api/array: @@ -7438,8 +7438,7 @@ components: - key - max_parallel - last_run_at - - last_run_file - - last_success_file + - statefile_key - next_run_at - require - require_collector @@ -7454,9 +7453,7 @@ components: last_run_at: type: string format: date-time - last_run_file: - type: string - last_success_file: + statefile_key: type: string max_parallel: type: integer @@ -7476,8 +7473,7 @@ components: - action - schedule - key - - last_run_file - - last_success_file + - statefile_key - max_parallel - require - require_collector @@ -7489,9 +7485,7 @@ components: type: string key: type: string - last_run_file: - type: string - last_success_file: + statefile_key: type: string max_parallel: type: integer diff --git a/daemon/api/codegen_server_gen.go b/daemon/api/codegen_server_gen.go index 4cab0c0ff..73685a19f 100644 --- a/daemon/api/codegen_server_gen.go +++ b/daemon/api/codegen_server_gen.go @@ -6367,144 +6367,144 @@ var swaggerSpec = []string{ "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", + "88cl8Cxpfby5ubn5PwEAAP//G2JFs459AgA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/daemon/api/codegen_type_gen.go b/daemon/api/codegen_type_gen.go index d3b968601..1f00365a5 100644 --- a/daemon/api/codegen_type_gen.go +++ b/daemon/api/codegen_type_gen.go @@ -1550,27 +1550,25 @@ type Schedule struct { Action string `json:"action"` Key string `json:"key"` LastRunAt time.Time `json:"last_run_at"` - LastRunFile string `json:"last_run_file"` - LastSuccessFile string `json:"last_success_file"` MaxParallel int `json:"max_parallel"` NextRunAt time.Time `json:"next_run_at"` Require string `json:"require"` RequireCollector bool `json:"require_collector"` RequireProvisioned bool `json:"require_provisioned"` Schedule string `json:"schedule"` + StatefileKey string `json:"statefile_key"` } // ScheduleConfig defines model for ScheduleConfig. type ScheduleConfig struct { Action string `json:"action"` Key string `json:"key"` - LastRunFile string `json:"last_run_file"` - LastSuccessFile string `json:"last_success_file"` MaxParallel int `json:"max_parallel"` Require string `json:"require"` RequireCollector bool `json:"require_collector"` RequireProvisioned bool `json:"require_provisioned"` Schedule string `json:"schedule"` + StatefileKey string `json:"statefile_key"` } // ScheduleItem defines model for ScheduleItem. diff --git a/daemon/api/unstructured.go b/daemon/api/unstructured.go index df72d77f6..271cfc4c3 100644 --- a/daemon/api/unstructured.go +++ b/daemon/api/unstructured.go @@ -80,13 +80,12 @@ func (t Schedule) Unstructured() map[string]any { "schedule": t.Schedule, "key": t.Key, "last_run_at": t.LastRunAt, - "last_run_file": t.LastRunFile, - "last_success_file": t.LastSuccessFile, "max_parallel": t.MaxParallel, "next_run_at": t.NextRunAt, "require": t.Require, "require_collector": t.RequireCollector, "require_Provisioned": t.RequireProvisioned, + "statefile_key": t.StatefileKey, } } diff --git a/daemon/daemonapi/get_instance_schedule.go b/daemon/daemonapi/get_instance_schedule.go index 37babe6fa..ac8d88adc 100644 --- a/daemon/daemonapi/get_instance_schedule.go +++ b/daemon/daemonapi/get_instance_schedule.go @@ -51,14 +51,13 @@ func (a *DaemonAPI) getLocalInstanceSchedule(ctx echo.Context, namespace string, Action: e.Action, Key: e.Key, LastRunAt: e.LastRunAt, - LastRunFile: e.LastRunFile, - LastSuccessFile: e.LastSuccessFile, MaxParallel: e.MaxParallel, NextRunAt: e.NextRunAt, Require: e.Require, RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, + StatefileKey: e.StatefileKey, }, } resp.Items = append(resp.Items, item) diff --git a/daemon/daemonapi/get_node_schedule.go b/daemon/daemonapi/get_node_schedule.go index 8a09b02b0..a34c92136 100644 --- a/daemon/daemonapi/get_node_schedule.go +++ b/daemon/daemonapi/get_node_schedule.go @@ -43,12 +43,11 @@ func (a *DaemonAPI) getLocalSchedule(ctx echo.Context) error { Action: e.Action, Key: e.Key, LastRunAt: e.LastRunAt, - LastRunFile: e.LastRunFile, - LastSuccessFile: e.LastSuccessFile, NextRunAt: e.NextRunAt, RequireCollector: e.RequireCollector, RequireProvisioned: e.RequireProvisioned, Schedule: e.Schedule, + StatefileKey: e.StatefileKey, }, } resp.Items = append(resp.Items, item) From 38e0cf7214982868d5c303c23ddf4379e09d4a64 Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 18 Feb 2026 13:01:53 +0100 Subject: [PATCH 10/11] Don't log the schedulers job executions --- core/env/env.go | 9 ++++++++ core/om/root.go | 3 +++ core/resource/resource.go | 4 +--- daemon/scheduler/jobs.go | 13 +++++++++-- util/logging/logging.go | 11 +++++++--- util/plog/main.go | 46 +++++++++++++++++++++------------------ 6 files changed, 57 insertions(+), 29 deletions(-) diff --git a/core/env/env.go b/core/env/env.go index e8aa89ae2..33255b43f 100644 --- a/core/env/env.go +++ b/core/env/env.go @@ -24,6 +24,8 @@ var ( NamespaceVar = "OSVC_NAMESPACE" KindVar = "OSVC_KIND" ContextVar = "OSVC_CONTEXT" + + NoLogFileVar = "OSVC_NO_LOG_FILE" ) // HasDaemonOrigin returns true if the environment variable OSVC_ACTION_ORIGIN @@ -91,3 +93,10 @@ func Kind() string { func Context() string { return os.Getenv(ContextVar) } + +func NoLogFile() bool { + return os.Getenv(NoLogFileVar) == "1" +} +func NoLogFileSetenvArg() string { + return fmt.Sprintf("%s=1", NoLogFileVar) +} diff --git a/core/om/root.go b/core/om/root.go index 97c8a04d7..210060816 100644 --- a/core/om/root.go +++ b/core/om/root.go @@ -92,10 +92,13 @@ func configureLogger() error { if traceFlag { level = "trace" } + noLogFile := env.NoLogFile() err := logging.Configure(logging.Config{ WithConsoleLog: !quietFlag || debugFlag || traceFlag || foregroundFlag, WithColor: colorFlag != "no", WithCaller: callerFlag, + WithJournald: !noLogFile, + WithSyslogd: !noLogFile, Level: level, }) if err != nil { diff --git a/core/resource/resource.go b/core/resource/resource.go index e25e28852..e7f749c5e 100644 --- a/core/resource/resource.go +++ b/core/resource/resource.go @@ -533,9 +533,7 @@ func (t *T) GetObjectDriver() ObjectDriver { } func (t *T) getLoggerFromObjectDriver(o ObjectDriver) *plog.Logger { - oLog := o.Log() - prefix := fmt.Sprintf("%s%s: ", oLog.Prefix(), t.ResourceID) - l := plog.NewLogger(oLog.Logger()).WithPrefix(prefix).Attr("rid", t.ResourceID) + l := o.Log().AddPrefix(t.ResourceID.String()+": ").Attr("rid", t.ResourceID) if t.Subset != "" { l = l.Attr("subset", t.Subset) } diff --git a/daemon/scheduler/jobs.go b/daemon/scheduler/jobs.go index d9ed4dd58..071fb4238 100644 --- a/daemon/scheduler/jobs.go +++ b/daemon/scheduler/jobs.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/uuid" + "github.com/rs/zerolog" "github.com/opensvc/om3/v3/core/env" "github.com/opensvc/om3/v3/core/schedule" @@ -68,13 +69,21 @@ func (o *T) action(e schedule.Entry) error { "OSVC_SESSION_ID="+sid.String(), ) + // Unless the daemon runs with --debug or --trace, we don't want to + // log the execution in journald nor syslogd to avoid uncontrolled + // growth or rotation of the logging backend files. + if lvl := zerolog.GlobalLevel(); lvl > zerolog.DebugLevel { + // OSVC_NO_LOG_FILE=1 + cmdEnv = append(cmdEnv, env.NoLogFileSetenvArg()) + } + cmd := command.New( command.WithName(os.Args[0]), command.WithArgs(cmdArgs), command.WithLogger(o.log), command.WithEnv(cmdEnv), ) - o.log.Tracef("-> exec %s", cmd) + o.log.Debugf("-> exec %s", cmd) o.publisher.Pub(&msgbus.Exec{Command: cmd.String(), Node: o.localhost, Origin: "scheduler", SessionID: sid}, labels...) startTime := time.Now() if err := cmd.Run(); err != nil { @@ -85,6 +94,6 @@ func (o *T) action(e schedule.Entry) error { } duration := time.Now().Sub(startTime) o.publisher.Pub(&msgbus.ExecSuccess{Command: cmd.String(), Duration: duration, Node: o.localhost, Origin: "scheduler", SessionID: sid}, labels...) - o.log.Tracef("<- exec %s", cmd) + o.log.Debugf("<- exec %s", cmd) return nil } diff --git a/util/logging/logging.go b/util/logging/logging.go index 63d397083..e9958f93b 100644 --- a/util/logging/logging.go +++ b/util/logging/logging.go @@ -29,6 +29,9 @@ type Config struct { // Enable console logging coloring WithColor bool + WithJournald bool + WithSyslogd bool + // LogFile makes the framework log to a file LogFile string @@ -131,12 +134,14 @@ func Configure(config Config) error { zerolog.TimeFieldFormat = time.RFC3339Nano - if journalEnabled() { + if config.WithJournald && journalEnabled() { if writer := journald.NewJournalDWriter(); writer != nil { writers = append(writers, writer) } - } else if writer, err := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "om"); err == nil { - writers = append(writers, writer) + } else if config.WithSyslogd { + if writer, err := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "om"); err == nil { + writers = append(writers, writer) + } } if config.WithConsoleLog { diff --git a/util/plog/main.go b/util/plog/main.go index 2ec6d9936..f37efd654 100644 --- a/util/plog/main.go +++ b/util/plog/main.go @@ -41,14 +41,21 @@ func NewLogger(logger zerolog.Logger) *Logger { } } +func (t *Logger) clone() *Logger { + return &Logger{ + logger: t.logger, + prefix: t.prefix, + } +} + func (t *Logger) AddPrefix(prefix string) *Logger { - n := NewLogger(t.logger) + n := t.clone() n.prefix = t.prefix + prefix return n } func (t *Logger) WithPrefix(prefix string) *Logger { - n := NewLogger(t.logger) + n := t.clone() n.prefix = prefix return n } @@ -97,41 +104,38 @@ func (t *Logger) Levelf(level zerolog.Level, format string, a ...any) { } func (t *Logger) Attr(k string, v any) *Logger { - logger := Logger{ - logger: t.logger, - prefix: t.prefix, - } + n := t.clone() switch i := v.(type) { case string: - logger.logger = t.logger.With().Str(k, i).Logger() + n.logger = t.logger.With().Str(k, i).Logger() case []string: - logger.logger = t.logger.With().Strs(k, i).Logger() + n.logger = t.logger.With().Strs(k, i).Logger() case []byte: - logger.logger = t.logger.With().Bytes(k, i).Logger() + n.logger = t.logger.With().Bytes(k, i).Logger() case float32: - logger.logger = t.logger.With().Float32(k, i).Logger() + n.logger = t.logger.With().Float32(k, i).Logger() case float64: - logger.logger = t.logger.With().Float64(k, i).Logger() + n.logger = t.logger.With().Float64(k, i).Logger() case bool: - logger.logger = t.logger.With().Bool(k, i).Logger() + n.logger = t.logger.With().Bool(k, i).Logger() case int: - logger.logger = t.logger.With().Int(k, i).Logger() + n.logger = t.logger.With().Int(k, i).Logger() case int32: - logger.logger = t.logger.With().Int32(k, i).Logger() + n.logger = t.logger.With().Int32(k, i).Logger() case int64: - logger.logger = t.logger.With().Int64(k, i).Logger() + n.logger = t.logger.With().Int64(k, i).Logger() case uint: - logger.logger = t.logger.With().Uint(k, i).Logger() + n.logger = t.logger.With().Uint(k, i).Logger() case uint32: - logger.logger = t.logger.With().Uint32(k, i).Logger() + n.logger = t.logger.With().Uint32(k, i).Logger() case uint64: - logger.logger = t.logger.With().Uint64(k, i).Logger() + n.logger = t.logger.With().Uint64(k, i).Logger() case time.Duration: - logger.logger = t.logger.With().Dur(k, i).Logger() + n.logger = t.logger.With().Dur(k, i).Logger() default: - logger.logger = t.logger.With().Interface(k, v).Logger() + n.logger = t.logger.With().Interface(k, v).Logger() } - return &logger + return n } func (t *Logger) Level(level zerolog.Level) *Logger { From 1988c114bd2fffe9f82f72cfb5a049042ef03f2c Mon Sep 17 00:00:00 2001 From: Christophe Varoqui Date: Wed, 18 Feb 2026 15:10:32 +0100 Subject: [PATCH 11/11] Restore a "abort" on scheduler job alarm log to the Info level Unduly switched to Trace level. --- daemon/scheduler/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/scheduler/main.go b/daemon/scheduler/main.go index a727818bc..070796631 100644 --- a/daemon/scheduler/main.go +++ b/daemon/scheduler/main.go @@ -378,7 +378,7 @@ func (t *T) onJobAlarm(c eventJobAlarm) { } if satisfied, ok := t.reqSatisfied.Get(e.Path, e.Key); ok { if satisfied != nil { - logger.Tracef("abort (requirements no longer met)") + logger.Infof("abort (requirements no longer met)") return } } else if e.Require != "" {