com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader Maven / Gradle / Ivy
/*
* 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 com.datastax.oss.driver.shaded.netty.util.concurrent.EventExecutor;
import com.datastax.oss.driver.shaded.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 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 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).
*
* 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 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 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 reload() {
if (supportsReloading) {
CompletableFuture 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 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:
*
* {@code
* CqlSession session = CqlSession.builder()
* .withConfigLoader(DefaultDriverConfigLoader.builder()
* .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofMillis(500))
* .build())
* .build();
* }
*
* 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 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);
}
}
}
}