NatsService is now a container singleton, so a queue worker no longer opens a new
TLS connection to the broker for every NatsPublisherJob. In production that handshake
timed out ('Connection timed out') for ~7% of publishes, each timeout holding a worker
for 5-10 s and some events being lost, and leaked file descriptors (FD_SETSIZE).
- A reused connection is ping()-checked after 15 s idle (a publish-only client never
answers the broker's PINGs), and replaced after 60 s or 1000 publishes. A connection
holding subscriptions (nats:listen) is never recycled.
- The socket is closed explicitly on failure instead of waiting for garbage collection.
- json_encode uses JSON_INVALID_UTF8_SUBSTITUTE so a model with bad bytes no longer
becomes a lost event.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>