/*
* 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.repl;
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.BsonValue;
import com.eightkdata.mongowp.bson.utils.DefaultBsonValues;
import com.eightkdata.mongowp.exceptions.MongoException;
import com.eightkdata.mongowp.fields.ArrayField;
import com.eightkdata.mongowp.fields.BooleanField;
import com.eightkdata.mongowp.fields.DateTimeField;
import com.eightkdata.mongowp.fields.DoubleField;
import com.eightkdata.mongowp.fields.HostAndPortField;
import com.eightkdata.mongowp.fields.IntField;
import com.eightkdata.mongowp.fields.StringField;
import com.eightkdata.mongowp.fields.TimestampField;
import com.eightkdata.mongowp.server.api.impl.AbstractNotAliasableCommand;
import com.eightkdata.mongowp.server.api.tools.Empty;
import com.eightkdata.mongowp.utils.BsonDocumentBuilder;
import com.google.common.collect.Lists;
import com.google.common.net.HostAndPort;
import com.google.common.primitives.UnsignedInteger;
import com.torodb.mongodb.commands.pojos.MemberConfig;
import com.torodb.mongodb.commands.pojos.MemberHeartbeatData;
import com.torodb.mongodb.commands.pojos.MemberState;
import com.torodb.mongodb.commands.pojos.ReplicaSetConfig;
import com.torodb.mongodb.commands.signatures.repl.ReplSetGetStatusCommand.ReplSetGetStatusReply;
import com.torodb.mongodb.commands.tools.EmptyCommandArgumentMarshaller;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* Report status of a replica set from the POV of this server.
*/
@Immutable
public class ReplSetGetStatusCommand
extends AbstractNotAliasableCommand<Empty, ReplSetGetStatusReply> {
public static final ReplSetGetStatusCommand INSTANCE = new ReplSetGetStatusCommand();
private ReplSetGetStatusCommand() {
super("replSetGetStatus");
}
@Override
public Class<? extends Empty> getArgClass() {
return Empty.class;
}
@Override
public Empty unmarshallArg(BsonDocument requestDoc) {
return Empty.getInstance();
}
@Override
public BsonDocument marshallArg(Empty request) {
return EmptyCommandArgumentMarshaller.marshallEmptyArgument(this);
}
@Override
public Class<? extends ReplSetGetStatusReply> getResultClass() {
return ReplSetGetStatusReply.class;
}
@Override
public BsonDocument marshallResult(ReplSetGetStatusReply reply) {
return reply.marshall();
}
@Override
public ReplSetGetStatusReply unmarshallResult(BsonDocument resultDoc) throws
MongoException, UnsupportedOperationException {
throw new UnsupportedOperationException("Not supported");
}
@Immutable
public abstract static class ReplSetGetStatusReply {
public abstract ErrorCode getErrorCode();
public abstract String getErrMsg();
public abstract String getSetName();
public abstract MemberState getMyState();
public abstract Instant getDate();
protected abstract BsonDocument marshall();
}
@Immutable
public static class InvalidReplSetGetStatusReply extends ReplSetGetStatusReply {
@Nonnull private final ErrorCode errorCode;
@Nonnull private final String errMsg;
@Nonnull private final MemberState state;
@Nonnull private final Duration uptime;
@Nonnull private final OpTime optime;
@Nonnegative private final int maintenanceModeCalls;
@Nullable private final String heartbeatMessage;
public InvalidReplSetGetStatusReply(
@Nonnull ErrorCode errorCode,
@Nonnull String errMsg,
MemberState state,
Duration uptime,
OpTime optime,
int maintenanceModeCalls,
String heartbeatMessage) {
this.errorCode = errorCode;
this.errMsg = errMsg;
this.state = state;
this.uptime = uptime;
this.optime = optime;
this.maintenanceModeCalls = maintenanceModeCalls;
this.heartbeatMessage = heartbeatMessage;
}
@Override
public ErrorCode getErrorCode() {
return errorCode;
}
@Override
public String getErrMsg() {
return errMsg;
}
@Override
public String getSetName() {
return null;
}
@Override
public MemberState getMyState() {
return state;
}
@Override
public Instant getDate() {
return null;
}
private static final StringField ERR_MSG_FIELD_NAME = new StringField("errmsg");
private static final IntField ERROR_CODE_FIELD_NAME = new IntField("code");
private static final DoubleField OK_FIELD_NAME = new DoubleField("ok");
private static final IntField STATE_FIELD = new IntField("state");
private static final StringField STATE_STR_FIELD = new StringField("stateStr");
private static final IntField UPTIME_FIELD = new IntField("uptime");
private static final TimestampField OPTIME_FIELD = new TimestampField("optime");
private static final DateTimeField OPTIME_DATE_FIELD = new DateTimeField("optimeDate");
private static final IntField MAINTENANCE_MODE_FIELD = new IntField("maintenanceMode");
private static final StringField INFO_MESSAGE_FIELD = new StringField("infoMessage");
@Override
protected BsonDocument marshall() {
BsonDocumentBuilder builder = new BsonDocumentBuilder();
builder.append(ERR_MSG_FIELD_NAME, errMsg)
.append(ERROR_CODE_FIELD_NAME, ErrorCode.INVALID_REPLICA_SET_CONFIG.getErrorCode())
.append(OK_FIELD_NAME, MongoConstants.KO)
.append(STATE_FIELD, state.getId())
.append(STATE_STR_FIELD, state.name())
//TODO: Check if cast to int is correct
.append(UPTIME_FIELD, UnsignedInteger.valueOf(uptime.getSeconds()).intValue())
.append(OPTIME_FIELD, optime)
.appendInstant(OPTIME_DATE_FIELD, DefaultBsonValues.newDateTime(optime.getTimestamp())
.getMillisFromUnix());
if (maintenanceModeCalls != 0) {
builder.append(MAINTENANCE_MODE_FIELD, maintenanceModeCalls);
}
if (heartbeatMessage != null) {
builder.append(INFO_MESSAGE_FIELD, heartbeatMessage);
}
return builder.build();
}
}
public static class CorrectReplSetGetStatusReply extends ReplSetGetStatusReply {
private static final StringField SET_NAME_FIELD = new StringField("set");
private static final DateTimeField DATE_FIELD = new DateTimeField("date");
private static final IntField MY_STATE_FIELD = new IntField("myState");
private static final HostAndPortField SYNCING_TO_FIELD = new HostAndPortField("syncingTo");
private static final ArrayField MEMBERS_FIELD = new ArrayField("members");
private static final DoubleField OK_FIELD_NAME = new DoubleField("ok");
private static final IntField MEMBER_ID_FIELD = new IntField("_id");
private static final HostAndPortField MEMBER_NAME_FIELD = new HostAndPortField("name");
private static final DoubleField MEMBER_HEALTH_FIELD = new DoubleField("health");
private static final IntField MEMBER_STATE_FIELD = new IntField("state");
private static final StringField MEMBER_STATE_STR_FIELD = new StringField("stateStr");
private static final IntField MEMBER_UPTIME_FIELD = new IntField("uptime");
private static final TimestampField MEMBER_OPTIME_FIELD = new TimestampField("optime");
private static final DateTimeField MEMBER_OPTIME_DATE_FIELD = new DateTimeField("optimeDate");
private static final HostAndPortField MEMBER_SYNCING_TO_FIELD =
new HostAndPortField("syncingTo");
private static final IntField MEMBER_MAINTENANCE_MODE_FIELD = new IntField("maintenanceMode");
private static final StringField MEMBER_INFO_MESSAGE_FIELD = new StringField("infoMessage");
private static final TimestampField MEMBER_ELECTION_TIME_FIELD = new TimestampField(
"electionTime");
private static final DateTimeField MEMBER_ELECTION_DATE_FIELD =
new DateTimeField("electionDate");
private static final DoubleField MEMBER_CONFIG_VERSION_FIELD = new DoubleField("configVersion");
private static final BooleanField MEMBER_SELF_FIELD = new BooleanField("self");
private static final DateTimeField MEMBER_LAST_HEARTBEAT = new DateTimeField("lastHeartbeat");
private static final DateTimeField MEMBER_LAST_HEARTBEAT_RECIVED = new DateTimeField(
"lastHeartbeatRecv");
private static final IntField MEMBER_PING_MS = new IntField("pingMs");
private static final StringField MEMBER_LAST_HEARTBEAT_MESSAGE = new StringField(
"lastHeartbeatMessage");
private static final BooleanField MEMBER_AUTHENTICATED = new BooleanField("authenticated");
@Nonnull
private final SelfData selfData;
@Nonnull
private final String setName;
@Nonnull
private final Instant now;
@Nullable
private final HostAndPort syncTo;
private final Map<MemberConfig, MemberHeartbeatData> membersInfo;
private final Map<MemberConfig, Integer> pings;
@Nonnull
private final ReplicaSetConfig replConfig;
public CorrectReplSetGetStatusReply(
@Nonnull SelfData selfData,
@Nonnull String setName,
@Nonnull Instant now,
@Nullable HostAndPort syncTo,
Map<MemberConfig, MemberHeartbeatData> membersInfo,
Map<MemberConfig, Integer> pings,
@Nonnull ReplicaSetConfig replConfig) {
this.selfData = selfData;
this.setName = setName;
this.now = now;
this.syncTo = syncTo;
this.membersInfo = membersInfo;
this.pings = pings;
this.replConfig = replConfig;
}
@Override
public ErrorCode getErrorCode() {
return ErrorCode.OK;
}
@Override
public String getErrMsg() {
return "";
}
@Override
public String getSetName() {
return setName;
}
@Override
public MemberState getMyState() {
return null;
}
@Override
public Instant getDate() {
return now;
}
@Override
protected BsonDocument marshall() {
BsonDocumentBuilder builder = new BsonDocumentBuilder();
MemberState state = selfData.state;
builder.append(SET_NAME_FIELD, setName)
.append(DATE_FIELD, now)
.append(MY_STATE_FIELD, state.getId());
if (syncTo != null && !state.equals(MemberState.RS_PRIMARY) && !state.equals(
MemberState.RS_REMOVED)) {
builder.append(SYNCING_TO_FIELD, syncTo);
}
List<BsonValue<?>> membersList = Lists.newArrayListWithCapacity(membersInfo.size());
for (Entry<MemberConfig, MemberHeartbeatData> entrySet : membersInfo.entrySet()) {
MemberConfig memberConfig = entrySet.getKey();
MemberHeartbeatData memberData = entrySet.getValue();
if (memberConfig.getId() == selfData.getId()) {
membersList.add(marshallSelfMember());
} else {
membersList.add(marshallOtherMember(memberConfig, memberData));
}
}
//TODO: find out how mongo actually sort the members array
Collections.sort(membersList, (BsonValue<?> o1, BsonValue<?> o2) -> {
BsonValue<?> v1 = o1.asDocument().get(MEMBER_ID_FIELD.getFieldName());
assert v1 != null;
BsonValue<?> v2 = o2.asDocument().get(MEMBER_ID_FIELD.getFieldName());
return v1.compareTo(v2);
});
builder.append(MEMBERS_FIELD, DefaultBsonValues.newArray(membersList));
return builder.build();
}
private BsonDocument marshallSelfMember() {
BsonDocumentBuilder builder = new BsonDocumentBuilder();
builder.append(MEMBER_ID_FIELD, selfData.getId())
.append(MEMBER_NAME_FIELD, selfData.getName())
.append(MEMBER_HEALTH_FIELD, 1.0)
.append(OK_FIELD_NAME, MongoConstants.OK)
.append(MEMBER_STATE_FIELD, selfData.getState().getId())
.append(MEMBER_STATE_STR_FIELD, selfData.getState().name())
.append(MEMBER_UPTIME_FIELD, UnsignedInteger.valueOf(selfData.getUptime().getSeconds())
.intValue()); //TODO: Check if cast to int is correct
if (!selfData.getState().equals(MemberState.RS_ARBITER)) {
builder.append(MEMBER_OPTIME_FIELD, selfData.getOpTime());
selfData.getOpTime().appendAsOldBson(builder, MEMBER_OPTIME_DATE_FIELD);
}
if (syncTo != null && !selfData.getState().equals(MemberState.RS_PRIMARY)) {
builder.append(MEMBER_SYNCING_TO_FIELD, syncTo);
}
if (selfData.getMaintenanceModeCalls() != 0) {
builder.append(MEMBER_MAINTENANCE_MODE_FIELD, selfData.getMaintenanceModeCalls());
}
if (selfData.getHeartbeatMessage() != null && !selfData.getHeartbeatMessage().isEmpty()) {
builder.append(MEMBER_INFO_MESSAGE_FIELD, selfData.getHeartbeatMessage());
}
if (selfData.getState().equals(MemberState.RS_PRIMARY)) {
builder.append(MEMBER_ELECTION_TIME_FIELD, selfData.getElectionTime());
selfData.getElectionTime().appendAsOldBson(builder, MEMBER_ELECTION_DATE_FIELD);
}
builder.appendNumber(MEMBER_CONFIG_VERSION_FIELD, replConfig.getConfigVersion());
builder.append(MEMBER_SELF_FIELD, true);
return builder.build();
}
private BsonDocument marshallOtherMember(MemberConfig config, MemberHeartbeatData data) {
BsonDocumentBuilder builder = new BsonDocumentBuilder();
builder.append(MEMBER_ID_FIELD, config.getId())
.append(MEMBER_NAME_FIELD, config.getHostAndPort());
MemberHeartbeatData.Health h = data.getHealth();
builder.append(MEMBER_HEALTH_FIELD, h.getId())
.append(MEMBER_STATE_FIELD, data.getState().getId());
if (h.equals(MemberHeartbeatData.Health.UNREACHABLE)) {
builder.append(MEMBER_STATE_STR_FIELD, "(not reachable/healthy)");
} else {
builder.append(MEMBER_STATE_STR_FIELD, data.getState().name());
}
Instant upSince = data.getUpSince();
if (upSince == null || upSince.equals(Instant.EPOCH)) {
builder.append(MEMBER_UPTIME_FIELD, 0);
} else {
int uptime = (int) (now.getEpochSecond() - upSince.getEpochSecond());
builder.append(MEMBER_UPTIME_FIELD, uptime);
}
if (!config.isArbiter()) {
OpTime opTime = data.getOpTime();
builder.append(MEMBER_OPTIME_FIELD, opTime);
builder.appendUnsafe(MEMBER_OPTIME_DATE_FIELD.getFieldName(), opTime.toOldBson());
}
builder.append(MEMBER_LAST_HEARTBEAT, data.getLastHeartbeat());
builder.append(MEMBER_LAST_HEARTBEAT_RECIVED, data.getLastHeartbeatRecv());
Integer ping = pings.get(config);
if (ping != null) {
builder.append(MEMBER_PING_MS, ping);
String hbmsg = data.getLastHeartbeatMessage();
builder.append(MEMBER_LAST_HEARTBEAT_MESSAGE, hbmsg);
}
if (data.isAuthIssue()) {
builder.append(MEMBER_AUTHENTICATED, true);
}
if (!data.getState().equals(MemberState.RS_PRIMARY)) {
if (data.getSyncSource() != null) {
builder.append(MEMBER_SYNCING_TO_FIELD, data.getSyncSource());
}
} else {
//is primary
builder.append(MEMBER_ELECTION_TIME_FIELD, data.getElectionTime());
//TODO(gortiz): Check if that is consistent with MongoDB source code
builder.appendInstant(MEMBER_ELECTION_DATE_FIELD, data.getElectionTime()
.getSecondsSinceEpoch());
}
builder.appendNumber(MEMBER_CONFIG_VERSION_FIELD, data.getConfigVersion());
return builder.build();
}
public static final class SelfData {
private final int id;
private final HostAndPort name;
private final MemberState state;
@Nonnull
private final Duration uptime;
@Nonnull
private final OpTime opTime;
private final int maintenanceModeCalls;
@Nullable
private final String heartbeatMessage;
private final OpTime electionTime;
public SelfData(int id, HostAndPort name, MemberState state, Duration uptime, OpTime opTime,
int maintenanceModeCalls, String heartbeatMessage, OpTime electionTime) {
this.id = id;
this.name = name;
this.state = state;
this.uptime = uptime;
this.opTime = opTime;
this.maintenanceModeCalls = maintenanceModeCalls;
this.heartbeatMessage = heartbeatMessage;
this.electionTime = electionTime;
}
public int getId() {
return id;
}
public HostAndPort getName() {
return name;
}
public MemberState getState() {
return state;
}
public Duration getUptime() {
return uptime;
}
public OpTime getOpTime() {
return opTime;
}
public int getMaintenanceModeCalls() {
return maintenanceModeCalls;
}
public String getHeartbeatMessage() {
return heartbeatMessage;
}
public OpTime getElectionTime() {
return electionTime;
}
}
}
}