From 97f20f4e613b1b5b16fadad64227500ee344478d Mon Sep 17 00:00:00 2001 From: king Date: Wed, 14 Oct 2026 14:00:02 +0800 Subject: [PATCH 1/3] fix(testnode): set ForkParaFee before the para node starts testnode.NewWithConfig immediately starts the blockchain and other background goroutines, which read the config's Forks map (Block.Hash -> cfg.IsFork). chain33's Forks has no lock, so writing it afterwards - as NewParaNode did for ForkParaFee - is a data race with those goroutines and randomly fails TestParaNode under -race (seen in CI: plugin/consensus/para, 'WARNING: DATA RACE', GetFork vs setFork). Move the override before the node is created so the map is only written while the config is still single threaded; behaviour is unchanged (ForkParaFee stays disabled). Add a regression test pinning that the override survives node construction. --- plugin/dapp/paracross/testnode/paranode.go | 9 +++++--- .../paracross/testnode/paranode_fork_test.go | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 plugin/dapp/paracross/testnode/paranode_fork_test.go diff --git a/plugin/dapp/paracross/testnode/paranode.go b/plugin/dapp/paracross/testnode/paranode.go index 3424ff0df..9ed41de91 100644 --- a/plugin/dapp/paracross/testnode/paranode.go +++ b/plugin/dapp/paracross/testnode/paranode.go @@ -26,10 +26,13 @@ func NewParaNode(main *testnode.Chain33Mock, para *testnode.Chain33Mock) *ParaNo if para == nil { cfg := types.NewChain33Config(DefaultConfig) cfg.GetModuleConfig().RPC.ParaChain.MainChainGrpcAddr = main.GetCfg().RPC.GrpcBindAddr - para = testnode.NewWithConfig(cfg, nil) // chain33 ForkParaFee 默认-1,测试模式 SetAllFork(0) 激活收费 - // 导致 para 链处理同步区块时收手续费破坏共识,手动覆盖 - para.GetClient().GetConfig().SetFork("ForkParaFee", types.MaxHeight) + // 导致 para 链处理同步区块时收手续费破坏共识,手动覆盖。 + // 必须在创建节点之前设置:testnode.NewWithConfig 会立即启动 blockchain 等后台 goroutine, + // 之后任何对配置里 Forks 的写入都会与它们并发读 Forks 形成数据竞争(chain33 的 Forks 无锁), + // 在 -race 下表现为 TestParaNode 随机失败。 + cfg.SetFork("ForkParaFee", types.MaxHeight) + para = testnode.NewWithConfig(cfg, nil) para.Listen() } return &ParaNode{Main: main, Para: para} diff --git a/plugin/dapp/paracross/testnode/paranode_fork_test.go b/plugin/dapp/paracross/testnode/paranode_fork_test.go new file mode 100644 index 000000000..b190be57a --- /dev/null +++ b/plugin/dapp/paracross/testnode/paranode_fork_test.go @@ -0,0 +1,23 @@ +package testnode + +import ( + "testing" + + "github.com/33cn/chain33/types" + "github.com/stretchr/testify/assert" +) + +// TestParaNodeForkParaFeeDisabled 固化 NewParaNode 的配置约定: +// ForkParaFee 必须在节点创建**之前**被置为 MaxHeight(禁用)。原因有两条: +// 1. 测试模式下 chain33 会把所有 fork 置 0,导致 para 链收手续费破坏共识; +// 2. testnode.NewWithConfig 会立即启动 blockchain 等后台 goroutine, +// 之后再写配置里的 Forks 会与这些 goroutine 并发读 Forks 形成数据竞争 +// (chain33 的 Forks 无锁),在 -race 下表现为 TestParaNode 随机失败。 +func TestParaNodeForkParaFeeDisabled(t *testing.T) { + node := NewParaNode(nil, nil) + defer node.Close() + + paraCfg := node.Para.GetAPI().GetConfig() + assert.False(t, paraCfg.IsFork(0, "ForkParaFee"), "ForkParaFee 应保持禁用") + assert.Equal(t, int64(types.MaxHeight), paraCfg.GetFork("ForkParaFee")) +} From 4d13abb3b0289b07af30a3eb3d9560a60cb43dcc Mon Sep 17 00:00:00 2001 From: king Date: Wed, 14 Oct 2026 14:00:02 +0800 Subject: [PATCH 2/3] ci: shard unit-test into 4 parallel runners make test runs go test -race over ~370 packages and took ~13 minutes on a single 4 vCPU runner. Split the packages by index modulo across four parallel shard jobs so wall time drops under 10 minutes, and keep a unit-test aggregator job with the exact same name so the required branch-protection check keeps working. Race detection is unchanged (still -race, all packages). --- .github/workflows/build.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d48f5eab6..ad57b220b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,9 +35,29 @@ jobs: make linter make + # 全量 go test -race 单 job 约 13 分钟,拆成 4 个并行分片执行; + # 聚合 job 仍命名为 unit-test,保持分支保护里 Required check 的名称不变。 unit-test: name: unit-test + needs: unit-test-shard + if: always() runs-on: ubuntu-22.04 + steps: + - name: check shards + run: | + echo "shards result: ${{ needs.unit-test-shard.result }}" + if [ "${{ needs.unit-test-shard.result }}" != "success" ]; then + echo "::error::unit-test 分片未全部通过" + exit 1 + fi + + unit-test-shard: + name: unit-test shard + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] steps: - name: checkout uses: actions/checkout@v4 @@ -49,7 +69,11 @@ jobs: - name: set go env run: export PATH=${PATH}:`go env GOPATH`/bin - name: unit-test - run: make test + run: | + # 按包序号取模分片,不维护硬编码包列表,新增包会自动落到某个分片 + PKGS=$(go list ./... | grep -v pbft | awk -v s=${{ matrix.shard }} 'NR % 4 == s % 4') + echo "shard ${{ matrix.shard }}: $(echo "$PKGS" | wc -l) packages" + go test -parallel=8 -race $PKGS coverage: name: coverage From f2499f43692a5645f3b8049923226be06da31d1b Mon Sep 17 00:00:00 2001 From: king Date: Wed, 14 Oct 2026 16:30:02 +0800 Subject: [PATCH 3/3] test(testnode): make the ForkParaFee regression test node free The first version created real nodes and closed them immediately, so background initialization hit ErrChannelClosed and panicked; that also failed the coverage and Test GOARCH 386 jobs. Extract the config construction into newParaConfig (all config writes, including Forks, now happen before any node exists) and assert that contract without starting a node, so the test cannot flake on node start/stop ordering. --- plugin/dapp/paracross/testnode/paranode.go | 23 +++++++++++-------- .../paracross/testnode/paranode_fork_test.go | 23 ++++++++++--------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/plugin/dapp/paracross/testnode/paranode.go b/plugin/dapp/paracross/testnode/paranode.go index 9ed41de91..bb18ceecc 100644 --- a/plugin/dapp/paracross/testnode/paranode.go +++ b/plugin/dapp/paracross/testnode/paranode.go @@ -24,20 +24,25 @@ func NewParaNode(main *testnode.Chain33Mock, para *testnode.Chain33Mock) *ParaNo main.Listen() } if para == nil { - cfg := types.NewChain33Config(DefaultConfig) - cfg.GetModuleConfig().RPC.ParaChain.MainChainGrpcAddr = main.GetCfg().RPC.GrpcBindAddr - // chain33 ForkParaFee 默认-1,测试模式 SetAllFork(0) 激活收费 - // 导致 para 链处理同步区块时收手续费破坏共识,手动覆盖。 - // 必须在创建节点之前设置:testnode.NewWithConfig 会立即启动 blockchain 等后台 goroutine, - // 之后任何对配置里 Forks 的写入都会与它们并发读 Forks 形成数据竞争(chain33 的 Forks 无锁), - // 在 -race 下表现为 TestParaNode 随机失败。 - cfg.SetFork("ForkParaFee", types.MaxHeight) - para = testnode.NewWithConfig(cfg, nil) + para = testnode.NewWithConfig(newParaConfig(main.GetCfg().RPC.GrpcBindAddr), nil) para.Listen() } return &ParaNode{Main: main, Para: para} } +// newParaConfig 构造平行链节点配置。对配置的所有写入(尤其是 Forks)都必须在创建节点之前完成: +// testnode.NewWithConfig 会立即启动 blockchain 等后台 goroutine 并并发读取 Forks +// (Block.Hash() -> cfg.IsFork()),而 chain33 的 Forks 没有锁, +// 之后再写 Forks 会构成数据竞争,在 -race 下表现为 TestParaNode 随机失败。 +func newParaConfig(mainGrpcAddr string) *types.Chain33Config { + cfg := types.NewChain33Config(DefaultConfig) + cfg.GetModuleConfig().RPC.ParaChain.MainChainGrpcAddr = mainGrpcAddr + // chain33 ForkParaFee 默认-1,测试模式 SetAllFork(0) 激活收费, + // 导致 para 链处理同步区块时收手续费破坏共识,这里覆盖为禁用。 + cfg.SetFork("ForkParaFee", types.MaxHeight) + return cfg +} + // Close 关闭系统 func (node *ParaNode) Close() { node.Para.Close() diff --git a/plugin/dapp/paracross/testnode/paranode_fork_test.go b/plugin/dapp/paracross/testnode/paranode_fork_test.go index b190be57a..83071edfc 100644 --- a/plugin/dapp/paracross/testnode/paranode_fork_test.go +++ b/plugin/dapp/paracross/testnode/paranode_fork_test.go @@ -7,17 +7,18 @@ import ( "github.com/stretchr/testify/assert" ) -// TestParaNodeForkParaFeeDisabled 固化 NewParaNode 的配置约定: -// ForkParaFee 必须在节点创建**之前**被置为 MaxHeight(禁用)。原因有两条: +// TestNewParaConfigDisablesForkParaFee 固化 newParaConfig 的约定: +// ForkParaFee 必须在**创建节点之前**被置为 MaxHeight(禁用)。原因有两条: // 1. 测试模式下 chain33 会把所有 fork 置 0,导致 para 链收手续费破坏共识; -// 2. testnode.NewWithConfig 会立即启动 blockchain 等后台 goroutine, -// 之后再写配置里的 Forks 会与这些 goroutine 并发读 Forks 形成数据竞争 -// (chain33 的 Forks 无锁),在 -race 下表现为 TestParaNode 随机失败。 -func TestParaNodeForkParaFeeDisabled(t *testing.T) { - node := NewParaNode(nil, nil) - defer node.Close() +// 2. testnode.NewWithConfig 会立即启动 blockchain 等后台 goroutine 并发读取 Forks, +// 之后再写 Forks 会构成数据竞争(chain33 的 Forks 无锁), +// 在 -race 下表现为 TestParaNode 随机失败。 +// +// 这里刻意不启动节点:断言配置本身即可,避免测试自身的启停时序引入新的不稳定。 +func TestNewParaConfigDisablesForkParaFee(t *testing.T) { + cfg := newParaConfig("127.0.0.1:0") - paraCfg := node.Para.GetAPI().GetConfig() - assert.False(t, paraCfg.IsFork(0, "ForkParaFee"), "ForkParaFee 应保持禁用") - assert.Equal(t, int64(types.MaxHeight), paraCfg.GetFork("ForkParaFee")) + assert.False(t, cfg.IsFork(0, "ForkParaFee"), "ForkParaFee 应保持禁用") + assert.Equal(t, int64(types.MaxHeight), cfg.GetFork("ForkParaFee")) + assert.Equal(t, "127.0.0.1:0", cfg.GetModuleConfig().RPC.ParaChain.MainChainGrpcAddr) }