# Shared TiProxy for Multi-tenant SharedPool

## Overview

This document describes how Cluster Service NG should support a **shared TiProxy** for a **multi-tenant SharedPool**.

Today the codebase assumes:

- each TiDB cluster owns one TiProxyGroup,
- the TiProxy component id is equal to `clusterId`,
- TiDBGroup resolves its TiProxy indirectly through `clusterId`.

That assumption works for dedicated-per-cluster TiProxy, but it breaks the requirement below:

- one multi-tenant SharedPool has exactly one shared TiProxyGroup,
- the shared TiProxyGroup is created only when the SharedPool itself is created,
- TiDB clusters inside that multi-tenant SharedPool reuse the shared TiProxy instead of creating a new one.

This design keeps the existing external API shape and does **not** add a persisted `tiproxy_id` field. TiProxy ownership is always derived from the SharedPool.

## Goals

- Support one shared TiProxyGroup per multi-tenant SharedPool.
- Keep the current dedicated TiProxy behavior for non-multi-tenant SharedPools.
- Derive TiProxy ownership from SharedPool rules instead of persisting a new `tiproxy_id`.
- Minimize API changes.
- Keep existing TiDBGroup / LB / external DNS workflows working after the routing change.

## Non-goals

- No protobuf API change for TiDBGroup or TiDBCluster.
- No new MySQL column for `tiproxy_id`.
- No compatibility layer for mixed TiProxy ownership inside the same SharedPool.
- No attempt to preserve `UserClusterId` or `UserTenantId` in shared TiProxy template rendering. Shared TiProxy does not need them.

## Behavior

### SharedPool types

There are two effective SharedPool modes for TiProxy ownership:

- **multi-tenant SharedPool**
  - owns one shared TiProxyGroup,
  - TiDB clusters in the SharedPool reuse that TiProxyGroup,
  - deleting a TiDB cluster must not delete the shared TiProxy.
- **non-multi-tenant SharedPool**
  - keeps the current behavior,
  - each TiDB cluster owns one dedicated TiProxyGroup,
  - deleting the TiDB cluster deletes its dedicated TiProxy.

### TiProxy ownership rule

TiProxy ownership must be resolved only from:

- the SharedPool,
- the target cluster id.

The rule is:

- if the SharedPool is non-multi-tenant, `tiproxyID = clusterId`
- if the SharedPool is multi-tenant, `tiproxyID = sharedPool default component id`

The shared TiProxy id should reuse the existing SharedPool default component id returned by `sharedPool.GetDefaultComponenId()`. This avoids introducing a new naming convention.

## Source of Truth

The TiProxy ownership rule must be centralized behind shared helpers. Business code must stop directly encoding assumptions like:

- `comp.GetId() == clusterId`
- `tidbGroup.GetTiproxyId() == tidbGroup.GetClusterId()`
- `tidbClusterRepo.Load(ctx, comp.GetId())` for every TiProxy

Recommended helper set:

```go
func IsMultiTenantSharedPool(sharedPool *entity.SharedPool) bool
func ResolveTiProxyID(sharedPool *entity.SharedPool, clusterId string) string
func FindTiProxyComponent(sharedPool *entity.SharedPool, clusterId string) *entity.Component
```

Implementation notes:

- the multi-tenant decision should be encapsulated in `IsMultiTenantSharedPool`
- callers should not compare against `"shared-multi-tenant"` directly
- all TiProxy lookup logic should go through these helpers

## Creation Flow

### CreateSharedPool

For a multi-tenant SharedPool:

- generate a SharedPool-level `TIPROXY` component during SharedPool creation,
- size it through existing `genDefaultTiProxyComponent`,
- include it in `sharedPool.ComponentGroups`,
- apply it through SharedPool DAG.

For a non-multi-tenant SharedPool:

- do not create a SharedPool-level TiProxy,
- keep current behavior unchanged.

### CreateTiDBCluster

For a multi-tenant SharedPool:

- do not create a cluster-level TiProxy component,
- do not submit cluster create DAG targets for a dedicated TiProxy,
- the cluster reuses the SharedPool-level shared TiProxy.

For a non-multi-tenant SharedPool:

- keep the existing `clusterId -> TiProxyGroup` behavior.

### CreateTiDBGroup

TiDBGroup creation must resolve its target TiProxy through SharedPool helpers.

This affects:

- DAG target component ids,
- load balancer target lookup,
- external DNS target lookup,
- TiDBGroup status composition.

## Deletion Flow

### DeleteTiDBCluster

For a multi-tenant SharedPool:

- do not mark the shared TiProxy as deleting,
- do not scale its node count to zero,
- do not submit delete actions for shared TiProxy as part of cluster deletion.

For a non-multi-tenant SharedPool:

- keep the existing dedicated TiProxy deletion behavior.

### DeleteSharedPool

For a multi-tenant SharedPool:

- the shared TiProxy lifecycle stays attached to SharedPool deletion,
- no extra cluster-specific cleanup path is needed.

## ApplyTiProxyGroup Action

`ApplyTiProxyGroupAction` is the main behavior change.

Current code assumes:

- TiProxy component id equals TiDB cluster id,
- every TiProxy can load exactly one TiDB cluster by `comp.GetId()`,
- template args always include cluster-specific values.

That must be split into two paths:

- **dedicated TiProxy path**
  - used for non-multi-tenant SharedPools,
  - preserves current behavior.
- **shared TiProxy path**
  - used for multi-tenant SharedPools,
  - must not load TiDB cluster by `comp.GetId()`,
  - must not require `UserClusterId`,
  - must not require `UserTenantId`,
  - must render TiProxy from SharedPool-level information only.

### Public endpoint config

`setTiProxyPublicEndpointConfig()` also currently assumes `tiproxy id == cluster id`.

The behavior should become:

- dedicated TiProxy: aggregate TiDBGroups belonging to that cluster
- shared TiProxy: aggregate all TiDBGroups in the SharedPool that route through the shared TiProxy

Without this change, the shared TiProxy config would not reflect all tenants using it.

## Query and Status Behavior

The following behavior must resolve TiProxy through SharedPool ownership rules:

- TiDB cluster TiProxy quota query
- TiDBGroup status composition
- LB target lookup
- external DNS service lookup
- update paths that mark TiProxy `MODIFYING`

The old implicit rule `tiproxy == clusterId` must not remain in query or status paths, otherwise the system will create correctly but report or mutate incorrectly.

## Compatibility

This design is compatible with current non-multi-tenant SharedPools because:

- their TiProxy id resolution still returns `clusterId`,
- no external API changes are introduced,
- no storage migration is required for TiProxy ownership.

Existing helper `TiDBGroup.GetTiproxyId()` should not remain a source of truth. It either needs to be removed or reduced to a thin deprecated wrapper that delegates to SharedPool-based resolution in the caller layer.

## Risks

### Shared TiProxy config aggregation

Shared TiProxy config is no longer cluster-scoped. Any code that derives TiProxy config from a single cluster must be updated to aggregate over all TiDBGroups using the shared TiProxy.

### Shared service annotations

External DNS and LB-related behavior now operates on a shared TiProxy service in multi-tenant mode. Mutations must remain additive and idempotent so one tenant does not overwrite another tenant's hostnames or endpoint list.

### Hidden hardcoded assumptions

The largest regression risk is not create/delete flow. It is small hardcoded checks scattered across query, update, and workflow code:

- `ComponentType_TIPROXY && comp.GetId() == clusterId`
- `GetTiproxyId()`
- `Load(ctx, comp.GetId())` for TiProxy

All of them must be replaced.

## Code Modification Checklist

### 1. Introduce shared helper functions

Files:

- `domain/entity/utils.go` or a new helper file under `domain/server/` / `domain/entity/`

Tasks:

- add `IsMultiTenantSharedPool(sharedPool)`
- add `ResolveTiProxyID(sharedPool, clusterId)`
- add `FindTiProxyComponent(sharedPool, clusterId)`
- stop adding new direct comparisons against `clusterId` for TiProxy lookup

### 2. Create shared TiProxy during SharedPool creation

Files:

- `domain/server/create_sharedpool.go`
- `domain/server/gen_component.go`

Tasks:

- during SharedPool creation, detect multi-tenant SharedPool
- generate one SharedPool-level `TIPROXY` component only for multi-tenant SharedPools
- size it with existing RU / node spec logic
- ensure it is stored in `sharedPool.ComponentGroups`

### 3. Apply shared TiProxy in SharedPool DAG

Files:

- `domain/workflow/dag/node.go`

Tasks:

- add `action.ApplyTiProxyGroupActionName` to `createSharedPoolDag()`
- ensure the shared TiProxy can be applied during SharedPool creation
- keep existing DAG behavior for non-multi-tenant SharedPools

### 4. Stop creating dedicated TiProxy for multi-tenant clusters

Files:

- `domain/server/create_tidbcluster.go`

Tasks:

- detect multi-tenant SharedPool
- skip `generatedTiProxyGroup()` for multi-tenant SharedPools
- avoid adding dedicated TiProxy targets into cluster create DAG args for multi-tenant SharedPools
- preserve current behavior for non-multi-tenant SharedPools

