Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction context is lost when running a recursive call in a loop #1996

Open
DivyanshGothwal opened this issue May 16, 2024 · 8 comments
Open

Comments

@DivyanshGothwal
Copy link

DivyanshGothwal commented May 16, 2024

Transaction context is lost when running a recursive call in a loop.

Below is codesandbox to reproduce this error.
https://codesandbox.io/p/sandbox/dexie-issue-cxkwvj

Also, I have attached below screen recording on how to reproduce this error.
https://drive.google.com/file/d/1U7GpJ-fGLjX01XaikBNrMUB1P2Cn2GFs/view?usp=sharing

I am not able to understand why this error is happening.

@dfahlander
Copy link
Collaborator

dfahlander commented May 16, 2024

To make transaction context kept correctly in async functions, dexie need to schedule context maintainance. This will only be done if the callback to db.transaction is an async function. In this case it seems to be a mix where the root transaction has a non-async callback so it doesnt do the extra care for async context handling and then it enters a sub transaction with an async function.

Either mark the root transaction callback as async, or if you are more confident with explicit transaction arguments instead of relying on Dexie.currentTransaction, use the tx argument to your callback:

db.transaction('rw', db.nodes, async (tx) => {
  const nodes = await tx.nodes.toArray(); // instead of db.nodes.toArray();
});

@DivyanshGothwal
Copy link
Author

DivyanshGothwal commented May 16, 2024

Thanks for the quick reply.
I made root transaction callback as async, but it didn't work out it is still failing with same issue.

this change is available in code sandbox as well

@DivyanshGothwal
Copy link
Author

I found a wired solution but don't know why it is working, here is a video explanation.
it would be great if I get explanation of why it is working
https://drive.google.com/file/d/1zSxFOXrDZGKVj7K5xDPLkfCniwWOzDjZ/view?usp=sharing

@dfahlander
Copy link
Collaborator

The reason it started working when skipping await is most likely that the little "await" will jump out of the current scope and lose the context unless everything is awaited properly.

The forEach loop spawns multiple async tasks in parallell things without awaiting them. If consistently awaiting all spawned promises, it should work.

  const onClickCall = () => {
    // calling all methods in a for loop wrapped in a transaction.
    // here there is no await in transaction
    db.transaction("rw", db.nodes, async () => {
      await Promise.all(
        methods.map((met) => {
          return met();
        })
      );
    });
  };

There might be other places too where promises aren't awaited.

But if awaiting these promises are overkill, and you want to have an non-magical control over the current transaction, I recommend to use the tx argument instead.

@DivyanshGothwal
Copy link
Author

DivyanshGothwal commented May 16, 2024

Thanks then we have 2 solutions for this.

  1. await everything properly otherwise we might face these issues.
  2. Use "tx" input of transaction callback so that even if we lose transaction context we are safe by using "tx".

@dfahlander
But I have a question, check the screen recording for it, I have explained a behaviour which I could not understand.

https://drive.google.com/file/d/1gF5SEehYfGb0cUm5bvMKO3Q_-IqhWhlh/view?usp=sharing

@DivyanshGothwal
Copy link
Author

One more last question I have (just trying to understand how things are working).

If I remove the recursive call function getParentNodes, even though I have not awaited properly this works.
How is this happening??

if you want more context on the question below is a screen recording.

https://drive.google.com/file/d/1oj3heM91GTVb9NqTO2WCDtzSXSDHGIy6/view?usp=sharing

@dfahlander
Copy link
Collaborator

Thanks then we have 2 solutions for this.

  1. await everything properly otherwise we might face these issues.
  2. Use "tx" input of transaction callback so that even if we lose transaction context we are safe by using "tx".

@dfahlander But I have a question, check the screen recording for it, I have explained a behaviour which I could not understand.

https://drive.google.com/file/d/1gF5SEehYfGb0cUm5bvMKO3Q_-IqhWhlh/view?usp=sharing

Correct. When using the provided tx argument, the code is keeping the transaction alive by doing work on it. This prevents the transaction from auto-committing. Dexie listens to IDBTransaction event "success" event which will be fired when no more work are put onto the transaction.

One more last question I have (just trying to understand how things are working).

If I remove the recursive call function getParentNodes, even though I have not awaited properly this works. How is this happening??

if you want more context on the question below is a screen recording.

https://drive.google.com/file/d/1oj3heM91GTVb9NqTO2WCDtzSXSDHGIy6/view?usp=sharing

The trickiest part with keeping context alive across async calls is when awaiting things that never performs any dexie tasks. This is probably the case when getParentNode falls into returning [] because node is falsy. We schedule context switching in advance to support those situations but this only works when the callback to db.transacation is an async function and when all things are awaited properly. When having a complex setup with recursive sub transactions that you have, this can affect performance and you are probably better of using the provided tx.

As soon as this lovely stage 2 TC39 proposal is going public, Dexie will be able to use that API instead to keep contexts between awaits. Until then, transaction context can be lost if not declaring the callback async or not awaiting promises properly.

@DivyanshGothwal
Copy link
Author

@dfahlander
Understood and thanks for your help.

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

No branches or pull requests

2 participants