Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

grpc-js: default service config (`grpc.service_config`) is not used when the first resolver result has an invalid service config

未关闭 适合新手
#3,121 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
76/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
grpc, node.js, typescript

调研方向

阅读 packages/grpc-js/src/resolving-load-balancer.ts,重点查看第290–301行的 service config 选择以及第217–226行的默认配置初始化。检查构造函数如何区分显式提供的 grpc.service_config 与空的默认配置,然后运行 issue 中描述的复现测试。完成标准是:只有在不存在更早的有效配置时,无效的 resolver config 才回退到显式默认配置;没有默认配置时仍报告解析失败。

由索引模型根据 Issue 内容生成。

描述

Problem description

Location (grpc/grpc-node @ default branch ca7f83f):

if (serviceConfig === null) {
  workingServiceConfig = this.defaultServiceConfig;
} else if (serviceConfig.ok) {
  workingServiceConfig = serviceConfig.value;
} else {
  if (this.previousServiceConfig !== null) {
    workingServiceConfig = this.previousServiceConfig;
  } else {
    resultAccepted = false;
    this.handleResolutionFailure(serviceConfig.error);
  }
}

Problem

When a channel has an explicit default service config (grpc.service_config channel option) and the resolver's first result contains valid addresses but an invalid service config, ResolvingLoadBalancer reports a resolution failure instead of falling back to the default service config.

gRFC A21 (Service Config Error Handling) says:

If a default service config is provided, the client should fallback on the default service config (in case of an invalid config with no valid service config received earlier). On the other hand, if there is no default service config provided as a channel argument, the client should fail to start and keep waiting for a valid service config.

So the expected behavior differs only in the case "invalid config, no previous valid config, explicit default configured". The other cases already match A21 and should stay as they are:

  • a previously accepted valid config keeps taking priority over the default;
  • with no grpc.service_config option, the result should still be treated as a resolution failure.

This is separate from #3116, where resolver errors were explained to be expected transient errors. This report is not about those. It concerns only a service config that fails validation while the addresses are usable, such as the DNS resolver's service-config error when TXT parsing fails. That is the kind of error #1510 (comment by @murgatroid99) said serviceConfigError should be limited to. Handling of address errors and transient lookup failures would not change.

Reproduction steps

Reproduction

  1. Check out commit 6741e13.
  2. Add the test below as packages/grpc-js/test/test-default-service-config.ts. It registers a resolver that returns one valid endpoint plus an error StatusOr<ServiceConfig>, and creates a ResolvingLoadBalancer with a valid grpc.service_config default. This exercises the balancer directly; I did not verify it with a live RPC.
  3. Run npm test (gulp test) in packages/grpc-js.
test-default-service-config.ts
import * as assert from 'assert';
import { ResolvingLoadBalancer } from '../src/resolving-load-balancer';
import {
  registerResolver,
  ResolverListener,
  Resolver,
  ResolverConstructor,
} from '../src/resolver';
import { ChannelControlHelper } from '../src/load-balancer';
import { Picker } from '../src/picker';
import { ConnectivityState } from '../src/connectivity-state';
import {
  statusOrFromValue,
  statusOrFromError,
} from '../src/call-interface';
import { ServiceConfig } from '../src/service-config';
import { Endpoint, subchannelAddressToString } from '../src/subchannel-address';
import { GrpcUri } from '../src/uri-parser';
import { Status } from '../src/constants';
import { MockSubchannel } from './common';
import { ChannelOptions } from '../src/channel-options';

// Ensure all resolvers and load balancers are registered
import '../src/index';

/**
 * A resolver that returns a valid endpoint list but an invalid/error
 * service config. This simulates a resolver that encounters an error
 * while parsing or fetching the service config.
 */
class InvalidServiceConfigResolver implements Resolver {
  constructor(
    _target: GrpcUri,
    private listener: ResolverListener,
    _channelOptions: ChannelOptions
  ) {}

  updateResolution() {
    setImmediate(() => {
      this.listener(
        statusOrFromValue<Endpoint[]>([
          { addresses: [{ host: 'localhost', port: 50051 }] },
        ]),
        {},
        statusOrFromError<ServiceConfig>({
          code: Status.UNAVAILABLE,
          details: 'Invalid service config from resolver',
        }),
        ''
      );
    });
  }

  destroy() {}

  static getDefaultAuthority(target: GrpcUri): string {
    return target.path;
  }
}

describe('Default Service Config', () => {
  before(() => {
    registerResolver(
      'test-invalid-sc',
      InvalidServiceConfigResolver as unknown as ResolverConstructor
    );
  });

  it('should use default service config when resolver returns invalid service config', function (done) {
    this.timeout(5000);

    const defaultServiceConfig: ServiceConfig = {
      loadBalancingConfig: [],
      methodConfig: [
        {
          name: [{ service: 'TestService', method: 'TestMethod' }],
          waitForReady: true,
        },
      ],
    };

    let subchannels: MockSubchannel[] = [];
    const channelControlHelper: ChannelControlHelper = {
      createSubchannel: (subchannelAddress, subchannelArgs) => {
        const subchannel = new MockSubchannel(
          subchannelAddressToString(subchannelAddress)
        );
        subchannels.push(subchannel);
        return subchannel;
      },
      addChannelzChild: () => {},
      removeChannelzChild: () => {},
      requestReresolution: () => {},
      updateState: (
        state: ConnectivityState,
        picker: Picker,
        errorMessage: string | null
      ) => {},
    };

    const rlb = new ResolvingLoadBalancer(
      { scheme: 'test-invalid-sc', authority: undefined, path: 'test-target' },
      channelControlHelper,
      {
        'grpc.service_config': JSON.stringify(defaultServiceConfig),
      },
      (serviceConfig: ServiceConfig, configSelector) => {
        // onSuccessfulResolution: The default service config should be used.
        try {
          assert.strictEqual(
            serviceConfig.methodConfig.length,
            1,
            'Default service config should have one method config entry'
          );
          assert.strictEqual(
            serviceConfig.methodConfig[0].waitForReady,
            true,
            'waitForReady should be true from default service config'
          );
        } finally {
          rlb.destroy();
        }
        done();
      },
      status => {
        // onFailedResolution: This should NOT be called when a default
        // service config is available.
        rlb.destroy();
        done(
          new Error(
            'Resolution failed, but should have fallen back to default service config. ' +
              `Status: ${status.code} - ${status.details}`
          )
        );
      }
    );

    rlb.exitIdle();
  });
});

Observed:

  1) Default Service Config should use default service config when resolver returns invalid service config:
     Error: Resolution failed, but should have fallen back to default service config. Status: 14 - Invalid service config from resolver
      at ResolvingLoadBalancer.onFailedResolution (packages/grpc-js/test/test-default-service-config.ts:147:11)
      at ResolvingLoadBalancer.handleResolutionFailure (packages/grpc-js/src/resolving-load-balancer.ts:371:12)
      at ResolvingLoadBalancer.handleResolverResult (packages/grpc-js/src/resolving-load-balancer.ts:299:14)

The rest of the suite passed in the same run (505 passing, 9 pending, 1 failing).

Environment
  • OS: Linux x64 (Debian 11)
  • Node v20.19.6
  • Package: @grpc/grpc-js 1.13.4 at commit 6741e13f210f119d4d3da37691dd01055541663b
  • I compared current master ca7f83f by blob hash: resolving-load-balancer.ts and resolver-dns.ts are identical to the tested commit. I did not re-run the test on that commit.
Additional context

Already implemented in grpc/grpc-go

PR: https://github.com/grpc/grpc-go/pull/2686 — "service config: default service config" (merged 2019-04-03T17:50:29Z, merge commit ea5e6da287d). It added default service config support to grpc-go, including this fallback: the default is applied when the resolver returns no config or an invalid one, and only if no service config has been applied yet. A21 remains the basis for this report; this PR shows another gRPC implementation handling the same case, and I am only citing the PR, not the current grpc-go code.

How grpc-go fixed it:

--- a/clientconn.go
+++ b/clientconn.go
@@ -457,6 +465,24 @@ func (cc *ClientConn) waitForResolvedAddrs(ctx context.Context) error {
 	}
 }
 
+// gRPC should resort to default service config when:
+// * resolver service config is disabled
+// * or, resolver does not return a service config or returns an invalid one.
+func (cc *ClientConn) fallbackToDefaultServiceConfig(sc string) bool {
+	if cc.dopts.disableServiceConfig {
+		return true
+	}
+	// The logic below is temporary, will be removed once we change the resolver.State ServiceConfig field type.
+	// Right now, we assume that empty service config string means resolver does not return a config.
+	if sc == "" {
+		return true
+	}
+	// TODO: the logic below is temporary. Once we finish the logic to validate service config
+	// in resolver, we will replace the logic below.
+	_, err := parseServiceConfig(sc)
+	return err != nil
+}
+
 func (cc *ClientConn) updateResolverState(s resolver.State) error {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
...
@@ -467,29 +493,26 @@ func (cc *ClientConn) updateResolverState(s resolver.State) error {
-	if !cc.dopts.disableServiceConfig && cc.scRaw != s.ServiceConfig {
-		// New service config; apply it.
+	if cc.fallbackToDefaultServiceConfig(s.ServiceConfig) {
+		if cc.dopts.defaultServiceConfig != nil && cc.sc == nil {
+			cc.applyServiceConfig(cc.dopts.defaultServiceConfig)
+		}
+	} else {

Suggested fix

Use the default service config in the !serviceConfig.ok && previousServiceConfig === null branch when grpc.service_config was set, as A21 specifies and as grpc-go does. The check reads the channel option because the constructor always fills defaultServiceConfig; without the option the result is still a resolution failure:

diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts
--- a/packages/grpc-js/src/resolving-load-balancer.ts
+++ b/packages/grpc-js/src/resolving-load-balancer.ts
@@ -294,6 +294,8 @@ export class ResolvingLoadBalancer implements LoadBalancer {
     } else {
       if (this.previousServiceConfig !== null) {
         workingServiceConfig = this.previousServiceConfig;
+      } else if (this.channelOptions['grpc.service_config']) {
+        workingServiceConfig = this.defaultServiceConfig;
       } else {
         resultAccepted = false;
         this.handleResolutionFailure(serviceConfig.error);

Verified on 6741e13 (Node.js v22.23.4, mocha 10.8.2): the test above fails without this patch and passes with it; no other test results change.

I can submit a PR for this.

Related: I searched existing issues and PRs ("default service config" invalid, "service config" resolver invalid fallback, previousServiceConfig). The only related item is #1510 (closed, 2020), which was about TXT lookup failures being reported as service-config errors; it was fixed in the DNS resolver and did not change this fallback branch.

主要语言
TypeScript
星标
4.8k
派生
720
平均合并
2 天 17 小时
30 天内合并 PR
11

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

grpc/grpc-node 的其他 Issue

查看 grpc/grpc-node 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。