Skip to content

Make mtree parallelism controllable and predictable - #170

Open
mason-sharp wants to merge 2 commits into
mainfrom
fix/ACE-218/mtree-cpu
Open

mason-sharp wants to merge 2 commits into
mainfrom
fix/ACE-218/mtree-cpu

Conversation

@mason-sharp

Copy link
Copy Markdown
Member

A customer running mtree build on 28M-row tables saw their 16-core database servers fully loaded for the whole leaf-hash phase, and lowering --max-cpu-ratio to 0.1 made no real difference. Reproduced on 2-core nodes from a 14-core client with -m 0.1: 3 hash backends per node, 2 Postgres parallel workers under each, and 1 idle pooled connection.

Causes:

  • mtree build and update computed workers as ceil(cores * ratio * 2). The doubling was undocumented and the other mtree paths did not have it, so -m 0.1 on 14 cores gave 3 workers instead of 1.
  • The count is a share of the cores on the host running ACE, never the database server, and nothing capped it.
  • Each hash query could also fan out into Postgres parallel workers, which ACE never controlled.
  • Nothing logged the worker count.

Changes:

  • One worker formula for all mtree paths: round(cores * ratio), never below 1. The doubling is gone, and rounding matches table-diff. The default of 0.5 now runs half as many build and update workers.
  • --max-connections / -M on mtree build, update, and table-diff, with mtree.max_connections in ace.yaml and max_connections on the HTTP API. Same meaning as on the diff commands: a hard cap on the pool per node, applied to every pool the task opens. One connection holds the tree's transaction, so N connections means N - 1 hash workers, and a value of 1 is rejected.
  • Every ACE connection now sends max_parallel_workers_per_gather = 0 as a session setting. postgres.max_parallel_workers_per_gather in ace.yaml overrides it; -1 leaves the server's setting alone.
  • Each mtree phase logs the worker count with the host CPU count and the flags it came from; build also logs the pool size per node. The task store records the ratio and cap for build, update, and diff.

A customer running mtree build on 28M-row tables saw their 16-core
database servers fully loaded for the whole leaf-hash phase, and
lowering --max-cpu-ratio to 0.1 made no real difference. Reproduced on
2-core nodes from a 14-core client with -m 0.1: 3 hash backends per
node, 2 Postgres parallel workers under each, and 1 idle pooled
connection.

Causes:
- mtree build and update computed workers as ceil(cores * ratio * 2).
  The doubling was undocumented and the other mtree paths did not
  have it, so -m 0.1 on 14 cores gave 3 workers instead of 1.
- The count is a share of the cores on the host running ACE, never the
  database server, and nothing capped it.
- Each hash query could also fan out into Postgres parallel workers,
  which ACE never controlled.
- Nothing logged the worker count.

Changes:
- One worker formula for all mtree paths: round(cores * ratio), never
  below 1. The doubling is gone, and rounding matches table-diff. The
  default of 0.5 now runs half as many build and update workers.
- --max-connections / -M on mtree build, update, and table-diff, with
  mtree.max_connections in ace.yaml and max_connections on the HTTP
  API. Same meaning as on the diff commands: a hard cap on the pool
  per node, applied to every pool the task opens. One connection holds
  the tree's transaction, so N connections means N - 1 hash workers,
  and a value of 1 is rejected.
- Every ACE connection now sends max_parallel_workers_per_gather = 0
  as a session setting. postgres.max_parallel_workers_per_gather in
  ace.yaml overrides it; -1 leaves the server's setting alone.
- Each mtree phase logs the worker count with the host CPU count and
  the flags it came from; build also logs the pool size per node. The
  task store records the ratio and cap for build, update, and diff.
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 56ae10bb-5304-4064-89e9-6d8e11eb7e8b

📥 Commits

Reviewing files that changed from the base of the PR and between 77ff7be and e310cd3.

📒 Files selected for processing (3)
  • docs/CHANGELOG.md
  • internal/consistency/mtree/merkle.go
  • internal/consistency/mtree/workers_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/CHANGELOG.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds per-node connection limits and a shared worker-count calculation to Merkle-tree commands. CLI and HTTP inputs expose the limit. PostgreSQL connections also receive a configurable max_parallel_workers_per_gather setting.

Changes

Merkle resource controls

Layer / File(s) Summary
Worker calculation and connection pools
pkg/config/config.go, internal/cli/default_config.yaml, ace.sample.yaml, internal/consistency/mtree/merkle.go, internal/consistency/mtree/workers_test.go, docs/CHANGELOG.md
MerkleTreeTask resolves and validates MaxConnections. A shared calculation rounds the CPU-ratio worker count, enforces at least one worker, and caps workers at MaxConnections - 1. Build, update, and stale-block refresh use this calculation. Connection options apply the pool limit. Tests cover calculations, validation, fallback, and pool options.
Shared pools for table-diff
internal/consistency/mtree/merkle.go, internal/consistency/mtree/workers_test.go, docs/CHANGELOG.md
DiffMtree creates one pool per node and passes those pools to range comparison. Workers use the shared pools. If a node pool is missing, the pair is marked incomplete. Tests cover pool sizing and incomplete comparisons.
CLI and HTTP connection-limit inputs
internal/cli/cli.go, internal/api/http/handler.go, docs/api.md, docs/commands/mtree/*, docs/http-api.md, docs/openapi.yaml, docs/configuration.md, docs/best_practices.md, docs/performance.md, docs/CHANGELOG.md
The build, update, and table-diff CLI commands and HTTP handlers pass max_connections to the task. The documentation describes the input, defaults, minimum value, worker counts, and pool sizing.
PostgreSQL parallel-worker setting
pkg/config/config.go, ace.sample.yaml, internal/cli/default_config.yaml, internal/infra/db/auth.go, internal/infra/db/auth_test.go, docs/configuration.md, docs/performance.md, docs/CHANGELOG.md
The runtime setting defaults to 0, uses a configured nonnegative value, and is omitted when configured as -1. Tests check these cases. The documentation describes the setting and its effect on PostgreSQL parallel workers.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to e310c

The resource controls appear mergeable after normal checks. No concrete connection-limit or incomplete-comparison failure was established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to e310c

The change generally reduces database load and preserves certificate-authenticated access. Connection limits are adjustable per-task settings, not a server-wide budget. No introduced security vulnerability was established, but deployment-wide effects and connection usage have not been fully validated.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A caller able to submit an authenticated Merkle task can select resource settings affecting that task’s selected database nodes and potentially competing database workloads. The global connection-setting change also affects non-Merkle ACE sessions. Exposure depends on configured clusters and concurrent tasks; no tenant-wide or environment-wide isolation guarantee was established.

Trust Boundaries and Controls

  • observed — The API requires a verified client certificate. Middleware validates certificate lifetime, optional CN allowlisting and revocation policy, then carries the CN-derived identity into the task. No separate resource-override authorization is applied: mtree.max_connections is a fallback setting, not an administrator-enforced ceiling.

Resilience and Maintainability Implications

  • observed — Incomplete row comparisons fail closed. Stale-hash refresh remains best effort: each node refresh has its own transaction, failures are logged, and successful healing is recorded only when both sides succeed. Base-to-head comparison shows this non-fatal refresh policy predates the PR; it is not evidence of newly weakened comparison guarantees.

Hardening Proposals

  • proposed — If deployment policy requires protection against resource-heavy authenticated callers, add a separately defined server-enforced connection ceiling or aggregate task budget. Keep it distinct from the current caller-overridable default, and specify whether replication sessions count toward that budget.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 7 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: making mtree parallelism configurable and predictable.
Description check ✅ Passed The description directly explains the motivating issue and the changes to worker calculation, connection limits, PostgreSQL parallelism, and logging.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 7 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


A rabbit checks the worker count,
And caps each node’s connections.
Shared pools help compare trees,
While settings guide the query streams.
I twitch my nose and hop along,
The limits now are clearly drawn.

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 6 complexity · 6 duplication

Metric Results
Complexity 6
Duplication 6

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@danolivo danolivo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commitable, if author fixes 'hard cap' issue - '--max-connections' for mtree table-diff

mtree table-diff opened a pool for every compare worker and another
pair for every node pair during the tree traversal, all held open until
the diff finished. --max-connections bounded each of those pools on its
own, so with W workers on a three-node cluster one node could see W + 2
pools, each as large as the cap. The cap was not a cap.

The diff now opens one pool per node up front and hands it to the
traversal, the compare workers, and the stale-block refresh. Under
--max-connections the pool is the cap; without one it is a connection
per worker plus one for the refresh transaction, as build already does.
Workers that find no pool for a node record the pair as incomplete
instead of dialing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants