Skip to content

Relationship field validation does not respect the current transaction #4078

Closed

Description

@diesieben07

Link to reproduction

https://github.com/diesieben07/payload/tree/repro/relationship-validation-transactions

Describe the Bug

The validation code for filterOptions on relationship fields does not respect the current transaction. As a result, you cannot create an object which has a relationship to an object that was created in the same transaction, because the validation code will not find the previously created object, because it is checking without a transaction.

The reason for this is that validateFilterOptions does not use (or receive) the PayloadRequest and does not pass it to the find operation it does.
There may be other validation functions that have the same issue.

To Reproduce

  1. Create a collection X and a collection Y, where Y has a relationship field to X with filterOptions set (can be an empty object).
  2. Start a transaction.
  3. Create object A of collection X.
  4. Create object B of collection Y with a relationship to A

Step 4 will fail with a validation error, claiming that A is not a valid option.
It will work fine when not using transactions.

Given the above reproduction link, this can be reproduced as follows:

  1. docker compose up -d, this starts a MongoDB ReplicaSet
  2. export PAYLOAD_TEST_MONGO_URL='mongodb://127.0.0.1/payloadtests?replicaSet=payload'
  3. pnpm run test:int _community

Payload Version

2.1.0

Adapters and Plugins

db-mongodb, db-postgres

Activity

  1. diesieben07 commented on Nov 9, 2023

    @diesieben07
    ContributorAuthor

    I am currently working on a pull request to fix this issue.

  2. BrianJM commented on Feb 22, 2024

    @BrianJM

    I am currently working on a pull request to fix this issue.

    @diesieben07 I ran into this issue when trying to debug #4350. With transactions enabled, the error still occurs.

  3. diesieben07 commented on Feb 22, 2024

    @diesieben07
    ContributorAuthor

    @BrianJM that is quite likely. I discovered while working on #4087 (and other PRs I did regarding transactions) that in my honest opinion the way transactions were implemented into the Payload API is fundamentally flawed. I spent a lot of time trying to work around it and fix it up, but ultimately could not dedicate any more time to it a) because it is no longer relevant to my work and b) I just got frustrated by it.

  4. DanRibbens commented on Feb 22, 2024

    @DanRibbens
    Contributor

    Sorry for the frustrations, I've been working on other things but I am making some breakthroughs.

  5. BrianJM commented on Feb 22, 2024

    @BrianJM

    in my honest opinion the way transactions were implemented into the Payload API is fundamentally flawed.

    @diesieben07 I am looking into this more. If you have anything else to share, I'd love to hear more about the struggles you ran into. I'm on the discord server as well.

  6. diesieben07 commented on Feb 23, 2024

    @diesieben07
    ContributorAuthor

    @BrianJM Here is how transactions work in Payload at the moment:

    Every operation (find, findByID, create, update, etc.) does its own transaction management. To do this, the code for every operation includes the following boilerplate:

    1. Check if there is already a transaction, if not, start one.
    2. Do the actual operation.
    3. If everything went well, commit the transaction.
    4. If an error happened, roll back the transaction.

    Now this on its own would not be very useful, because you've essentially invented autocommit mode. Therefor every operation1 also has a req parameter of type PayloadRequest, which stores the transaction ID. Therefor if multiple operations use the same PayloadRequest, they can coordinate - right? Well, you run into a bunch of race conditions if you do this with the naive implementation above:

    1. Creating the transaction is asynchronous, so if two operations use the same request, they can cause two transactions to be started. Most of the time only one of them will be used and committed, but depending on the timing you could even have some operations still run on the transaction that never gets committed (this is very unlikely though) or even have both transactions get committed (even more unlikely). This was fixed in fix: fixes more race conditions with transactions #4164.
    2. If someone else created the transaction, you don't really want to be committing it. You don't even know if the other operation is done yet. If the operations run "in parallel" you could have one of them commit the transaction when it is done and then the other operations will continue running, but now without a transaction (the transactionID was removed from the PayloadRequest by committing it). They might even fail, and now part of their work has been committed, even though they failed.
      I think most of this was also fixed by fix: fixes more race conditions with transactions #4164. Now operations only commit transactions that they themself have started. If you run operations in parallel, this still requires the foresight to start a transaction before you run them in parallel, otherwise you could still have the situation where the operation who "wins" at starting the transaction is also the first to finish - committing the transaction before the others are done.
    3. Now for the (imho) worst part, and the one that is not fixed yet as far as I know. If an operation encounters an error, it immediately rolls back the transaction no matter what. If there are operations running in parallel with it, they will not know the transaction was rolled back and just keep moving forward. This obviously causes problems.
      1. Rolling back a transaction is (again) asynchronous. So you might have one of the other operations try to use a transaction that is already rolled back - causing an error from the database.
      2. Once the transaction is rolled back, the PayloadRequest object no longer has a transactionID, meaning everything else that is still running will not be rolled back, but instead just start a new transaction and commit that. This will either fail with integrity constraints (because part of the work was rolled back) or cause inconsistent data in the database (again, because part of the work was rolled back).

    Even if point 3 is fixed, in my opinion the whole design is still brittle and error-prone and unlike anything I have ever seen in other systems that use database transactions. If you forget to pass along the PayloadRequest at every step of the way things go wrong (see #4087 for an example in Payload and it happend constantly in my own application code). If you forget to adhere to the exact calling requirements (see point 2 above, you must start a transaction yourself if you are going to call multiple things in parallel), things go wrong. And the way things go wrong is not at all fail-fast. If things go wrong, basically anything can happen:

    • Everything looks okay, but you now have inconsistent data.
    • You get random and spurious "tried to use an already committed/rolled back transaction" errors
    • You get random inexplicable behavior, where an object you just created seems to be no longer known to the database, because you forgot to pass along the req or the transaction got killed out from under you.

    And if you try again, it most likely worked, because everything is full of race conditions.

    I wish you the best of luck in your endeavors, but I am no longer interested in trying to make this system work.

    Footnotes

    1. A bunch were missing this parameter, fixed in fix: make sure all local operations accept a PayloadRequest parameter #3651. ↩

  7. xHomu commented on Feb 24, 2024

    @xHomu
    Contributor

    Thanks for the detailed breakdown, @diesieben07, that was very informative!

  8. BrianJM commented on Feb 25, 2024

    @BrianJM

    This defect also exists in 2.11.1. #5079 may fix this.

  9. wkentdag commented on Jun 17, 2024

    @wkentdag

    I think the root cause of this bug might be related to #6800 (pending fix in #6821)

  10. github-actions commented on Sep 7, 2024

    @github-actions
    Contributor

    This issue has been automatically locked.
    Please open a new issue if this issue persists with any additional detail.

  11. locked as resolved and limited conversation to collaborators on Sep 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: needs-triagePossible bug which hasn't been reproduced yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions