Skip to content

refactor(conn): Get/Put ref-counting with balancer quarantine#2235

Merged
asmyasnikov merged 51 commits into
masterfrom
refactor/pool-get-put-quarantine
Jul 15, 2026
Merged

refactor(conn): Get/Put ref-counting with balancer quarantine#2235
asmyasnikov merged 51 commits into
masterfrom
refactor/pool-get-put-quarantine

Conversation

@asmyasnikov

@asmyasnikov asmyasnikov commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

Refactors connection lifecycle to reference-counted conn.Pool cache and balancer-side quarantine:

  • conn.Pool — reference-counted cache: Get increments connValue.useCount (no gRPC dial), Put decrements; at zero the entry is removed from the map and gRPC is closed outside p.mu via tryPut
  • Balancer — owns a quarantine for dropped endpoints: a removed node stays alive for one discovery interval; the next successful ListEndpoints calls Put on the quarantine slice and closes gRPC when useCount reaches zero
  • nextState — the only place that calls Pool.Get/Put for discovery rotation (not nextConn / RPC path)
  • Removed AcquireConn, UpdateEndpointUsage, and per-conn usageMu; connectionsState.All() returns a defensive copy again
  • Balancer.ClosecloseMu.Lock(), sets b.closed, stops repeater, clearState + Put on active/quarantine, closes dedicated discovery gRPC (b.cc)

Builds on #2238 (Conn interface / endpoint API cleanup extracted there).

Discovery lifecycle (nextState)

On each successful ListEndpoints, applyDiscoveredEndpoints runs:

active := b.connections().All()
quarantine, newActive := nextState(ctx, b.pool, quarantine, active, endpoints)

Algorithm:

  1. Get every endpoint in the new discovery result → newActive
  2. Put every connection in the current quarantine (release previous quarantine)
  3. return (active, newActive) — old active becomes newQuarantine, freshly acquired refs become newActive

connValue.useCount is incremented by Pool.Get and decremented by Pool.Put. When it reaches 0, the map entry is deleted under p.mu and conn.Close runs outside the lock.

A dropped node is closed on the second discovery after removal: the first discovery only moves it to newQuarantine with useCount ≥ 1 and gRPC still alive.

Sequence diagram — pool ref-counts & quarantine

Five discovery phases with endpoints A, B (bootstrap), then C added and dropped. useCount is per endpoint key in conn.Pool.

sequenceDiagram
    autonumber
    participant LE as ListEndpoints
    participant B as Balancer
    participant P as Pool (connValue.useCount)
    participant cA as conn A
    participant cB as conn B
    participant cC as conn C

    Note over B,P: Phase 1 — bootstrap [A, B]<br/>quarantine=[], active=[]
    LE->>B: discovery [A, B]
    B->>P: Get(A), Get(B)
    P->>cA: new conn, useCount 0→1
    P->>cB: new conn, useCount 0→1
    B->>P: Put(quarantine ∅)
    Note over B: newQuarantine=[], newActive=[cA, cB]

    Note over B,P: Phase 2 — same [A, B]<br/>quarantine=[], active=[cA, cB]
    LE->>B: discovery [A, B]
    B->>P: Get(A), Get(B)
    P->>cA: useCount 1→2
    P->>cB: useCount 1→2
    B->>P: Put(quarantine ∅)
    Note over B: newQuarantine=[cA, cB], newActive=[cA, cB]

    Note over B,P: Phase 3 — same [A, B], release quarantine<br/>quarantine=[cA, cB], active=[cA, cB]
    LE->>B: discovery [A, B]
    B->>P: Get(A), Get(B)
    P->>cA: useCount 2→3
    P->>cB: useCount 2→3
    B->>P: Put(cA, cB from quarantine)
    P->>cA: useCount 3→2
    P->>cB: useCount 3→2
    Note over B: newQuarantine=[cA, cB], newActive=[cA, cB]<br/>net useCount unchanged (2)

    Note over B,P: Phase 4 — [A, B, C] node C added<br/>quarantine=[cA, cB], active=[cA, cB]
    LE->>B: discovery [A, B, C]
    B->>P: Get(A), Get(B), Get(C)
    P->>cA: useCount 2→3
    P->>cB: useCount 2→3
    P->>cC: new conn, useCount 0→1
    B->>P: Put(cA, cB from quarantine)
    P->>cA: useCount 3→2
    P->>cB: useCount 3→2
    Note over B: newQuarantine=[cA, cB], newActive=[cA, cB, cC]

    Note over B,P: Phase 5 — [A, B] node C dropped<br/>quarantine=[cA, cB], active=[cA, cB, cC]
    LE->>B: discovery [A, B]
    B->>P: Get(A), Get(B)
    P->>cA: useCount 2→3
    P->>cB: useCount 2→3
    B->>P: Put(cA, cB, cC from quarantine)
    P->>cA: useCount 3→2
    P->>cB: useCount 3→2
    Note over cC: useCount stays 1 — gRPC alive<br/>cC in newQuarantine only
    Note over B: newQuarantine=[cA, cB, cC], newActive=[cA, cB]

    Note over B,P: Phase 6 — same [A, B], quarantine released, C closed<br/>quarantine=[cA, cB, cC], active=[cA, cB]
    LE->>B: discovery [A, B]
    B->>P: Get(A), Get(B)
    P->>cA: useCount 2→3
    P->>cB: useCount 2→3
    B->>P: Put(cA, cB, cC from quarantine)
    P->>cA: useCount 3→2
    P->>cB: useCount 3→2
    P->>cC: useCount 1→0
    P->>cC: Close gRPC, remove from pool
    Note over B: newQuarantine=[cA, cB], newActive=[cA, cB]

    Note over B,P: Balancer.Close() — release all refs
    B->>B: stop discovery repeater
    B->>P: Put(cA, cB from active)
    P->>cA: useCount 2→1
    P->>cB: useCount 2→1
    B->>P: Put(cA, cB from quarantine)
    P->>cA: useCount 1→0 → Close gRPC
    P->>cB: useCount 1→0 → Close gRPC
    Note over B: also closes dedicated discovery gRPC (b.cc)
Loading

Test plan

  • go test -race ./internal/conn/...
  • go test -race ./internal/balancer/...
  • TestNextState — ref-count / quarantine unit test for nextState (7 discovery rounds incl. empty cluster)
  • TestBalancerDiscoveryDropClosesGRPC — E2E gRPC mock: node drop → quarantine → close on next discovery
  • TestPool_GetPut — deadlock, concurrent Get/Put, nil after Release, extra Put no-op, slow close outside mutex
  • TestBalancer_Close — Invoke after close, concurrent close idempotency, discovery during close
  • TestApplyDiscoveredEndpointsClosedPool, TestNextStateClosedPool, TestConnectionsState_AllReturnsDefensiveCopy

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

summary

Inferred base version: v3.144.2
Suggested version: v3.144.3

Copilot AI 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.

Pull request overview

Refactors the internal connection lifecycle to a reference-counted pool model, where balancer discovery rotates connection references through a one-interval quarantine to ensure dropped endpoints get their pooled gRPC connections closed promptly. This also removes the legacy “connection parking” feature and deprecates the associated TTL options.

Changes:

  • Introduces Pool.Get(ctx)/Put(ctx) ref-counting semantics and removes parking/last-usage tracking.
  • Adds balancer-side quarantine rotation (rotateConnectionRefs) and E2E coverage for “node dropped → gRPC closes next discovery”.
  • Deprecates WithConnectionTTL options and updates changelog/tests accordingly.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/integration/discovery_test.go Removes parking-based assertions/options from integration discovery test.
options.go Deprecates public ydb.WithConnectionTTL (no-op).
internal/xsync/map.go Removes size tracking; adds LoadOrStore.
internal/xsync/map_test.go Updates tests for removed map length/clear behavior.
internal/xsync/last_usage_guard.go Removes last-usage tracking implementation.
internal/xsync/last_usage_guard_test.go Removes tests/bench for last-usage guard.
internal/xsync/last_usage_guard_start.go Removes last-usage Start/stop implementation.
internal/mock/conn.go Updates conn mock for removed LastUsage API.
internal/endpoint/endpoint.go Adds endpoint comparator for stable diffing.
internal/conn/pool.go Implements ref-counted Get/Put, removes parking, adjusts removal semantics.
internal/conn/pool_test.go Reworks tests to cover refcount/quarantine behavior and keying.
internal/conn/grpc_client_stream.go Removes last-usage updates from stream operations.
internal/conn/grpc_client_stream_test.go Updates tests for removed last-usage field.
internal/conn/conn.go Removes last-usage/parking APIs; adds useCount.
internal/conn/conn_test.go Removes parking tests.
internal/balancer/connections_state.go Changes All() to return []conn.Conn.
internal/balancer/balancer.go Adds quarantine rotation and releases refs on Close().
internal/balancer/balancer_test.go Updates tests for new connection types and ref APIs; adds unit test for rotation.
internal/balancer/balancer_discovery_close_test.go Adds E2E test verifying gRPC close after endpoint drop + quarantine cycle.
driver.go Updates bootstrap discovery client to use new pool.Get(ctx, ...).
config/config.go Deprecates ConnectionTTL and config.WithConnectionTTL (no-op).
CHANGELOG.md Documents refactor and TTL deprecations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/xsync/map.go Outdated
Comment thread internal/balancer/connections_state.go
Comment thread internal/conn/pool.go Outdated

@robot-vibe-db robot-vibe-db Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Verdict: ❌ 1 critical issue(s) found

Critical issues

  • Critical | Medium: Pool.Get non-atomic getOrCreate + useCount.Add(1) allows a concurrent Put to close a conn between lookup and increment, returning a closed *conn whose useCount is resurrected from 0→1 — internal/conn/pool.go:46-54

Other findings

  • Major | Medium: Pool.Get returns nil when pool is closed; nil propagates through rotateConnectionRefs into applyDiscoveredEndpoints where cc.Endpoint().Touch() panics with nil dereference — internal/conn/pool.go:47, internal/balancer/balancer.go:214
  • Minor | High: connectionsState.All() now returns the internal s.all slice directly instead of a defensive copy, removing a safety net against future callers mutating the slice — internal/balancer/connections_state.go:48-54
  • Nit | High: ctx parameter in Pool.Get is unused in the method body (understood to be for API symmetry with Put) — internal/conn/pool.go:46

This review was generated automatically. Critical issues require attention; other findings are advisory.
If this comment was useful, please give it a 👍 — it helps us improve the review bot.

Comment thread internal/conn/pool.go Outdated
Comment thread internal/conn/pool.go
Comment thread internal/balancer/connections_state.go Outdated
Comment thread internal/conn/pool.go Outdated
@robot-vibe-db

robot-vibe-db Bot commented Jul 10, 2026

Copy link
Copy Markdown

Full analysis log

Analysis performed by claude, claude-opus-4-6.

Copilot AI 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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Comment thread CHANGELOG.md Outdated
Comment thread internal/conn/pool.go Outdated
Comment thread internal/conn/conn.go Outdated

@robot-vibe-db robot-vibe-db Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Verdict: ✅ No critical issues found

Critical issues

No critical issues found.

Other findings

  • Minor | Medium: Race window between Pool.Put (removeRef → Delete → Close) and concurrent Pool.Get (tryAddRef) — a concurrent Get can obtain a reference to a conn that Put is about to close — internal/conn/pool.go:78
  • Minor | High: conn.removeRef() allows useCount to go negative on mismatched Get/Put with no detection — internal/conn/conn.go:241
  • Minor | Medium: Pool.Get retry loop has no upper bound, no context cancellation check, and no isClosed recheck — internal/conn/pool.go:53
  • Nit | Medium: Pool.Get context parameter is documented as "reserved" but prevents callers from cancelling the retry loop — internal/conn/pool.go:48
  • Nit | Low: Balancer.quarantine has no synchronization primitive — safety depends on repeater.Stop() being synchronous, which is non-obvious — internal/balancer/balancer.go:79

This review was generated automatically. Critical issues require attention; other findings are advisory.
If this comment was useful, please give it a 👍 — it helps us improve the review bot.

Comment thread internal/conn/pool.go Outdated
Comment thread internal/conn/conn.go Outdated
Comment thread internal/conn/pool.go Outdated
Comment thread internal/balancer/balancer.go
Comment thread driver.go
Comment thread internal/conn/pool.go
@robot-vibe-db

robot-vibe-db Bot commented Jul 14, 2026

Copy link
Copy Markdown

Full analysis log

Analysis performed by claude, claude-opus-4-6.

Copilot AI 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.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread driver.go
asmyasnikov and others added 2 commits July 14, 2026 18:37
Stop discovery repeater after releasing closeMu to avoid deadlock with
in-flight applyDiscoveredEndpoints. Fail fast when discovery bootstrap
Get returns nil. Call internal discovery Client.Close from driver wrapper.
Export conn.ErrClosedPool and add regression tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the flaky scenario that held closeMu externally (which blocked
Close itself) with a stub repeater that verifies closeMu is released
before Stop is invoked.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread internal/balancer/balancer.go Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

@robot-vibe-db robot-vibe-db Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Verdict: ✅ No critical issues found

Critical issues

No critical issues found.

Other findings

  • Minor | Medium: Discovery gRPC connection leak during concurrent Close() + discoveryConn()internal/balancer/balancer.go:316
  • Minor | Medium: clearState double-Put on connections in both quarantine and all is correct but undocumented invariant — internal/balancer/balancer.go:234
  • Minor | Medium: Invoke/NewStream no longer guard against closed balancer at entry — internal/balancer/balancer.go:433
  • Nit | Medium: Pool.closed uses atomic.Bool but nearly all reads are under p.muinternal/conn/pool.go:36

This review was generated automatically. Critical issues require attention; other findings are advisory.
If this comment was useful, please give it a 👍 — it helps us improve the review bot.

Comment thread internal/balancer/balancer.go
Comment thread internal/balancer/balancer.go Outdated
Comment thread internal/balancer/balancer.go
Comment thread internal/conn/pool.go
@robot-vibe-db

robot-vibe-db Bot commented Jul 14, 2026

Copy link
Copy Markdown

Full analysis log

Analysis performed by claude, claude-opus-4-6.

Copilot AI 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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

Comment thread internal/balancer/balancer.go
Comment thread internal/balancer/balancer.go Outdated
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.94%. Comparing base (91665aa) to head (35a3ba3).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2235      +/-   ##
==========================================
+ Coverage   77.77%   77.94%   +0.17%     
==========================================
  Files         453      453              
  Lines       46084    46204     +120     
==========================================
+ Hits        35841    36013     +172     
+ Misses       8150     8105      -45     
+ Partials     2093     2086       -7     
Flag Coverage Δ
experiment 77.80% <100.00%> (+0.15%) ⬆️
go-1.21.x 74.78% <100.00%> (+0.15%) ⬆️
go-1.26.x 77.93% <100.00%> (+0.18%) ⬆️
integration 55.47% <61.94%> (-0.09%) ⬇️
macOS 48.33% <99.55%> (+0.51%) ⬆️
ubuntu 77.92% <100.00%> (+0.15%) ⬆️
unit 48.33% <99.55%> (+0.51%) ⬆️
windows 48.32% <99.55%> (+0.52%) ⬆️
ydb-24.4 55.04% <61.94%> (-0.07%) ⬇️
ydb-edge 55.31% <61.94%> (-0.10%) ⬇️
ydb-latest 55.21% <61.94%> (-0.08%) ⬇️
ydb-nightly 77.80% <100.00%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Comment thread internal/conn/pool.go
@github-actions

Copy link
Copy Markdown

🌋 SLO Test Results

🔴 8 workload(s) tested — 3 workload(s) exceeded failure thresholds

Commit: cb81e54 · View run

Workload Thresholds Duration Report
database-sql-query 🟡 Warning 10m 1s 📄 Report
native-query 🟡 Warning 10m 1s 📄 Report
native-table-node-hints 🟢 OK 10m 11s 📄 Report
native-table 🔴 Failure 10m 1s 📄 Report
native-bulk-upsert 🔴 Failure 10m 📄 Report
native-query-node-hints 🟢 OK 10m 11s 📄 Report
database-sql-table 🟡 Warning 10m 1s 📄 Report
native-table-over-query-service 🔴 Failure 10m 1s 📄 Report

Threshold violations:

database-sql-query:

  • read_retry_attempts: Value 0.01 > warning max 0

native-query:

  • write_retry_attempts: ▲ 25.9% (≥ 20% warn)

native-table:

  • read_retry_attempts: Value 0.02 > warning max 0
  • write_retry_attempts: Value 0.01 > warning max 0
  • write_retry_attempts: ▲ 95.8% (≥ 50% fail)

native-bulk-upsert:

  • read_retry_attempts: ▲ 159.6% (≥ 50% fail)

database-sql-table:

  • write_retry_attempts: ▲ 42.5% (≥ 20% warn)

native-table-over-query-service:

  • read_retry_attempts: ▲ 466.7% (≥ 50% fail)

Generated by ydb-slo-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants