Uploaded image for project: 'WiredTiger'
  1. WiredTiger
  2. WT-11957

Calling next() is not idempotent with bounded cursors and prepare conflicts

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Major - P3 Major - P3
    • WT11.3.0, 7.3.0-rc0
    • Affects Version/s: None
    • Component/s: None
    • Storage Engines
    • 5
    • 2023-11-28 - Anthill Tiger, 2023-12-12 - Heisenbug

      Calling next() on a bounded cursor after receiving a WT_PREPARE_CONFLICT continues to advance the cursor past the conflicting key. This is in contrast to the behavior of cursors without bounds, which return WT_PREPARE_CONFLICT after every call to next() until the conflict is resolved.

      Passing test (no cursor bounds):

      TEST_F(WiredTigerUtilTest, SkipPreparedUpdateNoBound) {
          // Initialize WiredTiger.
          WiredTigerEventHandler eventHandler;
          WiredTigerUtilHarnessHelper harnessHelper("", &eventHandler);
      
          WT_SESSION* session1;
          ASSERT_OK(wtRCToStatus(
              harnessHelper.getSessionCache()->conn()->open_session(
                  harnessHelper.getSessionCache()->conn(), nullptr, "isolation=snapshot", &session1),
              nullptr));
      
          WT_SESSION* session2;
          ASSERT_OK(wtRCToStatus(
              harnessHelper.getSessionCache()->conn()->open_session(
                  harnessHelper.getSessionCache()->conn(), nullptr, "isolation=snapshot", &session2),
              nullptr));
      
      
          const std::string uri = "table:test";
          ASSERT_OK(wtRCToStatus(session1->create(session1, uri.c_str(), "key_format=S,value_format=S"),
                                 session1));
          WT_CURSOR* cursor1;
          ASSERT_EQ(0, session1->begin_transaction(session1, "ignore_prepare=false"));
          ASSERT_EQ(0, session1->open_cursor(session1, uri.c_str(), nullptr, nullptr, &cursor1));
          cursor1->set_key(cursor1, "abc");
          cursor1->set_value(cursor1, "test");
          ASSERT_EQ(0, cursor1->insert(cursor1));
          session1->prepare_transaction(session1, "prepare_timestamp=1");
      
          WT_CURSOR* cursor2;
          ASSERT_EQ(0, session2->begin_transaction(session2, "ignore_prepare=false"));
          ASSERT_EQ(0, session2->open_cursor(session2, uri.c_str(), nullptr, nullptr, &cursor2));
      
          // Continuously returns WT_PREPARE_CONFLICT
          ASSERT_EQ(WT_PREPARE_CONFLICT, cursor2->next(cursor2));
          ASSERT_EQ(WT_PREPARE_CONFLICT, cursor2->next(cursor2));
          ASSERT_EQ(WT_PREPARE_CONFLICT, cursor2->next(cursor2));
      }
      

      Failing test (with cursor bounds):

      TEST_F(WiredTigerUtilTest, SkipPreparedUpdateBounded) {
          // Initialize WiredTiger.
          WiredTigerEventHandler eventHandler;
          WiredTigerUtilHarnessHelper harnessHelper("", &eventHandler);
      
          WT_SESSION* session1;
          ASSERT_OK(wtRCToStatus(
              harnessHelper.getSessionCache()->conn()->open_session(
                  harnessHelper.getSessionCache()->conn(), nullptr, "isolation=snapshot", &session1),
              nullptr));
      
          WT_SESSION* session2;
          ASSERT_OK(wtRCToStatus(
              harnessHelper.getSessionCache()->conn()->open_session(
                  harnessHelper.getSessionCache()->conn(), nullptr, "isolation=snapshot", &session2),
              nullptr));
      
      
          const std::string uri = "table:test";
          ASSERT_OK(wtRCToStatus(session1->create(session1, uri.c_str(), "key_format=S,value_format=S"),
                                 session1));
          WT_CURSOR* cursor1;
          ASSERT_EQ(0, session1->begin_transaction(session1, "ignore_prepare=false"));
          ASSERT_EQ(0, session1->open_cursor(session1, uri.c_str(), nullptr, nullptr, &cursor1));
          cursor1->set_key(cursor1, "abc");
          cursor1->set_value(cursor1, "test");
          ASSERT_EQ(0, cursor1->insert(cursor1));
          session1->prepare_transaction(session1, "prepare_timestamp=1");
      
          WT_CURSOR* cursor2;
          ASSERT_EQ(0, session2->begin_transaction(session2, "ignore_prepare=false"));
          ASSERT_EQ(0, session2->open_cursor(session2, uri.c_str(), nullptr, nullptr, &cursor2));
      
          cursor2->set_key(cursor2, "abc");
          cursor2->bound(cursor2, "bound=lower");
      
          ASSERT_EQ(WT_PREPARE_CONFLICT, cursor2->next(cursor2));
          ASSERT_EQ(WT_PREPARE_CONFLICT, cursor2->next(cursor2));  // Returns WT_NOTFOUND
      }
      

      This is problematic because we depend on being able to retry WT API calls as many times as necessary until they stop returning WT_PREPARE_CONFLICT. This pattern is everywhere in our code:

      do {
        auto ret = cursor->next();
      } while (ret == WT_PREPARE_CONFLICT);
      

      Not being able to repeatedly call a cursor API call after a prepare conflict means that we may skip records because we don't know when it is safe to stop.

            Assignee:
            luke.pearson@mongodb.com Luke Pearson
            Reporter:
            louis.williams@mongodb.com Louis Williams
            Votes:
            0 Vote for this issue
            Watchers:
            7 Start watching this issue

              Created:
              Updated:
              Resolved: