This is an automated email from the ASF dual-hosted git repository.
sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new 2a15f850be feat(plc4go): add StopWarn callbacks to handle lifecycle
2a15f850be is described below
commit 2a15f850be5a01f5486de602f55b615b63e51ea5
Author: Sebastian Rühl <[email protected]>
AuthorDate: Thu May 28 09:04:57 2026 +0200
feat(plc4go): add StopWarn callbacks to handle lifecycle
---
plc4go/spi/utils/StopWarn.go | 58 +++++++++++++++++++++++++++++++++++--
plc4go/spi/utils/StopWarn_test.go | 60 +++++++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+), 3 deletions(-)
diff --git a/plc4go/spi/utils/StopWarn.go b/plc4go/spi/utils/StopWarn.go
index cfdeea6581..c045a8ad49 100644
--- a/plc4go/spi/utils/StopWarn.go
+++ b/plc4go/spi/utils/StopWarn.go
@@ -37,6 +37,7 @@ type stopWarnOptions struct {
interval time.Duration
extraSkipOffset int
includeGoroutinesStack bool
+ registrar func(initial StopWarnTick) (onTick
func(StopWarnTick), onEnd func())
}
// StopWarn gives out warning every interval (default 5 seconds) when a
function doesn't terminate. Usage: `defer StopWarn(log)()`
@@ -58,9 +59,19 @@ func StopWarn(localLog zerolog.Logger, opts
...func(*stopWarnOptions)) func() {
ticker := time.NewTicker(o.interval)
wg := new(sync.WaitGroup)
done := make(chan struct{})
+ startTime := time.Now()
+ var onTick func(StopWarnTick)
+ var onEnd func()
+ if o.registrar != nil {
+ onTick, onEnd = o.registrar(StopWarnTick{
+ ProcessID: o.processId,
+ ProcessInfo: o.processInfo,
+ StartTime: startTime,
+ WarnTime: startTime,
+ })
+ }
wg.Go(func() {
localLog.Trace().Msgf("start checking")
- startTime := time.Now()
for {
localLog.Trace().Msgf("check cycle")
select {
@@ -90,14 +101,24 @@ func StopWarn(localLog zerolog.Logger, opts
...func(*stopWarnOptions)) func() {
TimeDiff("inProgressFor", warnTime,
startTime).
Stringer("stackInfo", stackInfo).
Msgf("%sstill in progress", processId)
+ if onTick != nil {
+ onTick(StopWarnTick{
+ ProcessID: o.processId,
+ ProcessInfo: o.processInfo,
+ StartTime: startTime,
+ WarnTime: warnTime,
+ })
+ }
}
}
})
- start := time.Now()
return func() {
- localLog.Trace().TimeDiff("check duration", time.Now(),
start).Msg("done")
+ localLog.Trace().TimeDiff("check duration", time.Now(),
startTime).Msg("done")
close(done)
wg.Wait() // This is to avoid late logs in case when the
shutdown is really fast
+ if onEnd != nil {
+ onEnd()
+ }
}
}
@@ -135,3 +156,34 @@ func WithStopWarnIncludeGoroutinesStack()
func(*stopWarnOptions) {
o.includeGoroutinesStack = true
}
}
+
+// StopWarnTick describes a single "still in progress" event passed to a
registrar.
+type StopWarnTick struct {
+ ProcessID string
+ ProcessInfo string
+ StartTime time.Time
+ WarnTime time.Time
+}
+
+// WithStopWarnRegistrar registers an external observer that is notified of
the lifecycle
+// of a StopWarn invocation. This allows callers to centralise policies such
as rate-limited
+// goroutine stack dumps across many concurrent StopWarn instances without
each instance
+// emitting its own dump on every tick.
+//
+// register is called once when StopWarn arms, with the initial tick info
(StartTime ==
+// WarnTime). It returns two callbacks:
+// - onTick: invoked on every warn tick, immediately after the existing Warn
log line.
+// May be nil; if nil, ticks are silently ignored by the registrar.
+// - onEnd: invoked exactly once when the returned stop func runs, after
the warner
+// goroutine has drained. May be nil.
+//
+// Neither callback should block for long; the warner goroutine waits on
onTick before
+// processing the next tick. Heavy work (stack dumps, I/O) should be
dispatched to a
+// background goroutine inside the callback.
+func WithStopWarnRegistrar(
+ register func(initial StopWarnTick) (onTick func(StopWarnTick), onEnd
func()),
+) func(*stopWarnOptions) {
+ return func(o *stopWarnOptions) {
+ o.registrar = register
+ }
+}
diff --git a/plc4go/spi/utils/StopWarn_test.go
b/plc4go/spi/utils/StopWarn_test.go
index bb03795dfb..2d737734de 100644
--- a/plc4go/spi/utils/StopWarn_test.go
+++ b/plc4go/spi/utils/StopWarn_test.go
@@ -20,6 +20,7 @@
package utils
import (
+ "sync"
"testing"
"time"
@@ -68,6 +69,65 @@ func TestStopWarn(t *testing.T) {
}
assert.Equalf(t, 0, foundMessages, "%s should contain at least
three warning. Found %d times", logHook.messages, foundMessages)
})
+ t.Run("registrar receives ticks and end", func(t *testing.T) {
+ logger := produceTestingLogger(t)
+ var mu sync.Mutex
+ var initial StopWarnTick
+ var ticks []StopWarnTick
+ ended := false
+ register := func(in StopWarnTick) (func(StopWarnTick), func()) {
+ mu.Lock()
+ initial = in
+ mu.Unlock()
+ return func(t StopWarnTick) {
+ mu.Lock()
+ ticks = append(ticks, t)
+ mu.Unlock()
+ }, func() {
+ mu.Lock()
+ ended = true
+ mu.Unlock()
+ }
+ }
+ func() {
+ defer StopWarn(logger,
+ WithStopWarnInterval(10*time.Millisecond),
+ WithStopWarnProcessId("TestStopWarn"),
+ WithStopWarnRegistrar(register),
+ )()
+ time.Sleep(75 * time.Millisecond)
+ }()
+ mu.Lock()
+ defer mu.Unlock()
+ assert.Equal(t, "TestStopWarn", initial.ProcessID, "registrar
should receive initial tick at arm time")
+ assert.False(t, initial.StartTime.IsZero(), "initial StartTime
should be set")
+ assert.Equal(t, initial.StartTime, initial.WarnTime, "initial
WarnTime equals StartTime")
+ assert.GreaterOrEqual(t, len(ticks), 3, "registrar should
observe multiple ticks")
+ assert.True(t, ended, "registrar onEnd should fire when stop
func returns")
+ for _, tk := range ticks {
+ assert.Equal(t, "TestStopWarn", tk.ProcessID)
+ assert.Equal(t, initial.StartTime, tk.StartTime,
"StartTime should be stable across ticks")
+ assert.False(t, tk.WarnTime.Before(initial.StartTime),
"WarnTime should be at or after StartTime")
+ }
+ })
+ t.Run("registrar onEnd only when nil onTick", func(t *testing.T) {
+ logger := produceTestingLogger(t)
+ var mu sync.Mutex
+ ended := false
+ register := func(StopWarnTick) (func(StopWarnTick), func()) {
+ return nil, func() { mu.Lock(); ended = true;
mu.Unlock() }
+ }
+ func() {
+ defer StopWarn(logger,
+ WithStopWarnInterval(10*time.Millisecond),
+ WithStopWarnRegistrar(register),
+ )()
+ time.Sleep(35 * time.Millisecond)
+ }()
+ mu.Lock()
+ defer mu.Unlock()
+ assert.True(t, ended, "nil onTick should not prevent onEnd from
firing")
+ })
}
type logHook struct {