Driver context

The context holds the driver’s internal components. It is exposed in the public API as DriverContext, accessible via session.getContext(). Internally, the child interface InternalDriverContext adds access to more components; finally, DefaultDriverContext is the implementing class.

The dependency graph

Most components initialize lazily (see LazyReference). They also reference each other, typically by taking the context as a constructor argument, and extracting the dependencies they need:

public DefaultTopologyMonitor(InternalDriverContext context) {
  ...
  this.controlConnection = context.getControlConnection();
}

This avoids having to handle the initialization order ourselves. It is also convenient for unit tests: you can run a component in isolation by mocking all of its dependencies.

Obviously, things won’t go well if there are cyclic dependencies; if you make changes to the context, you can set a system property to check the dependency graph, it will throw if a cycle is detected (see CycleDetector):

-Dcom.datastax.oss.driver.DETECT_CYCLES=true

This is disabled by default, because we don’t expect it to be very useful outside of testing cycles.

Why not use a DI framework?

As should be clear by now, the context is a poor man’s Dependency Injection framework. We deliberately avoided third-party solutions:

  • to keep things as simple as possible,
  • to avoid an additional library dependency,
  • to allow end users to access components and add their own (which wouldn’t work well with compile-time approaches like Dagger).

Overriding a context component

The basic approach to plug in a custom internal component is to subclass the context.

For example, let’s say you wrote a custom NettyOptions implementation (maybe you have multiple sessions, and want to reuse the event loop groups instead of recreating them every time):

public class CustomNettyOptions implements NettyOptions {
  ...
} 

In the default context, here’s how the component is managed:

public class DefaultDriverContext {

  // some content omitted for brevity

  private final LazyReference<NettyOptions> nettyOptionsRef =
      new LazyReference<>("nettyOptions", this::buildNettyOptions, cycleDetector);

  protected NettyOptions buildNettyOptions() {
    return new DefaultNettyOptions(this);
  }

  @NonNull
  @Override
  public NettyOptions getNettyOptions() {
    return nettyOptionsRef.get();
  }
}

To switch in your implementation, you only need to override the build method:

public class CustomContext extends DefaultDriverContext {

  public CustomContext(DriverConfigLoader configLoader, ProgrammaticArguments programmaticArguments) {
    super(configLoader, programmaticArguments);
  }

  @Override
  protected NettyOptions buildNettyOptions() {
    return new CustomNettyOptions(this);
  }
}

Then you need a way to create a session that uses your custom context. The session builder is extensible as well:

public class CustomBuilder extends SessionBuilder<CustomBuilder, CqlSession> {

  @Override
  protected DriverContext buildContext(
      DriverConfigLoader configLoader, ProgrammaticArguments programmaticArguments) {
    return new CustomContext(configLoader, programmaticArguments);
  }

  @Override
  protected CqlSession wrap(@NonNull CqlSession defaultSession) {
    // Nothing to do here, nothing changes on the session type
    return defaultSession;
  }
}

Finally, you can use your custom builder like the regular CqlSession.builder(), it inherits all the methods:

CqlSession session = new CustomBuilder()
    .addContactPoint(new InetSocketAddress("1.2.3.4", 9042))
    .withLocalDatacenter("datacenter1")
    .build();