🐛 fix(driver-agent): 修复 Windows 测试连接释放失败

- 优先等待驱动代理自然退出,超时后再执行强制终止
- 进程已退出时忽略 Windows 终止权限竞态,避免连接成功被误判为失败
- 统一可选驱动与 MySQL agent 清理流程并补充回归测试
This commit is contained in:
Syngnat
2026-07-24 14:27:09 +08:00
parent c76daa0727
commit 3bb0866633
4 changed files with 135 additions and 26 deletions

View File

@@ -0,0 +1,60 @@
package db
import (
"errors"
"io"
"os/exec"
"time"
)
const agentProcessExitTimeout = 2 * time.Second
func closeAgentProcess(stdin io.Closer, cmd *exec.Cmd) error {
if stdin != nil {
_ = stdin.Close()
}
if cmd == nil || cmd.Process == nil {
return nil
}
return waitForAgentExit(cmd.Wait, cmd.Process.Kill, agentProcessExitTimeout)
}
func waitForAgentExit(wait func() error, kill func() error, timeout time.Duration) error {
if wait == nil {
return nil
}
if timeout <= 0 {
timeout = agentProcessExitTimeout
}
waitCh := make(chan error, 1)
go func() {
waitCh <- wait()
}()
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-waitCh:
return nil
case <-timer.C:
}
var killErr error
if kill != nil {
killErr = kill()
}
timer.Reset(timeout)
select {
case <-waitCh:
// The process is already reaped. A concurrent Kill can report access
// denied on Windows even though cleanup completed successfully.
return nil
case <-timer.C:
if killErr != nil {
return killErr
}
return errors.New("driver agent process did not exit after termination")
}
}

View File

@@ -0,0 +1,73 @@
package db
import (
"errors"
"sync/atomic"
"testing"
"time"
)
func TestWaitForAgentExitIgnoresKillErrorAfterProcessExit(t *testing.T) {
waitStarted := make(chan struct{})
processExited := make(chan struct{})
killErr := errors.New("TerminateProcess: Access is denied")
err := waitForAgentExit(
func() error {
close(waitStarted)
<-processExited
return nil
},
func() error {
<-waitStarted
close(processExited)
return killErr
},
50*time.Millisecond,
)
if err != nil {
t.Fatalf("process exit should make the racing kill error irrelevant, got %v", err)
}
}
func TestWaitForAgentExitWaitsBeforeKilling(t *testing.T) {
var killCalled atomic.Bool
err := waitForAgentExit(
func() error { return nil },
func() error {
killCalled.Store(true)
return nil
},
time.Second,
)
if err != nil {
t.Fatalf("graceful process exit returned error: %v", err)
}
if killCalled.Load() {
t.Fatal("process was killed even though it exited during the graceful wait")
}
}
func TestWaitForAgentExitReturnsKillErrorWhenProcessDoesNotExit(t *testing.T) {
waitStarted := make(chan struct{})
processExited := make(chan struct{})
killErr := errors.New("TerminateProcess: Access is denied")
err := waitForAgentExit(
func() error {
close(waitStarted)
<-processExited
return nil
},
func() error {
<-waitStarted
return killErr
},
20*time.Millisecond,
)
close(processExited)
if !errors.Is(err, killErr) {
t.Fatalf("expected kill error when the process never exits, got %v", err)
}
}

View File

@@ -176,19 +176,7 @@ func (c *mysqlAgentClient) call(req mysqlAgentRequest, out interface{}, fields *
func (c *mysqlAgentClient) close() error {
c.mu.Lock()
defer c.mu.Unlock()
var closeErr error
if c.stdin != nil {
_ = c.stdin.Close()
}
if c.cmd != nil && c.cmd.Process != nil {
if err := c.cmd.Process.Kill(); err != nil {
closeErr = err
}
}
if c.cmd != nil {
_ = c.cmd.Wait()
}
return closeErr
return closeAgentProcess(c.stdin, c.cmd)
}
type MySQLAgentDB struct {

View File

@@ -416,19 +416,7 @@ func (c *optionalDriverAgentClient) forceTerminate() {
func (c *optionalDriverAgentClient) close() error {
c.mu.Lock()
defer c.mu.Unlock()
var closeErr error
if c.stdin != nil {
_ = c.stdin.Close()
}
if c.cmd != nil && c.cmd.Process != nil {
if err := c.cmd.Process.Kill(); err != nil {
closeErr = err
}
}
if c.cmd != nil {
_ = c.cmd.Wait()
}
return closeErr
return closeAgentProcess(c.stdin, c.cmd)
}
type OptionalDriverAgentDB struct {