Configuration

Quick overview

The driver’s configuration is composed of options, organized in a hierarchical manner. Optionally, it can define profiles that customize a set of options for a particular kind of request.

  • the default implementation is based on the Typesafe Config framework:
    • the driver JAR comes with a reference.conf file that defines the defaults.
    • you can add an application.conf file in the classpath (or an absolute path, or an URL). It only needs to contain the options that you override.
    • hot reloading is supported out of the box.
  • the config mechanism can be completely overridden by implementing a set of driver interfaces (DriverConfig, DriverExecutionProfile and DriverConfigLoader)

Concepts

Options

Essentially, an option is a path in the configuration with an expected type, for example basic.request.timeout, representing a duration.

Execution profiles

Imagine an application that does both transactional and analytical requests. Transactional requests are simpler and must return quickly, so they will typically use a short timeout, let’s say 100 milliseconds; analytical requests are more complex and less frequent so a higher SLA is acceptable, for example 5 seconds. In addition, maybe you want to use a different consistency level.

Instead of manually adjusting the options on every request, you can create execution profiles:

datastax-java-driver {
  profiles {
    oltp {
      basic.request.timeout = 100 milliseconds
      basic.request.consistency = ONE
    }
    olap {
      basic.request.timeout = 5 seconds
      basic.request.consistency = QUORUM
    }
}

Now each request only needs a profile name:

SimpleStatement s =
  SimpleStatement.builder("SELECT name FROM user WHERE id = 1")
      .setExecutionProfileName("oltp")
      .build();
session.execute(s);

The configuration has an anonymous default profile that is always present. It can define an arbitrary number of named profiles. They inherit from the default profile, so you only need to override the options that have a different value.

Default implementation: Typesafe Config

Out of the box, the driver uses Typesafe Config.

It looks at the following locations, according to the standard behavior of that library:

  • system properties
  • application.conf (all resources on the classpath with this name)
  • application.json (all resources on the classpath with this name)
  • application.properties (all resources on the classpath with this name)
  • reference.conf (all resources on the classpath with this name)

The driver ships with a reference.conf that defines sensible defaults for all the options. That file is heavily documented, so refer to it for details about each option. It is included in the core driver JAR, so it is in your application’s classpath. If you need to customize something, add an application.conf to the classpath. There are various ways to do it:

  • place the file in a directory that is on your application or application server’s classpath (example for Apache Tomcat);
  • if you use Maven, place it in the src/main/resources directory.

Since application.conf inherits from reference.conf, you only need to redeclare what you override:

# Sample application.conf: overrides one option and adds a profile
datastax-java-driver {
  advanced.protocol.version = V4
  profiles {
    slow {
      basic.request.timeout = 10 seconds
    }
  }
}

.conf files are in the HOCON format, an improved superset of JSON; refer to the HOCON spec for details.

By default, configuration files are reloaded regularly, and the driver will adjust to the new values (on a “best effort” basis: some options, like protocol version and policy configurations, cannot be changed at runtime and will be ignored). The reload interval is defined in the configuration:

# To disable periodic reloading, set this to 0.
datastax-java-driver.basic.config-reload-interval = 5 minutes

As mentioned previously, system properties can also be used to override individual options. This is great for temporary changes, for example in your development environment:

# Increase heartbeat interval to limit the amount of debug logs:
java -Ddatastax-java-driver.advanced.heartbeat.interval="5 minutes" ...

For array options, provide each element separately by appending an index to the path:

-Ddatastax-java-driver.basic.contact-points.0="127.0.0.1:9042"
-Ddatastax-java-driver.basic.contact-points.1="127.0.0.2:9042"

We recommend reserving system properties for the early phases of the project; in production, having all the configuration in one place will make it easier to manage and review.

As shown so far, all options live under a datastax-java-driver prefix. This can be changed, for example if you need multiple driver instances in the same VM with different configurations. See the Advanced topics section.

Alternate application config locations

If loading application.conf from the classpath doesn’t work for you, other loader implementations are available:

To use any of those loaders, pass it to the session builder:

File file = new File("/path/to/application.conf");
CqlSession session = CqlSession.builder()
    .withConfigLoader(DriverConfigLoader.fromFile(file))
    .build();

Apart from application-specific configuration, they work exactly like the default loader: they fall back to the driver’s built-in reference.conf for defaults, accept overrides via system properties, and reload at the interval specified by the basic.config-reload-interval option.

Programmatic application config

Alternatively, you can use DriverConfigLoader.programmaticBuilder to specify configuration options programmatically instead of loading them from a static resource:

DriverConfigLoader loader =
    DriverConfigLoader.programmaticBuilder()
        .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(5))
        .startProfile("slow")
        .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofSeconds(30))
        .endProfile()
        .build();
CqlSession session = CqlSession.builder().withConfigLoader(loader).build();

This is useful for frameworks and tools that already have their own configuration mechanism.

The configuration API

You don’t need the configuration API for everyday usage of the driver, but it can be useful if:

  • you’re writing custom policies or a custom config implementation;
  • use dynamic profiles (see below);
  • or simply want to read configuration options at runtime.

Basics

The driver’s context exposes a DriverConfig instance:

DriverConfig config = session.getContext().getConfig();
DriverExecutionProfile defaultProfile = config.getDefaultProfile();
DriverExecutionProfile olapProfile = config.getProfile("olap");

config.getProfiles().forEach((name, profile) -> ...);

DriverExecutionProfile has typed option getters:

Duration requestTimeout = defaultProfile.getDuration(DefaultDriverOption.REQUEST_TIMEOUT);
int maxRequestsPerConnection = defaultProfile.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS);

Manual reloading

In addition to periodic reloading, you can trigger a reload programmatically. This returns a CompletionStage that you can use for example to register a callback when the reload is complete:

DriverConfigLoader loader = session.getContext().getConfigLoader();
if (loader.supportsReloading()) {
  CompletionStage<Boolean> reloaded = loader.reload();
  reloaded.whenComplete(
      (configChanged, error) -> {
        if (error != null) {
          // handle error
        } else if (configChanged) {
          // do something after the config change
        }
      });
}

Manual reloading is optional, this can be checked with supportsReloading(); the driver’s built-in loader supports it.

Derived profiles

Execution profiles are hard-coded in the configuration, and can’t be changed at runtime (except by modifying and reloading the files). What if you want to adjust an option for a single request, without having a dedicated profile for it?

To allow this, you start from an existing profile in the configuration and build a derived profile that overrides a subset of options:

DriverExecutionProfile defaultProfile = session.getContext().getConfig().getDefaultProfile();
DriverExecutionProfile dynamicProfile =
  defaultProfile.withString(
      DefaultDriverOption.REQUEST_CONSISTENCY, DefaultConsistencyLevel.EACH_QUORUM.name());
SimpleStatement s =
    SimpleStatement.builder("SELECT name FROM user WHERE id = 1")
        .setExecutionProfile(dynamicProfile)
        .build();
session.execute(s);

A derived profile keeps a reference to its base profile, and reflects the change if the configuration gets reloaded.

Do not overuse derived profiles, as they can have an impact on performance: each withXxx method creates a new copy, and propagating the changes from the base profile also has an overhead. We strongly suggest defining all your profiles ahead of time in the configuration file; at the very least, try to cache derived profiles if you reuse them multiple times.

Advanced topics

Note: all the features described in this section use the driver’s internal API, which is subject to the restrictions explained in API conventions.

Changing the config prefix

As mentioned earlier, all configuration options are looked up under the datastax-java-driver prefix. This might be a problem if you have multiple instances of the driver executing in the same VM, but with different configurations. What you want instead is separate option trees, like this:

# application.conf
session1 {
  basic.session-name = "session1"
  advanced.protocol-version = V4
  // etc.
}
session2 {
  basic.session-name = "session2"
  advanced.protocol-version = V3
  // etc.
}

To achieve that, first write a method that loads the configuration under your prefix, and uses the driver’s reference.conf as a fallback:

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

private static Config loadConfig(String prefix) {
  // Make sure we see the changes when reloading: 
  ConfigFactory.invalidateCaches();

  // Every config file in the classpath, without stripping the prefixes 
  Config root = ConfigFactory.load();

  // The driver's built-in defaults, under the default prefix in reference.conf:
  Config reference = root.getConfig("datastax-java-driver");

  // Everything under your custom prefix in application.conf:
  Config application = root.getConfig(prefix);

  return application.withFallback(reference);
}

Next, create a DriverConfigLoader. This is the component that abstracts the configuration implementation to the rest of the driver. Here we use the built-in class, but tell it to load the Typesafe Config object with the previous method:

import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader;

DriverConfigLoader session1ConfigLoader =
    new DefaultDriverConfigLoader(
        () -> loadConfig("session1"), DefaultDriverOption.values());

Finally, pass the config loader when building the driver:

CqlSession session1 =
    CqlSession.builder()
        .withConfigLoader(session1ConfigLoader)
        .build();

Loading from a different source

If you don’t want to use a config file, you can write custom code to create the Typesafe Config object (refer to the documentation for more details).

Then reuse the examples from the previous section to merge it with the driver’s reference file, and pass it to the driver. Here’s a contrived example that loads the configuration from a string:

String configSource = "protocol.version = V3";
DriverConfigLoader loader =
    new DefaultDriverConfigLoader(
        () -> {
          ConfigFactory.invalidateCaches();
          Config reference = ConfigFactory.load().getConfig("datastax-java-driver");
          Config application = ConfigFactory.parseString(configSource);
          return application.withFallback(reference);
        },
        DefaultDriverOption.values());

CqlSession session = CqlSession.builder().withConfigLoader(loader).build();

Bypassing Typesafe Config

If Typesafe Config doesn’t work for you, it is possible to get rid of it entirely.

You will need to provide your own implementations of DriverConfig and DriverExecutionProfile. Then write a DriverConfigLoader and pass it to the session at initialization, as shown in the previous sections. Study the built-in implementation (package com.datastax.oss.driver.internal.core.config.typesafe) for reference.

Reloading is not mandatory: you can choose not to implement it, and the driver will simply keep using the initial configuration.

Note that the option getters (DriverExecutionProfile.getInt and similar) are invoked very frequently on the hot code path; if your implementation is slow, consider caching the results between reloads.

Configuration change event

If you’re writing your own policies, you might want them to be reactive to configuration changes. You can register a callback to ConfigChangeEvent, which gets emitted any time a manual or periodic reload detects changes since the last reload:

import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent;

InternalDriverContext context = (InternalDriverContext) session.getContext();

Object key =
    eventBus.register(
        ConfigChangeEvent.class, (e) -> {
          System.out.println("The configuration changed");
          // re-read the config option(s) you're interested in, and apply changes if needed
        });

// If your component has a shorter lifecycle than the driver, make sure to unregister when it closes
eventBus.unregister(key, ConfigChangeEvent.class);

For example, the driver uses this mechanism internally to resize connection pools if you change the options in advanced.connection.pool.

The event is emitted by the config loader. If you write a custom loader, study the source of DefaultDriverConfigLoader to reproduce the behavior.

Policies

The preferred way to instantiate policies (load balancing policy, retry policy, etc.) is via the configuration:

datastax-java-driver {
  basic.load-balancing-policy.class = DefaultLoadBalancingPolicy
  advanced.reconnection-policy {
    class = ExponentialReconnectionPolicy
    base-delay = 1 second
    max-delay = 60 seconds
  }
}

When the driver encounters such a declaration, it will load the class and use reflection to invoke a constructor with the following signature:

  • for policies that can be overridden in a profile (load balancing policy, retry policy, speculative execution policy):

    public DefaultLoadBalancingPolicy(DriverContext context, String profileName)
    
  • for session-wide policies (all the others):

    public ExponentialReconnectionPolicy(DriverContext context)
    

Where DriverContext is the object returned by session.getContext(), which allows the policy to access other driver components (for example the configuration).

If you write custom policy implementations, you should follow that same pattern; it provides an elegant way to switch policies without having to recompile the application (if your policy needs custom options, see the next section). Study the built-in implementations for reference.

If for some reason you really can’t use reflection, there is a way out; subclass DefaultDriverContext and override the corresponding method:

import com.datastax.oss.driver.internal.core.context.DefaultDriverContext;

public class MyDriverContext extends DefaultDriverContext {

  public MyDriverContext(DriverConfigLoader configLoader, List<TypeCodec<?>> typeCodecs) {
    super(configLoader, typeCodecs);
  }

  @Override
  protected ReconnectionPolicy buildReconnectionPolicy() {
    return myReconnectionPolicy;
  }
}

Then you’ll need to pass an instance of this context to DefaultSession.init. You can either do so directly, or subclass SessionBuilder and override the buildContext method.

Custom options

You can add your own options to the configuration. This is useful for custom components, or even as a way to associate arbitrary key/value pairs with the session instance.

First, write an enum that implements DriverOption:

public enum MyCustomOption implements DriverOption {

  ADMIN_NAME("admin.name"),
  ADMIN_EMAIL("admin.email"),
  AWESOMENESS_FACTOR("awesomeness-factor"),
  ;

  private final String path;

  MyCustomOption(String path) {
    this.path = path;
  }

  @Override
  public String getPath() {
    return path;
  }
}

You can now add the options to your configuration:

datastax-java-driver {
  admin {
    name = "Bob"
    email = "bob@example.com"
  }
  awesomeness-factor = 11
}

And access them from the code:

DriverConfig config = session.getContext().getConfig();
config.getDefaultProfile().getString(MyCustomOption.ADMIN_EMAIL);
config.getDefaultProfile().getInt(MyCustomOption.AWESOMENESS_FACTOR);