fix: resolve critical DB connection leaks in image-related functions#465
fix: resolve critical DB connection leaks in image-related functions#465Aditya30ag wants to merge 2 commits intoAOSSIE-Org:mainfrom
Conversation
WalkthroughThe changes refactor database access functions to ensure connections are always closed using Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant API
participant DB
participant FaceCluster
User->>API: Request to delete image
API->>DB: Delete image from images and image_id_mapping tables
API->>FaceCluster: Remove image from face clusters
API->>DB: Delete associated face embeddings
API-->>User: Respond with deletion result
Possibly related issues
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
backend/app/database/images.py (1)
151-186: Optimize database connections to use a single connection.Using separate connections for related database operations is inefficient and can lead to performance issues. A single connection with proper try-finally handling would be more efficient.
def get_objects_db(path): image_id = get_id_from_path(path) if image_id is None: return None - # Get class_ids from images table - conn_images = sqlite3.connect(DATABASE_PATH) - cursor_images = conn_images.cursor() - - try: - cursor_images.execute("SELECT class_ids FROM images WHERE id = ?", (image_id,)) - result = cursor_images.fetchone() - - if not result: - return None - - class_ids_json = result[0] - class_ids = json.loads(class_ids_json) - if isinstance(class_ids, list): - class_ids = [str(class_id) for class_id in class_ids] - else: - class_ids = class_ids.split(",") - finally: - conn_images.close() - - # Get class names from mappings table - conn_mappings = sqlite3.connect(DATABASE_PATH) - cursor_mappings = conn_mappings.cursor() - - try: - class_names = [] - for class_id in class_ids: - cursor_mappings.execute( - "SELECT name FROM mappings WHERE class_id = ?", (class_id,) - ) - name_result = cursor_mappings.fetchone() - if name_result: - class_names.append(name_result[0]) - finally: - conn_mappings.close() + # Use a single connection for both operations + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + + try: + # Get class_ids from images table + cursor.execute("SELECT class_ids FROM images WHERE id = ?", (image_id,)) + result = cursor.fetchone() + + if not result: + return None + + class_ids_json = result[0] + class_ids = json.loads(class_ids_json) + if isinstance(class_ids, list): + class_ids = [str(class_id) for class_id in class_ids] + else: + class_ids = class_ids.split(",") + + # Get class names from mappings table + class_names = [] + for class_id in class_ids: + cursor.execute( + "SELECT name FROM mappings WHERE class_id = ?", (class_id,) + ) + name_result = cursor.fetchone() + if name_result: + class_names.append(name_result[0]) + finally: + conn.close() class_names = list(set(class_names)) return class_names
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
backend/app/database/images.py(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/app/database/images.py (4)
backend/app/database/albums.py (1)
remove_image_from_all_albums(206-223)backend/app/database/faces.py (1)
delete_face_embeddings(102-109)backend/app/facecluster/init_face_cluster.py (1)
get_face_cluster(35-39)backend/app/facecluster/facecluster.py (1)
remove_image(283-308)
🔇 Additional comments (1)
backend/app/database/images.py (1)
212-219: LGTM! Proper resource management implemented.The try-finally block ensures the database connection is always closed, effectively preventing connection leaks. The implementation correctly handles the case where no results are found.
| try: | ||
| # Get image ID from path | ||
| cursor.execute("SELECT id FROM image_id_mapping WHERE path = ?", (abs_path,)) | ||
| result = cursor.fetchone() | ||
|
|
||
| if result: | ||
| image_id = result[0] | ||
| # Remove from both tables | ||
| cursor.execute("DELETE FROM images WHERE id = ?", (image_id,)) | ||
| cursor.execute("DELETE FROM image_id_mapping WHERE id = ?", (image_id,)) | ||
|
|
||
| # Remove image from albums (handled separately to avoid circular imports) | ||
| remove_image_from_all_albums(image_id) | ||
|
|
||
| # Import only after removing image from albums to avoid circular import error | ||
| from app.database.faces import delete_face_embeddings | ||
|
|
||
| conn.commit() | ||
|
|
||
| # Remove image from face clusters | ||
| clusters = get_face_cluster() | ||
| clusters.remove_image(image_id) | ||
|
|
||
| # Delete associated face embeddings | ||
| delete_face_embeddings(image_id) | ||
| finally: | ||
| # Always close the connection, regardless of success or failure | ||
| conn.close() |
There was a problem hiding this comment.
Fix transaction consistency issue in delete_image_db.
The current implementation commits the database transaction before performing face cluster operations, which can lead to data inconsistency if the face cluster operations fail. Additionally, the import statement after commit could cause issues if it fails.
def delete_image_db(path):
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
abs_path = os.path.abspath(path)
+
+ # Import at the beginning to avoid potential import errors after commit
+ from app.database.faces import delete_face_embeddings
try:
# Get image ID from path
cursor.execute("SELECT id FROM image_id_mapping WHERE path = ?", (abs_path,))
result = cursor.fetchone()
if result:
image_id = result[0]
# Remove from both tables
cursor.execute("DELETE FROM images WHERE id = ?", (image_id,))
cursor.execute("DELETE FROM image_id_mapping WHERE id = ?", (image_id,))
# Remove image from albums (handled separately to avoid circular imports)
remove_image_from_all_albums(image_id)
- # Import only after removing image from albums to avoid circular import error
- from app.database.faces import delete_face_embeddings
-
- conn.commit()
-
# Remove image from face clusters
clusters = get_face_cluster()
clusters.remove_image(image_id)
# Delete associated face embeddings
delete_face_embeddings(image_id)
+
+ # Commit only after all operations succeed
+ conn.commit()
finally:
# Always close the connection, regardless of success or failure
conn.close()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| # Get image ID from path | |
| cursor.execute("SELECT id FROM image_id_mapping WHERE path = ?", (abs_path,)) | |
| result = cursor.fetchone() | |
| if result: | |
| image_id = result[0] | |
| # Remove from both tables | |
| cursor.execute("DELETE FROM images WHERE id = ?", (image_id,)) | |
| cursor.execute("DELETE FROM image_id_mapping WHERE id = ?", (image_id,)) | |
| # Remove image from albums (handled separately to avoid circular imports) | |
| remove_image_from_all_albums(image_id) | |
| # Import only after removing image from albums to avoid circular import error | |
| from app.database.faces import delete_face_embeddings | |
| conn.commit() | |
| # Remove image from face clusters | |
| clusters = get_face_cluster() | |
| clusters.remove_image(image_id) | |
| # Delete associated face embeddings | |
| delete_face_embeddings(image_id) | |
| finally: | |
| # Always close the connection, regardless of success or failure | |
| conn.close() | |
| def delete_image_db(path): | |
| conn = sqlite3.connect(DATABASE_PATH) | |
| cursor = conn.cursor() | |
| abs_path = os.path.abspath(path) | |
| # Import at the beginning to avoid potential import errors after commit | |
| from app.database.faces import delete_face_embeddings | |
| try: | |
| # Get image ID from path | |
| cursor.execute("SELECT id FROM image_id_mapping WHERE path = ?", (abs_path,)) | |
| result = cursor.fetchone() | |
| if result: | |
| image_id = result[0] | |
| # Remove from both tables | |
| cursor.execute("DELETE FROM images WHERE id = ?", (image_id,)) | |
| cursor.execute("DELETE FROM image_id_mapping WHERE id = ?", (image_id,)) | |
| # Remove image from albums (handled separately to avoid circular imports) | |
| remove_image_from_all_albums(image_id) | |
| # Remove image from face clusters | |
| clusters = get_face_cluster() | |
| clusters.remove_image(image_id) | |
| # Delete associated face embeddings | |
| delete_face_embeddings(image_id) | |
| # Commit only after all operations succeed | |
| conn.commit() | |
| finally: | |
| # Always close the connection, regardless of success or failure | |
| conn.close() |
🤖 Prompt for AI Agents
In backend/app/database/images.py around lines 87 to 114, the database commit
occurs before face cluster operations and the import of delete_face_embeddings,
risking data inconsistency if those operations fail. To fix this, move the
conn.commit() call to after all face cluster operations and the import, ensuring
the entire deletion process is atomic. Also, move the import statement to the
top of the try block or just before its first use but before commit, so import
errors are caught before committing the transaction.
|
@rahulharpal1603 please have a look |
Hi @Aditya30ag, I cannot merge this PR because the issue is legit but the backend code you have contributed to will be completely changed after my #466 will merge. So what I am suggesting is that you should work on this issue, but only after my PR is merged (hopefully today.) I am closing this PR for now. |
|
Yaa , I know it's okay |
✅ Issue Successfully Fixed!
I have properly solved the critical database connection leak issue in the PictoPy project.
🔧 What Was Fixed
delete_image_db()Functiontry-finallyblock to ensure connection is always closedget_objects_db()Functiontry-finallyget_all_images_from_folder_id()Functiontry-finallyblock for guaranteed cleanup🔑 Key Improvements
✅ Testing Verified
The fix was thoroughly tested and confirmed:
📋 Files Modified
backend/app/database/images.py— Fixed all three functions with connection leaks✅ The fix is #464 backward compatible and does not change the public API of any functions.
Summary by CodeRabbit
Summary by CodeRabbit