1// Copyright (C) MongoDB, Inc. 2017-present.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7package integration
8
9import (
10	"bytes"
11	"testing"
12
13	"go.mongodb.org/mongo-driver/bson"
14	"go.mongodb.org/mongo-driver/internal/testutil/assert"
15	"go.mongodb.org/mongo-driver/mongo"
16	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
17	"go.mongodb.org/mongo-driver/mongo/options"
18	"go.mongodb.org/mongo-driver/x/mongo/driver"
19)
20
21func TestRetryableWritesProse(t *testing.T) {
22	clientOpts := options.Client().SetRetryWrites(true).SetWriteConcern(mtest.MajorityWc).
23		SetReadConcern(mtest.MajorityRc)
24	mtOpts := mtest.NewOptions().ClientOptions(clientOpts).MinServerVersion("3.6").CreateClient(false)
25	mt := mtest.New(t, mtOpts)
26	defer mt.Close()
27
28	includeOpts := mtest.NewOptions().Topologies(mtest.ReplicaSet, mtest.Sharded).CreateClient(false)
29	mt.RunOpts("txn number included", includeOpts, func(mt *mtest.T) {
30		updateDoc := bson.D{{"$inc", bson.D{{"x", 1}}}}
31		insertOneDoc := bson.D{{"x", 1}}
32		insertManyOrderedArgs := bson.D{
33			{"options", bson.D{{"ordered", true}}},
34			{"documents", []interface{}{insertOneDoc}},
35		}
36		insertManyUnorderedArgs := bson.D{
37			{"options", bson.D{{"ordered", true}}},
38			{"documents", []interface{}{insertOneDoc}},
39		}
40
41		testCases := []struct {
42			operationName   string
43			args            bson.D
44			expectTxnNumber bool
45		}{
46			{"deleteOne", bson.D{}, true},
47			{"deleteMany", bson.D{}, false},
48			{"updateOne", bson.D{{"update", updateDoc}}, true},
49			{"updateMany", bson.D{{"update", updateDoc}}, false},
50			{"replaceOne", bson.D{}, true},
51			{"insertOne", bson.D{{"document", insertOneDoc}}, true},
52			{"insertMany", insertManyOrderedArgs, true},
53			{"insertMany", insertManyUnorderedArgs, true},
54			{"findOneAndReplace", bson.D{}, true},
55			{"findOneAndUpdate", bson.D{{"update", updateDoc}}, true},
56			{"findOneAndDelete", bson.D{}, true},
57		}
58		for _, tc := range testCases {
59			mt.Run(tc.operationName, func(mt *mtest.T) {
60				tcArgs, err := bson.Marshal(tc.args)
61				assert.Nil(mt, err, "Marshal error: %v", err)
62				crudOp := crudOperation{
63					Name:      tc.operationName,
64					Arguments: tcArgs,
65				}
66
67				mt.ClearEvents()
68				runCrudOperation(mt, "", crudOp, crudOutcome{})
69				started := mt.GetStartedEvent()
70				assert.NotNil(mt, started, "expected CommandStartedEvent, got nil")
71				_, err = started.Command.LookupErr("txnNumber")
72				if tc.expectTxnNumber {
73					assert.Nil(mt, err, "expected txnNumber in command %v", started.Command)
74					return
75				}
76				assert.NotNil(mt, err, "did not expect txnNumber in command %v", started.Command)
77			})
78		}
79	})
80	errorOpts := mtest.NewOptions().Topologies(mtest.ReplicaSet, mtest.Sharded)
81	mt.RunOpts("wrap mmapv1 error", errorOpts, func(mt *mtest.T) {
82		res, err := mt.DB.RunCommand(mtest.Background, bson.D{{"serverStatus", 1}}).DecodeBytes()
83		assert.Nil(mt, err, "serverStatus error: %v", err)
84		storageEngine, ok := res.Lookup("storageEngine", "name").StringValueOK()
85		if !ok || storageEngine != "mmapv1" {
86			mt.Skip("skipping because storage engine is not mmapv1")
87		}
88
89		_, err = mt.Coll.InsertOne(mtest.Background, bson.D{{"x", 1}})
90		assert.Equal(mt, driver.ErrUnsupportedStorageEngine, err,
91			"expected error %v, got %v", driver.ErrUnsupportedStorageEngine, err)
92	})
93
94	standaloneOpts := mtest.NewOptions().Topologies(mtest.Single).CreateClient(false)
95	mt.RunOpts("transaction number not sent on writes", standaloneOpts, func(mt *mtest.T) {
96		mt.Run("explicit session", func(mt *mtest.T) {
97			// Standalones do not support retryable writes and will error if a transaction number is sent
98
99			sess, err := mt.Client.StartSession()
100			assert.Nil(mt, err, "StartSession error: %v", err)
101			defer sess.EndSession(mtest.Background)
102
103			mt.ClearEvents()
104
105			err = mongo.WithSession(mtest.Background, sess, func(ctx mongo.SessionContext) error {
106				doc := bson.D{{"foo", 1}}
107				_, err := mt.Coll.InsertOne(ctx, doc)
108				return err
109			})
110			assert.Nil(mt, err, "InsertOne error: %v", err)
111
112			_, wantID := sess.ID().Lookup("id").Binary()
113			command := mt.GetStartedEvent().Command
114			lsid, err := command.LookupErr("lsid")
115			assert.Nil(mt, err, "Error getting lsid: %v", err)
116			_, gotID := lsid.Document().Lookup("id").Binary()
117			assert.True(mt, bytes.Equal(wantID, gotID), "expected session ID %v, got %v", wantID, gotID)
118			txnNumber, err := command.LookupErr("txnNumber")
119			assert.NotNil(mt, err, "expected no txnNumber, got %v", txnNumber)
120		})
121		mt.Run("implicit session", func(mt *mtest.T) {
122			// Standalones do not support retryable writes and will error if a transaction number is sent
123
124			mt.ClearEvents()
125
126			doc := bson.D{{"foo", 1}}
127			_, err := mt.Coll.InsertOne(mtest.Background, doc)
128			assert.Nil(mt, err, "InsertOne error: %v", err)
129
130			command := mt.GetStartedEvent().Command
131			lsid, err := command.LookupErr("lsid")
132			assert.Nil(mt, err, "Error getting lsid: %v", err)
133			_, gotID := lsid.Document().Lookup("id").Binary()
134			assert.NotNil(mt, gotID, "expected session ID, got nil")
135			txnNumber, err := command.LookupErr("txnNumber")
136			assert.NotNil(mt, err, "expected no txnNumber, got %v", txnNumber)
137		})
138	})
139}
140