Skip to content

Flaky: Food.fts orders by rank with no tie-breaker, making food_spec order-dependent #876

Description

@suttondemlong

Symptom

spec/models/food_spec.rb fails intermittently in full-suite runs:

Failure/Error: expect(Food.send(:fts, query[:name], MAX_ROWS, user_food.user_id)).to eq [global_food, personal_food]

Measured rates on master (Ruby 3.2.3, Postgres 15):

  • full suite: 1 failure in 20 runs
  • spec/models/food_spec.rb alone: 0 failures in 25 runs

Root cause

Food.fts orders by relevance with no tie-breaker:

https://github.com/rubyforgood/Flaredown/blob/master/backend/app/models/food.rb#L49

ORDER BY ts_rank_cd(f.searchable, to_tsquery(:lang, :query), 1) DESC
LIMIT :limit

The fixtures produce ties. Actual ranks for the query TestFood:* under the english configuration:

long_desc ts_rank_cd
TestFood 0.14426951
TestFood <word> 0.09102392
TestFood <word> 0.09102392

With LIMIT 2 and no secondary sort key, Postgres is free to return equally ranked rows in any order, and both examples in the fts block depend on that order:

  • "retrun local and global foods for author" (line 35) — the candidate set is global_food ("TestFood"), personal_food ("TestFood"), and the two "TestFood <word>" rows. global_food and personal_food have identical long_desc, so they tie at 0.14426951. The spec asserts the exact order [global_food, personal_food]. This is the example that has been failing.
  • "return same global foods" (line 26) — the candidate set is the three global rows. same_food_1 and same_food_2 tie at 0.09102392, and LIMIT 2 picks one of them arbitrarily, but the spec asserts same_food_1 specifically. Same latent problem, just not observed failing yet.

Running the file alone is stable because the tables are freshly truncated and the physical row order matches insertion order. In a full-suite run the earlier specs have left the heap in a different state, so tied rows can come back in a different order.

Not just a test problem

The missing tie-breaker also affects the deployed food search: for any query where several foods rank equally, the same search can return a different set (because of LIMIT) and a different order on each request. Adding a deterministic secondary sort fixes both the flake and the user-visible instability.

Suggested fix

Add a deterministic tie-breaker to the ORDER BY in fts_sql, e.g.

ORDER BY ts_rank_cd(f.searchable, to_tsquery(:lang, :query), 1) DESC, f.id ASC

and, if the fixtures are meant to exercise ranking rather than ordering, give the food records distinct long_desc values so the expectations do not depend on tie-break behaviour at all.

Notes

Found while verifying #875; unrelated to that change (0/20 full-suite failures on that branch, 1/20 on master). Filing separately so it is not fixed silently inside an unrelated PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions