Skip to content

grpc-js: DNS resolution stops retrying after failures, so a channel created before its name exists never connects #3128

Open

Description

@realies

A channel started before its DNS name exists can stop resolving after two failures. Adding the name later leaves a connectivity watcher or a single queued wait-for-ready RPC stuck in TRANSIENT_FAILURE. Starting new RPCs can keep resolution moving. Reproduced on master ca7f83f (package version 1.13.4), Node 24.22.1.

Run this inside an isolated offline container as root with a built @grpc/grpc-js. It changes that container's resolver and hosts files. Enable GRPC_VERBOSITY=DEBUG GRPC_TRACE=dns_resolver to see lookups; GRPC_JS_PATH can select a checkout's built entry point.

const fs = require('node:fs');
const grpc = require(process.env.GRPC_JS_PATH || '@grpc/grpc-js');
fs.writeFileSync('/etc/resolv.conf', 'nameserver 127.0.0.1\noptions attempts:1 timeout:1\n');
const host = 'grpc-late.invalid';
const server = new grpc.Server();
server.bindAsync('127.0.0.1:0', grpc.ServerCredentials.createInsecure(), (err, port) => {
  if (err) throw err;
  const client = new grpc.Client(`dns:${host}:${port}`, grpc.credentials.createInsecure(), {
    'grpc.initial_reconnect_backoff_ms': 100,
    'grpc.max_reconnect_backoff_ms': 400,
    'grpc.service_config_disable_resolution': 1
  });
  const channel = client.getChannel();
  function watch() {
    const state = channel.getConnectivityState(true);
    console.log(grpc.connectivityState[state]);
    if (state === grpc.connectivityState.SHUTDOWN || state === grpc.connectivityState.READY) return;
    channel.watchConnectivityState(state, Infinity, watch);
  }
  watch();
  setTimeout(() => fs.appendFileSync('/etc/hosts', `\n127.0.0.1 ${host}\n`), 1500);
  setTimeout(() => {
    console.log('Final:', grpc.connectivityState[channel.getConnectivityState(false)]);
    client.close();
    server.forceShutdown();
  }, 5000);
});

Expected: failed DNS resolution retries with backoff while the channel is active, so the same channel reaches READY after the name appears.

This deliberately reverses the “don't repeat failed resolutions” behavior pinned by #2105 after #2080. The 1.6.4 OOM involved double scheduling; #2098's per-attempt continueResolving reset still prevents that. The proposed retry uses the existing backoff and stops at IDLE or close. Close also needs a SHUTDOWN guard in getConnectivityState(true), because synchronous watchers, including Client.waitForReady, can otherwise restart the destroyed resolver.

The client-channel draft, grpc/grpc#38953, says: “For polling resolvers, this will cause the resolver to re-resolve with appropriate backoff” (in its discussion of rejected resolver results). C-core's polling resolver and grpc-go's DNS watcher already retry failed results with backoff. This is a proposed behavior change in grpc-js, supported by that draft and those implementations.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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