package actuator import ( "context" "fmt" "sync" "time" "golang.org/x/crypto/ssh" ) type poolEntry struct { client *ssh.Client createdAt time.Time } type DialPool struct { mu sync.RWMutex entries map[string]*poolEntry ttl time.Duration done chan struct{} stopped bool } func NewDialPool(ttl time.Duration) *DialPool { p := &DialPool{ entries: make(map[string]*poolEntry), ttl: ttl, done: make(chan struct{}), } if ttl > 0 { go p.evictLoop() } return p } func (p *DialPool) key(opts DialOptions) string { port := opts.Port if port <= 0 { port = 22 } return fmt.Sprintf("%s:%d", opts.Host, port) } func (p *DialPool) Get(ctx context.Context, opts DialOptions) (*ssh.Client, error) { k := p.key(opts) p.mu.RLock() entry, ok := p.entries[k] p.mu.RUnlock() if ok { // Quick health check: a session can be created without running a // command — if it fails, the connection is dead and we evict it. testSession, err := entry.client.NewSession() if err == nil { testSession.Close() return entry.client, nil } p.mu.Lock() if p.entries[k] == entry { entry.client.Close() delete(p.entries, k) } p.mu.Unlock() // Fall through to dial below } client, err := Dial(ctx, opts) if err != nil { return nil, err } p.mu.Lock() if p.stopped { p.mu.Unlock() client.Close() return nil, fmt.Errorf("ssh dial pool: closed") } if existing, ok2 := p.entries[k]; ok2 { p.mu.Unlock() client.Close() return existing.client, nil } p.entries[k] = &poolEntry{client: client, createdAt: time.Now()} p.mu.Unlock() return client, nil } func (p *DialPool) Close() { p.mu.Lock() p.stopped = true for k, entry := range p.entries { entry.client.Close() delete(p.entries, k) } p.mu.Unlock() if p.ttl > 0 { close(p.done) } } func (p *DialPool) evictLoop() { ticker := time.NewTicker(p.ttl / 2) defer ticker.Stop() for { select { case <-p.done: return case <-ticker.C: p.evict() } } } func (p *DialPool) evict() { deadline := time.Now().Add(-p.ttl) p.mu.Lock() defer p.mu.Unlock() for k, entry := range p.entries { if entry.createdAt.Before(deadline) { entry.client.Close() delete(p.entries, k) } } }