graphile / crystal

🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!
https://graphile.org/
Other
12.61k stars 569 forks source link

Grafast does not reuse operation plan when using sorting enums #2226

Open clayton-sayer opened 2 hours ago

clayton-sayer commented 2 hours ago

Summary

Reopening a previous ticket that was closed, as we don't have the ability to reopen.

We are finding an inordinate amount of time is spent in grafastPrepare and establishOperationPlan, 50-60% of all CPU and wall time is spent in there. It seems like the query caching is not working correctly, as we only have a couple distinct queries that are called.

image

We previously thought that the query was not being reused when the variables were different but when debugging the server locally we found that even with the same variables we are not reusing the cached plan. Arya and I did some debugging ourselves and found the issue. It seems that the value constraints don't match even for the same query because it's trying to compare two enum arrays for equality which doesn't work, ie: ['OCCURRED_AT_DESC'] === ['OCCURRED_AT_DESC'] is false.

Steps to reproduce

We have a query like the following, which includes an ordering enum generated by Postgraphile:

query FetchActivities($first: Int, $cursor: Cursor, $condition: ActivityCondition, $orderBy: [ActivitiesOrderBy!]) {
  activities(
    first: $first
    after: $cursor
    condition: $condition
    orderBy: $orderBy
  ) {
    edges {
      node {
       .... fields here
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

with variables similar to:

{
  "orderBy":"OCCURRED_AT_DESC",
  "condition":{
    "accountIds":[
      "account-1",
      "account-2"
    ],
    "endDate":"2024-11-02T06:59:59.999Z"
  },
  "first":50
}

Expected results

When debugging on establishOperationPlan I would expect that the second time we make a call with the same variables we would re-use that operation plan and return it.

Actual results

An operation plan is found in the cache but is not compatible with our current request even though the variables are exactly the same. Specifically, the variable values constraints fail.

When digging further, we see that the orderBy constraint fails validation:

image

The constraint object ends up looking like this, which has the orderBy in an array:

{
    "first": 50,
    "condition": {
        "endDate": "2024-11-02T06:59:59.999Z",
        "accountIds": [
            "account-1",
            "account-2"
        ]
    },
    "orderBy": [
        "OCCURRED_AT_DESC"
    ]
}

and the constraint itself looks like:

  {
    "type": "value",
    "path": [
      "orderBy"
    ],
    "value": [
      "OCCURRED_AT_DESC"
    ]
  }

This occurs because it's trying to do an equality check on two arrays without doing a deep-equals, which will always return false.

Additional context

Arya has a unit test that shows off this problem, so it should be easy to fix.

Possible Solution

Perform a deep-equals on the two arrays, or you may want to skip the value equality check altogether and just do an array length check like you do with the accountIds constraints.

aryascripts commented 2 hours ago

Thanks @clayton-sayer for bringing this up again! Here's the unit test that describes the problem in the function matchesConstraints:

PR: https://github.com/graphile/crystal/pull/2225

Screenshot 2024-11-01 at 3 04 48 PM

clayton-sayer commented 2 hours ago

FYI we do notice that we're sending the orderBy as a single field in our GraphQL query, yet the variableValues being processed by Grafast has it as an array. That might be another part of the bug, I'm not sure.

Let me know if you want us to dig further into that part, as that seems quite suspicious.