### 5. Stop deleting shared TiProxy on cluster deletion

Files:

- `domain/server/delete_tidbcluster.go`

Tasks:

- replace direct `clusterId -> TiProxy` lookup with helper-based resolution
- if SharedPool is multi-tenant, skip TiProxy delete path entirely
- if SharedPool is non-multi-tenant, preserve current delete behavior

### 6. Replace TiProxy lookup in TiDBGroup create / update flows

Files:

- `domain/server/create_tidbgroup.go`
- `domain/server/update_tidbgroup.go`

Tasks:

- replace uses of `GetTiproxyId()` or `clusterId` equality with helper-based resolution
- ensure TiDBGroup DAG targets include the resolved TiProxy component id
- ensure endpoint update flow marks the correct TiProxy component as `MODIFYING`

### 7. Refactor ApplyTiProxyGroupAction

Files:

- `domain/workflow/action/apply_tiproxy_group.go`

Tasks:

- split action logic into dedicated TiProxy path and shared TiProxy path
- dedicated path keeps current cluster-based behavior
- shared path must not call `tidbClusterRepo.Load(ctx, comp.GetId(), ...)`
- shared path must not require `UserClusterId`
- shared path must not require `UserTenantId`
- keep template rendering driven by SharedPool-level inputs

### 8. Refactor shared public endpoint aggregation

Files:

- `domain/workflow/action/apply_tiproxy_group.go`

Tasks:

- update `setTiProxyPublicEndpointConfig()`
- dedicated path: aggregate TiDBGroups for the cluster
- shared path: aggregate all TiDBGroups in the SharedPool routed through shared TiProxy
- keep config mutation idempotent

### 9. Replace TiProxy lookup in LB and external DNS flows

Files:

- `domain/workflow/action/apply_lb.go`
- `domain/workflow/action/delete_lb.go`
- `domain/workflow/action/apply_tiproxy_external_dns.go`

Tasks:

- replace direct `GetTiproxyId()` usage with helper-based resolution
- keep lookup based on SharedPool ownership rules
- preserve additive external DNS annotation behavior for shared services

### 10. Replace TiProxy lookup in query / status paths

Files:

- `domain/server/get_tidbcluster.go`
- `pkg/storage/mysql/tidbgroup/cvt.go`

Tasks:

- resolve TiProxy quota through SharedPool helper logic
- resolve TiDBGroup status composition through SharedPool helper logic
- remove dependence on `tiproxy == clusterId`

### 11. Remove or deprecate implicit helper

Files:

- `domain/entity/utils.go`

Tasks:

- remove `func (tidbGroup *TiDBGroup) GetTiproxyId() string`, or
- clearly deprecate it and ensure business code no longer relies on it as source of truth

### 12. Update tests

Files likely affected:

- `domain/server/create_tidbcluster_test.go`
- `domain/server/create_tidbgroup_test.go`
- `domain/server/update_tidbgroup_test.go`
- `domain/server/delete_tidbcluster_test.go`
- `domain/workflow/action/apply_tiproxy_group_test.go`
- `domain/workflow/action/apply_lb_test.go`
- `domain/workflow/action/delete_lb_test.go`
- `domain/workflow/action/apply_tiproxy_external_dns_test.go`
- `domain/server/get_tidbcluster_test.go`
- `pkg/storage/mysql/tidbgroup/*_test.go`

Tasks:

- add multi-tenant SharedPool cases
- verify SharedPool create produces one shared TiProxy
- verify cluster create in multi-tenant mode does not create dedicated TiProxy
- verify cluster delete in multi-tenant mode does not delete shared TiProxy
- verify TiDBGroup / LB / external DNS resolve to shared TiProxy
- verify non-multi-tenant behavior remains unchanged

## Suggested Delivery Order

1. Add shared helper functions and replace lookup call sites.
2. Enable shared TiProxy creation in SharedPool create path and DAG.
3. Change cluster create/delete flow for multi-tenant SharedPools.
4. Refactor `ApplyTiProxyGroupAction` and public endpoint aggregation.
5. Update query/status/LB/external DNS paths.
6. Add regression tests for both multi-tenant and non-multi-tenant SharedPools.

## Acceptance Criteria

- creating a multi-tenant SharedPool creates exactly one shared TiProxyGroup
- creating a TiDB cluster in that SharedPool does not create a new TiProxyGroup
- deleting that TiDB cluster does not delete the shared TiProxyGroup
- TiDBGroup, LB, external DNS, and status paths all resolve to the shared TiProxy correctly
- non-multi-tenant SharedPools continue to use dedicated cluster-level TiProxyGroups
