Skip to content

4.x: onThrottleReady exceptions can escape or leave request futures incomplete #949

Description

@dkropachev

Problem

Request handlers do not have a top-level error boundary around onThrottleReady().

The throttler calls onThrottleReady() directly. If query-plan creation or sendRequest() throws synchronously, behavior depends on throttler timing:

  • immediate throttler path: exception can escape from executeAsync() instead of returning a failed CompletionStage
  • queued throttler path: exception happens later on the throttler/event-loop thread and the request future may not complete cleanly

Proof

Throttlers call onThrottleReady() directly:

CQL handler does query-plan creation and request scheduling inside onThrottleReady() without a catch-all boundary:

@Override
public void onThrottleReady(boolean wasDelayed) {
if (wasDelayed
// avoid call to nanoTime() if metric is disabled:
&& sessionMetricUpdater.isEnabled(
DefaultSessionMetric.THROTTLING_DELAY, executionProfile.getName())) {
sessionMetricUpdater.updateTimer(
DefaultSessionMetric.THROTTLING_DELAY,
executionProfile.getName(),
System.nanoTime() - startTimeNanos,
TimeUnit.NANOSECONDS);
}
Queue<Node> queryPlan;
if (this.initialStatement.getNode() != null) {
queryPlan = new SimpleQueryPlan(this.initialStatement.getNode());
} else {
queryPlan =
context
.getLoadBalancingPolicyWrapper()
.newQueryPlan(initialStatement, executionProfile.getName(), session);
}
sendRequest(initialStatement, null, queryPlan, 0, 0, true);

LoadBalancingPolicyWrapper.newQueryPlan() calls the policy directly in RUNNING state:

@NonNull
public Queue<Node> newQueryPlan(
@Nullable Request request, @NonNull String executionProfileName, @Nullable Session session) {
switch (stateRef.get()) {
case BEFORE_INIT:
case DURING_INIT:
// The contact points are not stored in the metadata yet:
List<Node> nodes = new ArrayList<>(context.getMetadataManager().getContactPoints());
Collections.shuffle(nodes);
return new ConcurrentLinkedQueue<>(nodes);
case RUNNING:
LoadBalancingPolicy policy = policiesPerProfile.get(executionProfileName);
if (policy == null) {
policy = policiesPerProfile.get(DriverExecutionProfile.DEFAULT_NAME);
}
return policy.newQueryPlan(request, session);
default:

Similar unguarded onThrottleReady() scheduling exists in:

  • prepare:
    @Override
    public void onThrottleReady(boolean wasDelayed) {
    DriverExecutionProfile executionProfile =
    Conversions.resolveExecutionProfile(initialRequest, context);
    if (wasDelayed) {
    session
    .getMetricUpdater()
    .updateTimer(
    DefaultSessionMetric.THROTTLING_DELAY,
    executionProfile.getName(),
    System.nanoTime() - startTimeNanos,
    TimeUnit.NANOSECONDS);
    }
    sendRequest(initialRequest, null, 0);
    }
  • graph:
    @Override
    public void onThrottleReady(boolean wasDelayed) {
    DriverExecutionProfile executionProfile =
    Conversions.resolveExecutionProfile(initialStatement, context);
    if (wasDelayed
    // avoid call to nanoTime() if metric is disabled:
    && sessionMetricUpdater.isEnabled(
    DefaultSessionMetric.THROTTLING_DELAY, executionProfile.getName())) {
    sessionMetricUpdater.updateTimer(
    DefaultSessionMetric.THROTTLING_DELAY,
    executionProfile.getName(),
    System.nanoTime() - startTimeNanos,
    TimeUnit.NANOSECONDS);
    }
    Queue<Node> queryPlan =
    initialStatement.getNode() != null
    ? new SimpleQueryPlan(initialStatement.getNode())
    : context
    .getLoadBalancingPolicyWrapper()
    .newQueryPlan(initialStatement, executionProfile.getName(), session);
    sendRequest(initialStatement, null, queryPlan, 0, 0, true);
  • continuous CQL:
    @Override
    public void onThrottleReady(boolean wasDelayed) {
    DriverExecutionProfile executionProfile =
    Conversions.resolveExecutionProfile(initialStatement, context);
    if (wasDelayed
    // avoid call to nanoTime() if metric is disabled:
    && sessionMetricUpdater.isEnabled(
    DefaultSessionMetric.THROTTLING_DELAY, executionProfile.getName())) {
    session
    .getMetricUpdater()
    .updateTimer(
    DefaultSessionMetric.THROTTLING_DELAY,
    executionProfile.getName(),
    System.nanoTime() - startTimeNanos,
    TimeUnit.NANOSECONDS);
    }
    activeExecutionsCount.incrementAndGet();
    sendRequest(initialStatement, null, 0, 0, specExecEnabled);

Expected

Any exception thrown from onThrottleReady() scheduling should complete the request with a failed future consistently, regardless of throttler implementation or whether the request was delayed.

Suggested fix

Add a top-level try/catch around handler onThrottleReady() scheduling and route failures through existing final-error paths.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions