/*
* ToroDB
* Copyright © 2014 8Kdata Technology (www.8kdata.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.torodb.mongodb.commands.signatures.internal;
import static com.eightkdata.mongowp.bson.BsonType.DATETIME;
import static com.eightkdata.mongowp.bson.BsonType.DOCUMENT;
import static com.eightkdata.mongowp.bson.BsonType.TIMESTAMP;
import com.eightkdata.mongowp.ErrorCode;
import com.eightkdata.mongowp.MongoConstants;
import com.eightkdata.mongowp.OpTime;
import com.eightkdata.mongowp.bson.BsonDocument;
import com.eightkdata.mongowp.bson.BsonDocument.Entry;
import com.eightkdata.mongowp.bson.BsonNumber;
import com.eightkdata.mongowp.bson.BsonTimestamp;
import com.eightkdata.mongowp.bson.BsonType;
import com.eightkdata.mongowp.bson.BsonValue;
import com.eightkdata.mongowp.exceptions.BadValueException;
import com.eightkdata.mongowp.exceptions.FailedToParseException;
import com.eightkdata.mongowp.exceptions.InconsistentReplicaSetNamesException;
import com.eightkdata.mongowp.exceptions.MongoException;
import com.eightkdata.mongowp.exceptions.NoSuchKeyException;
import com.eightkdata.mongowp.exceptions.TypesMismatchException;
import com.eightkdata.mongowp.exceptions.UnknownErrorException;
import com.eightkdata.mongowp.fields.BooleanField;
import com.eightkdata.mongowp.fields.DateTimeField;
import com.eightkdata.mongowp.fields.DocField;
import com.eightkdata.mongowp.fields.DoubleField;
import com.eightkdata.mongowp.fields.HostAndPortField;
import com.eightkdata.mongowp.fields.IntField;
import com.eightkdata.mongowp.fields.LongField;
import com.eightkdata.mongowp.fields.StringField;
import com.eightkdata.mongowp.fields.TimestampField;
import com.eightkdata.mongowp.utils.BsonDocumentBuilder;
import com.eightkdata.mongowp.utils.BsonReaderTool;
import com.google.common.net.HostAndPort;
import com.torodb.mongodb.commands.pojos.MemberState;
import com.torodb.mongodb.commands.pojos.ReplicaSetConfig;
import java.time.Duration;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
*
*/
public class ReplSetHeartbeatReplyMarshaller {
private static final DocField CONFIG_FIELD = new DocField("config");
private static final LongField CONFIG_VERSION_FIELD = new LongField("v");
private static final TimestampField ELECTION_TIME_FIELD = new TimestampField("electionTime");
private static final StringField ERR_MSG_FIELD = new StringField("errmsg");
private static final IntField ERROR_CODE_FIELD = new IntField("code");
private static final BooleanField HAS_DATA_FIELD = new BooleanField("hasData");
@SuppressWarnings("checkstyle:LineLength")
private static final BooleanField HAS_STATE_DISAGREEMENT_FIELD = new BooleanField("stateDisagreement");
private static final StringField HB_MESSAGE_FIELD = new StringField("hbmsg");
private static final BooleanField IS_ELECTABLE_FIELD = new BooleanField("e");
private static final BooleanField IS_REPL_SET_FIELD = new BooleanField("rs");
private static final IntField MEMBER_STATE_FIELD = new IntField("state");
private static final BooleanField MISMATCH_FIELD = new BooleanField("mismatch");
private static final DoubleField OK_FIELD = new DoubleField("ok");
private static final String APPLIED_OP_TIME_FIELD_NAME = "opTime";
@SuppressWarnings("checkstyle:LineLength")
private static final DocField APPLIED_OP_TIME_DOC_FIELD = new DocField(APPLIED_OP_TIME_FIELD_NAME);
@SuppressWarnings("checkstyle:LineLength")
private static final DateTimeField APPLIED_OP_TIME_DT_FIELD = new DateTimeField(APPLIED_OP_TIME_FIELD_NAME);
private static final StringField REPL_SET_FIELD = new StringField("set");
private static final HostAndPortField SYNC_SOURCE_FIELD = new HostAndPortField("syncingTo");
private static final LongField TIME_FIELD = new LongField("time");
private static final LongField TERM_FIELD = new LongField("term");
private static final DocField DURABLE_OP_TIME_FIELD = new DocField("durableOpTime");
private static final LongField PRIMARY_ID_FIELD = new LongField("primaryId");
private ReplSetHeartbeatReplyMarshaller() {
}
static BsonDocument marshall(ReplSetHeartbeatReply reply, boolean asV1) {
BsonDocumentBuilder doc = new BsonDocumentBuilder();
if (reply.isMismatch()) {
doc.append(OK_FIELD, MongoConstants.KO)
.append(MISMATCH_FIELD, true);
return doc.build();
}
if (reply.getErrorCode().isOk()) {
doc.append(OK_FIELD, MongoConstants.OK);
} else {
doc.append(OK_FIELD, MongoConstants.KO);
doc.append(ERROR_CODE_FIELD, reply.getErrorCode().getErrorCode());
if (reply.getErrMsg().isPresent()) {
doc.append(ERR_MSG_FIELD, reply.getErrMsg().get());
}
}
reply.getTime().ifPresent(time ->
doc.append(TIME_FIELD, time.getSeconds()));
reply.getElectionTime().ifPresent(electionTime ->
doc.append(ELECTION_TIME_FIELD, electionTime));
reply.getConfig().ifPresent(config ->
doc.append(CONFIG_FIELD, config.toBson()));
reply.getElectable().ifPresent(electable ->
doc.append(IS_ELECTABLE_FIELD, electable));
reply.getIsReplSet().ifPresent(isRepSet ->
doc.append(IS_REPL_SET_FIELD, isRepSet));
doc.append(HAS_STATE_DISAGREEMENT_FIELD, reply.isStateDisagreement());
reply.getState().ifPresent(state ->
doc.append(MEMBER_STATE_FIELD, state.getId()));
doc.append(CONFIG_VERSION_FIELD, reply.getConfigVersion());
doc.append(HB_MESSAGE_FIELD, reply.getHbmsg());
reply.getSetName().ifPresent(setName ->
doc.append(REPL_SET_FIELD, setName));
reply.getSyncingTo().ifPresent(syncingTo ->
doc.append(SYNC_SOURCE_FIELD, syncingTo.toString()));
reply.getHasData().ifPresent(hasData ->
doc.append(HAS_DATA_FIELD, hasData));
if (reply.getTerm() != -1) {
doc.append(TERM_FIELD, reply.getTerm());
}
reply.getPrimaryId().ifPresent(primaryId ->
doc.append(PRIMARY_ID_FIELD, primaryId));
reply.getDurableOptime().ifPresent(durableOptime ->
doc.append(DURABLE_OP_TIME_FIELD, durableOptime.toBson()));
reply.getAppliedOpTime().ifPresent(applyied -> {
if (asV1) {
doc.append(APPLIED_OP_TIME_DOC_FIELD, applyied.toBson());
} else {
applyied.appendAsOldBson(doc, APPLIED_OP_TIME_FIELD_NAME);
}
});
return doc.build();
}
static ReplSetHeartbeatReply unmarshall(BsonDocument bson) throws
TypesMismatchException, NoSuchKeyException, BadValueException,
FailedToParseException, MongoException {
// Old versions set this even though they returned not "ok"
boolean mismatch = BsonReaderTool.getBoolean(bson, MISMATCH_FIELD, false);
if (mismatch) {
throw new InconsistentReplicaSetNamesException();
}
ReplSetHeartbeatReplyBuilder builder = new ReplSetHeartbeatReplyBuilder();
// Old versions sometimes set the replica set name ("set") but ok:0
String setName;
try {
setName = BsonReaderTool.getString(bson, REPL_SET_FIELD, null);
builder.setSetName(setName);
} catch (TypesMismatchException ex) {
throw ex.newWithMessage("Expected \"" + REPL_SET_FIELD
+ "\" field in response to replSetHeartbeat to have type " + "String, but found " + ex
.getFoundType());
}
checkCommandError(bson, setName);
builder.setHasData(readHasData(bson))
.setElectionTime(readElectionTime(bson))
.setTime(readTime(bson))
.setIsReplSet(BsonReaderTool.isPseudoTrue(bson, IS_REPL_SET_FIELD));
long term = BsonReaderTool.getLong(bson, TERM_FIELD, -1);
builder.setTerm(term)
.setDurableOpTime(readNullableOpTime(bson, DURABLE_OP_TIME_FIELD));
parseAppliedOpTime(bson, builder, term);
builder.setIsReplSet(BsonReaderTool.getBoolean(bson, IS_ELECTABLE_FIELD, false))
.setState(readMemberState(bson))
.setStateDisagreement(BsonReaderTool.isPseudoTrue(bson, HAS_STATE_DISAGREEMENT_FIELD))
.setConfigVersion(readConfigVersion(bson, builder.getAppliedOpTime()))
.setHbmsg(readHbmsg(bson))
.setSyncingTo(readSyncingTo(bson))
.setConfig(readConfig(bson));
return builder.build();
}
@Nullable
private static BsonTimestamp readElectionTime(BsonDocument bson) throws
TypesMismatchException {
Entry<?> electionTimeEntry = BsonReaderTool.getEntry(bson,
ELECTION_TIME_FIELD.getFieldName(), null);
if (electionTimeEntry != null) {
switch (electionTimeEntry.getValue().getType()) {
case DATETIME: {
return BsonReaderTool.getTimestampFromDateTime(electionTimeEntry);
}
case TIMESTAMP:
return electionTimeEntry.getValue().asTimestamp();
default: {
BsonType foundType =
electionTimeEntry.getValue().getType();
throw new TypesMismatchException(ELECTION_TIME_FIELD.getFieldName(), "Date or Timestamp",
foundType, "Expected \"" + ELECTION_TIME_FIELD + "\" field in "
+ "response to replSetHeartbeat command to "
+ "have type Date or Timestamp, but found " + "type " + foundType);
}
}
}
return null;
}
private static boolean readHasData(BsonDocument bson) throws TypesMismatchException,
NoSuchKeyException {
if (!bson.containsKey(HAS_DATA_FIELD.getFieldName())) {
return false;
}
return BsonReaderTool.getBoolean(bson, HAS_DATA_FIELD);
}
private static void checkCommandError(BsonDocument bson, String setName) throws MongoException {
if (setName == null && !BsonReaderTool.isPseudoTrue(bson, OK_FIELD)) {
String errMsg = BsonReaderTool.getString(bson, ERR_MSG_FIELD, "");
assert errMsg != null;
Entry<?> errorCodeEntry = BsonReaderTool.getEntry(bson,
ERROR_CODE_FIELD, null);
if (errorCodeEntry != null) {
if (!errorCodeEntry.getValue().isNumber()) {
throw new BadValueException(ERROR_CODE_FIELD + " is "
+ "not a number");
}
ErrorCode errorCode = ErrorCode.fromErrorCode(
errorCodeEntry.getValue().asNumber().intValue());
throw new MongoException(errMsg, errorCode);
}
throw new UnknownErrorException(errMsg);
}
}
@Nullable
private static Duration readTime(BsonDocument bson) throws TypesMismatchException {
BsonNumber timeNumber = BsonReaderTool.getNumeric(bson, TIME_FIELD, null);
if (timeNumber == null) {
return null;
}
return Duration.ofSeconds(timeNumber.longValue());
}
@Nullable
private static OpTime readNullableOpTime(BsonDocument bson, DocField field) throws
TypesMismatchException, NoSuchKeyException {
BsonDocument subDoc = BsonReaderTool.getDocument(bson, field, null);
if (subDoc == null) {
return null;
}
return OpTime.fromBson(subDoc);
}
private static void parseAppliedOpTime(BsonDocument bson, ReplSetHeartbeatReplyBuilder builder,
long term) throws TypesMismatchException, NoSuchKeyException {
// In order to support both the 3.0(V0) and 3.2(V1) heartbeats we must parse the OpTime
// field based on its type. If it is a Date, we parse it as the timestamp and use
// initialize's term argument to complete the OpTime type. If it is an Object, then it's
// V1 and we construct an OpTime out of its nested fields.
Entry<?> entry = BsonReaderTool.getEntry(bson, APPLIED_OP_TIME_FIELD_NAME, null);
if (entry == null) {
return;
}
BsonValue<?> value = entry.getValue();
OpTime opTime;
switch (value.getType()) {
case TIMESTAMP:
opTime = new OpTime(value.asTimestamp(), term);
break;
case DATETIME:
BsonTimestamp ts = BsonReaderTool.getTimestampFromDateTime(entry);
opTime = new OpTime(ts, term);
break;
case DOCUMENT: //repl v1
opTime = OpTime.fromBson(value.asDocument());
builder.setIsReplSet(true);
break;
default:
throw new TypesMismatchException(APPLIED_OP_TIME_FIELD_NAME, "Date or Timestamp", value
.getType());
}
builder.setAppliedOpTime(opTime);
}
@Nullable
private static MemberState readMemberState(BsonDocument bson) throws BadValueException,
TypesMismatchException, NoSuchKeyException {
MemberState state;
if (!bson.containsKey(MEMBER_STATE_FIELD.getFieldName())) {
state = null;
} else {
int memberId = BsonReaderTool.getNumeric(bson, MEMBER_STATE_FIELD)
.intValue();
try {
state = MemberState.fromId(memberId);
} catch (IllegalArgumentException ex) {
throw new BadValueException("Value for \"" + MEMBER_STATE_FIELD + "\" in response to "
+ "replSetHeartbeat is out of range; legal values are "
+ "non-negative and no more than " + MemberState.getMaxId());
}
}
return state;
}
private static long readConfigVersion(BsonDocument bson, Optional<OpTime> appliedOpTime) throws
NoSuchKeyException, TypesMismatchException {
Entry<?> configVersionEntry = BsonReaderTool.getEntry(bson,
CONFIG_VERSION_FIELD, null);
if (appliedOpTime.isPresent() && configVersionEntry == null) {
throw new NoSuchKeyException(CONFIG_VERSION_FIELD.getFieldName(),
"Response to replSetHeartbeat missing required \""
+ CONFIG_VERSION_FIELD + "\" field even though "
+ "initialized"
);
}
if (configVersionEntry != null && !configVersionEntry.getValue().isInt32()) {
throw new TypesMismatchException(CONFIG_VERSION_FIELD.getFieldName(),
BsonType.INT32, configVersionEntry.getValue().getType(),
"Expected \"" + CONFIG_VERSION_FIELD + "\" field in "
+ "response to replSetHeartbeat to have type NumberInt, but"
+ " found " + configVersionEntry.getValue().getType()
);
}
if (configVersionEntry != null) {
return configVersionEntry.getValue().asInt32().getValue();
}
return 0;
}
@Nonnull
private static String readHbmsg(BsonDocument bson) throws TypesMismatchException {
try {
return BsonReaderTool.getString(bson, HB_MESSAGE_FIELD, "");
} catch (TypesMismatchException ex) {
throw ex.newWithMessage("Expected \"" + HB_MESSAGE_FIELD
+ "\" field in response to replSetHeartbeat to have type "
+ "String, but found " + ex.getFoundType());
}
}
@Nullable
private static HostAndPort readSyncingTo(BsonDocument bson) throws TypesMismatchException {
try {
return BsonReaderTool.getHostAndPort(bson, SYNC_SOURCE_FIELD, null);
} catch (TypesMismatchException ex) {
throw ex.newWithMessage("Expected \"" + SYNC_SOURCE_FIELD
+ "\" field in response to replSetHeartbeat to have type " + "String, but found " + ex
.getFoundType());
}
}
@Nullable
private static ReplicaSetConfig readConfig(BsonDocument bson) throws MongoException,
FailedToParseException {
BsonDocument uncastedConf;
try {
uncastedConf = BsonReaderTool.getDocument(bson, CONFIG_FIELD, null);
} catch (TypesMismatchException ex) {
throw ex.newWithMessage("Expected \"" + CONFIG_FIELD + "\" in response to replSetHeartbeat "
+ "to have type Object, but " + "found " + ex.getFoundType());
}
if (uncastedConf == null) {
return null;
} else {
return ReplicaSetConfig.fromDocument(uncastedConf);
}
}
}