Skip to content

Commit 6e1e262

Browse files
authored
Fix file descriptor exhaustion in NetBox connections (#1850)
Implement shared session management to prevent "[Errno 24] No file descriptors available" error when connecting to multiple NetBox instances. Changes: - Add NetBoxSessionManager class with lazy initialization - Use single shared HTTP session for all NetBox connections - Add connection pool limits (pool_connections=10, pool_maxsize=10) - Register atexit handler for automatic session cleanup AI-assisted: Claude Code Signed-off-by: Christian Berendt <[email protected]>
1 parent 07394e4 commit 6e1e262

File tree

1 file changed

+70
-16
lines changed

1 file changed

+70
-16
lines changed

osism/utils/__init__.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3+
import atexit
4+
import threading
35
import time
46
import uuid
57
import os
@@ -90,20 +92,77 @@ def __exit__(self, exc_type, exc_val, exc_tb):
9092
class TimeoutHTTPAdapter(HTTPAdapter):
9193
"""HTTPAdapter that sets a default timeout for all requests."""
9294

93-
def __init__(self, timeout=None, *args, **kwargs):
95+
def __init__(
96+
self, timeout=None, pool_connections=10, pool_maxsize=10, *args, **kwargs
97+
):
9498
self.timeout = timeout
95-
super().__init__(*args, **kwargs)
99+
super().__init__(
100+
pool_connections=pool_connections,
101+
pool_maxsize=pool_maxsize,
102+
*args,
103+
**kwargs,
104+
)
96105

97106
def send(self, request, **kwargs):
98107
if kwargs.get("timeout") is None and self.timeout is not None:
99108
kwargs["timeout"] = self.timeout
100109
return super().send(request, **kwargs)
101110

102111

112+
class NetBoxSessionManager:
113+
"""Manages a shared HTTP session for all NetBox connections.
114+
115+
This class implements lazy initialization of a single shared session
116+
to prevent file descriptor exhaustion from multiple session instances.
117+
"""
118+
119+
_session = None
120+
_lock = None
121+
122+
@classmethod
123+
def get_session(cls, ignore_ssl_errors=False, timeout=20):
124+
"""Get or create the shared session (lazy initialization).
125+
126+
Args:
127+
ignore_ssl_errors: Whether to ignore SSL certificate errors
128+
timeout: Request timeout in seconds (default: 20)
129+
130+
Returns:
131+
requests.Session: The shared session instance
132+
"""
133+
if cls._session is None:
134+
if cls._lock is None:
135+
cls._lock = threading.Lock()
136+
with cls._lock:
137+
if cls._session is None:
138+
cls._session = requests.Session()
139+
adapter = TimeoutHTTPAdapter(
140+
timeout=timeout, pool_connections=10, pool_maxsize=10
141+
)
142+
cls._session.mount("http://", adapter)
143+
cls._session.mount("https://", adapter)
144+
if ignore_ssl_errors:
145+
urllib3.disable_warnings()
146+
cls._session.verify = False
147+
return cls._session
148+
149+
@classmethod
150+
def close_session(cls):
151+
"""Close the shared session and release resources."""
152+
if cls._session is not None:
153+
cls._session.close()
154+
cls._session = None
155+
156+
157+
def cleanup_netbox_sessions():
158+
"""Cleanup function to close all NetBox sessions."""
159+
NetBoxSessionManager.close_session()
160+
161+
103162
def get_netbox_connection(
104163
netbox_url, netbox_token, ignore_ssl_errors=False, timeout=20
105164
):
106-
"""Create a NetBox API connection.
165+
"""Create a NetBox API connection with shared session.
107166
108167
Args:
109168
netbox_url: NetBox URL
@@ -118,19 +177,10 @@ def get_netbox_connection(
118177
nb = pynetbox.api(netbox_url, token=netbox_token)
119178

120179
if nb:
121-
# Create session with timeout adapter
122-
session = requests.Session()
123-
124-
# Mount timeout adapter for both http and https
125-
adapter = TimeoutHTTPAdapter(timeout=timeout)
126-
session.mount("http://", adapter)
127-
session.mount("https://", adapter)
128-
129-
if ignore_ssl_errors:
130-
urllib3.disable_warnings()
131-
session.verify = False
132-
133-
nb.http_session = session
180+
# Use shared session instead of creating new one
181+
nb.http_session = NetBoxSessionManager.get_session(
182+
ignore_ssl_errors=ignore_ssl_errors, timeout=timeout
183+
)
134184

135185
else:
136186
nb = None
@@ -207,6 +257,10 @@ def get_netbox_connection(
207257
secondary_nb_list = []
208258

209259

260+
# Register cleanup handler to close sessions on program exit
261+
atexit.register(cleanup_netbox_sessions)
262+
263+
210264
def get_openstack_connection():
211265
try:
212266
conn = openstack.connect()

0 commit comments

Comments
 (0)