# Test that all the operations go to the same mongos.
#
# In tests that don't include command-started events the assertion is implicit:
# that all the read operations succeed. If the driver does not properly pin to
# a single mongos then one of the operations in a transaction will eventually
# be sent to a different mongos, which is unaware of the transaction, and the
# mongos will return a command error. An example of such an error is:
# {
#   'ok': 0.0,
#   'errmsg': 'cannot continue txnId -1 for session 28938f50-9d29-4ca5-8de5-ddaf261267c4 - 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= with txnId 1',
#   'code': 251,
#   'codeName': 'NoSuchTransaction',
#   'errorLabels': ['TransientTransactionError']
# }
runOn:
    -
        minServerVersion: "4.1.8"
        topology: ["sharded"]
        # serverless proxy doesn't append error labels to errors in transactions
        # caused by failpoints (CLOUDP-88216)
        serverless: "forbid"

database_name: &database_name "transaction-tests"
collection_name: &collection_name "test"

data: &data
  - {_id: 1}
  - {_id: 2}

tests:
  - description: countDocuments
    useMultipleMongoses: true
    operations:
      - &startTransaction
        name: startTransaction
        object: session0
      - &countDocuments
        name: countDocuments
        object: collection
        arguments:
          filter:
            _id: 2
          session: session0
        result: 1
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - &commitTransaction
        name: commitTransaction
        object: session0

    outcome:
      collection:
        data: *data

  - description: distinct
    useMultipleMongoses: true
    operations:
      - *startTransaction
      - &distinct
        name: distinct
        object: collection
        arguments:
          fieldName: _id
          session: session0
        result: [1, 2]
      - *distinct
      - *distinct
      - *distinct
      - *distinct
      - *distinct
      - *distinct
      - *distinct
      - *commitTransaction

    outcome:
      collection:
        data: *data

  - description: find
    useMultipleMongoses: true
    operations:
      - name: startTransaction
        object: session0
      - &find
        name: find
        object: collection
        arguments:
          filter:
            _id: 2
          session: session0
        result:
          - {_id: 2}
      - *find
      - *find
      - *find
      - *find
      - *find
      - *find
      - *find
      - *commitTransaction

    outcome:
      collection:
        data: *data

  - description: insertOne
    useMultipleMongoses: true
    operations:
      - name: startTransaction
        object: session0
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 3
          session: session0
        result:
          insertedId: 3
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 4
          session: session0
        result:
          insertedId: 4
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 5
          session: session0
        result:
          insertedId: 5
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 6
          session: session0
        result:
          insertedId: 6
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 7
          session: session0
        result:
          insertedId: 7
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 8
          session: session0
        result:
          insertedId: 8
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 9
          session: session0
        result:
          insertedId: 9
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 10
          session: session0
        result:
          insertedId: 10
      - *commitTransaction

    outcome:
      collection:
        data:
          - {_id: 1}
          - {_id: 2}
          - {_id: 3}
          - {_id: 4}
          - {_id: 5}
          - {_id: 6}
          - {_id: 7}
          - {_id: 8}
          - {_id: 9}
          - {_id: 10}

  - description: mixed read write operations
    useMultipleMongoses: true
    operations:
      - name: startTransaction
        object: session0
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 3
          session: session0
        result:
          insertedId: 3
      - &countDocuments
        name: countDocuments
        object: collection
        arguments:
          filter:
            _id: 3
          session: session0
        result: 1
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - *countDocuments
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 4
          session: session0
        result:
          insertedId: 4
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 5
          session: session0
        result:
          insertedId: 5
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 6
          session: session0
        result:
          insertedId: 6
      - name: insertOne
        object: collection
        arguments:
          document:
            _id: 7
          session: session0
        result:
          insertedId: 7
      - *commitTransaction

    outcome:
      collection:
        data:
          - {_id: 1}
          - {_id: 2}
          - {_id: 3}
          - {_id: 4}
          - {_id: 5}
          - {_id: 6}
          - {_id: 7}

  - description: multiple commits
    useMultipleMongoses: true
    operations:
      - name: startTransaction
        object: session0
      - name: insertMany
        object: collection
        arguments:
          documents:
            - _id: 3
            - _id: 4
          session: session0
        result:
          insertedIds: {0: 3, 1: 4}
      # Session is pinned and remains pinned after successful commits.
      - &assertSessionPinned
        name: assertSessionPinned
        object: testRunner
        arguments:
          session: session0
      - *commitTransaction
      - *assertSessionPinned
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *commitTransaction
      - *assertSessionPinned

    outcome:
      collection:
        data:
          - {_id: 1}
          - {_id: 2}
          - {_id: 3}
          - {_id: 4}

  - description: remain pinned after non-transient error on commit
    useMultipleMongoses: true
    operations:
      - name: startTransaction
        object: session0
      - name: insertMany
        object: collection
        arguments:
          documents:
            - _id: 3
            - _id: 4
          session: session0
        result:
          insertedIds: {0: 3, 1: 4}
      # Session is pinned.
      - *assertSessionPinned
      # Fail the commit with a non-transient error.
      - name: targetedFailPoint
        object: testRunner
        arguments:
          session: session0
          failPoint:
            configureFailPoint: failCommand
            mode: { times: 1 }
            data:
              failCommands: ["commitTransaction"]
              errorCode: 51 # ManualInterventionRequired
      - name: commitTransaction
        object: session0
        result:
          errorLabelsOmit: ["TransientTransactionError"]
          errorCode: 51
      - *assertSessionPinned
      # The next commit should succeed.
      - name: commitTransaction
        object: session0
      - *assertSessionPinned

    outcome:
      collection:
        data:
          - {_id: 1}
          - {_id: 2}
          - {_id: 3}
          - {_id: 4}

  - description: unpin after transient error within a transaction
    useMultipleMongoses: true
    operations:
      - name: startTransaction
        object: session0
      - name: insertOne
        object: collection
        arguments:
          session: session0
          document:
            _id: 3
        result:
          insertedId: 3
      # Enable the fail point only on the Mongos that session0 is pinned to.
      - name: targetedFailPoint
        object: testRunner
        arguments:
          session: session0
          failPoint:
            configureFailPoint: failCommand
            mode: { times: 1 }
            data:
              failCommands: ["insert"]
              closeConnection: true
      - name: insertOne
        object: collection
        arguments:
          session: session0
          document:
            _id: 4
        result:
          errorLabelsContain: ["TransientTransactionError"]
          errorLabelsOmit: ["UnknownTransactionCommitResult"]
      # Session unpins from the first mongos after the insert error and
      # abortTransaction succeeds immediately on any mongos.
      - name: abortTransaction
        object: session0

    expectations:
      - command_started_event:
          command:
            insert: *collection_name
            documents:
              - _id: 3
            ordered: true
            readConcern:
            lsid: session0
            txnNumber:
              $numberLong: "1"
            startTransaction: true
            autocommit: false
            writeConcern:
          command_name: insert
          database_name: *database_name
      - command_started_event:
          command:
            insert: *collection_name
            documents:
              - _id: 4
            ordered: true
            readConcern:
            lsid: session0
            txnNumber:
              $numberLong: "1"
            startTransaction:
            autocommit: false
            writeConcern:
          command_name: insert
          database_name: *database_name
      - command_started_event:
          command:
            abortTransaction: 1
            lsid: session0
            txnNumber:
              $numberLong: "1"
            startTransaction:
            autocommit: false
            writeConcern:
            recoveryToken: 42
          command_name: abortTransaction
          database_name: admin

    outcome:
      collection:
        data:
          - _id: 1
          - _id: 2

  # Applications should not run commitTransaction after transient errors but
  # the transactions API allows it and this test confirms unpinning behavior.
  # In a sharded cluster, a transient error within a transaction unpins the
  # session. This way a subsequent abort can "succeed" immediately instead of
  # blocking for serverSelectionTimeoutMS in the case the mongos went down.
  # However since the abortTransaction helper ignores errors, this test uses
  # commitTransaction to prove the session was unpinned.
  - description: unpin after transient error within a transaction and commit
    useMultipleMongoses: true
    clientOptions:
      # Increase heartbeatFrequencyMS to avoid the race condition where an in
      # flight heartbeat refreshes the first mongoes' SDAM state in between
      # the insert connection error and the single commit attempt.
      heartbeatFrequencyMS: 30000
    operations:
      - name: startTransaction
        object: session0
      - name: insertOne
        object: collection
        arguments:
          session: session0
          document:
            _id: 3
        result:
          insertedId: 3
      # Enable the fail point only on the Mongos that session0 is pinned to.
      # Fail hello/legacy hello to prevent the heartbeat requested directly after the
      # insert error from racing with server selection for the commit.
      # Note: times: 7 is slightly artbitrary but it accounts for one failed
      # insert and some SDAM heartbeats. A test runner will have multiple
      # clients connected to this server so this fail point configuration
      # is also racy.
      - name: targetedFailPoint
        object: testRunner
        arguments:
          session: session0
          failPoint:
            configureFailPoint: failCommand
            mode: { times: 7 }
            data:
              failCommands: ["insert", "isMaster", "hello"]
              closeConnection: true
      - name: insertOne
        object: collection
        arguments:
          session: session0
          document:
            _id: 4
        result:
          errorLabelsContain: ["TransientTransactionError"]
          errorLabelsOmit: ["UnknownTransactionCommitResult"]
      # Session unpins from the first mongos after the insert error and
      # commitTransaction selects the second mongos which is unaware of the
      # transaction and therefore fails with NoSuchTransaction error. If this
      # commit succeeds it indicates a bug, either:
      # - the driver mistakenly remained pinned even after the insert error, or
      # - the test client was initialized with a single mongos seed
      #
      # Note that the commit attempt should not select the original mongos
      # because that server's SDAM state is reset by the connection error,
      # heartbeatFrequencyMS is high, and subsequent heartbeats
      # should fail.
      - name: commitTransaction
        object: session0
        result:
          errorLabelsContain: ["TransientTransactionError"]
          errorLabelsOmit: ["UnknownTransactionCommitResult"]
          errorCodeName: NoSuchTransaction


    expectations:
      - command_started_event:
          command:
            insert: *collection_name
            documents:
              - _id: 3
            ordered: true
            readConcern:
            lsid: session0
            txnNumber:
              $numberLong: "1"
            startTransaction: true
            autocommit: false
            writeConcern:
          command_name: insert
          database_name: *database_name
      - command_started_event:
          command:
            insert: *collection_name
            documents:
              - _id: 4
            ordered: true
            readConcern:
            lsid: session0
            txnNumber:
              $numberLong: "1"
            startTransaction:
            autocommit: false
            writeConcern:
          command_name: insert
          database_name: *database_name
      - command_started_event:
          command:
            commitTransaction: 1
            lsid: session0
            txnNumber:
              $numberLong: "1"
            startTransaction:
            autocommit: false
            writeConcern:
            recoveryToken: 42
          command_name: commitTransaction
          database_name: admin

    outcome:
      collection:
        data:
          - _id: 1
          - _id: 2
