mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-22 08:53:46 +08:00
🐛 fix(driver-agent): 修复 Windows 测试连接释放失败
- 优先等待驱动代理自然退出,超时后再执行强制终止 - 进程已退出时忽略 Windows 终止权限竞态,避免连接成功被误判为失败 - 统一可选驱动与 MySQL agent 清理流程并补充回归测试
This commit is contained in:
60
internal/db/agent_process.go
Normal file
60
internal/db/agent_process.go
Normal 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")
|
||||
}
|
||||
}
|
||||
73
internal/db/agent_process_test.go
Normal file
73
internal/db/agent_process_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user