org.graylog2.indexer.MongoIndexSet Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* 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
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* .
*/
package org.graylog2.indexer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.inject.assistedinject.Assisted;
import jakarta.inject.Inject;
import org.graylog2.audit.AuditActor;
import org.graylog2.audit.AuditEventSender;
import org.graylog2.indexer.indexset.IndexSetConfig;
import org.graylog2.indexer.indices.HealthStatus;
import org.graylog2.indexer.indices.Indices;
import org.graylog2.indexer.indices.TooManyAliasesException;
import org.graylog2.indexer.indices.jobs.SetIndexReadOnlyAndCalculateRangeJob;
import org.graylog2.indexer.ranges.IndexRange;
import org.graylog2.indexer.ranges.IndexRangeService;
import org.graylog2.plugin.system.NodeId;
import org.graylog2.shared.system.activities.Activity;
import org.graylog2.shared.system.activities.ActivityWriter;
import org.graylog2.system.jobs.SystemJob;
import org.graylog2.system.jobs.SystemJobConcurrencyException;
import org.graylog2.system.jobs.SystemJobManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Objects.requireNonNull;
import static org.graylog2.audit.AuditEventTypes.ES_WRITE_INDEX_UPDATE;
import static org.graylog2.indexer.indices.Indices.checkIfHealthy;
public class MongoIndexSet implements IndexSet {
public static final String SEPARATOR = "_";
public static final String DEFLECTOR_SUFFIX = "deflector";
// TODO: Hardcoded archive suffix. See: https://github.com/Graylog2/graylog2-server/issues/2058
// TODO 3.0: Remove this in 3.0, only used for pre 2.2 backwards compatibility.
public static final String RESTORED_ARCHIVE_SUFFIX = "_restored_archive";
public static final String WARM_INDEX_INFIX = "warm_";
private static final Logger LOG = LoggerFactory.getLogger(MongoIndexSet.class);
private final IndexSetConfig config;
private final String writeIndexAlias;
private final Indices indices;
private final Pattern indexPattern;
private final Pattern deflectorIndexPattern;
private final String indexWildcard;
private final IndexRangeService indexRangeService;
private final AuditEventSender auditEventSender;
private final NodeId nodeId;
private final SystemJobManager systemJobManager;
private final SetIndexReadOnlyAndCalculateRangeJob.Factory jobFactory;
private final ActivityWriter activityWriter;
@Inject
public MongoIndexSet(@Assisted final IndexSetConfig config,
final Indices indices,
final NodeId nodeId,
final IndexRangeService indexRangeService,
final AuditEventSender auditEventSender,
final SystemJobManager systemJobManager,
final SetIndexReadOnlyAndCalculateRangeJob.Factory jobFactory,
final ActivityWriter activityWriter
) {
this.config = requireNonNull(config);
this.writeIndexAlias = config.indexPrefix() + SEPARATOR + DEFLECTOR_SUFFIX;
this.indices = requireNonNull(indices);
this.nodeId = requireNonNull(nodeId);
this.indexRangeService = requireNonNull(indexRangeService);
this.auditEventSender = requireNonNull(auditEventSender);
this.systemJobManager = requireNonNull(systemJobManager);
this.jobFactory = requireNonNull(jobFactory);
this.activityWriter = requireNonNull(activityWriter);
// Part of the pattern can be configured in IndexSetConfig. If set we use the indexMatchPattern from the config.
final String indexPattern = isNullOrEmpty(config.indexMatchPattern())
? Pattern.quote(config.indexPrefix())
: config.indexMatchPattern();
this.indexPattern = Pattern.compile("^" + indexPattern + SEPARATOR + "(?:" + WARM_INDEX_INFIX + ")?" + "\\d+(?:" + RESTORED_ARCHIVE_SUFFIX + ")?");
this.deflectorIndexPattern = Pattern.compile("^" + indexPattern + SEPARATOR + "(?:" + WARM_INDEX_INFIX + ")?" + "\\d+");
// The index wildcard can be configured in IndexSetConfig. If not set we use a default one based on the index
// prefix.
if (isNullOrEmpty(config.indexWildcard())) {
this.indexWildcard = config.indexPrefix() + SEPARATOR + "*";
} else {
this.indexWildcard = config.indexWildcard();
}
}
@Override
public String[] getManagedIndices() {
final Set indexNames = indices.getIndexNamesAndAliases(getIndexWildcard()).keySet();
// also allow restore archives to be returned
final List result = indexNames.stream()
.filter(this::isManagedIndex)
.collect(Collectors.toList());
return result.toArray(new String[result.size()]);
}
@Override
public String getWriteIndexAlias() {
return writeIndexAlias;
}
@Override
public String getIndexWildcard() {
return indexWildcard;
}
@Override
public String getNewestIndex() throws NoTargetIndexException {
return buildIndexName(getNewestIndexNumber());
}
@VisibleForTesting
int getNewestIndexNumber() throws NoTargetIndexException {
final Set indexNames = indices.getIndexNamesAndAliases(getIndexWildcard()).keySet();
if (indexNames.isEmpty()) {
throw new NoTargetIndexException("Couldn't find any indices for wildcard " + getIndexWildcard());
}
int highestIndexNumber = -1;
for (String indexName : indexNames) {
if (!isGraylogDeflectorIndex(indexName)) {
continue;
}
final int currentHighest = highestIndexNumber;
highestIndexNumber = extractIndexNumber(indexName)
.map(indexNumber -> Math.max(indexNumber, currentHighest))
.orElse(highestIndexNumber);
}
if (highestIndexNumber == -1) {
throw new NoTargetIndexException("Couldn't get newest index number for indices " + indexNames);
}
return highestIndexNumber;
}
@Override
public Optional extractIndexNumber(final String indexName) {
final int beginIndex = indexPrefixLength(indexName);
if (indexName.length() < beginIndex) {
return Optional.empty();
}
final String suffix = indexName.substring(beginIndex);
try {
return Optional.of(Integer.parseInt(suffix));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
public static boolean indexHasWarmInfix(String indexName) {
return indexName.contains("_" + WARM_INDEX_INFIX);
}
private int indexPrefixLength(String indexName) {
int length = config.indexPrefix().length() + 1;
if (indexHasWarmInfix(indexName)) {
length += WARM_INDEX_INFIX.length();
}
return length;
}
@VisibleForTesting
String buildIndexName(final int number) {
return config.indexPrefix() + SEPARATOR + number;
}
@VisibleForTesting
boolean isGraylogDeflectorIndex(final String indexName) {
return !isNullOrEmpty(indexName) && !isWriteIndexAlias(indexName) && deflectorIndexPattern.matcher(indexName).matches();
}
@Override
@Nullable
public String getActiveWriteIndex() throws TooManyAliasesException {
return indices.aliasTarget(getWriteIndexAlias()).orElse(null);
}
@Override
public Map> getAllIndexAliases() {
final Map> indexNamesAndAliases = indices.getIndexNamesAndAliases(getIndexWildcard());
// filter out the restored archives from the result set
return indexNamesAndAliases.entrySet().stream()
.filter(e -> isGraylogDeflectorIndex(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@Override
public String getIndexPrefix() {
return config.indexPrefix();
}
@Override
public boolean isUp() {
return indices.aliasExists(getWriteIndexAlias());
}
@Override
public boolean isWriteIndexAlias(String index) {
return getWriteIndexAlias().equals(index);
}
@Override
public boolean isManagedIndex(String index) {
return !isNullOrEmpty(index) && !isWriteIndexAlias(index) && indexPattern.matcher(index).matches();
}
@Override
public void setUp() {
if (!getConfig().isWritable()) {
LOG.debug("Not setting up non-writable index set <{}> ({})", getConfig().id(), getConfig().title());
return;
}
// Check if there already is an deflector index pointing somewhere.
if (isUp()) {
LOG.info("Found deflector alias <{}>. Using it.", getWriteIndexAlias());
} else {
LOG.info("Did not find a deflector alias. Setting one up now.");
// Do we have a target index to point to?
try {
final String currentTarget = getNewestIndex();
LOG.info("Pointing to already existing index target <{}>", currentTarget);
pointTo(currentTarget);
} catch (NoTargetIndexException ex) {
final String msg = "There is no index target to point to. Creating one now.";
LOG.info(msg);
activityWriter.write(new Activity(msg, IndexSet.class));
cycle(); // No index, so automatically cycling to a new one.
}
}
}
@Override
public void cycle() {
if (!getConfig().isWritable()) {
LOG.debug("Not cycling non-writable index set <{}> ({})", getConfig().id(), getConfig().title());
return;
}
int oldTargetNumber;
try {
oldTargetNumber = getNewestIndexNumber();
} catch (NoTargetIndexException ex) {
oldTargetNumber = -1;
}
final int newTargetNumber = oldTargetNumber + 1;
final String newTarget = buildIndexName(newTargetNumber);
final String oldTarget = buildIndexName(oldTargetNumber);
if (oldTargetNumber == -1) {
LOG.info("Cycling from to <{}>.", newTarget);
} else {
LOG.info("Cycling from <{}> to <{}>.", oldTarget, newTarget);
}
// Create new index.
LOG.info("Creating target index <{}>.", newTarget);
if (!indices.create(newTarget, this)) {
throw new RuntimeException("Could not create new target index <" + newTarget + ">.");
}
LOG.info("Waiting for allocation of index <{}>.", newTarget);
final HealthStatus healthStatus = indices.waitForRecovery(newTarget);
checkIfHealthy(healthStatus, (status) -> new RuntimeException("New target index did not become healthy (target index: <" + newTarget + ">)"));
LOG.debug("Health status of index <{}>: {}", newTarget, healthStatus);
addDeflectorIndexRange(newTarget);
LOG.info("Index <{}> has been successfully allocated.", newTarget);
// Point deflector to new index.
final String indexAlias = getWriteIndexAlias();
LOG.info("Pointing index alias <{}> to new index <{}>.", indexAlias, newTarget);
final Activity activity = new Activity(IndexSet.class);
if (oldTargetNumber == -1) {
// Only pointing, not cycling.
pointTo(newTarget);
activity.setMessage("Cycled index alias <" + indexAlias + "> from to <" + newTarget + ">.");
} else {
// Re-pointing from existing old index to the new one.
LOG.debug("Switching over index alias <{}>.", indexAlias);
pointTo(newTarget, oldTarget);
setIndexReadOnlyAndCalculateRange(oldTarget);
activity.setMessage("Cycled index alias <" + indexAlias + "> from <" + oldTarget + "> to <" + newTarget + ">.");
}
LOG.info("Successfully pointed index alias <{}> to index <{}>.", indexAlias, newTarget);
activityWriter.write(activity);
auditEventSender.success(AuditActor.system(nodeId), ES_WRITE_INDEX_UPDATE, ImmutableMap.of("indexName", newTarget));
}
private void setIndexReadOnlyAndCalculateRange(String indexName) {
// perform these steps after a delay, so we don't race with indexing into the alias
// it can happen that an index request still writes to the old deflector target, while we cycled it above.
// setting the index to readOnly would result in ClusterBlockExceptions in the indexing request.
// waiting 30 seconds to perform the background task should completely get rid of these errors.
final SystemJob setIndexReadOnlyAndCalculateRangeJob = jobFactory.create(indexName);
try {
systemJobManager.submitWithDelay(setIndexReadOnlyAndCalculateRangeJob, 30, TimeUnit.SECONDS);
} catch (SystemJobConcurrencyException e) {
LOG.error("Cannot set index <" + indexName + "> to read only and calculate its range. It won't be optimized.", e);
}
}
private void addDeflectorIndexRange(String indexName) {
final IndexRange deflectorRange = indexRangeService.createUnknownRange(indexName);
indexRangeService.save(deflectorRange);
}
@Override
public void cleanupAliases(Set indexNames) {
final SortedSet sortedSet = ImmutableSortedSet
.orderedBy(new IndexNameComparator(this))
.addAll(indexNames)
.build();
indices.removeAliases(getWriteIndexAlias(), sortedSet.headSet(sortedSet.last()));
}
@Override
public void pointTo(String newIndexName, String oldIndexName) {
indices.cycleAlias(getWriteIndexAlias(), newIndexName, oldIndexName);
}
private void pointTo(final String indexName) {
indices.cycleAlias(getWriteIndexAlias(), indexName);
}
@Override
public IndexSetConfig getConfig() {
return config;
}
@Override
public int compareTo(IndexSet o) {
return ComparisonChain.start()
.compare(this.getConfig(), o.getConfig())
.result();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MongoIndexSet that = (MongoIndexSet) o;
return Objects.equals(config, that.config);
}
@Override
public int hashCode() {
return config.hashCode();
}
@Override
public String toString() {
return "MongoIndexSet{" + "config=" + config + '}';
}
public interface Factory {
MongoIndexSet create(IndexSetConfig config);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy