[Bug] GPORCA long-lived backend leaks memory per query (process-level OptimizerMemoryContext, RSS never returned to OS)
Maintainer thường phản hồi trong vòng 3 ngày
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 35/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Lĩnh vực
- backend, databases, performance
Hướng nghiên cứu
Bắt đầu bằng cách chạy orca_leak_setup.sql và orca_leak_reproduce.sh ở các chế độ default, nocache và noop để xác nhận hành vi RSS của backend tồn tại lâu dài. Sau đó đọc planner.c, CMemoryPoolPalloc.cpp, CMDCache.cpp, CCacheFactory.cpp và CMemoryPoolManager.cpp, cùng với postinit.c, để theo dõi vòng đời ở cấp tiến trình. Hoàn thành khi kết nối tồn tại lâu dài mặc định không còn tích lũy bộ nhớ qua các truy vấn, trong khi bộ nhớ đệm siêu dữ liệu vẫn được quản lý chính xác.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
Long-lived backends running ORCA queries accumulate memory continuously (never returned to the OS). In production a single connection grew to 769 MB after 6,802 commands, and the accumulated memory is only released at process exit, causing cascading OOM across the cluster over time (5,094 live PIDs at the OOM moment).
The same design flaw exists identically in Apache Cloudberry 2.1.0 and GPDB 6.27.1; upgrading between them does not fix it.
Environment
- Apache Cloudberry 2.1.0-incubating (root cause also reproduces on GPDB 6.27.1)
- Test environment: GPDB 6.27.1 (single-node, ORCA default)
optimizer = on(ORCA),optimizer_metadata_caching = on(default),optimizer_use_gpdb_allocators = on(default)
Reproduction
Setup two tables (see orca_leak_setup.sql, 10k + 50k rows), then repeatedly run identical simple queries on a single psql connection while watching the backend RSS:
./orca_leak_setup.sql # psql -d <db> -f orca_leak_setup.sql
./orca_leak_reproduce.sh <db> 5000 default
Measured results (single connection, GPDB 6.27.1, RSS of backend process)
| Iteration | RSS (MB) | Notes |
|---|---|---|
| 0 | 54.1 | baseline |
| 200 | 57.6 | ~0.018 MB/cmd |
| 1600 | 76.1 | ~0.016 MB/cmd |
| 2800 | 97.6 | ~0.018 MB/cmd |
| 4000 | 117.1 | ~0.016 MB/cmd |
| 5000 | 135.6 | ~0.019 MB/cmd |
- Total growth: 81.5 MB over 5,000 queries (~0.016 MB/cmd), and the growth is accelerating over time (fragmentation).
- Production estimate: ~0.113 MB/cmd on real tables (more tables / larger/JSON metadata → more ORCA temp allocations).
Controlled comparison
| Test | Config | Initial RSS | Final RSS | Growth |
|---|---|---|---|---|
| #1 | ORCA default (metadata caching on) | 56.5 MB | 98.5 MB | +42.0 MB |
| #2 | ORCA, optimizer_metadata_caching=off |
56.9 MB | 56.9 MB | +0 MB |
| #3 | ORCA off (optimizer=off) |
40.5 MB | 40.5 MB | +0 MB |
| #4 | Short-lived connections (new conn per query) | — | — | no growth (released at exit) |
Findings:
- ORCA default long-lived connections do accumulate memory.
- PostgreSQL's own planner (
optimizer=off) does not grow. - Short connections don't grow (memory is only released at process exit).
- Turning off metadata caching removes the growth — cache entries are never recycled on the steady-state query path, while temp query memory is never physically returned to the OS.
Root cause analysis (source-level)
The leak is a design issue, not a one-off code bug.
-
OptimizerMemoryContexthas a process-level lifetime. It is created as a child ofTopMemoryContext(Cloudberry: lazy-created on first ORCA query insrc/backend/optimizer/plan/planner.c:388-417; GPDB 6.27.1: at startup inpostinit.c:657).TopMemoryContextis only cleaned up at process exit, so every ORCA allocation under it lives for the entire backend lifetime. -
Per-query pools are only logically freed. Each query's
CAutoMemoryPoolcreates a childCMemoryPoolPalloccontext (src/backend/gpopt/utils/CMemoryPoolPalloc.cpp) whoseTearDown()callsMemoryContextDelete— but glibcfree()marks themallocblock free withoutmunmap, so RSS never drops. AllocSet'smaxBlockSizeis 8 MB; large blocks can't be returned even when fully free. -
CMDCacheentries are reused across queries and never cleared on the normal path.COptTasks::OptimizeTask()does not callCMDCache::Reset(); cache pools live under the same process-level context. This is whyoptimizer_metadata_caching=offeliminates the growth. -
CCacheFactoryuses its ownCMemoryPoolManagerpool — also process-level. -
optimizer_use_gpdb_allocatorscannot fix it. Whether true (Palloc → MemoryContext, process-level) or false (glibc malloc/free, which also doesn't return memory to the OS), memory is never physically returned after a query.
So Cloudberry 2.1.0 and GPDB 6.27.1 share exactly the same defect; the only Cloudberry differences (lazy context creation, MemoryContextDeclareAccountingRoot) are cosmetic and don't change allocation/release behavior.
Suggested fixes
- Short term (ops): cap long-lived connection lifetime (
pg_terminate_backendon sessions older than ~4h), use application connection-poolmaxLifetime, and/or setoptimizer_metadata_caching=offfor high-command-count ETL/COPY sessions (eliminates growth at a QPS cost — observed ~90→35 in test). - Long term (code):
- (A, recommended) Make
OptimizerMemoryContextper-query — create at query start, delete at end — and relocateCMDCache/CCacheFactorypools so cross-query metadata caching is managed independently. - (B)
MemoryContextReset(OptimizerMemoryContext)at end ofOptimizeTask()after movingCMDCache's pool out from under it. - (C) Use
mmap/munmapfor large allocations so freed blocks are returned to the OS.
- (A, recommended) Make
Repro scripts
orca_leak_setup.sql, orca_leak_reproduce.sh/.py (default/nocache/noop modes), orca_leak_shortconn.sh, orca_leak_run_all.sh.
References / source locations (Cloudberry)
src/backend/optimizer/plan/planner.c:388-417(lazyOptimizerMemoryContextcreate)src/backend/utils/init/postinit.c(cleanup at exit)src/backend/gpopt/utils/CMemoryPoolPalloc.cpp(Palloc bridge, logical-only free)src/backend/gpopt/gpdbwrappers.cpp:2505-2528(GPDBAllocSetContextCreate+ accounting root)src/backend/gpora/libgpopt/src/mdcache/CMDCache.cpp(cross-query metadata cache)src/backend/gpora/libgpos/src/memory/CCacheFactory.cppsrc/backend/gpora/libgpos/src/memory/CMemoryPoolManager.cpp
Reproduction scripts
Full self-contained reproduction scripts (tested on GPDB 6.27.1; portable to Cloudberry).
1. Setup tables (orca_leak_setup.sql)
-- orca_leak_setup.sql - 创建 GPORCA 内存泄漏复现测试表
-- 用法: psql -d <dbname> -f orca_leak_setup.sql
-- 确认 ORCA 生效
SHOW optimizer;
-- 创建有足够复杂度的表,确保 ORCA 优化器被调用
DROP TABLE IF EXISTS leak_test_t2;
DROP TABLE IF EXISTS leak_test_t1;
CREATE TABLE leak_test_t1 (
id serial PRIMARY KEY,
name text,
value numeric,
created_at timestamp default now()
) DISTRIBUTED BY (id);
CREATE TABLE leak_test_t2 (
id serial PRIMARY KEY,
t1_id int REFERENCES leak_test_t1(id),
status text,
amount numeric
) DISTRIBUTED BY (id);
-- 插入基础数据
INSERT INTO leak_test_t1 (name, value)
SELECT 'user_' || i, random() * 10000
FROM generate_series(1, 10000) i;
INSERT INTO leak_test_t2 (t1_id, status, amount)
SELECT (random() * 9999 + 1)::int,
(ARRAY['active','inactive','pending'])[ceil(random()*3)::int],
random() * 5000
FROM generate_series(1, 50000) i;
-- 验证数据
SELECT 'leak_test_t1' AS tbl, count(*) FROM leak_test_t1
UNION ALL
SELECT 'leak_test_t2', count(*) FROM leak_test_t2;
\echo '测试表创建完成'
2. Long-lived connection leak repro (orca_leak_reproduce.sh)
usage: ./orca_leak_reproduce.sh <db> <iterations> <mode> — modes: default | nocache | noop
#!/bin/bash
# orca_leak_reproduce.sh - 复现 GPORCA 长连接内存累积
#
# 核心思路: 生成 SQL 文件,psql -f 保持单连接执行,
# 同时后台监控进程 RSS
#
# 用法:
# ./orca_leak_reproduce.sh [数据库名] [迭代次数] [模式]
# 模式: default (ORCA默认) | nocache (ORCA无缓存) | noop (关闭ORCA)
set -euo pipefail
DBNAME=${1:-postgres}
ITERATIONS=${2:-1000}
MODE=${3:-default}
mode_desc() {
case "$1" in
default) echo "ORCA默认 (optimizer=on, metadata_caching=on)" ;;
nocache) echo "ORCA无缓存 (optimizer=on, metadata_caching=off)" ;;
noop) echo "无ORCA (optimizer=off)" ;;
esac
}
echo "============================================================"
echo "GPORCA 长连接内存累积复现测试"
echo "============================================================"
echo "数据库: $DBNAME"
echo "迭代次数: $ITERATIONS"
echo "测试模式: $(mode_desc "$MODE")"
echo ""
# 生成 SQL 文件
SQL_FILE=$(mktemp /tmp/orca_leak_XXXXXX.sql)
MONITOR_FILE=$(mktemp /tmp/orca_leak_XXXXXX.out)
trap 'rm -f "$SQL_FILE" "$MONITOR_FILE"' EXIT
# 输出 PID 标记
echo "\echo LEAK_TEST_PID_START" >> "$SQL_FILE"
echo "SELECT pg_backend_pid();" >> "$SQL_FILE"
echo "\echo LEAK_TEST_PID_END" >> "$SQL_FILE"
# 应用配置
case "$MODE" in
nocache) echo "SET optimizer_metadata_caching=off;" >> "$SQL_FILE" ;;
noop) echo "SET optimizer=off;" >> "$SQL_FILE" ;;
esac
# 确认配置
echo "SHOW optimizer;" >> "$SQL_FILE"
echo "SHOW optimizer_metadata_caching;" >> "$SQL_FILE"
# 预热
echo "SELECT count(*) FROM leak_test_t1 WHERE value > 5000;" >> "$SQL_FILE"
if [[ "$MODE" != "noop" ]]; then
echo "SELECT t1.name FROM leak_test_t1 t1 JOIN leak_test_t2 t2 ON t1.id=t2.t1_id LIMIT 1;" >> "$SQL_FILE"
echo "SELECT * FROM leak_test_t1 WHERE id IN (SELECT t1_id FROM leak_test_t2 WHERE amount > 4000) LIMIT 1;" >> "$SQL_FILE"
fi
echo "\echo LEAK_TEST_WARMUP_DONE" >> "$SQL_FILE"
# 生成循环查询
for ((i=1; i<=$ITERATIONS; i++)); do
case $(( i % 3 )) in
1) echo "SELECT count(*) FROM leak_test_t1 WHERE value > 5000;" >> "$SQL_FILE" ;;
2) if [[ "$MODE" != "noop" ]]; then
echo "SELECT t1.name FROM leak_test_t1 t1 JOIN leak_test_t2 t2 ON t1.id=t2.t1_id LIMIT 1;" >> "$SQL_FILE"
else
echo "SELECT count(*) FROM leak_test_t1 WHERE name LIKE 'user_1%';" >> "$SQL_FILE"
fi ;;
0) if [[ "$MODE" != "noop" ]]; then
echo "SELECT * FROM leak_test_t1 WHERE id IN (SELECT t1_id FROM leak_test_t2 WHERE amount > 4000) LIMIT 1;" >> "$SQL_FILE"
else
echo "SELECT max(value) FROM leak_test_t1;" >> "$SQL_FILE"
fi ;;
esac
# 每200次插入进度标记
if (( i % 200 == 0 )); then
echo "\echo LEAK_TEST_PROGRESS:$i" >> "$SQL_FILE"
fi
done
echo "\echo LEAK_TEST_DONE" >> "$SQL_FILE"
echo "SQL 文件已生成: $SQL_FILE ($(wc -l < "$SQL_FILE") 行)"
echo "正在执行查询..."
# 后台启动 psql
psql -X -d "$DBNAME" -f "$SQL_FILE" > "$MONITOR_FILE" 2>&1 &
PSQL_PID=$!
# 读取 RSS (KB)
get_rss() {
awk '/VmRSS/ {print $2}' /proc/$1/status 2>/dev/null || echo 0
}
# 等待 PID 出现
BACKEND_PID=""
for attempt in $(seq 1 30); do
if [[ -f "$MONITOR_FILE" ]]; then
BACKEND_PID=$(sed -n '/LEAK_TEST_PID_START/,/LEAK_TEST_PID_END/{/LEAK_TEST/d;p;}' "$MONITOR_FILE" 2>/dev/null | grep -oP '\d+' | head -1 || true)
if [[ -n "$BACKEND_PID" ]] && [[ "$BACKEND_PID" -gt 0 ]] 2>/dev/null; then
break
fi
fi
sleep 1
done
if [[ -z "$BACKEND_PID" ]] || ! [[ "$BACKEND_PID" -gt 0 ]] 2>/dev/null; then
echo "ERROR: 无法获取 Backend PID"
echo "psql 输出:"
head -20 "$MONITOR_FILE"
kill "$PSQL_PID" 2>/dev/null || true
exit 1
fi
echo "Backend PID: $BACKEND_PID"
# 等待预热完成
for attempt in $(seq 1 30); do
if grep -q "LEAK_TEST_WARMUP_DONE" "$MONITOR_FILE" 2>/dev/null; then
break
fi
sleep 1
done
# 显示当前配置
OPT_VAL=$(sed -n '/LEAK_TEST_PID_END/,/LEAK_TEST_WARMUP_DONE/{/on\|off/p;}' "$MONITOR_FILE" 2>/dev/null | head -1 | tr -d '[:space:]' || echo "?")
echo "当前配置: optimizer=$OPT_VAL"
INIT_RSS=$(get_rss "$BACKEND_PID")
PREV_RSS=$INIT_RSS
echo ""
printf "%-8s %-12s %-12s %-12s\n" "ROUND" "RSS_MB" "DELTA_KB" "AVG_KB"
echo "--------------------------------------------------"
START_TIME=$(date +%s)
LAST_PROGRESS=0
# 监控循环
while kill -0 "$PSQL_PID" 2>/dev/null; do
RSS=$(get_rss "$BACKEND_PID")
if [[ "$RSS" -eq 0 ]]; then
break
fi
# 从日志检测进度
CURRENT_PROGRESS=$(grep -c 'LEAK_TEST_PROGRESS' "$MONITOR_FILE" 2>/dev/null || echo 0)
CURRENT_PROGRESS=${CURRENT_PROGRESS//[^0-9]/}
CURRENT_PROGRESS=${CURRENT_PROGRESS:-0}
if [[ "$CURRENT_PROGRESS" -ne "$LAST_PROGRESS" ]]; then
ROUNDS=$((CURRENT_PROGRESS * 200))
DELTA=$((RSS - PREV_RSS))
AVG_KB=$(echo "scale=1; ($RSS - $INIT_RSS) / $ROUNDS" | bc 2>/dev/null || echo "0")
printf "%-8d %-12.1f %-12d %-12s\n" "$ROUNDS" "$(echo "scale=1; $RSS/1024" | bc)" "$DELTA" "$AVG_KB"
PREV_RSS=$RSS
LAST_PROGRESS=$CURRENT_PROGRESS
fi
sleep 3
done
# 等待 psql 完成
wait "$PSQL_PID" 2>/dev/null || true
# 最终采样
FINAL_RSS=$(get_rss "$BACKEND_PID")
if [[ "$FINAL_RSS" -eq 0 ]]; then
FINAL_RSS=$PREV_RSS
fi
END_TIME=$(date +%s)
ELAPSED=$((END_TIME - START_TIME))
GROWTH_KB=$((FINAL_RSS - INIT_RSS))
GROWTH_MB=$(echo "scale=1; $GROWTH_KB/1024" | bc)
AVG_PER_CMD=$(echo "scale=3; $GROWTH_KB/$ITERATIONS/1024" | bc)
QPS=$(echo "scale=1; $ITERATIONS/$ELAPSED" | bc 2>/dev/null || echo "N/A")
echo ""
echo "============================================================"
echo "测试结果"
echo "============================================================"
echo "初始 RSS: $(echo "scale=1; $INIT_RSS/1024" | bc) MB"
echo "最终 RSS: $(echo "scale=1; $FINAL_RSS/1024" | bc) MB"
echo "总增长: $GROWTH_MB MB"
echo "每查询平均增长: $AVG_PER_CMD MB"
echo "线上估算速率: 0.113 MB/cmd"
echo "总耗时: ${ELAPSED}s ($QPS qps)"
echo ""
case "$MODE" in
default)
if (( $(echo "$AVG_PER_CMD > 0.05" | bc -l 2>/dev/null || echo 0) )); then
echo ">>> 确认: ORCA 默认配置下长连接存在内存累积 <<<"
elif (( $(echo "$AVG_PER_CMD < 0.01" | bc -l 2>/dev/null || echo 0) )); then
echo ">>> 未检测到明显累积,可能需要更多迭代或更复杂查询 <<<"
else
echo ">>> 检测到轻微累积,与线上估算相比偏低,可能需要更长测试 <<<"
fi
;;
nocache)
echo ">>> 对比: 关闭缓存后每查询增长 $AVG_PER_CMD MB <<<"
;;
noop)
echo ">>> 对比: 关闭 ORCA 后每查询增长 $AVG_PER_CMD MB <<<"
;;
esac
3. Short-connection comparison (orca_leak_shortconn.sh)
usage: ./orca_leak_shortconn.sh <db> <iterations>
#!/bin/bash
# orca_leak_shortconn.sh - 短连接对比测试
# 每次查询新建连接,验证短连接模式下内存不会持续增长
#
# 用法: ./orca_leak_shortconn.sh [数据库名] [迭代次数]
set -euo pipefail
DBNAME=${1:-postgres}
ITERATIONS=${2:-200}
echo "============================================================"
echo "短连接对比测试 - 每次查询新建连接"
echo "============================================================"
echo "数据库: $DBNAME"
echo "迭代次数: $ITERATIONS"
echo ""
QUERY="SELECT count(*) FROM leak_test_t1 WHERE value > 5000"
# 采集所有 postgres 后端进程的总 RSS
get_total_postgres_rss() {
ps -eo rss,args 2>/dev/null | grep 'postgres:' | grep -v grep | awk '{sum+=$1} END {print sum+0}'
}
INIT_TOTAL=$(get_total_postgres_rss)
PREV_TOTAL=$INIT_TOTAL
echo "初始 postgres 总 RSS: $(echo "scale=1; $INIT_TOTAL/1024" | bc) MB"
echo ""
printf "%-8s %-14s %-12s\n" "ROUND" "TOTAL_RSS_MB" "DELTA_MB"
echo "----------------------------------"
for ((i=1; i<=$ITERATIONS; i++)); do
# 每次新建连接
psql -X -t -A -d "$DBNAME" -c "$QUERY" > /dev/null 2>&1
if (( i <= 10 || i % 50 == 0 || i == ITERATIONS )); then
TOTAL=$(get_total_postgres_rss)
DELTA=$((TOTAL - PREV_TOTAL))
printf "%-8d %-14.1f %-12.1f\n" "$i" "$(echo "scale=1; $TOTAL/1024" | bc)" "$(echo "scale=1; $DELTA/1024" | bc)"
PREV_TOTAL=$TOTAL
fi
done
FINAL_TOTAL=$(get_total_postgres_rss)
GROWTH_MB=$(echo "scale=1; ($FINAL_TOTAL - $INIT_TOTAL)/1024" | bc)
echo ""
echo "============================================================"
echo "短连接测试结果"
echo "============================================================"
echo "初始 postgres 总 RSS: $(echo "scale=1; $INIT_TOTAL/1024" | bc) MB"
echo "最终 postgres 总 RSS: $(echo "scale=1; $FINAL_TOTAL/1024" | bc) MB"
echo "总增长: $GROWTH_MB MB"
echo ""
if (( $(echo "scale=0; $GROWTH_MB / 1" | bc 2>/dev/null || echo 0) < 50 )); then
echo ">>> 短连接模式下无持续内存增长,连接退出后内存释放 <<<"
else
echo ">>> 注意: 存在可观增长 ($GROWTH_MB MB),可能有其他进程干扰 <<<"
fi
4. Run everything
# 1. setup (first run only)
psql -d <db> -f orca_leak_setup.sql
# 2. single tests
./orca_leak_reproduce.sh <db> 3000 default # ORCA default
./orca_leak_reproduce.sh <db> 3000 nocache # ORCA, metadata_caching=off
./orca_leak_reproduce.sh <db> 3000 noop # ORCA off
# 3. short-connection comparison
./orca_leak_shortconn.sh <db> 200
- Ngôn ngữ chính
- C
- Star
- 1.4k
- Fork
- 262
- Merge trung bình
- 4 ngày 20 giờ
- Pull request đã merge (30 ngày)
- 57
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của apache/cloudberry
-
[Bug] Expand fails to recognize user-defined tablespacesCó thể đã có người làm @nix-oss đã nhận 61 ngày trước. Đang mởtype: Bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
apache/cloudberry#1885 · 2 reaction ·
Maintainer thường phản hồi trong vòng 3 ngày
-
gpexpand first stage fails: 'SyncPackages' object has no attribute 'ret' (extension sync never runs; regression)Có thể đã có người làm @talmacschen-arch đã nhận 122 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
apache/cloudberry#1825 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Lance support: contrib/lance_fdw and the MCP server's Lance toolsCó thể đã có người làm @MisterRaindrop đã nhận 3 ngày trước. Đang mở
apache/cloudberry#2139 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 3 ngày
-
type: Bug
Độ khó 3/5 Nửa ngày Mức phù hợp với người mới 58/100
apache/cloudberry#2137 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
[Proposal] Remove `gpcontrib/gpmapreduce` starting from Cloudberry 3.0Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mởtype: Proposal
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 22/100
apache/cloudberry#2136 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 3 ngày
Tất cả issue của apache/cloudberry
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
vercel-labs/zerolang#442 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
Maintainer thường phản hồi trong vòng 1 ngày
-
triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
Maintainer thường phản hồi trong vòng 1 ngày
-
mewencode: base256[] and base128[] are indexed out of bounds by a non-ASCII octetCó thể đã có người làm @kazu-yamamoto đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
kazu-yamamoto/Mew#276 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[iwm] transaction_get() copies the whole payload regardless of the length asked forCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 91/100
FujiNetWIFI/fujinet-firmware#2045 ·
Maintainer thường phản hồi trong vòng 1 ngày