Skip to content

多进程并发执行 tccli 时配置文件竞争导致 exit 255 / JSON 解析错误 #109

@techird

Description

@techird

多进程并发执行 tccli 时配置文件竞争导致 exit 255 / JSON 解析错误

问题简述

当多个 tccli 进程并发执行时,会同时读写 ~/.tccli/*.configure,且读写过程无文件锁、非原子写入,导致一个进程可能读到空文件写了一半的文件,从而以 exit 255 退出,并出现如下错误:

  • Expecting value: line 1 column 1 (char 0)(读到空文件)
  • Expecting value: line 366 column 16(读到截断的 JSON)

上述错误发生在真正发起腾讯云 API 请求之前,异常内容来自本地配置文件,而非云 API 返回。

环境

  • tccli:3.0.x(pip / homebrew 安装)
  • 系统:Linux / macOS
  • 复现方式:任意并发调用 tccli(例如 12 个 tccli vpc DescribeSecurityGroupPolicies --region ap-hongkong,或 tccli vpc DescribeRegions --region ap-guangzhou

复现步骤

  1. 先配置一次 tccli:执行 tccli configuretccli auth login
  2. 并发执行多个 tccli 进程(例如 12 个):
for i in $(seq 1 12); do
  tccli vpc DescribeRegions --region ap-guangzhou > "out_$i.json" 2> "err_$i.txt" & done
wait
# 检查退出码,部分会是 255

或使用最小复现脚本(见附件/仓库):

chmod +x repro_tccli_concurrent_config.sh
./repro_tccli_concurrent_config.sh 12 ap-guangzhou
  1. 现象:部分进程以 255 退出,stderr 中出现 JSON 解析错误;其余可能成功。

预期与实际

  • 预期:所有调用应正常完成,或仅因 API/网络原因失败;配置文件的读写在并发下应安全。
  • 实际:并发时会有一定比例进程 exit 255,解析错误指向本地配置文件(空或截断的 JSON),而非 API 响应。失败可发生在进程尚未执行到具体服务 action 之前(已通过进程内探针验证)。

根因(代码链路)

  • 每次 tccli 启动都会构建 ConfigureCommand() 并调用 init_configures()
  • init_configures() 最终调用 _init_configure(),对当前 profile 及所有 *.configure 文件执行 open(..., "w") 重写无文件锁非原子写入
  • 另一进程若在读取同一配置(例如在 tccli/loaders.pyUtils.load_json_msg() 约第 77 行),可能读到:
    • 空文件body_len = 0Expecting value: line 1 column 1 (char 0)
    • 截断文件 → 例如 body_len = 8187Expecting value: line 366 column 16

相关调用链:

  • 读路径loaders.pyUtils.load_json_msg()(约第 77 行)读取 ~/.tccli/default.configure(或当前 profile 对应文件)。
  • 写路径command.py(约第 95 行)构建 ConfigureCommand()configure.py(约第 412 行)init_configures() →(约第 472 行)_init_configure() →(约第 45 行)open(..., "w");另见 utils.py(约第 83 行)。

因此,“非 JSON” 来自正在被其他 tccli 进程写入的本地配置文件

与现象的一致性

  • 串行执行:多数正常;若与其他 tccli 或工具同时碰同一配置,仍可能偶发失败。
  • 并发执行:失败率明显升高。
  • 请求前即失败:有时失败进程从未进入具体服务的 action,与在初始加载配置阶段失败相符。

修复建议

  • 方案 A:对每个 *.configure 的读、写使用文件锁(如 fcntl.flockfilelock),保证写时独占,读时与写互斥。
  • 方案 B原子写入:先写入同目录下的临时文件,再用 os.rename() 替换最终 *.configure 路径,避免读者看到半写状态。

任选其一(或两者同时采用)即可让并发 tccli 调用在配置读写上安全。


最小复现脚本repro_tccli_concurrent_config.sh(并发执行 N 次 tccli cvm DescribeRegions,统计成功与 exit 255 数量并输出示例 stderr)。

#!/usr/bin/env bash
#
# Minimal repro: concurrent tccli processes can corrupt ~/.tccli/*.configure
# (no file lock, non-atomic write). Result: exit 255, "Expecting value: line 1
# column 1" or truncated JSON parse errors — before any API request.
#
# Usage:
#   ./repro_tccli_concurrent_config.sh [N] [region]
# Default: N=12, region=ap-guangzhou
#
# Prereq: tccli configured (e.g. tccli configure or tccli auth login).
# Run in an environment where ~/.tccli is writable (e.g. your local terminal);
# in a sandbox that blocks ~/.tccli/log, tccli will fail with PermissionError before hitting the config race.

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${SCRIPT_DIR}/repro_out"
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"

N="${1:-12}"
REGION="${2:-ap-guangzhou}"

# Use any tccli call that loads config; DescribeRegions needs no extra args
TCCLI_CMD="tccli cvm DescribeRegions"

echo "Repro: $N concurrent invocations of: $TCCLI_CMD"
echo "Working dir: $WORK_DIR"
echo ""

for i in $(seq 1 "$N"); do
  (
    $TCCLI_CMD > "out_$i.json" 2> "err_$i.txt"
    echo $? > "exit_$i"
  ) &
done
wait

ok=0
fail=0
for i in $(seq 1 "$N"); do
  code=$(cat "exit_$i" 2>/dev/null || echo -1)
  if [ "$code" = "0" ]; then
    ((ok++)) || true
  else
    ((fail++)) || true
    echo "--- FAIL #$i (exit $code) stderr ---"
    cat "err_$i.txt" 2>/dev/null || true
    echo ""
  fi
done

echo "Result: $ok OK, $fail FAIL (of $N)"
if [ "$fail" -gt 0 ]; then
  echo "Sample failure stderr (first non-zero):"
  for i in $(seq 1 "$N"); do
    if [ "$(cat exit_$i)" != "0" ]; then
      cat "err_$i.txt"
      break
    fi
  done
  exit 1
fi
exit 0

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