fix(channels): make email Stop() wait for checkLoop (lifecycle compliance)
- Add loopWg sync.WaitGroup: Start() calls Add(1) before starting checkLoop; checkLoop defers Done(); Stop() calls Wait() after cancel so the goroutine fully exits before returning. - Harden TestEmailChannel_lifecycleCheck to assert Stop() takes at least 1s when CheckNewEmails is mocked to sleep 1s, [Refactor]: Channel System Refactoring / [Refactor]: Channel系统重构 #621
This commit is contained in:
parent
8e9c369312
commit
abe0375ce9
2 changed files with 62 additions and 0 deletions
|
|
@ -51,6 +51,9 @@ type EmailChannel struct {
|
|||
cancel context.CancelFunc
|
||||
checkTicker *time.Ticker
|
||||
|
||||
// loopWg waits for checkLoop goroutine to exit in Stop().
|
||||
loopWg sync.WaitGroup
|
||||
|
||||
// reconnect control
|
||||
reconnectClientVersion int
|
||||
reconnectMutex sync.Mutex
|
||||
|
|
@ -88,6 +91,7 @@ func (c *EmailChannel) Start(ctx context.Context) error {
|
|||
c.setRunning(true)
|
||||
logger.InfoC("email", "Email channel started")
|
||||
|
||||
c.loopWg.Add(1)
|
||||
go c.checkLoop(runCtx)
|
||||
|
||||
return nil
|
||||
|
|
@ -111,6 +115,8 @@ func (c *EmailChannel) Stop(ctx context.Context) error {
|
|||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.loopWg.Wait() // wait for checkLoop goroutine to exit
|
||||
|
||||
c.setRunning(false)
|
||||
logger.InfoC("email", "Email channel stopped")
|
||||
return nil
|
||||
|
|
@ -412,6 +418,7 @@ func (c *EmailChannel) reconnectWithBackoff(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (c *EmailChannel) checkLoop(ctx context.Context) {
|
||||
defer c.loopWg.Done()
|
||||
interval := time.Duration(c.config.CheckInterval) * time.Second
|
||||
if interval <= 0 {
|
||||
interval = 30 * time.Second
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -360,3 +361,57 @@ func TestEmailChannel_runIdleLoop(t *testing.T) {
|
|||
})
|
||||
|
||||
}
|
||||
|
||||
func TestEmailChannel_lifecycleCheck(t *testing.T) {
|
||||
// check if the current runtime is go1.25.xx
|
||||
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
||||
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
||||
t.Skip("skipping test in non-go1.25.xx environment")
|
||||
return
|
||||
}
|
||||
|
||||
mockey.PatchConvey("lifecycle test", t, func() {
|
||||
// --------------- mock start ---------------
|
||||
c := &EmailChannel{
|
||||
BaseChannel: &BaseChannel{
|
||||
bus: bus.NewMessageBus(),
|
||||
},
|
||||
config: config.EmailConfig{
|
||||
Enabled: true,
|
||||
CheckInterval: 1,
|
||||
ForcedPolling: true,
|
||||
IMAPServer: "imap.example.com",
|
||||
Username: "testuser",
|
||||
Password: "testpassword",
|
||||
},
|
||||
}
|
||||
// mock login and select to return mockClient
|
||||
mockey.Mock(mockey.GetMethod(c, "connect")).To(func(*EmailChannel) error {
|
||||
return nil
|
||||
}).Build()
|
||||
mockey.Mock(mockey.GetMethod(c, "CheckNewEmails")).To(func(*EmailChannel, context.Context) {
|
||||
time.Sleep(1 * time.Second)
|
||||
}).Build()
|
||||
// --------------- mock end ---------------
|
||||
ctx := context.Background()
|
||||
err := c.Start(ctx)
|
||||
assert.NoError(t, err)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
var stopDone time.Time
|
||||
stopStart := time.Now()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c.Stop(ctx)
|
||||
stopDone = time.Now()
|
||||
}()
|
||||
// wait for checkNewEmails to finish
|
||||
assert.True(t, c.IsRunning())
|
||||
wg.Wait()
|
||||
elapsed := stopDone.Sub(stopStart)
|
||||
// stop exit normally
|
||||
assert.False(t, c.IsRunning())
|
||||
// If Stop() did not wait for checkLoop, it would return in milliseconds.
|
||||
assert.GreaterOrEqual(t, elapsed, 1*time.Second, "Stop() must wait for checkLoop (lifecycle compliance)")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue