From 37288db487dd502229ada1ba768aa4cba9d63218 Mon Sep 17 00:00:00 2001 From: Benjamin ter Kuile Date: Mon, 6 Jul 2026 18:51:47 -0500 Subject: [PATCH] =?UTF-8?q?Deterministic=20tree=20ordering=20=E2=80=94=20t?= =?UTF-8?q?he=20actual=20fix=20for=20the=20'flaky=20ancestry'=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/simply_based/model/ancestry.rb | 7 +++++-- lib/simply_based/store.rb | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/simply_based/model/ancestry.rb b/lib/simply_based/model/ancestry.rb index d19e811..6be59e8 100644 --- a/lib/simply_based/model/ancestry.rb +++ b/lib/simply_based/model/ancestry.rb @@ -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 diff --git a/lib/simply_based/store.rb b/lib/simply_based/store.rb index 30f46ed..2b6aa86 100644 --- a/lib/simply_based/store.rb +++ b/lib/simply_based/store.rb @@ -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