blob: afe1dd7923a6fbfa2181c6a93a713f37acfae267 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.solr.cloud.api.collections;
import java.lang.invoke.MethodHandles;
import java.text.ParseException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.google.common.base.MoreObjects;
import org.apache.solr.client.solrj.RoutedAliasTypes;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.RequiredSolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.update.AddUpdateCommand;
import org.apache.solr.update.processor.RoutedAliasUpdateProcessor;
import org.apache.solr.util.DateMathParser;
import org.apache.solr.util.TimeZoneUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.solr.cloud.api.collections.RoutedAlias.CreationType.ASYNC_PREEMPTIVE;
import static org.apache.solr.cloud.api.collections.RoutedAlias.CreationType.NONE;
import static org.apache.solr.cloud.api.collections.RoutedAlias.CreationType.SYNCHRONOUS;
import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
import static org.apache.solr.common.params.CollectionAdminParams.ROUTER_PREFIX;
import static org.apache.solr.common.params.CommonParams.TZ;
/**
* Holds configuration for a routed alias, and some common code and constants.
*
* @see CreateAliasCmd
* @see MaintainRoutedAliasCmd
* @see RoutedAliasUpdateProcessor
*/
public class TimeRoutedAlias extends RoutedAlias {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final RoutedAliasTypes TYPE = RoutedAliasTypes.TIME;
// These two fields may be updated within the calling thread during processing but should
// never be updated by any async creation thread.
private List<Map.Entry<Instant, String>> parsedCollectionsDesc; // k=timestamp (start), v=collection. Sorted descending
private Aliases parsedCollectionsAliases; // a cached reference to the source of what we parse into parsedCollectionsDesc
// These are parameter names to routed alias creation, AND are stored as metadata with the alias.
@SuppressWarnings("WeakerAccess")
public static final String ROUTER_START = ROUTER_PREFIX + "start";
@SuppressWarnings("WeakerAccess")
public static final String ROUTER_INTERVAL = ROUTER_PREFIX + "interval";
@SuppressWarnings("WeakerAccess")
public static final String ROUTER_MAX_FUTURE = ROUTER_PREFIX + "maxFutureMs";
public static final String ROUTER_AUTO_DELETE_AGE = ROUTER_PREFIX + "autoDeleteAge";
public static final String ROUTER_PREEMPTIVE_CREATE_MATH = ROUTER_PREFIX + "preemptiveCreateMath";
// plus TZ and NAME
/**
* Parameters required for creating a routed alias
*/
@SuppressWarnings("WeakerAccess")
public static final Set<String> REQUIRED_ROUTER_PARAMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
CommonParams.NAME, ROUTER_TYPE_NAME, ROUTER_FIELD, ROUTER_START, ROUTER_INTERVAL)));
/**
* Optional parameters for creating a routed alias excluding parameters for collection creation.
*/
//TODO lets find a way to remove this as it's harder to maintain than required list
@SuppressWarnings("WeakerAccess")
public static final Set<String> OPTIONAL_ROUTER_PARAMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
ROUTER_MAX_FUTURE, ROUTER_AUTO_DELETE_AGE, ROUTER_PREEMPTIVE_CREATE_MATH, TZ))); // kinda special
// This format must be compatible with collection name limitations
static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE).appendPattern("[_HH[_mm[_ss]]]") //brackets mean optional
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.toFormatter(Locale.ROOT).withZone(ZoneOffset.UTC); // deliberate -- collection names disregard TZ
//
// Instance data and methods
//
private final String aliasName;
private final Map<String, String> aliasMetadata;
private final String routeField;
private final String intervalMath; // ex: +1DAY
private final long maxFutureMs;
private final String preemptiveCreateMath;
private final String autoDeleteAgeMath; // ex: /DAY-30DAYS *optional*
private final TimeZone timeZone;
private String start;
TimeRoutedAlias(String aliasName, Map<String, String> aliasMetadata) throws SolrException {
// Validate we got everything we need
if (!aliasMetadata.keySet().containsAll(TimeRoutedAlias.REQUIRED_ROUTER_PARAMS)) {
throw new SolrException(BAD_REQUEST, "A time routed alias requires these params: " + TimeRoutedAlias.REQUIRED_ROUTER_PARAMS
+ " plus some create-collection prefixed ones.");
}
this.aliasMetadata = aliasMetadata;
this.start = this.aliasMetadata.get(ROUTER_START);
this.aliasName = aliasName;
final MapSolrParams params = new MapSolrParams(this.aliasMetadata); // for convenience
final RequiredSolrParams required = params.required();
String type = required.get(ROUTER_TYPE_NAME).toLowerCase(Locale.ROOT);
if (!"time".equals(type)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Only 'time' routed aliases is supported by TimeRoutedAlias, found:" + type);
}
routeField = required.get(ROUTER_FIELD);
intervalMath = required.get(ROUTER_INTERVAL);
//optional:
maxFutureMs = params.getLong(ROUTER_MAX_FUTURE, TimeUnit.MINUTES.toMillis(10));
// the date math configured is an interval to be subtracted from the most recent collection's time stamp
String pcmTmp = params.get(ROUTER_PREEMPTIVE_CREATE_MATH);
preemptiveCreateMath = pcmTmp != null ? (pcmTmp.startsWith("-") ? pcmTmp : "-" + pcmTmp) : null;
autoDeleteAgeMath = params.get(ROUTER_AUTO_DELETE_AGE); // no default
timeZone = TimeZoneUtils.parseTimezone(this.aliasMetadata.get(CommonParams.TZ));
// More validation:
// check that the date math is valid
final Date now = new Date();
try {
final Date after = new DateMathParser(now, timeZone).parseMath(getIntervalMath());
if (!after.after(now)) {
throw new SolrException(BAD_REQUEST, "duration must add to produce a time in the future");
}
} catch (Exception e) {
throw new SolrException(BAD_REQUEST, "bad " + TimeRoutedAlias.ROUTER_INTERVAL + ", " + e, e);
}
if (autoDeleteAgeMath != null) {
try {
final Date before = new DateMathParser(now, timeZone).parseMath(autoDeleteAgeMath);
if (now.before(before)) {
throw new SolrException(BAD_REQUEST, "duration must round or subtract to produce a time in the past");
}
} catch (Exception e) {
throw new SolrException(BAD_REQUEST, "bad " + TimeRoutedAlias.ROUTER_AUTO_DELETE_AGE + ", " + e, e);
}
}
if (preemptiveCreateMath != null) {
try {
new DateMathParser().parseMath(preemptiveCreateMath);
} catch (ParseException e) {
throw new SolrException(BAD_REQUEST, "Invalid date math for preemptiveCreateMath:" + preemptiveCreateMath);
}
}
if (maxFutureMs < 0) {
throw new SolrException(BAD_REQUEST, ROUTER_MAX_FUTURE + " must be >= 0");
}
}
@Override
public String computeInitialCollectionName() {
return formatCollectionNameFromInstant(aliasName, parseStringAsInstant(this.start, timeZone));
}
@Override
String[] formattedRouteValues(SolrInputDocument doc) {
String routeField = getRouteField();
Date fieldValue = (Date) doc.getFieldValue(routeField);
String dest = calcCandidateCollection(fieldValue.toInstant()).getDestinationCollection();
int nonValuePrefix = getAliasName().length() + getRoutedAliasType().getSeparatorPrefix().length();
return new String[]{dest.substring(nonValuePrefix)};
}
public static Instant parseInstantFromCollectionName(String aliasName, String collection) {
String separatorPrefix = TYPE.getSeparatorPrefix();
final String dateTimePart;
if (collection.contains(separatorPrefix)) {
dateTimePart = collection.substring(collection.lastIndexOf(separatorPrefix) + separatorPrefix.length());
} else {
dateTimePart = collection.substring(aliasName.length() + 1);
}
return DATE_TIME_FORMATTER.parse(dateTimePart, Instant::from);
}
public static String formatCollectionNameFromInstant(String aliasName, Instant timestamp) {
String nextCollName = DATE_TIME_FORMATTER.format(timestamp);
for (int i = 0; i < 3; i++) { // chop off seconds, minutes, hours
if (nextCollName.endsWith("_00")) {
nextCollName = nextCollName.substring(0, nextCollName.length() - 3);
}
}
assert DATE_TIME_FORMATTER.parse(nextCollName, Instant::from).equals(timestamp);
return aliasName + TYPE.getSeparatorPrefix() + nextCollName;
}
private Instant parseStringAsInstant(String str, TimeZone zone) {
Instant start = DateMathParser.parseMath(new Date(), str, zone).toInstant();
checkMillis(start);
return start;
}
private void checkMillis(Instant date) {
if (!date.truncatedTo(ChronoUnit.SECONDS).equals(date)) {
throw new SolrException(BAD_REQUEST,
"Date or date math for start time includes milliseconds, which is not supported. " +
"(Hint: 'NOW' used without rounding always has this problem)");
}
}
@Override
public boolean updateParsedCollectionAliases(ZkStateReader zkStateReader, boolean contextualize) {
final Aliases aliases = zkStateReader.getAliases();
if (this.parsedCollectionsAliases != aliases) {
if (this.parsedCollectionsAliases != null) {
if (log.isDebugEnabled()) {
log.debug("Observing possibly updated alias: {}", getAliasName());
}
}
this.parsedCollectionsDesc = parseCollections(aliases);
this.parsedCollectionsAliases = aliases;
return true;
}
if (contextualize) {
this.parsedCollectionsDesc = parseCollections(aliases);
}
return false;
}
@Override
public String getAliasName() {
return aliasName;
}
@Override
public String getRouteField() {
return routeField;
}
@Override
public RoutedAliasTypes getRoutedAliasType() {
return RoutedAliasTypes.TIME;
}
@SuppressWarnings("WeakerAccess")
public String getIntervalMath() {
return intervalMath;
}
@SuppressWarnings("WeakerAccess")
public long getMaxFutureMs() {
return maxFutureMs;
}
@SuppressWarnings("WeakerAccess")
public String getPreemptiveCreateWindow() {
return preemptiveCreateMath;
}
@SuppressWarnings("WeakerAccess")
public String getAutoDeleteAgeMath() {
return autoDeleteAgeMath;
}
public TimeZone getTimeZone() {
return timeZone;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("aliasName", aliasName)
.add("routeField", routeField)
.add("intervalMath", intervalMath)
.add("maxFutureMs", maxFutureMs)
.add("preemptiveCreateMath", preemptiveCreateMath)
.add("autoDeleteAgeMath", autoDeleteAgeMath)
.add("timeZone", timeZone)
.toString();
}
/**
* Parses the elements of the collection list. Result is returned them in sorted order (most recent 1st)
*/
private List<Map.Entry<Instant, String>> parseCollections(Aliases aliases) {
final List<String> collections = getCollectionList(aliases);
if (collections == null) {
throw RoutedAlias.newAliasMustExistException(getAliasName());
}
// note: I considered TreeMap but didn't like the log(N) just to grab the most recent when we use it later
List<Map.Entry<Instant, String>> result = new ArrayList<>(collections.size());
for (String collection : collections) {
Instant colStartTime = parseInstantFromCollectionName(aliasName, collection);
result.add(new AbstractMap.SimpleImmutableEntry<>(colStartTime, collection));
}
result.sort((e1, e2) -> e2.getKey().compareTo(e1.getKey())); // reverse sort by key
return result;
}
/**
* Computes the timestamp of the next collection given the timestamp of the one before.
*/
private Instant computeNextCollTimestamp(Instant fromTimestamp) {
final Instant nextCollTimestamp =
DateMathParser.parseMath(Date.from(fromTimestamp), "NOW" + intervalMath, timeZone).toInstant();
assert nextCollTimestamp.isAfter(fromTimestamp);
return nextCollTimestamp;
}
@Override
public void validateRouteValue(AddUpdateCommand cmd) throws SolrException {
final Instant docTimestamp =
parseRouteKey(cmd.getSolrInputDocument().getFieldValue(getRouteField()));
// FUTURE: maybe in some cases the user would want to ignore/warn instead?
if (docTimestamp.isAfter(Instant.now().plusMillis(getMaxFutureMs()))) {
throw new SolrException(BAD_REQUEST,
"The document's time routed key of " + docTimestamp + " is too far in the future given " +
ROUTER_MAX_FUTURE + "=" + getMaxFutureMs());
}
// Although this is also checked later, we need to check it here too to handle the case in Dimensional Routed
// aliases where one can legally have zero collections for a newly encountered category and thus the loop later
// can't catch this.
// SOLR-13760 - we need to fix the date math to a specific instant when the first document arrives.
// If we don't do this DRA's with a time dimension have variable start times across the other dimensions
// and logic gets much to complicated, and depends too much on queries to zookeeper. This keeps life simpler.
// I have to admit I'm not terribly fond of the mutation during a validate method however.
Instant startTime;
try {
startTime = Instant.parse(start);
} catch (DateTimeParseException e) {
startTime = DateMathParser.parseMath(new Date(), start).toInstant();
SolrCore core = cmd.getReq().getCore();
ZkStateReader zkStateReader = core.getCoreContainer().getZkController().zkStateReader;
Aliases aliases = zkStateReader.getAliases();
Map<String, String> props = new HashMap<>(aliases.getCollectionAliasProperties(aliasName));
start = DateTimeFormatter.ISO_INSTANT.format(startTime);
props.put(ROUTER_START, start);
// This could race, but it only occurs when the alias is first used and the values produced
// should all be identical and who wins won't matter (baring cases of Date Math involving seconds,
// which is pretty far fetched). Putting this in a separate thread to ensure that any failed
// races don't cause documents to get rejected.
core.runAsync(() -> zkStateReader.aliasesManager.applyModificationAndExportToZk(
(a) -> aliases.cloneWithCollectionAliasProperties(aliasName, props)));
}
if (docTimestamp.isBefore(startTime)) {
throw new SolrException(BAD_REQUEST, "The document couldn't be routed because " + docTimestamp +
" is before the start time for this alias " +start+")");
}
}
@Override
public Map<String, String> getAliasMetadata() {
return aliasMetadata;
}
@Override
public Set<String> getRequiredParams() {
return REQUIRED_ROUTER_PARAMS;
}
@Override
public Set<String> getOptionalParams() {
return OPTIONAL_ROUTER_PARAMS;
}
@Override
protected String getHeadCollectionIfOrdered(AddUpdateCommand cmd) {
return parsedCollectionsDesc.get(0).getValue();
}
private Instant calcPreemptNextColCreateTime(String preemptiveCreateMath, Instant nextCollTimestamp) {
DateMathParser dateMathParser = new DateMathParser();
dateMathParser.setNow(Date.from(nextCollTimestamp));
try {
return dateMathParser.parseMath(preemptiveCreateMath).toInstant();
} catch (ParseException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Invalid Preemptive Create Window Math:'" + preemptiveCreateMath + '\'', e);
}
}
private Instant parseRouteKey(Object routeKey) {
final Instant docTimestamp;
if (routeKey instanceof Instant) {
docTimestamp = (Instant) routeKey;
} else if (routeKey instanceof Date) {
docTimestamp = ((Date) routeKey).toInstant();
} else if (routeKey instanceof CharSequence) {
docTimestamp = Instant.parse((CharSequence) routeKey);
} else {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unexpected type of routeKey: " + routeKey);
}
return docTimestamp;
}
/**
* Given the route key, finds the correct collection and an indication of any collection that needs to be created.
* Future docs will potentially cause creation of a collection that does not yet exist. This method presumes that the
* doc time stamp has already been checked to not exceed maxFutureMs
*
* @throws SolrException if the doc is too old to be stored in the TRA
*/
@Override
public CandidateCollection findCandidateGivenValue(AddUpdateCommand cmd) {
Object value = cmd.getSolrInputDocument().getFieldValue(getRouteField());
ZkStateReader zkStateReader = cmd.getReq().getCore().getCoreContainer().getZkController().zkStateReader;
String printableId = cmd.getPrintableId();
updateParsedCollectionAliases(zkStateReader, true);
final Instant docTimestamp = parseRouteKey(value);
// reparse explicitly such that if we are a dimension in a DRA, the list gets culled by our context
// This does not normally happen with the above updateParsedCollectionAliases, because at that point the aliases
// should be up to date and updateParsedCollectionAliases will short circuit
this.parsedCollectionsDesc = parseCollections(zkStateReader.getAliases());
CandidateCollection next1 = calcCandidateCollection(docTimestamp);
if (next1 != null) return next1;
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Doc " + printableId + " couldn't be routed with " + getRouteField() + "=" + docTimestamp);
}
private CandidateCollection calcCandidateCollection(Instant docTimestamp) {
// Lookup targetCollection given route key. Iterates in reverse chronological order.
// We're O(N) here but N should be small, the loop is fast, and usually looking for 1st.
Instant next = null;
if (this.parsedCollectionsDesc.isEmpty()) {
String firstCol = computeInitialCollectionName();
return new CandidateCollection(SYNCHRONOUS, firstCol);
} else {
Instant mostRecentCol = parsedCollectionsDesc.get(0).getKey();
// despite most logic hinging on the first element, we must loop so we can complain if the doc
// is too old and there's no valid collection.
for (int i = 0; i < parsedCollectionsDesc.size(); i++) {
Map.Entry<Instant, String> entry = parsedCollectionsDesc.get(i);
Instant colStartTime = entry.getKey();
if (i == 0) {
next = computeNextCollTimestamp(colStartTime);
}
if (!docTimestamp.isBefore(colStartTime)) { // (inclusive lower bound)
CandidateCollection candidate;
if (i == 0) {
if (docTimestamp.isBefore(next)) { // (exclusive upper bound)
candidate = new CandidateCollection(NONE, entry.getValue()); //found it
// simply goes to head collection no action required
} else {
// Although we create collections one at a time, this calculation of the ultimate destination is
// useful for contextualizing TRA's used as dimensions in DRA's
String creationCol = calcNextCollection(colStartTime);
Instant colDestTime = colStartTime;
Instant possibleDestTime = colDestTime;
while (!docTimestamp.isBefore(possibleDestTime) || docTimestamp.equals(possibleDestTime)) {
colDestTime = possibleDestTime;
possibleDestTime = computeNextCollTimestamp(colDestTime);
}
String destCol = TimeRoutedAlias.formatCollectionNameFromInstant(getAliasName(),colDestTime);
candidate = new CandidateCollection(SYNCHRONOUS, destCol, creationCol); //found it
}
} else {
// older document simply goes to existing collection, nothing created.
candidate = new CandidateCollection(NONE, entry.getValue()); //found it
}
if (candidate.getCreationType() == NONE && isNotBlank(getPreemptiveCreateWindow()) && !this.preemptiveCreateOnceAlready) {
// are we getting close enough to the (as yet uncreated) next collection to warrant preemptive creation?
Instant time2Create = calcPreemptNextColCreateTime(getPreemptiveCreateWindow(), computeNextCollTimestamp(mostRecentCol));
if (!docTimestamp.isBefore(time2Create)) {
String destinationCollection = candidate.getDestinationCollection(); // dest doesn't change
String creationCollection = calcNextCollection(mostRecentCol);
return new CandidateCollection(ASYNC_PREEMPTIVE, // add next collection
destinationCollection,
creationCollection);
}
}
return candidate;
}
}
}
return null;
}
/**
* Deletes some of the oldest collection(s) based on {@link TimeRoutedAlias#getAutoDeleteAgeMath()}. If
* getAutoDelteAgemath is not present then this method does nothing. Per documentation is relative to a
* collection being created. Therefore if nothing is being created, nothing is deleted.
* @param actions The previously calculated add action(s). This collection should not be modified within
* this method.
*/
private List<Action> calcDeletes(List<Action> actions) {
final String autoDeleteAgeMathStr = this.getAutoDeleteAgeMath();
if (autoDeleteAgeMathStr == null || actions .size() == 0) {
return Collections.emptyList();
}
if (actions.size() > 1) {
throw new IllegalStateException("We are not supposed to be creating more than one collection at a time");
}
String deletionReferenceCollection = actions.get(0).targetCollection;
Instant deletionReferenceInstant = parseInstantFromCollectionName(getAliasName(), deletionReferenceCollection);
final Instant delBefore;
try {
delBefore = new DateMathParser(Date.from(computeNextCollTimestamp(deletionReferenceInstant)), this.getTimeZone()).parseMath(autoDeleteAgeMathStr).toInstant();
} catch (ParseException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e); // note: should not happen by this point
}
List<Action> collectionsToDelete = new ArrayList<>();
//iterating from newest to oldest, find the first collection that has a time <= "before". We keep this collection
// (and all newer to left) but we delete older collections, which are the ones that follow.
int numToKeep = 0;
DateTimeFormatter dtf = null;
if (log.isDebugEnabled()) {
dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.n", Locale.ROOT);
dtf = dtf.withZone(ZoneId.of("UTC"));
}
for (Map.Entry<Instant, String> parsedCollection : parsedCollectionsDesc) {
numToKeep++;
final Instant colInstant = parsedCollection.getKey();
if (colInstant.isBefore(delBefore) || colInstant.equals(delBefore)) {
if (log.isDebugEnabled()) { // don't perform formatting unless debugging
assert dtf != null;
if (log.isDebugEnabled()) {
log.debug("{} is equal to or before {} deletions may be required", dtf.format(colInstant), dtf.format(delBefore));
}
}
break;
} else {
if (log.isDebugEnabled()) { // don't perform formatting unless debugging
assert dtf != null;
if (log.isDebugEnabled()) {
log.debug("{} is not before {} and will be retained", dtf.format(colInstant), dtf.format(delBefore));
}
}
}
}
log.debug("Collections will be deleted... parsed collections={}", parsedCollectionsDesc);
final List<String> targetList = parsedCollectionsDesc.stream().map(Map.Entry::getValue).collect(Collectors.toList());
log.debug("Iterating backwards on collection list to find deletions: {}", targetList);
for (int i = parsedCollectionsDesc.size() - 1; i >= numToKeep; i--) {
String toDelete = targetList.get(i);
log.debug("Adding to TRA delete list:{}", toDelete);
collectionsToDelete.add(new Action(this, ActionType.ENSURE_REMOVED, toDelete));
}
return collectionsToDelete;
}
private List<Action> calcAdd(String targetCol) {
List<String> collectionList = getCollectionList(parsedCollectionsAliases);
if (!collectionList.contains(targetCol) && !collectionList.isEmpty()) {
// Then we need to add the next one... (which may or may not be the same as our target
String mostRecentCol = collectionList.get(0);
String pfx = getRoutedAliasType().getSeparatorPrefix();
int sepLen = mostRecentCol.contains(pfx) ? pfx.length() : 1; // __TRA__ or _
String mostRecentTime = mostRecentCol.substring(getAliasName().length() + sepLen);
Instant parsed = DATE_TIME_FORMATTER.parse(mostRecentTime, Instant::from);
String nextCol = calcNextCollection(parsed);
return Collections.singletonList(new Action(this, ActionType.ENSURE_EXISTS, nextCol));
} else {
return Collections.emptyList();
}
}
private String calcNextCollection(Instant mostRecentCollTimestamp) {
final Instant nextCollTimestamp = computeNextCollTimestamp(mostRecentCollTimestamp);
return TimeRoutedAlias.formatCollectionNameFromInstant(aliasName, nextCollTimestamp);
}
@Override
protected List<Action> calculateActions(String targetCol) {
List<Action> actions = new ArrayList<>();
actions.addAll(calcAdd(targetCol));
actions.addAll(calcDeletes(actions));
return actions;
}
}