173 lines
4.2 KiB
Go
173 lines
4.2 KiB
Go
package instance
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/agentctl/server/config"
|
|
"github.com/kandev/kandev/pkg/agent"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestStopInstanceBoundsHTTPServerShutdown(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
mgr := NewManager(&config.Config{
|
|
Ports: config.PortConfig{Base: 0, Max: 0},
|
|
Defaults: config.InstanceDefaults{Protocol: agent.ProtocolACP},
|
|
}, log)
|
|
t.Cleanup(func() {
|
|
_ = mgr.Shutdown(context.Background())
|
|
})
|
|
|
|
requestStarted := make(chan struct{})
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(requestStarted)
|
|
select {
|
|
case <-r.Context().Done():
|
|
return
|
|
case <-time.After(2 * time.Second):
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
})
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
server := &http.Server{Handler: handler}
|
|
serveErr := make(chan error, 1)
|
|
go func() {
|
|
serveErr <- server.Serve(listener)
|
|
}()
|
|
|
|
inst := &Instance{
|
|
ID: "slow-http-shutdown",
|
|
Port: listener.Addr().(*net.TCPAddr).Port,
|
|
Status: "running",
|
|
CreatedAt: time.Now(),
|
|
server: server,
|
|
}
|
|
mgr.mu.Lock()
|
|
mgr.instances[inst.ID] = inst
|
|
mgr.mu.Unlock()
|
|
|
|
clientDone := make(chan struct{})
|
|
go func() {
|
|
defer close(clientDone)
|
|
resp, err := http.Get("http://" + listener.Addr().String() + "/probe")
|
|
if err == nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-requestStarted:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("test HTTP request did not reach server")
|
|
}
|
|
|
|
start := time.Now()
|
|
require.NoError(t, mgr.StopInstance(context.Background(), inst.ID))
|
|
elapsed := time.Since(start)
|
|
|
|
assert.Less(t, elapsed, 700*time.Millisecond,
|
|
"StopInstance should not wait for long-running probe handlers during shutdown")
|
|
|
|
select {
|
|
case <-clientDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("test HTTP client did not finish after instance shutdown")
|
|
}
|
|
|
|
select {
|
|
case err := <-serveErr:
|
|
require.ErrorIs(t, err, http.ErrServerClosed)
|
|
case <-time.After(time.Second):
|
|
t.Fatal("test HTTP server did not stop")
|
|
}
|
|
}
|
|
|
|
func TestStopHTTPServerReturnsCloseError(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
mgr := NewManager(&config.Config{
|
|
Ports: config.PortConfig{Base: 0, Max: 0},
|
|
Defaults: config.InstanceDefaults{Protocol: agent.ProtocolACP},
|
|
}, log)
|
|
|
|
closeErr := errors.New("listener close failed")
|
|
server := &fakeHTTPServer{
|
|
shutdownErr: context.DeadlineExceeded,
|
|
closeErr: closeErr,
|
|
}
|
|
|
|
err := mgr.stopHTTPServer(context.Background(), "close-failure", 12345, server)
|
|
require.ErrorIs(t, err, closeErr)
|
|
require.True(t, server.closed)
|
|
}
|
|
|
|
func TestStopInstanceReleasesPortWhenHTTPServerCloseFails(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
mgr := NewManager(&config.Config{
|
|
Ports: config.PortConfig{Base: 12345, Max: 12345},
|
|
Defaults: config.InstanceDefaults{Protocol: agent.ProtocolACP},
|
|
}, log)
|
|
|
|
port, err := mgr.portAlloc.Allocate("close-failure")
|
|
require.NoError(t, err)
|
|
|
|
closeErr := errors.New("listener close failed")
|
|
inst := &Instance{
|
|
ID: "close-failure",
|
|
Port: port,
|
|
Status: "running",
|
|
CreatedAt: time.Now(),
|
|
server: &fakeHTTPServer{
|
|
shutdownErr: context.DeadlineExceeded,
|
|
closeErr: closeErr,
|
|
},
|
|
}
|
|
|
|
mgr.mu.Lock()
|
|
mgr.instances[inst.ID] = inst
|
|
mgr.mu.Unlock()
|
|
|
|
err = mgr.StopInstance(context.Background(), inst.ID)
|
|
require.ErrorIs(t, err, closeErr)
|
|
|
|
reusedPort, err := mgr.portAlloc.Allocate("next-instance")
|
|
require.NoError(t, err)
|
|
require.Equal(t, port, reusedPort)
|
|
}
|
|
|
|
func TestStopHTTPServerTreatsCanceledShutdownAsStoppedAfterClose(t *testing.T) {
|
|
log := newTestLogger(t)
|
|
mgr := NewManager(&config.Config{
|
|
Ports: config.PortConfig{Base: 0, Max: 0},
|
|
Defaults: config.InstanceDefaults{Protocol: agent.ProtocolACP},
|
|
}, log)
|
|
|
|
server := &fakeHTTPServer{shutdownErr: context.Canceled}
|
|
|
|
err := mgr.stopHTTPServer(context.Background(), "canceled-shutdown", 12345, server)
|
|
require.NoError(t, err)
|
|
require.True(t, server.closed)
|
|
}
|
|
|
|
type fakeHTTPServer struct {
|
|
shutdownErr error
|
|
closeErr error
|
|
closed bool
|
|
}
|
|
|
|
func (s *fakeHTTPServer) Shutdown(context.Context) error {
|
|
return s.shutdownErr
|
|
}
|
|
|
|
func (s *fakeHTTPServer) Close() error {
|
|
s.closed = true
|
|
return s.closeErr
|
|
}
|