/*
* Copyright (c) 2008-2015 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb.connection;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.WriteConcernException;
import com.mongodb.WriteConcernResult;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.diagnostics.logging.Logger;
import com.mongodb.event.CommandListener;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.Decoder;
import org.bson.io.OutputBuffer;
import static com.mongodb.MongoNamespace.COMMAND_COLLECTION_NAME;
import static com.mongodb.connection.ProtocolHelper.encodeMessage;
import static com.mongodb.connection.ProtocolHelper.encodeMessageWithMetadata;
import static com.mongodb.connection.ProtocolHelper.getMessageSettings;
import static com.mongodb.connection.ProtocolHelper.getWriteResult;
import static com.mongodb.connection.ProtocolHelper.sendCommandFailedEvent;
import static com.mongodb.connection.ProtocolHelper.sendCommandStartedEvent;
import static com.mongodb.connection.ProtocolHelper.sendCommandSucceededEvent;
import static java.util.Collections.singletonList;
/**
* Base class for wire protocol messages that perform writes. In particular, it handles the write followed by the getlasterror command to
* apply the write concern.
*/
abstract class WriteProtocol implements Protocol<WriteConcernResult> {
private final MongoNamespace namespace;
private final boolean ordered;
private final WriteConcern writeConcern;
private CommandListener commandListener;
WriteProtocol(final MongoNamespace namespace, final boolean ordered, final WriteConcern writeConcern) {
this.namespace = namespace;
this.ordered = ordered;
this.writeConcern = writeConcern;
}
@Override
public void setCommandListener(final CommandListener commandListener) {
this.commandListener = commandListener;
}
@Override
public WriteConcernResult execute(final InternalConnection connection) {
WriteConcernResult writeConcernResult = null;
RequestMessage requestMessage = null;
do {
long startTimeNanos = System.nanoTime();
RequestMessage.EncodingMetadata encodingMetadata;
int messageId;
boolean sentCommandStartedEvent = false;
ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(connection);
try {
if (requestMessage == null) {
requestMessage = createRequestMessage(getMessageSettings(connection.getDescription()));
}
encodingMetadata = requestMessage.encodeWithMetadata(bsonOutput);
sendStartedEvent(connection, requestMessage, encodingMetadata, bsonOutput);
sentCommandStartedEvent = true;
messageId = requestMessage.getId();
if (shouldAcknowledge(encodingMetadata.getNextMessage())) {
CommandMessage getLastErrorMessage = new CommandMessage(new MongoNamespace(getNamespace().getDatabaseName(),
COMMAND_COLLECTION_NAME).getFullName(),
createGetLastErrorCommandDocument(), false,
getMessageSettings(connection.getDescription()));
getLastErrorMessage.encode(bsonOutput);
messageId = getLastErrorMessage.getId();
}
connection.sendMessage(bsonOutput.getByteBuffers(), messageId);
} catch (RuntimeException e) {
sendFailedEvent(connection, requestMessage, sentCommandStartedEvent, e, startTimeNanos);
throw e;
} finally {
bsonOutput.close();
}
if (shouldAcknowledge(encodingMetadata.getNextMessage())) {
ResponseBuffers responseBuffers = null;
try {
responseBuffers = connection.receiveMessage(messageId);
ReplyMessage<BsonDocument> replyMessage = new ReplyMessage<BsonDocument>(responseBuffers, new BsonDocumentCodec(),
messageId);
writeConcernResult = getWriteResult(replyMessage.getDocuments().get(0),
connection.getDescription().getServerAddress());
} catch (WriteConcernException e) {
sendSucceededEvent(connection, requestMessage, encodingMetadata.getNextMessage(), e, startTimeNanos);
if (writeConcern.isAcknowledged()) {
throw e;
} else if (ordered) {
break;
}
} catch (RuntimeException e) {
sendFailedEvent(connection, requestMessage, sentCommandStartedEvent, e, startTimeNanos);
throw e;
} finally {
if (responseBuffers != null) {
responseBuffers.close();
}
}
}
sendSucceededEvent(connection, requestMessage, encodingMetadata.getNextMessage(), writeConcernResult, startTimeNanos);
requestMessage = encodingMetadata.getNextMessage();
} while (requestMessage != null);
return writeConcern.isAcknowledged() ? writeConcernResult : WriteConcernResult.unacknowledged();
}
protected abstract void appendToWriteCommandResponseDocument(RequestMessage curMessage, RequestMessage nextMessage,
WriteConcernResult writeConcernResult, BsonDocument response);
@Override
public void executeAsync(final InternalConnection connection, final SingleResultCallback<WriteConcernResult> callback) {
executeAsync(createRequestMessage(getMessageSettings(connection.getDescription())), connection, callback);
}
private void executeAsync(final RequestMessage requestMessage, final InternalConnection connection,
final SingleResultCallback<WriteConcernResult> callback) {
long startTimeNanos = System.nanoTime();
boolean sentCommandStartedEvent = false;
try {
ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(connection);
RequestMessage.EncodingMetadata encodingMetadata = encodeMessageWithMetadata(requestMessage, bsonOutput);
sendStartedEvent(connection, requestMessage, encodingMetadata, bsonOutput);
sentCommandStartedEvent = true;
if (shouldAcknowledge(encodingMetadata.getNextMessage())) {
CommandMessage getLastErrorMessage = new CommandMessage(new MongoNamespace(getNamespace().getDatabaseName(),
COMMAND_COLLECTION_NAME).getFullName(),
createGetLastErrorCommandDocument(), false,
getMessageSettings(connection.getDescription()));
encodeMessage(getLastErrorMessage, bsonOutput);
SingleResultCallback<ResponseBuffers> receiveCallback = new WriteResultCallback(callback,
new BsonDocumentCodec(),
requestMessage,
encodingMetadata.getNextMessage(),
getLastErrorMessage.getId(),
connection, startTimeNanos);
connection.sendMessageAsync(bsonOutput.getByteBuffers(), getLastErrorMessage.getId(),
new SendMessageCallback<WriteConcernResult>(connection,
bsonOutput,
requestMessage,
getLastErrorMessage.getId(),
getCommandName(requestMessage),
startTimeNanos,
commandListener,
callback,
receiveCallback));
} else {
connection.sendMessageAsync(bsonOutput.getByteBuffers(), requestMessage.getId(),
new UnacknowledgedWriteResultCallback(callback,
requestMessage,
encodingMetadata.getNextMessage(),
bsonOutput,
connection, startTimeNanos));
}
} catch (Throwable t) {
sendFailedEvent(connection, requestMessage, sentCommandStartedEvent, t, startTimeNanos);
callback.onResult(null, t);
}
}
protected abstract BsonDocument getAsWriteCommand(ByteBufferBsonOutput bsonOutput, int firstDocumentPosition);
protected BsonDocument getBaseCommandDocument(final String commandName) {
BsonDocument baseCommandDocument = new BsonDocument(commandName, new BsonString(getNamespace().getCollectionName()))
.append("ordered", BsonBoolean.valueOf(isOrdered()));
if (!writeConcern.isServerDefault()) {
baseCommandDocument.append("writeConcern", writeConcern.asDocument());
}
return baseCommandDocument;
}
protected String getCommandName(final RequestMessage message) {
switch (message.getOpCode()) {
case OP_INSERT:
return "insert";
case OP_UPDATE:
return "update";
case OP_DELETE:
return "delete";
default:
throw new MongoInternalException("Unexpected op code for write: " + message.getOpCode());
}
}
private void sendStartedEvent(final InternalConnection connection, final RequestMessage message,
final RequestMessage.EncodingMetadata encodingMetadata, final ByteBufferBsonOutput bsonOutput) {
if (commandListener != null) {
sendCommandStartedEvent(message, namespace.getDatabaseName(), getCommandName(message),
getAsWriteCommand(bsonOutput, encodingMetadata.getFirstDocumentPosition()),
connection.getDescription(), commandListener);
}
}
private void sendSucceededEvent(final InternalConnection connection, final RequestMessage message,
final RequestMessage nextMessage,
final WriteConcernException e,
final long startTimeNanos) {
if (commandListener != null) {
sendSucceededEvent(connection, message,
getResponseDocument(message, nextMessage, e.getWriteConcernResult(), e), startTimeNanos);
}
}
private void sendSucceededEvent(final InternalConnection connection, final RequestMessage message,
final RequestMessage nextMessage,
final WriteConcernResult writeConcernResult,
final long startTimeNanos) {
if (commandListener != null) {
sendSucceededEvent(connection, message,
getResponseDocument(message, nextMessage, writeConcernResult, null), startTimeNanos);
}
}
private void sendSucceededEvent(final InternalConnection connection, final RequestMessage message,
final BsonDocument responseDocument, final long startTimeNanos) {
if (commandListener != null) {
sendCommandSucceededEvent(message, getCommandName(message), responseDocument, connection.getDescription(), startTimeNanos,
commandListener);
}
}
private void sendFailedEvent(final InternalConnection connection, final RequestMessage message,
final boolean sentCommandStartedEvent, final Throwable t, final long startTimeNanos) {
if (commandListener != null && sentCommandStartedEvent) {
sendCommandFailedEvent(message, getCommandName(message), connection.getDescription(), startTimeNanos, t, commandListener);
}
}
private BsonDocument getResponseDocument(final RequestMessage curMessage, final RequestMessage nextMessage,
final WriteConcernResult writeConcernResult,
final WriteConcernException writeConcernException) {
BsonDocument response = new BsonDocument("ok", new BsonInt32(1));
if (writeConcern.isAcknowledged()) {
if (writeConcernException == null) {
appendToWriteCommandResponseDocument(curMessage, nextMessage, writeConcernResult, response);
} else {
response.put("n", new BsonInt32(0));
BsonDocument writeErrorDocument = new BsonDocument("index", new BsonInt32(0))
.append("code", new BsonInt32(writeConcernException.getErrorCode()));
if (writeConcernException.getErrorMessage() != null) {
writeErrorDocument.append("errmsg", new BsonString(writeConcernException.getErrorMessage()));
}
response.put("writeErrors", new BsonArray(singletonList(writeErrorDocument)));
}
}
return response;
}
private boolean shouldAcknowledge(final RequestMessage nextMessage) {
return writeConcern.isAcknowledged() || (isOrdered() && nextMessage != null);
}
private BsonDocument createGetLastErrorCommandDocument() {
BsonDocument command = new BsonDocument("getlasterror", new BsonInt32(1));
command.putAll(writeConcern.asDocument());
return command;
}
/**
* Create the initial request message for the write.
*
* @param settings the message settings
* @return the request message
*/
protected abstract RequestMessage createRequestMessage(MessageSettings settings);
/**
* Gets the namespace.
*
* @return the namespace
*/
protected MongoNamespace getNamespace() {
return namespace;
}
/**
* Gets whether the writes are ordered.
*
* @return true if ordered
*/
protected boolean isOrdered() {
return ordered;
}
/**
* Gets the write concern.
*
* @return the write concern
*/
protected WriteConcern getWriteConcern() {
return writeConcern;
}
/**
* Gets the logger.
*
* @return the logger
*/
protected abstract Logger getLogger();
private final class WriteResultCallback extends CommandResultBaseCallback<BsonDocument> {
private final SingleResultCallback<WriteConcernResult> callback;
private final RequestMessage message;
private final RequestMessage nextMessage;
private final InternalConnection connection;
private final long startTimeNanos;
WriteResultCallback(final SingleResultCallback<WriteConcernResult> callback, final Decoder<BsonDocument> decoder,
final RequestMessage message, final RequestMessage nextMessage, final long requestId,
final InternalConnection connection, final long startTimeNanos) {
super(decoder, requestId, connection.getDescription().getServerAddress());
this.callback = callback;
this.message = message;
this.nextMessage = nextMessage;
this.connection = connection;
this.startTimeNanos = startTimeNanos;
}
@Override
protected void callCallback(final BsonDocument result, final Throwable throwableFromCallback) {
if (throwableFromCallback != null) {
sendFailedEvent(connection, message, true, throwableFromCallback, startTimeNanos);
callback.onResult(null, throwableFromCallback);
} else {
try {
try {
WriteConcernResult writeConcernResult = null;
boolean shouldWriteNextMessage = true;
try {
writeConcernResult = getWriteResult(result, connection.getDescription().getServerAddress());
} catch (WriteConcernException e) {
if (writeConcern.isAcknowledged()) {
throw e;
}
if (ordered) {
shouldWriteNextMessage = false;
}
}
sendSucceededEvent(connection, message, nextMessage, writeConcernResult, startTimeNanos);
if (shouldWriteNextMessage && nextMessage != null) {
executeAsync(nextMessage, connection, callback);
} else {
callback.onResult(writeConcernResult, null);
}
} catch (WriteConcernException e) {
sendSucceededEvent(connection, message, nextMessage, e, startTimeNanos);
throw e;
} catch (RuntimeException e) {
sendFailedEvent(connection, message, true, e, startTimeNanos);
throw e;
}
} catch (Throwable t) {
callback.onResult(null, t);
}
}
}
}
private final class UnacknowledgedWriteResultCallback implements SingleResultCallback<Void> {
private final SingleResultCallback<WriteConcernResult> callback;
private final RequestMessage message;
private final RequestMessage nextMessage;
private final OutputBuffer writtenBuffer;
private final InternalConnection connection;
private final long startTimeNanos;
UnacknowledgedWriteResultCallback(final SingleResultCallback<WriteConcernResult> callback,
final RequestMessage message,
final RequestMessage nextMessage,
final OutputBuffer writtenBuffer,
final InternalConnection connection, final long startTimeNanos) {
this.callback = callback;
this.message = message;
this.nextMessage = nextMessage;
this.connection = connection;
this.writtenBuffer = writtenBuffer;
this.startTimeNanos = startTimeNanos;
}
@Override
public void onResult(final Void result, final Throwable t) {
writtenBuffer.close();
if (t != null) {
sendFailedEvent(connection, message, true, t, startTimeNanos);
callback.onResult(null, t);
} else {
sendSucceededEvent(connection, message, nextMessage, (WriteConcernResult) null, startTimeNanos);
if (nextMessage != null) {
executeAsync(nextMessage, connection, callback);
} else {
callback.onResult(WriteConcernResult.unacknowledged(), null);
}
}
}
}
}