Skip to content

Commit fc94b7d

Browse files
authored
15 add connection pooling (#16)
# **Implement Connection Pooling** <!-- This PR fixes #15 --> ## **Description** <!-- πŸ“›πŸ“› Please include a summary of the change and/or which issue is fixed. List any dependencies required for this change, if there are any. πŸ“›πŸ“› --> * This PR introduces connection pooling to improve database performance and resource management. By implementing a pool of reusable database connections, the overhead of establishing new connections for each request is reduced, leading to lower latency and better scalability under high load conditions. --- ### **Additional context** <!-- Add any other context or additional information about the pull request.--> * This change addresses the issue of inefficient database connection management by ensuring that connections are reused rather than constantly opened and closed. This enhancement is particularly beneficial for applications with high database request volumes, as it optimizes resource utilization and performance. <!-- πŸ“›πŸ“›πŸ“›πŸ“› If it fixes any current issue please let us know this way: Uncomment the comment above "description", then add your number of issues after the "#". Example: # **This pull request fixes #NUMBER_OF_THE_ISSUE issue** If there are multiple issues to be closed with the merge of this pull request please do it like so: **This pull request fixes #NUMBER_OF_THE_ISSUE, fixes #NUMBER_OF_THE_ISSUE and fixes #NUMBER_OF_THE_ISSUE issue**. For more information on closing issues using keywords, please check https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords#closing-multiple-issues πŸ“›πŸ“›πŸ“›πŸ“› -->
2 parents cba12d8 + 86e649d commit fc94b7d

File tree

4 files changed

+114
-91
lines changed

4 files changed

+114
-91
lines changed

β€Ž.gitguardian.ymlβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
incident:
2+
ignore:
3+
- name: "Ignore example secrets"
4+
match: "src/core/config.py"
5+
reason: "Example credentials that are not used in production."

β€Žsrc/controller/api/endpoints/customer.pyβ€Ž

Lines changed: 83 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -30,46 +30,6 @@
3030
CommonDeps = Annotated[dict[str, Any], Depends(common_query_parameters)]
3131

3232

33-
@router.delete(
34-
"/v1/customers/{customer_id}",
35-
responses={
36-
204: {"description": "No Content."},
37-
400: {"model": ErrorMessage, "description": "Bad Request."},
38-
401: {"model": ErrorMessage, "description": "Unauthorized."},
39-
403: {"model": ErrorMessage, "description": "Forbidden."},
40-
404: {"model": ErrorMessage, "description": "Not Found."},
41-
405: {"model": ErrorMessage, "description": "Method Not Allowed."},
42-
422: {"model": ErrorMessage, "description": "Unprocessable Entity."},
43-
500: {"model": ErrorMessage, "description": "Internal Server Error."},
44-
502: {"model": ErrorMessage, "description": "Bad Gateway."},
45-
503: {"model": ErrorMessage, "description": "Service Unavailable."},
46-
504: {"model": ErrorMessage, "description": "Gateway Timeout."},
47-
},
48-
tags=["Customers"],
49-
summary="Delete specific customer.",
50-
response_model=None,
51-
)
52-
async def delete_customer_id(
53-
customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")],
54-
http_request_info: CommonDeps,
55-
db_connection: Annotated[Session, Depends(get_db_session)],
56-
) -> Response:
57-
"""Delete the information of the customer with the matching Id."""
58-
logger.info("Entering...")
59-
logger.debug("Deleting customer with id %s", customer_id)
60-
try:
61-
CustomerApplicationService.delete_customer(db_connection, customer_id)
62-
logger.debug("Customer with id %s deleted", customer_id)
63-
except ElementNotFoundError as error:
64-
logger.error("Customer with id %s not found", customer_id) # noqa: TRY400
65-
raise HTTP404NotFoundError from error
66-
except Exception as error:
67-
logger.exception("Error deleting customer with id %s", customer_id)
68-
raise HTTP500InternalServerError from error
69-
logger.info("Exiting...")
70-
return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info)
71-
72-
7333
@router.get(
7434
"/v1/customers",
7535
responses={
@@ -150,49 +110,6 @@ async def get_customers(
150110
)
151111

152112

153-
@router.get(
154-
"/v1/customers/{customer_id}",
155-
responses={
156-
200: {"model": CustomerDetailResponse, "description": "OK."},
157-
401: {"model": ErrorMessage, "description": "Unauthorized."},
158-
403: {"model": ErrorMessage, "description": "Forbidden."},
159-
404: {"model": ErrorMessage, "description": "Not Found."},
160-
422: {"model": ErrorMessage, "description": "Unprocessable Entity."},
161-
423: {"model": ErrorMessage, "description": "Locked."},
162-
500: {"model": ErrorMessage, "description": "Internal Server Error."},
163-
501: {"model": ErrorMessage, "description": "Not Implemented."},
164-
502: {"model": ErrorMessage, "description": "Bad Gateway."},
165-
503: {"model": ErrorMessage, "description": "Service Unavailable."},
166-
504: {"model": ErrorMessage, "description": "Gateway Timeout."},
167-
},
168-
tags=["Customers"],
169-
summary="Customer information.",
170-
response_model_by_alias=True,
171-
response_model=CustomerDetailResponse,
172-
)
173-
async def get_customer_id(
174-
http_request_info: CommonDeps,
175-
db_connection: Annotated[Session, Depends(get_db_session)],
176-
customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")],
177-
) -> JSONResponse:
178-
"""Retrieve the information of the customer with the matching code."""
179-
logger.info("Entering...")
180-
logger.debug("Getting customer with id %s", customer_id)
181-
try:
182-
api_data = CustomerApplicationService.get_customer_id(db_connection, customer_id)
183-
logger.debug("Customer with id %s retrieved", customer_id)
184-
except ElementNotFoundError as error:
185-
logger.error("Customer with id %s not found", customer_id) # noqa: TRY400
186-
raise HTTP404NotFoundError from error
187-
except Exception as error:
188-
logger.exception("Error getting customer with id %s", customer_id)
189-
raise HTTP500InternalServerError from error
190-
logger.info("Exiting...")
191-
return JSONResponse(
192-
content=api_data.model_dump(), status_code=status.HTTP_200_OK, headers=http_request_info
193-
)
194-
195-
196113
@router.post(
197114
"/v1/customers",
198115
responses={
@@ -278,6 +195,89 @@ async def put_customers_customer_id(
278195
return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info)
279196

280197

198+
@router.delete(
199+
"/v1/customers/{customer_id}",
200+
responses={
201+
204: {"description": "No Content."},
202+
400: {"model": ErrorMessage, "description": "Bad Request."},
203+
401: {"model": ErrorMessage, "description": "Unauthorized."},
204+
403: {"model": ErrorMessage, "description": "Forbidden."},
205+
404: {"model": ErrorMessage, "description": "Not Found."},
206+
405: {"model": ErrorMessage, "description": "Method Not Allowed."},
207+
422: {"model": ErrorMessage, "description": "Unprocessable Entity."},
208+
500: {"model": ErrorMessage, "description": "Internal Server Error."},
209+
502: {"model": ErrorMessage, "description": "Bad Gateway."},
210+
503: {"model": ErrorMessage, "description": "Service Unavailable."},
211+
504: {"model": ErrorMessage, "description": "Gateway Timeout."},
212+
},
213+
tags=["Customers"],
214+
summary="Delete specific customer.",
215+
response_model=None,
216+
)
217+
async def delete_customer_id(
218+
customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")],
219+
http_request_info: CommonDeps,
220+
db_connection: Annotated[Session, Depends(get_db_session)],
221+
) -> Response:
222+
"""Delete the information of the customer with the matching Id."""
223+
logger.info("Entering...")
224+
logger.debug("Deleting customer with id %s", customer_id)
225+
try:
226+
CustomerApplicationService.delete_customer(db_connection, customer_id)
227+
logger.debug("Customer with id %s deleted", customer_id)
228+
except ElementNotFoundError as error:
229+
logger.error("Customer with id %s not found", customer_id) # noqa: TRY400
230+
raise HTTP404NotFoundError from error
231+
except Exception as error:
232+
logger.exception("Error deleting customer with id %s", customer_id)
233+
raise HTTP500InternalServerError from error
234+
logger.info("Exiting...")
235+
return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info)
236+
237+
238+
@router.get(
239+
"/v1/customers/{customer_id}",
240+
responses={
241+
200: {"model": CustomerDetailResponse, "description": "OK."},
242+
401: {"model": ErrorMessage, "description": "Unauthorized."},
243+
403: {"model": ErrorMessage, "description": "Forbidden."},
244+
404: {"model": ErrorMessage, "description": "Not Found."},
245+
422: {"model": ErrorMessage, "description": "Unprocessable Entity."},
246+
423: {"model": ErrorMessage, "description": "Locked."},
247+
500: {"model": ErrorMessage, "description": "Internal Server Error."},
248+
501: {"model": ErrorMessage, "description": "Not Implemented."},
249+
502: {"model": ErrorMessage, "description": "Bad Gateway."},
250+
503: {"model": ErrorMessage, "description": "Service Unavailable."},
251+
504: {"model": ErrorMessage, "description": "Gateway Timeout."},
252+
},
253+
tags=["Customers"],
254+
summary="Customer information.",
255+
response_model_by_alias=True,
256+
response_model=CustomerDetailResponse,
257+
)
258+
async def get_customer_id(
259+
http_request_info: CommonDeps,
260+
db_connection: Annotated[Session, Depends(get_db_session)],
261+
customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")],
262+
) -> JSONResponse:
263+
"""Retrieve the information of the customer with the matching code."""
264+
logger.info("Entering...")
265+
logger.debug("Getting customer with id %s", customer_id)
266+
try:
267+
api_data = CustomerApplicationService.get_customer_id(db_connection, customer_id)
268+
logger.debug("Customer with id %s retrieved", customer_id)
269+
except ElementNotFoundError as error:
270+
logger.error("Customer with id %s not found", customer_id) # noqa: TRY400
271+
raise HTTP404NotFoundError from error
272+
except Exception as error:
273+
logger.exception("Error getting customer with id %s", customer_id)
274+
raise HTTP500InternalServerError from error
275+
logger.info("Exiting...")
276+
return JSONResponse(
277+
content=api_data.model_dump(), status_code=status.HTTP_200_OK, headers=http_request_info
278+
)
279+
280+
281281
@router.post(
282282
"/v1/customers/{customer_id}/addresses",
283283
responses={

β€Žsrc/core/config.pyβ€Ž

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ class Settings(BaseSettings):
3535
"""Represents the configuration settings for the application."""
3636

3737
# CORE SETTINGS
38-
## Could be improved by using a secret manager like AWS Secrets Manager or Hashicorp Vault
3938
SECRET_KEY: str = "HDx09iYK97MzUqezQ8InThpcEBk791oi"
4039
ENVIRONMENT: Literal["DEV", "PYTEST", "PREPROD", "PROD"] = "DEV"
4140
## BACKEND_CORS_ORIGINS and ALLOWED_HOSTS are a JSON-formatted list of origins
@@ -45,13 +44,23 @@ class Settings(BaseSettings):
4544
APP_LOG_FILE_PATH: str = "logs/app.log"
4645

4746
# POSTGRESQL DATABASE
48-
POSTGRES_SERVER: str = "db"
49-
POSTGRES_USER: str = "postgres"
50-
POSTGRES_PASSWORD: str = "postgres"
51-
POSTGRES_PORT: int = 5432
52-
POSTGRES_DB: str = "app-db"
47+
POSTGRES_SERVER: str = "db" # The name of the service in the docker-compose file
48+
POSTGRES_USER: str = "postgres" # The default username for the PostgreSQL database
49+
POSTGRES_PASSWORD: str = "postgres" # The default password for the PostgreSQL database
50+
POSTGRES_PORT: int = 5432 # The default port for the PostgreSQL database
51+
POSTGRES_DB: str = "app-db" # The default database name for the PostgreSQL database
5352
SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None
5453

54+
# CONNECTION POOL SETTINGS
55+
# The size of the pool to be maintained, defaults to 5
56+
POOL_SIZE: int = 10
57+
# Controls the number of connections that can be created after the pool reached its size
58+
MAX_OVERFLOW: int = 20
59+
# Number of seconds to wait before giving up on getting a connection from the pool
60+
POOL_TIMEOUT: int = 30
61+
# Number of seconds after which a connection is recycled (preventing stale connections)
62+
POOL_RECYCLE: int = 1800
63+
5564
@field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
5665
@classmethod
5766
def assemble_db_connection(

β€Žsrc/repository/session.pyβ€Ž

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,22 @@
55

66
from sqlalchemy import create_engine
77
from sqlalchemy.orm import Session, sessionmaker
8+
from sqlalchemy.pool import QueuePool
89

910
from src.core.config import settings
1011

1112
logger = logging.getLogger(__name__)
1213

13-
# Create a new engine
14-
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI), pool_pre_ping=True)
14+
# Create a new engine with connection pooling
15+
engine = create_engine(
16+
str(settings.SQLALCHEMY_DATABASE_URI),
17+
pool_pre_ping=True,
18+
poolclass=QueuePool,
19+
pool_size=settings.POOL_SIZE,
20+
max_overflow=settings.MAX_OVERFLOW,
21+
pool_timeout=settings.POOL_TIMEOUT,
22+
pool_recycle=settings.POOL_RECYCLE,
23+
)
1524

1625
# Create a session factory
1726
session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)

0 commit comments

Comments
Β (0)