content: /*
   * 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 com.datastax.oss.driver.internal.core.config.typesafe;
  
  import com.datastax.oss.driver.api.core.CqlSession;
  import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
  import com.datastax.oss.driver.api.core.config.DriverConfig;
  import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
  import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
  import com.datastax.oss.driver.api.core.context.DriverContext;
  import com.datastax.oss.driver.api.core.session.SessionBuilder;
  import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent;
  import com.datastax.oss.driver.internal.core.context.EventBus;
  import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
  import com.datastax.oss.driver.internal.core.util.Loggers;
  import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures;
  import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule;
  import com.typesafe.config.Config;
  import com.typesafe.config.ConfigFactory;
  import com.typesafe.config.ConfigParseOptions;
  import edu.umd.cs.findbugs.annotations.NonNull;
  import io.netty.util.concurrent.EventExecutor;
  import io.netty.util.concurrent.ScheduledFuture;
  import java.io.File;
  import java.net.URL;
  import java.time.Duration;
  import java.util.concurrent.CompletableFuture;
  import java.util.concurrent.CompletionStage;
  import java.util.concurrent.RejectedExecutionException;
  import java.util.concurrent.TimeUnit;
  import java.util.function.Supplier;
  import net.jcip.annotations.ThreadSafe;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * The default loader; it is based on Typesafe Config and optionally reloads at a configurable
   * interval.
   */
  @ThreadSafe
  public class DefaultDriverConfigLoader implements DriverConfigLoader {
  
    private static final Logger LOG = LoggerFactory.getLogger(DefaultDriverConfigLoader.class);
  
    public static final String DEFAULT_ROOT_PATH = "datastax-java-driver";
  
    public static final Supplier<Config> DEFAULT_CONFIG_SUPPLIER =
        () -> {
          ConfigFactory.invalidateCaches();
          // The thread's context class loader will be used for application classpath resources,
          // while the driver class loader will be used for reference classpath resources.
          return ConfigFactory.defaultOverrides()
              .withFallback(ConfigFactory.defaultApplication())
              .withFallback(ConfigFactory.defaultReference(CqlSession.class.getClassLoader()))
              .resolve()
              .getConfig(DEFAULT_ROOT_PATH);
        };
  
    @NonNull
    public static DefaultDriverConfigLoader fromClasspath(
        @NonNull String resourceBaseName, @NonNull ClassLoader appClassLoader) {
      return new DefaultDriverConfigLoader(
          () -> {
            ConfigFactory.invalidateCaches();
            Config config =
                ConfigFactory.defaultOverrides()
                    .withFallback(
                        ConfigFactory.parseResourcesAnySyntax(
                            resourceBaseName,
                            ConfigParseOptions.defaults().setClassLoader(appClassLoader)))
                    .withFallback(ConfigFactory.defaultReference(CqlSession.class.getClassLoader()))
                    .resolve();
            return config.getConfig(DEFAULT_ROOT_PATH);
          });
    }
  
    @NonNull
    public static DriverConfigLoader fromFile(@NonNull File file) {
      return new DefaultDriverConfigLoader(
          () -> {
            ConfigFactory.invalidateCaches();
            Config config =
                ConfigFactory.defaultOverrides()
                    .withFallback(ConfigFactory.parseFileAnySyntax(file))
                    .withFallback(ConfigFactory.defaultReference(CqlSession.class.getClassLoader()))
                    .resolve();
            return config.getConfig(DEFAULT_ROOT_PATH);
          });
    }
  
    @NonNull
    public static DriverConfigLoader fromUrl(@NonNull URL url) {
      return new DefaultDriverConfigLoader(
          () -> {
            ConfigFactory.invalidateCaches();
            Config config =
                ConfigFactory.defaultOverrides()
                    .withFallback(ConfigFactory.parseURL(url))
                    .withFallback(ConfigFactory.defaultReference(CqlSession.class.getClassLoader()))
                    .resolve();
            return config.getConfig(DEFAULT_ROOT_PATH);
          });
    }
  
    @NonNull
    public static DefaultDriverConfigLoader fromString(@NonNull String contents) {
      return new DefaultDriverConfigLoader(
          () -> {
            ConfigFactory.invalidateCaches();
            Config config =
                ConfigFactory.defaultOverrides()
                    .withFallback(ConfigFactory.parseString(contents))
                    .withFallback(ConfigFactory.defaultReference(CqlSession.class.getClassLoader()))
                    .resolve();
            return config.getConfig(DEFAULT_ROOT_PATH);
          },
          false);
    }
  
    private final Supplier<Config> configSupplier;
    private final TypesafeDriverConfig driverConfig;
    private final boolean supportsReloading;
  
    private volatile SingleThreaded singleThreaded;
  
    /**
     * Builds a new instance with the default Typesafe config loading rules (documented in {@link
     * SessionBuilder#withConfigLoader(DriverConfigLoader)}) and the core driver options. This
     * constructor enables config reloading (that is, {@link #supportsReloading} will return true).
     *
     * <p>Application-specific classpath resources will be located using the {@linkplain
     * Thread#getContextClassLoader() the current thread's context class loader}. This might not be
     * suitable for OSGi deployments, which should use {@link #DefaultDriverConfigLoader(ClassLoader)}
     * instead.
     */
    public DefaultDriverConfigLoader() {
      this(DEFAULT_CONFIG_SUPPLIER);
    }
  
    /**
     * Builds a new instance with the default Typesafe config loading rules (documented in {@link
     * SessionBuilder#withConfigLoader(DriverConfigLoader)}) and the core driver options, except that
     * application-specific classpath resources will be located using the provided {@link ClassLoader}
     * instead of {@linkplain Thread#getContextClassLoader() the current thread's context class
     * loader}. This constructor enables config reloading (that is, {@link #supportsReloading} will
     * return true).
     */
    public DefaultDriverConfigLoader(@NonNull ClassLoader appClassLoader) {
      this(
          () -> {
            ConfigFactory.invalidateCaches();
            return ConfigFactory.defaultOverrides()
                .withFallback(ConfigFactory.defaultApplication(appClassLoader))
                .withFallback(ConfigFactory.defaultReference(CqlSession.class.getClassLoader()))
                .resolve()
                .getConfig(DEFAULT_ROOT_PATH);
          });
    }
  
    /**
     * Builds an instance with custom arguments, if you want to load the configuration from somewhere
     * else. This constructor enables config reloading (that is, {@link #supportsReloading} will
     * return true).
     *
     * @param configSupplier A supplier for the Typesafe {@link Config}; it will be invoked once when
     *     this object is instantiated, and at each reload attempt, if reloading is enabled.
     */
    public DefaultDriverConfigLoader(@NonNull Supplier<Config> configSupplier) {
      this(configSupplier, true);
    }
  
    /**
     * Builds an instance with custom arguments, if you want to load the configuration from somewhere
     * else and/or modify config reload behavior.
     *
     * @param configSupplier A supplier for the Typesafe {@link Config}; it will be invoked once when
     *     this object is instantiated, and at each reload attempt, if reloading is enabled.
     * @param supportsReloading Whether config reloading should be enabled or not.
     */
    public DefaultDriverConfigLoader(
        @NonNull Supplier<Config> configSupplier, boolean supportsReloading) {
      this.configSupplier = configSupplier;
      this.driverConfig = new TypesafeDriverConfig(configSupplier.get());
      this.supportsReloading = supportsReloading;
    }
  
    @NonNull
    @Override
    public DriverConfig getInitialConfig() {
      return driverConfig;
    }
  
    @Override
    public void onDriverInit(@NonNull DriverContext driverContext) {
      this.singleThreaded = new SingleThreaded((InternalDriverContext) driverContext);
    }
  
    @NonNull
    @Override
    public final CompletionStage<Boolean> reload() {
      if (supportsReloading) {
        CompletableFuture<Boolean> result = new CompletableFuture<>();
        RunOrSchedule.on(singleThreaded.adminExecutor, () -> singleThreaded.reload(result));
        return result;
      } else {
        return CompletableFutures.failedFuture(
            new UnsupportedOperationException(
                "This instance of DefaultDriverConfigLoader does not support reloading"));
      }
    }
  
    @Override
    public final boolean supportsReloading() {
      return supportsReloading;
    }
  
    /** For internal use only, this leaks a Typesafe config type. */
    @NonNull
    public Supplier<Config> getConfigSupplier() {
      return configSupplier;
    }
  
    @Override
    public void close() {
      SingleThreaded singleThreaded = this.singleThreaded;
      if (singleThreaded != null && !singleThreaded.adminExecutor.terminationFuture().isDone()) {
        try {
          RunOrSchedule.on(singleThreaded.adminExecutor, singleThreaded::close);
        } catch (RejectedExecutionException e) {
          // Checking the future is racy, there is still a tiny window that could get us here.
          // We can safely ignore this error because, if the execution is rejected, the periodic
          // reload task, if any, has been already cancelled.
        }
      }
    }
  
    /**
     * Constructs a builder that may be used to provide additional configuration beyond those defined
     * in your configuration files programmatically. For example:
     *
     * <pre>{@code
     * CqlSession session = CqlSession.builder()
     *   .withConfigLoader(DefaultDriverConfigLoader.builder()
     *     .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofMillis(500))
     *     .build())
     *   .build();
     * }</pre>
     *
     * <p>In the general case, use of this is not recommended, but it may be useful in situations
     * where configuration must be defined at runtime or is derived from some other configuration
     * source.
     *
     * @deprecated this feature is now available in the public API. Use {@link
     *     DriverConfigLoader#programmaticBuilder()} instead.
     */
    @Deprecated
    @NonNull
    public static DefaultDriverConfigLoaderBuilder builder() {
      return new DefaultDriverConfigLoaderBuilder();
    }
  
    private class SingleThreaded {
      private final String logPrefix;
      private final EventExecutor adminExecutor;
      private final EventBus eventBus;
      private final DriverExecutionProfile config;
  
      private Duration reloadInterval;
      private ScheduledFuture<?> periodicTaskHandle;
      private boolean closeWasCalled;
  
      private SingleThreaded(InternalDriverContext context) {
        this.logPrefix = context.getSessionName();
        this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next();
        this.eventBus = context.getEventBus();
        this.config = context.getConfig().getDefaultProfile();
        this.reloadInterval =
            context
                .getConfig()
                .getDefaultProfile()
                .getDuration(DefaultDriverOption.CONFIG_RELOAD_INTERVAL);
  
        RunOrSchedule.on(adminExecutor, this::schedulePeriodicReload);
      }
  
      private void schedulePeriodicReload() {
        assert adminExecutor.inEventLoop();
        // Cancel any previously running task
        if (periodicTaskHandle != null) {
          periodicTaskHandle.cancel(false);
        }
        if (reloadInterval.isZero()) {
          LOG.debug("[{}] Reload interval is 0, disabling periodic reloading", logPrefix);
        } else {
          LOG.debug("[{}] Scheduling periodic reloading with interval {}", logPrefix, reloadInterval);
          periodicTaskHandle =
              adminExecutor.scheduleAtFixedRate(
                  this::reloadInBackground,
                  reloadInterval.toNanos(),
                  reloadInterval.toNanos(),
                  TimeUnit.NANOSECONDS);
        }
      }
  
      /**
       * @param reloadedFuture a future to complete when the reload is complete (might be null if the
       *     caller is not interested in being notified)
       */
      private void reload(CompletableFuture<Boolean> reloadedFuture) {
        assert adminExecutor.inEventLoop();
        if (closeWasCalled) {
          if (reloadedFuture != null) {
            reloadedFuture.completeExceptionally(new IllegalStateException("session is closing"));
          }
          return;
        }
        try {
          boolean changed = driverConfig.reload(configSupplier.get());
          if (changed) {
            LOG.info("[{}] Detected a configuration change", logPrefix);
            eventBus.fire(ConfigChangeEvent.INSTANCE);
            Duration newReloadInterval =
                config.getDuration(DefaultDriverOption.CONFIG_RELOAD_INTERVAL);
            if (!newReloadInterval.equals(reloadInterval)) {
              reloadInterval = newReloadInterval;
              schedulePeriodicReload();
            }
          } else {
            LOG.debug("[{}] Reloaded configuration but it hasn't changed", logPrefix);
          }
          if (reloadedFuture != null) {
            reloadedFuture.complete(changed);
          }
        } catch (Error | RuntimeException e) {
          if (reloadedFuture != null) {
            reloadedFuture.completeExceptionally(e);
          } else {
            Loggers.warnWithException(
                LOG, "[{}] Unexpected exception during scheduled reload", logPrefix, e);
          }
        }
      }
  
      private void reloadInBackground() {
        reload(null);
      }
  
      private void close() {
        assert adminExecutor.inEventLoop();
        if (closeWasCalled) {
          return;
        }
        closeWasCalled = true;
        if (periodicTaskHandle != null) {
          periodicTaskHandle.cancel(false);
        }
      }
    }
  }
  