All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.cloud.autoscaling.ScheduledTrigger Maven / Gradle / Ivy

There is a newer version: 9.6.1
Show newest version
/*
 * 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
 */
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 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 getState() {
    return Collections.singletonMap(LAST_RUN_AT, lastRunAt.toEpochMilli());
  }

  @Override
  protected void setState(Map 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);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy