Skip to content

Add TCP connection pooling for NUT server client#405

Open
Brandawg93 wants to merge 7 commits intomainfrom
claude/gallant-wu
Open

Add TCP connection pooling for NUT server client#405
Brandawg93 wants to merge 7 commits intomainfrom
claude/gallant-wu

Conversation

@Brandawg93
Copy link
Copy Markdown
Owner

Changes

New Connection Pool (nut-pool.ts)

  • Implements NutConnectionPool class to manage reusable TCP connections per host:port pair
  • Configurable max pool size (default 3) and idle timeout (default 30s)
  • LIFO connection reuse to keep connections warm
  • Automatic idle sweep timer that closes stale connections and cleans up when empty
  • drain() method for graceful shutdown

Updated NUT Client (nut.ts)

  • Refactored connection handling to use the pool for unauthenticated requests
  • Authenticated sessions (with credentials) bypass the pool and are always closed after use
  • New getAcquiredConnection() method that tries pool first, then creates fresh socket if needed
  • New releaseConnection() method that returns healthy connections to pool or closes on error
  • Pool sockets are not sent LOGOUT commands on successful release (stays authenticated to NUT daemon)
  • Errored pool sockets are destroyed immediately
  • getData() now reuses pooled connections across multiple variable lookups

Tests

  • Comprehensive test suite for NutConnectionPool (211 lines): acquire/release/drain, LIFO behavior, stale socket handling, idle sweep, pool capacity limits
  • Extended Nut tests (112 new lines): verify pool reuse, release on success/error, authenticated bypass, socket passing to helpers

Configuration

  • Added .claude to .gitignore

Benefits

  • Reduces connection overhead for frequent NUT queries
  • Improves performance by reusing authenticated sessions to NUT daemon
  • Prevents connection leaks with automatic idle timeout and cleanup
  • Separate pools per host:port for multi-UPS environments

Reuses idle TCP connections across sequential NUT protocol calls instead
of opening and closing a fresh socket per command. This reduces handshake
overhead on operations like getDevice() that fan out to many methods
(LIST VAR, GET DESC × N, GET TYPE × N, LIST CMD, GET UPSDESC) and keeps
the number of concurrent sockets bounded to prevent EventEmitter listener
buildup.

- Add NutConnectionPool (src/server/nut-pool.ts): module-level singleton
  keyed by host:port, LIFO idle pool capped at 3 per server, idle sweep
  timer with 30s timeout, drain() for clean shutdown
- Replace getConnection() in Nut with getAcquiredConnection() that tries
  the pool first for unauthenticated sessions; authenticated sessions
  (checkCredentials=true) always use a fresh socket and are destroyed
  after use (LOGOUT + close) to avoid sharing LOGIN state
- Add releaseConnection(): returns successful unauthenticated connections
  to the pool; destroys errored or authenticated ones
- Update getData() to use getAcquiredConnection/releaseConnection so the
  shared socket pattern (passing socket to getCachedVarDescription etc.)
  is preserved while the connection is pooled at the outer scope
- Add .claude to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 28, 2026 23:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds TCP connection pooling to the NUT (Network UPS Tools) client to reduce connection churn and improve performance for frequent variable queries, while keeping authenticated sessions isolated from pooling.

Changes:

  • Introduces NutConnectionPool to reuse idle TCP sockets per host:port with max size + idle timeout sweeping.
  • Refactors Nut connection lifecycle to acquire/release pooled connections for unauthenticated commands and force-close authenticated sessions.
  • Adds unit tests covering pool behavior and validates Nut integration with pooling.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/server/nut.ts Refactors command execution and getData() to acquire/release pooled connections; authenticated connections bypass pooling.
src/server/nut-pool.ts Implements host/port-based connection pool with LIFO reuse, idle sweeping, and drain support.
__tests__/unit/server/nut.test.ts Adds tests ensuring pooled reuse, correct release/close behavior on success/error, and authenticated bypass.
__tests__/unit/server/nut-pool.test.ts Adds a comprehensive test suite for pool acquire/release/drain, LIFO behavior, and idle sweep behavior.
.gitignore Ignores .claude directory/file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server/nut-pool.ts Outdated
Comment thread src/server/nut-pool.ts Outdated
Comment thread src/server/nut.ts
Comment thread src/server/nut.ts Outdated
Comment thread src/server/nut.ts
Comment thread src/server/nut-pool.ts Outdated
Brandawg93 and others added 3 commits March 28, 2026 19:46
Extract makePoolSocket factory in nut.test.ts to eliminate repeated
mock socket object literals across Connection Pool test cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix socket leak: wrap checkCredentials() in try/catch in the
  authenticated branch of getAcquiredConnection so the socket is always
  closed if credential validation throws
- Track hasError in getData() and pass to releaseConnection so broken
  sockets are not returned to the pool after a mid-request failure
- Update releaseConnection docstring to match actual behaviour (overflow
  connections are also returned to the pool, not closed directly)
- Remove empty buckets from the pools map in acquire(), sweepIdle(), and
  drain() to prevent unbounded map growth
- Fix drain(host, port) to only clear the global sweep timer when all
  buckets have been drained; scoped drains leave the timer running if
  other buckets still have idle connections
- Switch process.on('exit') to 'beforeExit' so the async drain() call
  can actually complete before the process exits
- Deduplicate mock socket construction in Connection Pool tests behind
  a makePoolSocket factory that accepts a custom readAll mock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…threshold

Extract openSocket() in nut.ts to eliminate the repeated connect/error-wrap
pattern across getAcquiredConnection and checkCredentials. Extract
mockGetDevicesWithUps() helper in nut.test.ts to deduplicate the repeated
getDevices spy setup across checkCredentials and Connection Pool tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/server/nut.ts:185

  • getCommand() converts any readAll() failure for LIST VAR ... into DEVICE_UNREACHABLE, which means transport errors (timeouts/connection resets) can be treated as a non-error and the underlying socket may be returned to the pool. If the socket still reports isConnected() === true, this can poison the pool with a connection that will keep timing out/failing. Consider distinguishing NUT protocol ERR ... responses from transport errors (or move the DEVICE_UNREACHABLE fallback into getData() so you can still mark the connection as errored and close it) so sockets that hit real I/O failures are not pooled.
      const data = await conn.socket.readAll(command, until).catch((error) => {
        this.debug.warn('Command failed, handling fallback', { command, error: error.message })
        if (command.startsWith('LIST VAR')) {
          return upsStatus.DEVICE_UNREACHABLE
        }
        throw error

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread __tests__/unit/server/nut.test.ts Outdated
Comment thread src/server/nut-pool.ts
Brandawg93 and others added 3 commits March 28, 2026 20:21
Use process.once('beforeExit') in nut-pool.ts to avoid duplicate listener
registration on hot reload. In getData(), mark hasError=true when getCommand
returns DEVICE_UNREACHABLE so transport-level failures destroy the socket
instead of returning a potentially broken connection to the pool.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants