<commit_before>/*
 * Copyright (C) 2014-2016 LinkedIn Corp. All rights reserved.
 *
 * Licensed 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.
 */

package gobblin.aws;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.eventbus.EventBus;
import com.typesafe.config.Config;

import gobblin.annotation.Alpha;
import gobblin.cluster.GobblinClusterConfigurationKeys;
import gobblin.cluster.GobblinHelixJobScheduler;
import gobblin.cluster.JobConfigurationManager;
import gobblin.cluster.event.NewJobConfigArrivalEvent;
import gobblin.configuration.ConfigurationKeys;
import gobblin.util.ExecutorsUtils;
import gobblin.util.SchedulerUtils;

/**
 * A class for managing AWS Gobblin job configurations.
 *
 * <p>
 *   This class reads all the job configuration at startup from S3
 *   and schedules a refresh to poll from S3 for any new job configurations.
 *   The jobs read are scheduled by the {@link GobblinHelixJobScheduler} by posting a
 *   {@link NewJobConfigArrivalEvent} for each job configuration file.
 * </p>
 *
 * @author <NAME>
 */
@Alpha
public class AWSJobConfigurationManager extends JobConfigurationManager {
  private static final Logger LOGGER = LoggerFactory.getLogger(AWSJobConfigurationManager.class);

  private Optional<String> jobConfS3Uri;
  private Map<String, Properties> jobConfFiles;

  private final long refreshIntervalInMinutes;

  private final ScheduledExecutorService fetchJobConfExecutor;

  public AWSJobConfigurationManager(EventBus eventBus, Config config) {
    super(eventBus, config);
    this.jobConfFiles = Maps.newHashMap();
    this.refreshIntervalInMinutes = config.getLong(GobblinAWSConfigurationKeys.JOB_CONF_REFRESH_INTERVAL_IN_MINUTES);

    this.fetchJobConfExecutor = Executors.newSingleThreadScheduledExecutor(
        ExecutorsUtils.newThreadFactory(Optional.of(LOGGER), Optional.of("FetchJobConfExecutor")));
  }

  private void fetchJobConfSettings() {
    this.jobConfDirPath =
        config.hasPath(GobblinClusterConfigurationKeys.JOB_CONF_PATH_KEY) ? Optional
            .of(config.getString(GobblinClusterConfigurationKeys.JOB_CONF_PATH_KEY)) : Optional.<String>absent();
    this.jobConfS3Uri =
        config.hasPath(GobblinAWSConfigurationKeys.JOB_CONF_S3_URI_KEY) ? Optional
            .of(config.getString(GobblinAWSConfigurationKeys.JOB_CONF_S3_URI_KEY)) : Optional.<String>absent();
  }

  @Override
  protected void startUp() throws Exception {
    LOGGER.info("Starting the " + AWSJobConfigurationManager.class.getSimpleName());

    LOGGER.info(
        String.format("Scheduling the job configuration refresh task with an interval of %d minute(s)",
            this.refreshIntervalInMinutes));

    // Schedule the login task
    this.fetchJobConfExecutor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          fetchJobConf();
        } catch (IOException | ConfigurationException e) {
          LOGGER.error("Failed to fetch job configurations", e);
          throw Throwables.propagate(e);
        }
      }
    }, 0, this.refreshIntervalInMinutes, TimeUnit.MINUTES);
  }

  private void fetchJobConf()
      throws IOException, ConfigurationException {
    // Refresh job config pull details from config
    fetchJobConfSettings();

    // TODO: Eventually when config store supports job files as well
    // .. we can replace this logic with config store
    if (this.jobConfS3Uri.isPresent() && this.jobConfDirPath.isPresent()) {

      // Download the zip file
      String zipFile = appendSlash(this.jobConfDirPath.get()) +
          StringUtils.substringAfterLast(this.jobConfS3Uri.get(), File.separator);
      LOGGER.debug("Downloading to zip: " + zipFile + " from uri: " + this.jobConfS3Uri.get());

      FileUtils.copyURLToFile(new URL(this.jobConfS3Uri.get()), new File(zipFile));
      String extractedPullFilesPath = appendSlash(this.jobConfDirPath.get()) + "files";

      // Extract the zip file
      LOGGER.debug("Extracting to directory: " + extractedPullFilesPath + " from zip: " + zipFile);
      unzipArchive(zipFile, new File(extractedPullFilesPath));

      // Load all new job configurations
      // TODO: Current new and updated jobs are handled, we should un-schedule deleted jobs as well
      File jobConfigDir = new File(extractedPullFilesPath);
      if (jobConfigDir.exists()) {
        LOGGER.info("Loading job configurations from " + jobConfigDir);
        Properties properties = new Properties();
        properties.setProperty(ConfigurationKeys.JOB_CONFIG_FILE_DIR_KEY, jobConfigDir.getAbsolutePath());
        List<Properties> jobConfigs = SchedulerUtils.loadJobConfigs(properties);
        LOGGER.info("Loaded " + jobConfigs.size() + " job configuration(s)");
        for (Properties config : jobConfigs) {
          LOGGER.debug("Config value: " + config);

          // If new config or existing config got updated, then post new job config arrival event
          String jobConfigPathIdentifier = config.getProperty(ConfigurationKeys.JOB_CONFIG_FILE_PATH_KEY);
          if (!jobConfFiles.containsKey(jobConfigPathIdentifier)) {
            jobConfFiles.put(jobConfigPathIdentifier, config);

            postNewJobConfigArrival(config.getProperty(ConfigurationKeys.JOB_NAME_KEY), config);
            LOGGER.info("New config arrived for job: " + jobConfigPathIdentifier);
          } else if (!config.equals(jobConfFiles.get(jobConfigPathIdentifier))) {
            jobConfFiles.put(jobConfigPathIdentifier, config);

            postNewJobConfigArrival(config.getProperty(ConfigurationKeys.JOB_NAME_KEY), config);
            LOGGER.info("Config updated for job: " + jobConfigPathIdentifier);
          } else {
            LOGGER.info("Config not changed for job: " + jobConfigPathIdentifier);
          }
        }
      } else {
        LOGGER.warn("Job configuration directory " + jobConfigDir + " not found");
      }
    }
  }

  public void unzipArchive(String file, File outputDir)
      throws IOException {

    try (ZipFile zipFile = new ZipFile(file)) {

      Enumeration<? extends ZipEntry> entries = zipFile.entries();
      while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        File entryDestination = new File(outputDir, entry.getName());

        if (entry.isDirectory()) {
          // If entry is directory, create directory
          entryDestination.mkdirs();
        } else {
          // Create parent dirs if required
          entryDestination.getParentFile().mkdirs();<commit_msg>Added handler for un-archiving failures in AWSJobConfigurationManager<commit_after>/*
 * Copyright (C) 2014-2016 LinkedIn Corp. All rights reserved.
 *
 * Licensed 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.
 */

package gobblin.aws;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.eventbus.EventBus;
import com.typesafe.config.Config;

import gobblin.annotation.Alpha;
import gobblin.cluster.GobblinClusterConfigurationKeys;
import gobblin.cluster.GobblinHelixJobScheduler;
import gobblin.cluster.JobConfigurationManager;
import gobblin.cluster.event.NewJobConfigArrivalEvent;
import gobblin.configuration.ConfigurationKeys;
import gobblin.util.ExecutorsUtils;
import gobblin.util.SchedulerUtils;

/**
 * A class for managing AWS Gobblin job configurations.
 *
 * <p>
 *   This class reads all the job configuration at startup from S3
 *   and schedules a refresh to poll from S3 for any new job configurations.
 *   The jobs read are scheduled by the {@link GobblinHelixJobScheduler} by posting a
 *   {@link NewJobConfigArrivalEvent} for each job configuration file.
 * </p>
 *
 * @author <NAME>
 */
@Alpha
public class AWSJobConfigurationManager extends JobConfigurationManager {
  private static final Logger LOGGER = LoggerFactory.getLogger(AWSJobConfigurationManager.class);

  private Optional<String> jobConfS3Uri;
  private Map<String, Properties> jobConfFiles;

  private final long refreshIntervalInMinutes;

  private final ScheduledExecutorService fetchJobConfExecutor;

  public AWSJobConfigurationManager(EventBus eventBus, Config config) {
    super(eventBus, config);
    this.jobConfFiles = Maps.newHashMap();
    this.refreshIntervalInMinutes = config.getLong(GobblinAWSConfigurationKeys.JOB_CONF_REFRESH_INTERVAL_IN_MINUTES);

    this.fetchJobConfExecutor = Executors.newSingleThreadScheduledExecutor(
        ExecutorsUtils.newThreadFactory(Optional.of(LOGGER), Optional.of("FetchJobConfExecutor")));
  }

  private void fetchJobConfSettings() {
    this.jobConfDirPath =
        config.hasPath(GobblinClusterConfigurationKeys.JOB_CONF_PATH_KEY) ? Optional
            .of(config.getString(GobblinClusterConfigurationKeys.JOB_CONF_PATH_KEY)) : Optional.<String>absent();
    this.jobConfS3Uri =
        config.hasPath(GobblinAWSConfigurationKeys.JOB_CONF_S3_URI_KEY) ? Optional
            .of(config.getString(GobblinAWSConfigurationKeys.JOB_CONF_S3_URI_KEY)) : Optional.<String>absent();
  }

  @Override
  protected void startUp() throws Exception {
    LOGGER.info("Starting the " + AWSJobConfigurationManager.class.getSimpleName());

    LOGGER.info(
        String.format("Scheduling the job configuration refresh task with an interval of %d minute(s)",
            this.refreshIntervalInMinutes));

    // Schedule the login task
    this.fetchJobConfExecutor.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          fetchJobConf();
        } catch (IOException | ConfigurationException e) {
          LOGGER.error("Failed to fetch job configurations", e);
          throw Throwables.propagate(e);
        }
      }
    }, 0, this.refreshIntervalInMinutes, TimeUnit.MINUTES);
  }

  private void fetchJobConf()
      throws IOException, ConfigurationException {
    // Refresh job config pull details from config
    fetchJobConfSettings();

    // TODO: Eventually when config store supports job files as well
    // .. we can replace this logic with config store
    if (this.jobConfS3Uri.isPresent() && this.jobConfDirPath.isPresent()) {

      // Download the zip file
      String zipFile = appendSlash(this.jobConfDirPath.get()) +
          StringUtils.substringAfterLast(this.jobConfS3Uri.get(), File.separator);
      LOGGER.debug("Downloading to zip: " + zipFile + " from uri: " + this.jobConfS3Uri.get());

      FileUtils.copyURLToFile(new URL(this.jobConfS3Uri.get()), new File(zipFile));
      String extractedPullFilesPath = appendSlash(this.jobConfDirPath.get()) + "files";

      // Extract the zip file
      LOGGER.debug("Extracting to directory: " + extractedPullFilesPath + " from zip: " + zipFile);
      unzipArchive(zipFile, new File(extractedPullFilesPath));

      // Load all new job configurations
      // TODO: Current new and updated jobs are handled, we should un-schedule deleted jobs as well
      File jobConfigDir = new File(extractedPullFilesPath);
      if (jobConfigDir.exists()) {
        LOGGER.info("Loading job configurations from " + jobConfigDir);
        Properties properties = new Properties();
        properties.setProperty(ConfigurationKeys.JOB_CONFIG_FILE_DIR_KEY, jobConfigDir.getAbsolutePath());
        List<Properties> jobConfigs = SchedulerUtils.loadJobConfigs(properties);
        LOGGER.info("Loaded " + jobConfigs.size() + " job configuration(s)");
        for (Properties config : jobConfigs) {
          LOGGER.debug("Config value: " + config);

          // If new config or existing config got updated, then post new job config arrival event
          String jobConfigPathIdentifier = config.getProperty(ConfigurationKeys.JOB_CONFIG_FILE_PATH_KEY);
          if (!jobConfFiles.containsKey(jobConfigPathIdentifier)) {
            jobConfFiles.put(jobConfigPathIdentifier, config);

            postNewJobConfigArrival(config.getProperty(ConfigurationKeys.JOB_NAME_KEY), config);
            LOGGER.info("New config arrived for job: " + jobConfigPathIdentifier);
          } else if (!config.equals(jobConfFiles.get(jobConfigPathIdentifier))) {
            jobConfFiles.put(jobConfigPathIdentifier, config);

            postNewJobConfigArrival(config.getProperty(ConfigurationKeys.JOB_NAME_KEY), config);
            LOGGER.info("Config updated for job: " + jobConfigPathIdentifier);
          } else {
            LOGGER.info("Config not changed for job: " + jobConfigPathIdentifier);
          }
        }
      } else {
        LOGGER.warn("Job configuration directory " + jobConfigDir + " not found");
      }
    }
  }

  public void unzipArchive(String file, File outputDir)
      throws IOException {

    try (ZipFile zipFile = new ZipFile(file)) {

      Enumeration<? extends ZipEntry> entries = zipFile.entries();
      while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        File entryDestination = new File(outputDir, entry.getName());

        if (entry.isDirectory()) {
          // If entry is directory, create directory
          if (!entryDestination.mkdirs()) {
            throw new IOException("Could not create directory: " + entryDestination
                + " while un-archiving zip: " + file);
          }
        } else {
          // Create parent dirs if required
          if (!entryDestination.getParentFile().mkdirs()) {
            throw new IOException("Could not create parent directory for: " + entryDestination
                + " while un-archiving zip: " + file);
          }
{'fraction_non_alphanumeric': 0.09318109736758642, 'fraction_numerical': 0.003171582619727244, 'mean_word_length': 3.7660217654171704, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4741294', 'n_tokens_mistral': 4383, 'n_tokens_neox': 4308, 'n_words': 1085}