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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions