Layered cursor returns WT_PREPARE_CONFLICT instead of visible stable key during prepared transaction

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Storage Engines - Foundations
    • 376.021
    • None
    • None

      Issue Summary

      When using layered cursors with prepared transactions, a fresh cursor opened on a layered table returns WT_PREPARE_CONFLICT immediately instead of returning the visible key from the stable table. Specifically, if key '1' is committed in the stable table and key '2' is prepared (but not committed) in the ingest table, calling next() on the cursor returns WT_PREPARE_CONFLICT rather than key '1'.

      Context

      • Table setup: layered table with key '1' in the stable table and key '2' in the ingest table (prepared, not committed).
      • Test case: Opening a fresh cursor and calling next().
      • Expected behavior: The cursor should return key '1' (from the stable table), and only return WT_PREPARE_CONFLICT when iterating to the prepared key.
      • Actual behavior: The cursor immediately returns WT_PREPARE_CONFLICT.
      • Analysis: This appears to be due to the layered cursor's internal logic, where encountering a prepared key in the ingest table causes an immediate conflict, preventing access to visible keys in the stable table. An ASC cursor does not exhibit this behavior in the same scenario.

      Python Test Example

      @disagg_test_class
      class test_layered_prepare_iterate_diff(wttest.WiredTigerTestCase):
          conn_base_config = 'precise_checkpoint=true,preserve_prepared=true,'
          conn_config = conn_base_config + 'disaggregated=(role="leader")'
      
          def test_layered(self):
              # Write '1' on the leader
              uri = 'table:lpid_layered'
              self.session.create(uri, 'key_format=S,value_format=S,block_manager=disagg,type=layered')
      
              with self.transaction(commit_timestamp=100):
                  self.session.open_cursor(uri)['1'] = '1'
              self.conn.set_timestamp('stable_timestamp=' + self.timestamp_str(200))
      
              # Pick up the stable table on the follower, then prepare '2' there.
              self.session.checkpoint()
      
              # Open a follower
              follow = self.wiredtiger_open('follower', self.extensionsConfig() +
                      ',create,' + self.conn_base_config + 'disaggregated=(role="follower")')
              self.disagg_advance_checkpoint(follow)
              follower_session = follow.open_session()
      
              # Prepare an insert of key '2' on the given session.
              follower_session.begin_transaction()
              follower_session.open_cursor(uri)['2'] = '2'
              follower_session.prepare_transaction('prepare_timestamp=' + self.timestamp_str(300) +
                  ',prepared_id=' + self.prepared_id_str(1))
      
              # Call next() on a fresh cursor.
              reader = follow.open_session()
              cursor = reader.open_cursor(uri)
              cursor.next() # Expected to return 1, but returns WT_PREPARE_CONFLICT
      

      Proposed Solution

      Review and update the layered cursor logic so that encountering a prepared key in the ingest table does not immediately return WT_PREPARE_CONFLICT if there are visible keys in the stable table. The cursor should return the visible stable key first, and only return WT_PREPARE_CONFLICT when iterating to the prepared key.


      Original Slack thread: https://mongodb.slack.com/archives/C0520RCK0C9/p1781161878203469
      This ticket was generated by AI from a Slack thread.

            Assignee:
            Unassigned
            Reporter:
            Memento Slack Bot
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: