diff --git a/docs/source/modules/admin.rst b/docs/source/modules/admin.rst index 3c88c96..7f20522 100644 --- a/docs/source/modules/admin.rst +++ b/docs/source/modules/admin.rst @@ -15,7 +15,8 @@ Configure admin client username='example-admin', password='secret', realm_name="master", - user_realm_name="only_if_other_realm_than_master") + user_realm_name="only_if_other_realm_than_master", + pool_maxsize=20) Configure admin client with connection @@ -34,6 +35,7 @@ Configure admin client with connection user_realm_name="only_if_other_realm_than_master", client_id="my_client", client_secret_key="client-secret", + pool_maxsize=25, verify=True) keycloak_admin = KeycloakAdmin(connection=keycloak_connection) diff --git a/docs/source/modules/openid_client.rst b/docs/source/modules/openid_client.rst index c3c0c90..a7ae009 100644 --- a/docs/source/modules/openid_client.rst +++ b/docs/source/modules/openid_client.rst @@ -16,7 +16,8 @@ Configure client OpenID keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/", client_id="example_client", realm_name="example_realm", - client_secret_key="secret") + client_secret_key="secret", + pool_maxsize=15) # Example: Set connection pool size Get .well_know diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index 19317e0..7f736ef 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -59,6 +59,8 @@ class ConnectionManager: :type cert: Union[str,Tuple[str,str]] :param max_retries: The total number of times to retry HTTP requests. :type max_retries: int + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ def __init__( @@ -70,6 +72,7 @@ class ConnectionManager: proxies: dict | None = None, cert: str | tuple | None = None, max_retries: int = 1, + pool_maxsize: int | None = None, ) -> None: """ Init method. @@ -91,19 +94,25 @@ class ConnectionManager: :type cert: Union[str,Tuple[str,str]] :param max_retries: The total number of times to retry HTTP requests. :type max_retries: int + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ self.base_url = base_url self.headers = headers self.timeout = timeout self.verify = verify self.cert = cert + self.pool_maxsize = pool_maxsize self._s = requests.Session() self._s.auth = lambda x: x # don't let requests add auth headers # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 for protocol in ("https://", "http://"): - adapter = HTTPAdapter(max_retries=max_retries) + adapter_kwargs = {"max_retries": max_retries} + if pool_maxsize is not None: + adapter_kwargs["pool_maxsize"] = pool_maxsize + adapter = HTTPAdapter(**adapter_kwargs) # adds POST to retry whitelist allowed_methods = set(adapter.max_retries.allowed_methods) allowed_methods.add("POST") @@ -114,7 +123,15 @@ class ConnectionManager: if proxies: self._s.proxies.update(proxies) - self.async_s = httpx.AsyncClient(verify=verify, mounts=proxies, cert=cert) + self.async_s = httpx.AsyncClient( + verify=verify, + mounts=proxies, + cert=cert, + limits=httpx.Limits( + max_connections=100 if pool_maxsize is None else pool_maxsize, + max_keepalive_connections=20, + ), + ) self.async_s.auth = None # don't let requests add auth headers self.async_s.transport = httpx.AsyncHTTPTransport(retries=1) @@ -184,6 +201,20 @@ class ConnectionManager: def cert(self, value: str | tuple) -> None: self._cert = value + @property + def pool_maxsize(self) -> int | None: + """ + Return the maximum number of connections to save in the pool. + + :returns: Pool maxsize + :rtype: int or None + """ + return self._pool_maxsize + + @pool_maxsize.setter + def pool_maxsize(self, value: int | None) -> None: + self._pool_maxsize = value + @property def headers(self) -> dict: """ diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index fab1a71..387b6d0 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -88,6 +88,8 @@ class KeycloakAdmin: :type max_retries: int :param connection: A KeycloakOpenIDConnection as an alternative to individual params. :type connection: KeycloakOpenIDConnection + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ PAGE_SIZE = 100 @@ -110,6 +112,7 @@ class KeycloakAdmin: cert: str | tuple | None = None, max_retries: int = 1, connection: KeycloakOpenIDConnection | None = None, + pool_maxsize: int | None = None, ) -> None: """ Init method. @@ -149,6 +152,8 @@ class KeycloakAdmin: :type max_retries: int :param connection: An OpenID Connection as an alternative to individual params. :type connection: KeycloakOpenIDConnection + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ self.connection = connection or KeycloakOpenIDConnection( server_url=server_url, @@ -166,6 +171,7 @@ class KeycloakAdmin: timeout=timeout, cert=cert, max_retries=max_retries, + pool_maxsize=pool_maxsize, ) @property diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 8e603b6..a35d6ac 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -87,7 +87,8 @@ class KeycloakOpenID: Either a path to an SSL certificate file, or two-tuple of (certificate file, key file). :param max_retries: The total number of times to retry HTTP requests. - :type max_retries: int + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ def __init__( @@ -102,6 +103,7 @@ class KeycloakOpenID: timeout: int = 60, cert: str | tuple | None = None, max_retries: int = 1, + pool_maxsize: int | None = None, ) -> None: """ Init method. @@ -129,6 +131,8 @@ class KeycloakOpenID: :type cert: Union[str,Tuple[str,str]] :param max_retries: The total number of times to retry HTTP requests. :type max_retries: int + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ self.client_id = client_id self.client_secret_key = client_secret_key @@ -142,6 +146,7 @@ class KeycloakOpenID: proxies=proxies, cert=cert, max_retries=max_retries, + pool_maxsize=pool_maxsize, ) self.authorization = Authorization() diff --git a/src/keycloak/openid_connection.py b/src/keycloak/openid_connection.py index 3ec0519..c22d376 100644 --- a/src/keycloak/openid_connection.py +++ b/src/keycloak/openid_connection.py @@ -82,6 +82,7 @@ class KeycloakOpenIDConnection(ConnectionManager): timeout: int | None = 60, cert: str | tuple | None = None, max_retries: int = 1, + pool_maxsize: int | None = None, ) -> None: """ Init method. @@ -120,6 +121,8 @@ class KeycloakOpenIDConnection(ConnectionManager): :type cert: Union[str,Tuple[str,str]] :param max_retries: The total number of times to retry HTTP requests. :type max_retries: int + :param pool_maxsize: The maximum number of connections to save in the pool. + :type pool_maxsize: int """ # token is renewed when it hits 90% of its lifetime. This is to account for any possible # clock skew. @@ -154,6 +157,7 @@ class KeycloakOpenIDConnection(ConnectionManager): verify=self.verify, cert=cert, max_retries=max_retries, + pool_maxsize=pool_maxsize, ) @property diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index a1ea81d..4edb17b 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -57,6 +57,7 @@ def test_keycloak_admin_init(env: KeycloakTestEnv) -> None: server_url=f"http://{env.keycloak_host}:{env.keycloak_port}", username=env.keycloak_admin, password=env.keycloak_admin_password, + pool_maxsize=5, ) assert admin.connection.server_url == f"http://{env.keycloak_host}:{env.keycloak_port}", ( admin.connection.server_url @@ -72,6 +73,7 @@ def test_keycloak_admin_init(env: KeycloakTestEnv) -> None: assert admin.connection.token is None, admin.connection.token assert admin.connection.user_realm_name is None, admin.connection.user_realm_name assert admin.connection.custom_headers is None, admin.connection.custom_headers + assert admin.connection.pool_maxsize == 5, admin.connection.pool_maxsize admin = KeycloakAdmin( server_url=f"http://{env.keycloak_host}:{env.keycloak_port}", diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index b820e56..712c312 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -35,6 +35,7 @@ def test_keycloak_openid_init(env: KeycloakTestEnv) -> None: server_url=f"http://{env.keycloak_host}:{env.keycloak_port}", realm_name="master", client_id="admin-cli", + pool_maxsize=5, ) assert oid.client_id == "admin-cli" @@ -42,6 +43,14 @@ def test_keycloak_openid_init(env: KeycloakTestEnv) -> None: assert oid.realm_name == "master" assert isinstance(oid.connection, ConnectionManager) assert isinstance(oid.authorization, Authorization) + assert oid.connection.pool_maxsize == 5 + + oid_default = KeycloakOpenID( + server_url=f"http://{env.keycloak_host}:{env.keycloak_port}", + realm_name="master", + client_id="admin-cli", + ) + assert oid_default.connection.pool_maxsize is None def test_well_known(oid: KeycloakOpenID) -> None: