blob: b48778d8cdc1d66d98c48065252edeb781e7b7f4 [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.autoscaling;
import java.lang.invoke.MethodHandles;
import java.text.ParseException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.cloud.autoscaling.TriggerEventType;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.AutoScalingParams;
import org.apache.solr.common.params.CollectionParams;
import org.apache.solr.common.util.TimeSource;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.util.DateMathParser;
import org.apache.solr.util.TimeZoneUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.params.AutoScalingParams.PREFERRED_OP;
/**
* A trigger which creates {@link TriggerEventType#SCHEDULED} events as per the configured schedule
*
* @deprecated to be removed in Solr 9.0 (see SOLR-14656)
*/
public class ScheduledTrigger extends TriggerBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String DEFAULT_GRACE_DURATION = "+15MINUTES";
private static final String LAST_RUN_AT = "lastRunAt";
static final String ACTUAL_EVENT_TIME = "actualEventTime";
private String everyStr;
private String graceDurationStr;
private String preferredOp;
private TimeZone timeZone;
private Instant lastRunAt;
public ScheduledTrigger(String name) {
super(TriggerEventType.SCHEDULED, name);
TriggerUtils.requiredProperties(requiredProperties, validProperties, "startTime", "every");
TriggerUtils.validProperties(validProperties, "timeZone", "graceDuration", AutoScalingParams.PREFERRED_OP);
}
@Override
public void configure(SolrResourceLoader loader, SolrCloudManager cloudManager, Map<String, Object> properties) throws TriggerValidationException {
super.configure(loader, cloudManager, properties);
String timeZoneStr = (String) properties.get("timeZone");
this.timeZone = TimeZoneUtils.parseTimezone(timeZoneStr); // defaults to UTC
String startTimeStr = (String) properties.get("startTime");
this.everyStr = (String) properties.get("every");
this.graceDurationStr = (String) properties.getOrDefault("graceDuration", DEFAULT_GRACE_DURATION);
preferredOp = (String) properties.get(PREFERRED_OP);
if (preferredOp != null &&
CollectionParams.CollectionAction.get(preferredOp) == null) {
throw new TriggerValidationException(getName(), PREFERRED_OP, "unrecognized value of: '" + preferredOp + "'");
}
// attempt parsing to validate date math strings
// explicitly set NOW because it may be different for simulated time
Date now = new Date(TimeUnit.NANOSECONDS.toMillis(cloudManager.getTimeSource().getEpochTimeNs()));
Instant startTime = parseStartTime(now, startTimeStr, timeZoneStr);
DateMathParser.parseMath(now, startTime + everyStr, timeZone);
DateMathParser.parseMath(now, startTime + graceDurationStr, timeZone);
// We set lastRunAt to be the startTime (which could be a date math expression such as 'NOW')
// Ordinarily, NOW will always be evaluated in this constructor so it may seem that
// the trigger will always fire the first time.
// However, the lastRunAt is overwritten with the value from ZK
// during restoreState() operation (which is performed before run()) so the trigger works correctly
this.lastRunAt = startTime;
}
private Instant parseStartTime(Date now, String startTimeStr, String timeZoneStr) throws TriggerValidationException {
try {
// try parsing startTime as an ISO-8601 date time string
return DateMathParser.parseMath(now, startTimeStr).toInstant();
} catch (SolrException e) {
if (e.code() != SolrException.ErrorCode.BAD_REQUEST.code) {
throw new TriggerValidationException("startTime", "error parsing value '" + startTimeStr + "': " + e.toString());
}
}
if (timeZoneStr == null) {
throw new TriggerValidationException("timeZone",
"Either 'startTime' should be an ISO-8601 date time string or 'timeZone' must be not be null");
}
TimeZone timeZone = TimeZone.getTimeZone(timeZoneStr);
DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE).appendPattern("['T'[HH[:mm[:ss]]]]")
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.toFormatter(Locale.ROOT).withZone(timeZone.toZoneId());
try {
return Instant.from(dateTimeFormatter.parse(startTimeStr));
} catch (Exception e) {
throw new TriggerValidationException("startTime", "error parsing startTime '" + startTimeStr + "': " + e.toString());
}
}
@Override
protected Map<String, Object> getState() {
return Collections.singletonMap(LAST_RUN_AT, lastRunAt.toEpochMilli());
}
@Override
protected void setState(Map<String, Object> state) {
if (state.containsKey(LAST_RUN_AT)) {
this.lastRunAt = Instant.ofEpochMilli((Long) state.get(LAST_RUN_AT));
}
}
@Override
public void restoreState(AutoScaling.Trigger old) {
assert old.isClosed();
if (old instanceof ScheduledTrigger) {
ScheduledTrigger scheduledTrigger = (ScheduledTrigger) old;
this.lastRunAt = scheduledTrigger.lastRunAt;
} else {
throw new SolrException(SolrException.ErrorCode.INVALID_STATE,
"Unable to restore state from an unknown type of trigger");
}
}
@Override
public void run() {
synchronized (this) {
if (isClosed) {
log.debug("ScheduledTrigger ran but was already closed");
return;
}
}
TimeSource timeSource = cloudManager.getTimeSource();
DateMathParser dateMathParser = new DateMathParser(timeZone);
dateMathParser.setNow(new Date(lastRunAt.toEpochMilli()));
Instant nextRunTime, nextPlusGrace;
try {
Date next = dateMathParser.parseMath(everyStr);
dateMathParser.setNow(next);
nextPlusGrace = dateMathParser.parseMath(graceDurationStr).toInstant();
nextRunTime = next.toInstant();
} catch (ParseException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"Unable to calculate next run time. lastRan: " + lastRunAt.toString() + " and date math string: " + everyStr, e);
}
Instant now = Instant.ofEpochMilli(
TimeUnit.NANOSECONDS.toMillis(timeSource.getEpochTimeNs()));
AutoScaling.TriggerEventProcessor processor = processorRef.get();
if (now.isBefore(nextRunTime)) {
return; // it's not time yet
}
if (now.isAfter(nextPlusGrace)) {
// we are past time and we could not run per schedule so skip this event
if (log.isWarnEnabled()) {
log.warn("ScheduledTrigger was not able to run event at scheduled time: {}. Now: {}",
nextRunTime, now);
}
// Even though we are skipping the event, we need to notify any listeners of the IGNORED stage
// so we create a dummy event with the ignored=true flag and ScheduledTriggers will do the rest
if (processor != null && processor.process(new ScheduledEvent(getEventType(), getName(), timeSource.getTimeNs(),
preferredOp, now.toEpochMilli(), true))) {
lastRunAt = nextRunTime;
return;
}
}
if (processor != null) {
if (log.isDebugEnabled()) {
log.debug("ScheduledTrigger {} firing registered processor for scheduled time {}, now={}", name,
nextRunTime, now);
}
if (processor.process(new ScheduledEvent(getEventType(), getName(), timeSource.getTimeNs(),
preferredOp, now.toEpochMilli()))) {
lastRunAt = nextRunTime; // set to nextRunTime instead of now to avoid drift
}
} else {
lastRunAt = nextRunTime; // set to nextRunTime instead of now to avoid drift
}
}
public static class ScheduledEvent extends TriggerEvent {
public ScheduledEvent(TriggerEventType eventType, String source, long eventTime, String preferredOp, long actualEventTime) {
this(eventType, source, eventTime, preferredOp, actualEventTime, false);
}
public ScheduledEvent(TriggerEventType eventType, String source, long eventTime, String preferredOp, long actualEventTime, boolean ignored) {
super(eventType, source, eventTime, null, ignored);
if (preferredOp != null) {
properties.put(PREFERRED_OP, preferredOp);
}
properties.put(ACTUAL_EVENT_TIME, actualEventTime);
}
}
}