Skip to content

fix: resolve critical DB connection leaks in image-related functions#465

Closed
Aditya30ag wants to merge 2 commits intoAOSSIE-Org:mainfrom
Aditya30ag:fix/Critical-Database-Connection
Closed

fix: resolve critical DB connection leaks in image-related functions#465
Aditya30ag wants to merge 2 commits intoAOSSIE-Org:mainfrom
Aditya30ag:fix/Critical-Database-Connection

Conversation

@Aditya30ag
Copy link
Contributor

@Aditya30ag Aditya30ag commented Jul 15, 2025

✅ Issue Successfully Fixed!

I have properly solved the critical database connection leak issue in the PictoPy project.


🔧 What Was Fixed

delete_image_db() Function

  • Problem: Double connection close and resource leak when image not found
  • Solution: Wrapped operations in try-finally block to ensure connection is always closed

get_objects_db() Function

  • Problem: Potential connection leak if early returns occur
  • Solution: Separated database operations and wrapped each connection in try-finally

get_all_images_from_folder_id() Function

  • Problem: Connection not properly protected
  • Solution: Added try-finally block for guaranteed cleanup

🔑 Key Improvements

  • Resource Management: All database connections are now properly closed
  • Error Resilience: Connections are closed even if exceptions occur
  • No More Leaks: Eliminates resource exhaustion under load
  • Consistent Behavior: All functions handle connections uniformly
  • Better Performance: Prevents connection pool exhaustion

✅ Testing Verified

The fix was thoroughly tested and confirmed:

  • ✅ Non-existent image deletion doesn't leak connections
  • ✅ Object retrieval for non-existent images works correctly
  • ✅ Folder queries for non-existent folders work correctly
  • ✅ Database remains accessible after all operations
  • ✅ No exceptions are raised due to connection issues

📋 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.

This resolves a critical issue that could have caused application crashes and resource exhaustion under high load conditions.

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when deleting images by ensuring they are also removed from face clusters.
    • Enhanced database stability by consistently closing connections after operations, reducing the risk of resource leaks.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 15, 2025

Walkthrough

The changes refactor database access functions to ensure connections are always closed using try/finally blocks. The image deletion logic is extended to explicitly remove images from face clusters in addition to albums and embeddings. No function signatures or public interfaces are modified; all updates are internal to existing functions.

Changes

File(s) Change Summary
backend/app/database/images.py Wrapped database queries and operations in try/finally blocks to guarantee connection closure; enhanced delete_image_db to remove images from face clusters before deleting embeddings.

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
Loading

Possibly related issues

Poem

In the warren where data flows deep,
Connections now close, no leaks left to creep.
Face clusters are tidy, images swept away,
Embeddings forgotten, all night and all day.
The database sighs, serene and at rest—
This rabbit ensures your cleanup is best! 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3342ae and 5204883.

📒 Files selected for processing (1)
  • backend/app/database/images.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/database/images.py
⏰ 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)
  • GitHub Check: Tauri Tests
✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bc6bf64 and a3342ae.

📒 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.

Comment on lines 87 to 114
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

@Aditya30ag
Copy link
Contributor Author

@rahulharpal1603 please have a look

@rahulharpal1603
Copy link
Contributor

@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.

@Aditya30ag
Copy link
Contributor Author

Yaa , I know it's okay
I had made this pr before the change.
It completely fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments