Skip to content

Comments

[WEB-2706] fix: Fix issue with SQLite transactions#5919

Merged
pushya22 merged 1 commit intopreviewfrom
fix-sqlite-transaction-issues
Oct 28, 2024
Merged

[WEB-2706] fix: Fix issue with SQLite transactions#5919
pushya22 merged 1 commit intopreviewfrom
fix-sqlite-transaction-issues

Conversation

@SatishGandham
Copy link
Contributor

@SatishGandham SatishGandham commented Oct 28, 2024

Implement transaction Q to avoid overlapped transactions.

Summary by CodeRabbit

  • New Features

    • Improved asynchronous handling of database operations, enhancing reliability and efficiency.
    • Enabled concurrent execution of metadata insertions for better performance.
  • Bug Fixes

    • Resolved potential race conditions in database updates by ensuring all asynchronous operations are awaited.
  • Refactor

    • Enhanced transaction management in the database class to support multiple concurrent transactions.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2024

Walkthrough

This pull request introduces several enhancements to the asynchronous handling of database operations across multiple files. In load-issues.ts, functions like deleteIssueFromLocal, updateIssue, and stageIssueInserts have been updated to use await, ensuring operations complete before proceeding. The DBClass in db.ts has been modified to manage multiple concurrent transactions using arrays for promises and resolvers. Additionally, the updatePersistentLayer function in utils.ts has been updated to await the completion of issue updates, improving overall control flow and reliability.

Changes

File Path Change Summary
web/core/local-db/utils/load-issues.ts Updated deleteIssueFromLocal, updateIssue, and stageIssueInserts to use await for async handling. Refactored metadata insertion to use an array of promises.
web/core/local-db/utils/utils.ts Added await before updateIssue in updatePersistentLayer for improved async handling.
web/core/local-db/worker/db.ts Modified DBClass to handle multiple concurrent transactions with arrays for promises and resolvers. Updated exec method for better transaction management.

Possibly related PRs

Suggested reviewers

  • rahulramesha
  • pablohashescobar
  • sriramveeraghanta

🐰 In the code we hop and play,
Awaiting tasks along the way.
With promises in a neat array,
Our database dances, come what may!
So let us cheer, for changes bright,
As bunnies leap into the night! 🌙✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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 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: 6

🧹 Outside diff range and nitpick comments (1)
web/core/local-db/utils/load-issues.ts (1)

108-130: Good use of Promise.all for concurrent metadata operations.

The refactoring to use Promise.all for metadata operations is a good improvement for performance. However, consider adding error handling to ensure all promises are properly handled.

Consider adding error handling:

   const metaPromises: Promise<any>[] = [];

   ARRAY_FIELDS.forEach((field) => {
     const values = issue[field];
     if (values && values.length) {
       values.forEach((val: any) => {
         const p = persistence.db.exec({
           sql: `INSERT OR REPLACE  into issue_meta(issue_id,key,value) values (?,?,?) `,
           bind: [issue_id, field, val],
         });
         metaPromises.push(p);
       });
     } else {
       const p = persistence.db.exec({
         sql: `INSERT OR REPLACE  into issue_meta(issue_id,key,value) values (?,?,?) `,
         bind: [issue_id, field, ""],
       });
       metaPromises.push(p);
     }
   });

-  await Promise.all(metaPromises);
+  try {
+    await Promise.all(metaPromises);
+  } catch (error) {
+    console.error('Failed to insert metadata:', error);
+    throw error;
+  }
🧰 Tools
🪛 Biome

[error] 112-112: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 6d08cf2 and ac4ab88.

📒 Files selected for processing (3)
  • web/core/local-db/utils/load-issues.ts (3 hunks)
  • web/core/local-db/utils/utils.ts (1 hunks)
  • web/core/local-db/worker/db.ts (3 hunks)
🧰 Additional context used
🪛 Biome
web/core/local-db/utils/load-issues.ts

[error] 112-112: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (2)
web/core/local-db/utils/utils.ts (1)

62-62: Verify transaction handling in the database layer.

Since this PR aims to fix SQLite transaction issues, let's verify the transaction handling in the related database operations.

✅ Verification successful

Let me gather more specific information about the database operations and transaction handling in the local-db layer.


Let me search more specifically for the transaction handling and updateIssue implementation.


Let me search for the addIssue implementation since it's called by updateIssue, and also look for any database initialization code.


Based on the code analysis, I can now provide a final response about the transaction handling:

Transaction handling is properly implemented in the database layer.

The code shows proper transaction management:

  1. updateIssue function uses transactions through its called functions:

    • Calls deleteIssueFromLocal which uses explicit BEGIN/COMMIT transaction
    • Calls addIssue which also uses explicit BEGIN/COMMIT transaction
  2. All major database operations use explicit transactions:

    • addIssue: Uses BEGIN; and COMMIT;
    • addIssuesBulk: Uses BEGIN; and COMMIT; for batch operations
    • deleteIssueFromLocal: Uses BEGIN; and COMMIT;
    • Table creation: Uses BEGIN; and COMMIT;
    • Workspace data loading: Uses BEGIN TRANSACTION; and COMMIT

The implementation ensures data consistency by wrapping related operations in transactions, particularly for operations that modify multiple tables like issue updates which affect both the issues and issue_meta tables.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for transaction-related code in the database layer
echo "Searching for transaction handling in database operations..."
rg -l "transaction|begin|commit|rollback" --type ts

# Look for potential transaction overlap points
echo "Checking for concurrent database operations..."
ast-grep --pattern 'await $db.$_($$$)'

Length of output: 1439


Script:

#!/bin/bash
# Search for transaction-related code specifically in the local-db directory
echo "Searching for transaction handling in local-db..."
rg -A 5 "transaction|begin|commit|rollback" web/core/local-db/

# Look for updateIssue implementation
echo "Looking for updateIssue implementation..."
ast-grep --pattern 'updateIssue = async ($$$) => {
  $$$
}'

# Search for any database operation wrappers
echo "Searching for database operation wrappers..."
rg -A 5 "class.*Database|interface.*Database" web/core/local-db/

Length of output: 131567


Script:

#!/bin/bash
# Look for updateIssue implementation and surrounding context
echo "Searching for updateIssue implementation with context..."
rg -A 10 "updateIssue" web/core/local-db/

# Look for transaction-related code in local-db
echo "Searching for transaction handling..."
rg -A 5 "transaction|begin|commit|rollback" web/core/local-db/ --type ts

# Look for database wrapper implementation
echo "Looking for database wrapper..."
rg -A 10 "class.*Database" web/core/local-db/ --type ts

Length of output: 8932


Script:

#!/bin/bash
# Look for addIssue implementation
echo "Searching for addIssue implementation..."
rg -A 15 "addIssue" web/core/local-db/

# Look for database initialization and setup
echo "Searching for database initialization..."
rg -A 10 "persistence.db|initDB|setupDB" web/core/local-db/

# Look for SQL statements related to issues
echo "Searching for issue-related SQL..."
rg -A 5 "INSERT INTO.*issues|UPDATE.*issues|DELETE FROM.*issues" web/core/local-db/

Length of output: 19765

web/core/local-db/worker/db.ts (1)

100-103: Ensure proper resolution of transaction promises on commit

In the COMMIT; block, the code resolves the first resolver from tpResolver. If there is any misalignment between tp and tpResolver, this could cause unexpected behavior.

To verify that transaction promises and resolvers are correctly managed, run the following script to check for potential mismatches:

Ensure that after every push or shift operation, the lengths of tp and tpResolver remain consistent.

✅ Verification successful

Based on the search results, I can now generate the final response since I have enough information to verify the concern.

Transaction promises and resolvers are correctly synchronized

The code maintains proper alignment between tp and tpResolver arrays:

  • In the BEGIN; block:
    • A new promise is created and pushed to tp
    • Its resolver is simultaneously pushed to tpResolver
  • In the COMMIT; block:
    • The code checks tp.length > 0 before attempting to resolve
    • Both arrays are modified using shift() operations in sync

The implementation ensures one-to-one correspondence between promises and their resolvers, preventing any misalignment issues.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the lengths of the transaction promises and resolvers arrays are always equal.

# Test: Search for all modifications to tp and tpResolver and check their lengths.
rg 'this\.tp' -A 3 -B 3
rg 'this\.tpResolver' -A 3 -B 3

Length of output: 2195

// delete the issue and its meta data
await deleteIssueFromLocal(issue_id);
addIssue(issue);
await addIssue(issue);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for the update operation.

While the await addition is correct, consider adding error handling to ensure atomicity of the delete-then-add operation.

 export const updateIssue = async (issue: TIssue & { is_local_update: number }) => {
   if (document.hidden || !rootStore.user.localDBEnabled || !persistence.db) return;
 
   const issue_id = issue.id;
-  // delete the issue and its meta data
-  await deleteIssueFromLocal(issue_id);
-  await addIssue(issue);
+  try {
+    await persistence.db.exec("BEGIN;");
+    await deleteIssueFromLocal(issue_id);
+    await addIssue(issue);
+    await persistence.db.exec("COMMIT;");
+  } catch (error) {
+    await persistence.db.exec("ROLLBACK;");
+    throw error;
+  }
 };

Committable suggestion was skipped due to low confidence.

Comment on lines +43 to +50
const deleteQuery = `DELETE from issues where id='${issue_id}'`;
const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;

persistence.db.exec("BEGIN;");
persistence.db.exec(deleteQuery);
persistence.db.exec(deleteMetaQuery);
persistence.db.exec("COMMIT;");
await persistence.db.exec("BEGIN;");

await persistence.db.exec(deleteQuery);
await persistence.db.exec(deleteMetaQuery);
await persistence.db.exec("COMMIT;");
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

Add SQL injection protection and error handling.

The current implementation has two security/reliability concerns:

  1. Direct string interpolation in SQL queries makes it vulnerable to SQL injection
  2. Missing error handling could leave transactions in an inconsistent state

Consider refactoring to use parameterized queries and add error handling:

 export const deleteIssueFromLocal = async (issue_id: any) => {
   if (!rootStore.user.localDBEnabled || !persistence.db) return;
 
-  const deleteQuery = `DELETE from issues where id='${issue_id}'`;
-  const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;
+  try {
+    await persistence.db.exec("BEGIN;");
 
-  await persistence.db.exec("BEGIN;");
+    await persistence.db.exec({
+      sql: "DELETE FROM issues WHERE id = ?",
+      bind: [issue_id]
+    });
+    await persistence.db.exec({
+      sql: "DELETE FROM issue_meta WHERE issue_id = ?",
+      bind: [issue_id]
+    });
 
-  await persistence.db.exec(deleteQuery);
-  await persistence.db.exec(deleteMetaQuery);
-  await persistence.db.exec("COMMIT;");
+    await persistence.db.exec("COMMIT;");
+  } catch (error) {
+    await persistence.db.exec("ROLLBACK;");
+    throw error;
+  }
 };
📝 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
const deleteQuery = `DELETE from issues where id='${issue_id}'`;
const deleteMetaQuery = `delete from issue_meta where issue_id='${issue_id}'`;
persistence.db.exec("BEGIN;");
persistence.db.exec(deleteQuery);
persistence.db.exec(deleteMetaQuery);
persistence.db.exec("COMMIT;");
await persistence.db.exec("BEGIN;");
await persistence.db.exec(deleteQuery);
await persistence.db.exec(deleteMetaQuery);
await persistence.db.exec("COMMIT;");
try {
await persistence.db.exec("BEGIN;");
await persistence.db.exec({
sql: "DELETE FROM issues WHERE id = ?",
bind: [issue_id]
});
await persistence.db.exec({
sql: "DELETE FROM issue_meta WHERE issue_id = ?",
bind: [issue_id]
});
await persistence.db.exec("COMMIT;");
} catch (error) {
await persistence.db.exec("ROLLBACK;");
throw error;
}


