Deterministic tree ordering — the actual fix for the 'flaky ancestry' failures

Reproduced the reported ~50% intermittent ancestry failure (3-4 of 8 runs) and
captured it: NamespacedDirectory.roots returned [dir4, dir1] vs [dir1, dir4].
The session report blamed 'several dependent saves that occasionally race' —
wrong. Both records share position 0 and run_tree ordered by position alone,
so the tie order was whatever the index scan produced that run. No race, no
CAS involvement: an ORDER BY tie.

- run_tree now tie-breaks by created_at then META(c).id, exactly like
  run_docs already did for finder queries.
- The two client-side tree sorts (descendants, build_tree) get the same
  tie-break: Ruby's sort_by is not stable, so [depth, position] alone could
  reorder same-position siblings between runs.

ancestry_spec re-run repeatedly post-fix: no failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bterkuileandClaude Sonnet 5 committed 2026-07-06 18:51:47 -05:00
1 parent 9ce5aea23d
commit 37288db487
2 files changed
+11 -3

No files matched your search

+5 -2
View File
@@ -68,8 +68,11 @@ module SimplyBased
return @descendants if @descendants
return @descendants = [] if id.blank?
prop = self.class.ancestry_by_property
# created_at/id tie-breaks: records habitually share position 0 and
# Ruby's sort_by is not stable, so [depth, position] alone reorders
# siblings between runs.
@descendants = self.class.database.tree_descendants(self.class, id,
by_property: prop, by_value: (prop && send(prop))).sort_by { |d| [d.path_ids.size, d.position] }
by_property: prop, by_value: (prop && send(prop))).sort_by { |d| [d.path_ids.size, d.position, d.created_at.to_s, d.id.to_s] }
@children = self.class.build_tree(@descendants)
@descendants
end
@@ -254,7 +257,7 @@ module SimplyBased
tree_wrapper = Struct.new(:children).new([]) # Dummy container as traversing begin, contains roots as children
old_tree_slice = tree_wrapper.children
new_tree_slice = []
pages.sort_by!{|p| [p.path_ids.size, p.position]}
pages.sort_by!{|p| [p.path_ids.size, p.position, p.created_at.to_s, p.id.to_s]} # deterministic tie order (sort_by is unstable)
root_depth = pages.first.path_ids.size
current_depth = root_depth + 1 # Start counting/swapping from one depth deeper than first one
for page in pages
+6 -1
View File
@@ -428,7 +428,12 @@ module SimplyBased
def run_tree(where, params, order_col)
sql = +"SELECT c.*, META(c).id AS `__id`, META(c).cas AS `__cas` FROM #{keyspace} AS c WHERE #{where.join(' AND ')}"
sql << " ORDER BY c.#{quote_ident(order_col)} ASC" if order_col
# Tie-break equal positions by creation time then id, like run_docs.
# Records habitually share position 0; ordering by position alone left
# the tie order to the index scan, which flips between runs — this was
# the actual cause of the "intermittently flaky ancestry" failures
# (misdiagnosed as a save/CAS race in the original session report).
sql << " ORDER BY c.#{quote_ident(order_col)} ASC, c.`created_at` ASC, META(c).id ASC" if order_col
query(sql, params).rows.to_a.map { |row| row_to_doc(row) }
end