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