const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});`;
persistence.db.exec(query);
await persistence.db.exec(query);
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

Protect against SQL injection in the main issue insert.

The main issue insert query is constructed using string interpolation, making it vulnerable to SQL injection.

Consider using parameterized queries here as well:

-  const query = `INSERT OR REPLACE INTO issues (${columns}) VALUES (${values});`;
-  await persistence.db.exec(query);
+  await persistence.db.exec({
+    sql: `INSERT OR REPLACE INTO issues (${columns}) VALUES (${placeholders})`,
+    bind: Object.values(sanitizedIssue)
+  });

Committable suggestion was skipped due to low confidence.

"description_html",
]);
updateIssue({ ...issuePartial, is_local_update: 1 });
await updateIssue({ ...issuePartial, is_local_update: 1 });
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 async operation handling in forEach loop.

While adding await to updateIssue is a step in the right direction, using forEach with async operations is problematic as it doesn't wait for all promises to complete and can lead to race conditions.

Refactor the function to properly handle concurrent updates:

 export const updatePersistentLayer = async (issueIds: string | string[]) => {
   if (typeof issueIds === "string") {
     issueIds = [issueIds];
   }
-  issueIds.forEach(async (issueId) => {
+  await Promise.all(issueIds.map(async (issueId) => {
     const dbIssue = await persistence.getIssue(issueId);
     const issue = rootStore.issue.issues.getIssueById(issueId);
 
     if (issue) {
       const issuePartial = pick({ ...dbIssue, ...JSON.parse(JSON.stringify(issue)) }, [
         // ... fields ...
       ]);
 
-      await updateIssue({ ...issuePartial, is_local_update: 1 });
+      try {
+        await updateIssue({ ...issuePartial, is_local_update: 1 });
+      } catch (error) {
+        logError(error);
+        throw error; // Re-throw to ensure Promise.all catches the failure
+      }
     }
-  });
+  }));
 };

This change:

  1. Uses Promise.all with map to properly wait for all updates
  2. Adds error handling for individual update failures
  3. Prevents transaction overlap by ensuring all updates complete before the function returns
📝 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
await updateIssue({ ...issuePartial, is_local_update: 1 });
export const updatePersistentLayer = async (issueIds: string | string[]) => {
if (typeof issueIds === "string") {
issueIds = [issueIds];
}
await Promise.all(issueIds.map(async (issueId) => {
const dbIssue = await persistence.getIssue(issueId);
const issue = rootStore.issue.issues.getIssueById(issueId);
if (issue) {
const issuePartial = pick({ ...dbIssue, ...JSON.parse(JSON.stringify(issue)) }, [
// ... fields ...
]);
try {
await updateIssue({ ...issuePartial, is_local_update: 1 });
} catch (error) {
logError(error);
throw error; // Re-throw to ensure Promise.all catches the failure
}
}
}));
};

Comment on lines +27 to +28
private tp: Promise<any>[] = [];
private tpResolver: any = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use more descriptive variable names and explicit types for clarity

The variables tp and tpResolver are not descriptive, which can make the code harder to understand and maintain. Additionally, tpResolver is typed as any, reducing type safety.

Consider renaming and typing the variables to improve readability and maintainability:

- private tp: Promise<any>[] = [];
- private tpResolver: any = [];
+ private transactionPromises: Promise<void>[] = [];
+ private transactionResolvers: Array<{ resolve: Function; reject: Function }> = [];
📝 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
private tp: Promise<any>[] = [];
private tpResolver: any = [];
private transactionPromises: Promise<void>[] = [];
private transactionResolvers: Array<{ resolve: Function; reject: Function }> = [];

Comment on lines +60 to +72
if (props === "BEGIN;") {
let promiseToAwait;
if (this.tp.length > 0) {
promiseToAwait = this.tp.shift();
}
const p = new Promise((resolve, reject) => {
this.tpResolver.push({ resolve, reject });
});
this.tp.push(p);

if (promiseToAwait) {
await promiseToAwait;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Refactor transaction management to prevent potential misalignment issues

Managing transactions using separate arrays for promises and resolvers (tp and tpResolver) may lead to misalignment and bugs if not handled carefully. This approach can make the code complex and harder to debug.

Refactor the transaction management to use a unified structure that pairs each promise with its resolver. This enhances code clarity and reduces the risk of synchronization issues.

Apply the following changes:

- if (props === "BEGIN;") {
-   let promiseToAwait;
-   if (this.tp.length > 0) {
-     promiseToAwait = this.tp.shift();
-   }
-   const p = new Promise((resolve, reject) => {
-     this.tpResolver.push({ resolve, reject });
-   });
-   this.tp.push(p);
-
-   if (promiseToAwait) {
-     await promiseToAwait;
-   }
- }

+ if (props === "BEGIN;") {
+   let previousTransaction;
+   if (this.transactionQueue.length > 0) {
+     previousTransaction = this.transactionQueue[this.transactionQueue.length - 1].promise;
+   }
+   let resolveFunction;
+   const promise = new Promise<void>((resolve, reject) => {
+     resolveFunction = resolve;
+   });
+   this.transactionQueue.push({ promise, resolve: resolveFunction });
+
+   if (previousTransaction) {
+     await previousTransaction;
+   }
+ }
📝 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
if (props === "BEGIN;") {
let promiseToAwait;
if (this.tp.length > 0) {
promiseToAwait = this.tp.shift();
}
const p = new Promise((resolve, reject) => {
this.tpResolver.push({ resolve, reject });
});
this.tp.push(p);
if (promiseToAwait) {
await promiseToAwait;
}
if (props === "BEGIN;") {
let previousTransaction;
if (this.transactionQueue.length > 0) {
previousTransaction = this.transactionQueue[this.transactionQueue.length - 1].promise;
}
let resolveFunction;
const promise = new Promise<void>((resolve, reject) => {
resolveFunction = resolve;
});
this.transactionQueue.push({ promise, resolve: resolveFunction });
if (previousTransaction) {
await previousTransaction;
}
}

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.

3 participants