Skip to content

Commit eaf4292

Browse files
committed
JAVA-2815: Add read preference property to transaction options
1 parent 227d53d commit eaf4292

34 files changed

+867
-81
lines changed

driver-async/src/main/com/mongodb/async/client/ClientSessionHelper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ private ClientSession createClientSession(final ClientSessionOptions options, fi
9595
TransactionOptions.builder()
9696
.readConcern(mongoClient.getSettings().getReadConcern())
9797
.writeConcern(mongoClient.getSettings().getWriteConcern())
98+
.readPreference(mongoClient.getSettings().getReadPreference())
9899
.build()))
99100
.build();
100101
return new ClientSessionImpl(serverSessionPool, mongoClient, mergedOptions, executor);

driver-async/src/main/com/mongodb/async/client/ClientSessionImpl.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.mongodb.ClientSessionOptions;
2020
import com.mongodb.MongoInternalException;
2121
import com.mongodb.ReadConcern;
22-
import com.mongodb.ReadPreference;
2322
import com.mongodb.TransactionOptions;
2423
import com.mongodb.async.SingleResultCallback;
2524
import com.mongodb.internal.session.BaseClientSessionImpl;
@@ -90,7 +89,7 @@ public void commitTransaction(final SingleResultCallback<Void> callback) {
9089
throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null");
9190
}
9291
executor.execute(new CommitTransactionOperation(transactionOptions.getWriteConcern()),
93-
ReadPreference.primary(), readConcern, this,
92+
readConcern, this,
9493
new SingleResultCallback<Void>() {
9594
@Override
9695
public void onResult(final Void result, final Throwable t) {
@@ -115,7 +114,7 @@ public void abortTransaction(final SingleResultCallback<Void> callback) {
115114
throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null");
116115
}
117116
executor.execute(new AbortTransactionOperation(transactionOptions.getWriteConcern()),
118-
ReadPreference.primary(), readConcern, this,
117+
readConcern, this,
119118
new SingleResultCallback<Void>() {
120119
@Override
121120
public void onResult(final Void result, final Throwable t) {

driver-async/src/main/com/mongodb/async/client/OperationExecutorImpl.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.mongodb.async.client;
1818

1919
import com.mongodb.MongoClientException;
20+
import com.mongodb.MongoInternalException;
2021
import com.mongodb.ReadConcern;
2122
import com.mongodb.ReadPreference;
2223
import com.mongodb.async.SingleResultCallback;
@@ -65,6 +66,9 @@ public void onResult(final ClientSession clientSession, final Throwable t) {
6566
} else {
6667
final AsyncReadBinding binding = getReadWriteBinding(readPreference, readConcern, clientSession,
6768
session == null && clientSession != null);
69+
if (session != null && session.hasActiveTransaction() && !binding.getReadPreference().equals(primary())) {
70+
throw new MongoClientException("Read preference in a transaction must be primary");
71+
}
6872
operation.executeAsync(binding, new SingleResultCallback<T>() {
6973
@Override
7074
public void onResult(final T result, final Throwable t) {
@@ -117,11 +121,9 @@ public void onResult(final T result, final Throwable t) {
117121
private AsyncReadWriteBinding getReadWriteBinding(final ReadPreference readPreference, final ReadConcern readConcern,
118122
@Nullable final ClientSession session, final boolean ownsSession) {
119123
notNull("readPreference", readPreference);
120-
AsyncReadWriteBinding readWriteBinding = new AsyncClusterBinding(mongoClient.getCluster(), readPreference);
124+
AsyncReadWriteBinding readWriteBinding = new AsyncClusterBinding(mongoClient.getCluster(),
125+
getReadPreferenceForBinding(readPreference, session));
121126
if (session != null) {
122-
if (session.hasActiveTransaction() && !readPreference.equals(primary())) {
123-
throw new MongoClientException("Read preference in a transaction must be primary");
124-
}
125127
if (!session.hasActiveTransaction() && session.getOptions().getAutoStartTransaction()) {
126128
session.startTransaction();
127129
}
@@ -130,4 +132,17 @@ private AsyncReadWriteBinding getReadWriteBinding(final ReadPreference readPrefe
130132
return readWriteBinding;
131133
}
132134

135+
private ReadPreference getReadPreferenceForBinding(final ReadPreference readPreference, @Nullable final ClientSession session) {
136+
if (session == null) {
137+
return readPreference;
138+
}
139+
if (session.hasActiveTransaction()) {
140+
ReadPreference readPreferenceForBinding = session.getTransactionOptions().getReadPreference();
141+
if (readPreferenceForBinding == null) {
142+
throw new MongoInternalException("Invariant violated. Transaction options read preference can not be null");
143+
}
144+
return readPreferenceForBinding;
145+
}
146+
return readPreference;
147+
}
133148
}

driver-async/src/test/functional/com/mongodb/async/client/MongoClientSessionSpecification.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class MongoClientSessionSpecification extends FunctionalSpecification {
7777
.defaultTransactionOptions(TransactionOptions.builder()
7878
.readConcern(ReadConcern.DEFAULT)
7979
.writeConcern(WriteConcern.ACKNOWLEDGED)
80+
.readPreference(ReadPreference.primary())
8081
.build())
8182
.build()
8283
clientSession.getClusterTime() == null

driver-async/src/test/functional/com/mongodb/async/client/TransactionsTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.mongodb.MongoWriteConcernException;
2424
import com.mongodb.ReadConcern;
2525
import com.mongodb.ReadConcernLevel;
26+
import com.mongodb.ReadPreference;
2627
import com.mongodb.TransactionOptions;
2728
import com.mongodb.WriteConcern;
2829
import com.mongodb.client.model.CreateCollectionOptions;
@@ -134,6 +135,7 @@ public void apply(final SocketSettings.Builder builder) {
134135
.retryWrites(clientOptions.getBoolean("retryWrites", BsonBoolean.FALSE).getValue())
135136
.writeConcern(getWriteConcern(clientOptions))
136137
.readConcern(getReadConcern(clientOptions))
138+
.readPreference(getReadPreference(clientOptions))
137139
.build());
138140

139141
MongoDatabase database = mongoClient.getDatabase(databaseName);
@@ -167,6 +169,14 @@ private WriteConcern getWriteConcern(final BsonDocument clientOptions) {
167169
}
168170
}
169171

172+
private ReadPreference getReadPreference(final BsonDocument clientOptions) {
173+
if (clientOptions.containsKey("readPreference")) {
174+
return ReadPreference.valueOf(clientOptions.getString("readPreference").getValue());
175+
} else {
176+
return ReadPreference.primary();
177+
}
178+
}
179+
170180
private ClientSession createSession(final String sessionName) {
171181
BsonDocument optionsDocument = definition.getDocument("sessionOptions", new BsonDocument())
172182
.getDocument(sessionName, new BsonDocument());
@@ -193,6 +203,9 @@ private TransactionOptions createDefaultTransactionOptions(final BsonDocument op
193203
if (defaultTransactionOptionsDocument.containsKey("writeConcern")) {
194204
builder.writeConcern(helper.getWriteConcern(defaultTransactionOptionsDocument));
195205
}
206+
if (defaultTransactionOptionsDocument.containsKey("readPreference")) {
207+
builder.readPreference(helper.getReadPreference(defaultTransactionOptionsDocument));
208+
}
196209
}
197210
return builder.build();
198211
}
@@ -233,6 +246,9 @@ public void shouldPassAllOutcomes() {
233246
if (options.containsKey("readConcern")) {
234247
builder.readConcern(helper.getReadConcern(options));
235248
}
249+
if (options.containsKey("readPreference")) {
250+
builder.readPreference(helper.getReadPreference(options));
251+
}
236252
nonNullClientSession(clientSession).startTransaction(builder.build());
237253
} else {
238254
nonNullClientSession(clientSession).startTransaction();

driver-core/src/main/com/mongodb/TransactionOptions.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import static com.mongodb.assertions.Assertions.notNull;
2424

2525
/**
26-
* Options to apply to transactions.
26+
* Options to apply to transactions. The default values for the options depend on context. For options specified per-transaction, the
27+
* default values come from the default transaction options. For the default transaction options themselves, the default values come from
28+
* the MongoClient on which the session was started.
2729
*
2830
* @see com.mongodb.session.ClientSession
2931
* @see ClientSessionOptions
@@ -34,11 +36,12 @@
3436
public final class TransactionOptions {
3537
private final ReadConcern readConcern;
3638
private final WriteConcern writeConcern;
39+
private final ReadPreference readPreference;
3740

3841
/**
3942
* Gets the read concern.
4043
*
41-
* @return the read concern, which defaults to {@link ReadConcern#SNAPSHOT}
44+
* @return the read concern
4245
*/
4346
@Nullable
4447
public ReadConcern getReadConcern() {
@@ -48,13 +51,23 @@ public ReadConcern getReadConcern() {
4851
/**
4952
* Gets the write concern.
5053
*
51-
* @return the write concern, which defaults to {@link WriteConcern#ACKNOWLEDGED}
54+
* @return the write concern
5255
*/
5356
@Nullable
5457
public WriteConcern getWriteConcern() {
5558
return writeConcern;
5659
}
5760

61+
/**
62+
* Gets the read preference.
63+
*
64+
* @return the write concern
65+
*/
66+
@Nullable
67+
public ReadPreference getReadPreference() {
68+
return readPreference;
69+
}
70+
5871
/**
5972
* Gets an instance of a builder
6073
*
@@ -79,6 +92,8 @@ public static TransactionOptions merge(final TransactionOptions options, final T
7992
? defaultOptions.getWriteConcern() : options.getWriteConcern())
8093
.readConcern(options.getReadConcern() == null
8194
? defaultOptions.getReadConcern() : options.getReadConcern())
95+
.readPreference(options.getReadPreference() == null
96+
? defaultOptions.getReadPreference() : options.getReadPreference())
8297
.build();
8398
}
8499

@@ -99,6 +114,9 @@ public boolean equals(final Object o) {
99114
if (writeConcern != null ? !writeConcern.equals(that.writeConcern) : that.writeConcern != null) {
100115
return false;
101116
}
117+
if (readPreference != null ? !readPreference.equals(that.readPreference) : that.readPreference != null) {
118+
return false;
119+
}
102120

103121
return true;
104122
}
@@ -107,14 +125,17 @@ public boolean equals(final Object o) {
107125
public int hashCode() {
108126
int result = readConcern != null ? readConcern.hashCode() : 0;
109127
result = 31 * result + (writeConcern != null ? writeConcern.hashCode() : 0);
128+
result = 31 * result + (readPreference != null ? readPreference.hashCode() : 0);
110129
return result;
111130
}
112131

113132
@Override
114133
public String toString() {
115-
return "TransactionOptions{readConcern="
116-
+ readConcern + ", writeConcern="
117-
+ writeConcern + '}';
134+
return "TransactionOptions{"
135+
+ "readConcern=" + readConcern
136+
+ ", writeConcern=" + writeConcern
137+
+ ", readPreference=" + readPreference
138+
+ '}';
118139
}
119140

120141
/**
@@ -123,6 +144,7 @@ public String toString() {
123144
public static final class Builder {
124145
private ReadConcern readConcern;
125146
private WriteConcern writeConcern;
147+
private ReadPreference readPreference;
126148

127149
/**
128150
* Sets the read concern.
@@ -147,6 +169,17 @@ public Builder writeConcern(@Nullable final WriteConcern writeConcern) {
147169
return this;
148170
}
149171

172+
/**
173+
* Sets the read preference.
174+
*
175+
* @param readPreference the read preference, which currently must be primary. This restriction may be relaxed in future versions.
176+
* @return this
177+
*/
178+
public Builder readPreference(@Nullable final ReadPreference readPreference) {
179+
this.readPreference = readPreference;
180+
return this;
181+
}
182+
150183
/**
151184
* Build the transaction options instance.
152185
*
@@ -164,5 +197,6 @@ private Builder() {
164197
private TransactionOptions(final Builder builder) {
165198
readConcern = builder.readConcern;
166199
writeConcern = builder.writeConcern;
200+
readPreference = builder.readPreference;
167201
}
168202
}

driver-core/src/main/com/mongodb/operation/TransactionOperation.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
import com.mongodb.WriteConcern;
2020
import com.mongodb.async.SingleResultCallback;
21-
import com.mongodb.binding.AsyncReadBinding;
22-
import com.mongodb.binding.ReadBinding;
21+
import com.mongodb.binding.AsyncWriteBinding;
22+
import com.mongodb.binding.WriteBinding;
2323
import com.mongodb.connection.AsyncConnection;
2424
import com.mongodb.connection.Connection;
2525
import com.mongodb.operation.OperationHelper.AsyncCallableWithConnection;
@@ -32,17 +32,17 @@
3232
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
3333
import static com.mongodb.operation.CommandOperationHelper.executeWrappedCommandProtocol;
3434
import static com.mongodb.operation.CommandOperationHelper.executeWrappedCommandProtocolAsync;
35+
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorTransformer;
3536
import static com.mongodb.operation.OperationHelper.LOGGER;
3637
import static com.mongodb.operation.OperationHelper.releasingCallback;
3738
import static com.mongodb.operation.OperationHelper.withConnection;
38-
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorTransformer;
3939

4040
/**
4141
* A base class for transaction-related operations
4242
*
4343
* @since 3.8
4444
*/
45-
public abstract class TransactionOperation implements ReadOperation<Void>, AsyncReadOperation<Void> {
45+
public abstract class TransactionOperation implements WriteOperation<Void>, AsyncWriteOperation<Void> {
4646
private final WriteConcern writeConcern;
4747

4848
/**
@@ -64,7 +64,7 @@ public WriteConcern getWriteConcern() {
6464
}
6565

6666
@Override
67-
public Void execute(final ReadBinding binding) {
67+
public Void execute(final WriteBinding binding) {
6868
isTrue("in transaction", binding.getSessionContext().hasActiveTransaction());
6969
return withConnection(binding, new CallableWithConnection<Void>() {
7070
@Override
@@ -76,7 +76,7 @@ public Void call(final Connection connection) {
7676
}
7777

7878
@Override
79-
public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback<Void> callback) {
79+
public void executeAsync(final AsyncWriteBinding binding, final SingleResultCallback<Void> callback) {
8080
isTrue("in transaction", binding.getSessionContext().hasActiveTransaction());
8181
withConnection(binding, new AsyncCallableWithConnection() {
8282
@Override

driver-core/src/test/resources/transactions/README.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ Test Format
2222

2323
Each YAML file has the following keys:
2424

25+
- ``database_name`` and ``collection_name``: The database and collection to use
26+
for testing.
27+
2528
- ``data``: The data that should exist in the collection under test before each
26-
test run. (TODO: not used yet.)
29+
test run.
2730

2831
- ``tests``: An array of tests that are to be run independently of each other.
2932
Each test will have some or all of the following fields:
@@ -73,6 +76,8 @@ For each YAML file, for each element in ``tests``:
7376
transactions from previous test failures. The command will fail with message
7477
"operation was interrupted", because it kills its own implicit session. Catch
7578
the exception and continue.
79+
#. Create a collection object from the MongoClient, using the ``database_name``
80+
and ``collection_name`` fields of the YAML file.
7681
#. Drop the test collection, using writeConcern "majority".
7782
#. Execute the "create" command to recreate the collection, using writeConcern
7883
"majority". (Creating the collection inside a transaction is prohibited, so
@@ -121,7 +126,9 @@ For each YAML file, for each element in ``tests``:
121126
#. For each element in ``outcome``:
122127

123128
- If ``name`` is "collection", verify that the test collection contains
124-
exactly the documents in the ``data`` array.
129+
exactly the documents in the ``data`` array. Ensure this find uses
130+
Primary read preference even when the MongoClient is configured with
131+
another read preference.
125132

126133
TODO:
127134

@@ -132,6 +139,9 @@ TODO:
132139
Command-Started Events
133140
``````````````````````
134141

142+
The event listener used for these tests MUST ignore the security commands
143+
listed in the Command Monitoring Spec.
144+
135145
Logical Session Id
136146
~~~~~~~~~~~~~~~~~~
137147

driver-core/src/test/resources/transactions/abort.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
2+
"database_name": "transaction-tests",
3+
"collection_name": "test",
24
"data": [],
35
"tests": [
46
{

driver-core/src/test/resources/transactions/auto-start.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
2+
"database_name": "transaction-tests",
3+
"collection_name": "test",
24
"data": [],
35
"tests": [
46
{

0 commit comments

Comments
 (0)