Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
519ee19
client: expose keyspace-level GC mode
wfxr Jul 27, 2026
b4e6606
pd-ctl: add GC state JSON projections
wfxr Jul 27, 2026
6066b9d
pd-ctl: cover empty global GC barriers
wfxr Jul 27, 2026
045d188
pd-ctl: add gc-state commands
wfxr Jul 27, 2026
6242d9e
pd-ctl: test gc-state against PD
wfxr Jul 27, 2026
b25d2cb
test: require GC state fixtures
wfxr Jul 27, 2026
1575e08
docs: document pd-ctl gc-state
wfxr Jul 27, 2026
d71f055
client: fix GC test license header
wfxr Jul 27, 2026
21761f2
pd-ctl: fix gc-state static checks
wfxr Jul 27, 2026
4901984
test: isolate gc-state JSON decodes
wfxr Jul 27, 2026
1897495
test: isolate cluster ID initialization
wfxr Jul 27, 2026
2ea4924
pd-ctl: add global GC state view
wfxr Jul 27, 2026
188ae40
pd-ctl: test and document global GC state
wfxr Jul 27, 2026
02b9ba5
pd-ctl: derive NullKeyspace ID in help
wfxr Jul 28, 2026
d0245cb
pd-ctl: omit unified GC placeholders
wfxr Jul 28, 2026
ae378a1
pd-ctl: harden GC state inspection
wfxr Jul 28, 2026
201ba64
docs: add GC barrier output examples
wfxr Jul 29, 2026
da195a8
pd-ctl: hide expired GC barriers by default
wfxr Jul 30, 2026
d1c509c
test: adapt GC state tests to API v3
wfxr Aug 5, 2026
162c550
docs: add GC state global barrier refactor design
wfxr Aug 11, 2026
a08163c
docs: add GC state refactor implementation plan
wfxr Aug 11, 2026
682f5ab
docs: merge GC state command refactor stages
wfxr Aug 11, 2026
bc9a7d8
client: preserve GC mode with global barriers
wfxr Aug 11, 2026
90d0453
pd-ctl: use GetGCState for global barriers
wfxr Aug 11, 2026
cfc81e8
test: cover optional global GC state output
wfxr Aug 11, 2026
78c56dc
docs: update GC state troubleshooting workflow
wfxr Aug 11, 2026
0696343
pd-ctl: fix GC state test formatting
wfxr Aug 11, 2026
4badda9
docs: remove GC state agent workflow artifacts
wfxr Aug 12, 2026
2b3ff09
pd-ctl: make GC state RPC timeout configurable
wfxr Aug 12, 2026
a43f633
pd-ctl: address GC state review comments
wfxr Aug 13, 2026
c239a4a
pd-ctl: stabilize GC state snapshot tests
wfxr Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions client/clients/gc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,14 @@ func (b *GlobalGCBarrierInfo) isExpiredImpl(now time.Time) bool {
//nolint:revive
type GCState struct {
// The ID of the keyspace this GC state belongs to.
KeyspaceID uint32
TxnSafePoint uint64
GCSafePoint uint64
hasGCBarriers bool
gcBarriers []*GCBarrierInfo
KeyspaceID uint32
// IsKeyspaceLevelGC reports whether this state belongs to an independent
// keyspace-level GC scope.
IsKeyspaceLevelGC bool
TxnSafePoint uint64
GCSafePoint uint64
hasGCBarriers bool
gcBarriers []*GCBarrierInfo

hasGlobalGCBarriers bool
globalGCBarriers []*GlobalGCBarrierInfo
Expand Down
17 changes: 10 additions & 7 deletions client/gc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,15 +339,18 @@ func pbToGCState(pb *pdpb.GCState, reqStartTime time.Time, excludeGCBarriers boo
if pb.KeyspaceScope != nil {
keyspaceID = pb.KeyspaceScope.GetKeyspaceId()
}
var state gc.GCState
if excludeGCBarriers {
return gc.NewGCStateWithoutGCBarriers(keyspaceID, pb.GetTxnSafePoint(), pb.GetGcSafePoint())
}

gcBarriers := make([]*gc.GCBarrierInfo, 0, len(pb.GetGcBarriers()))
for _, b := range pb.GetGcBarriers() {
gcBarriers = append(gcBarriers, pbToGCBarrierInfo(b, reqStartTime))
state = gc.NewGCStateWithoutGCBarriers(keyspaceID, pb.GetTxnSafePoint(), pb.GetGcSafePoint())
} else {
gcBarriers := make([]*gc.GCBarrierInfo, 0, len(pb.GetGcBarriers()))
for _, b := range pb.GetGcBarriers() {
gcBarriers = append(gcBarriers, pbToGCBarrierInfo(b, reqStartTime))
}
state = gc.NewGCStateWithGCBarriers(keyspaceID, pb.GetTxnSafePoint(), pb.GetGcSafePoint(), gcBarriers)
}
return gc.NewGCStateWithGCBarriers(keyspaceID, pb.GetTxnSafePoint(), pb.GetGcSafePoint(), gcBarriers)
state.IsKeyspaceLevelGC = pb.GetIsKeyspaceLevelGc()
return state
}

func pbToGCStateWithGlobalGCBarriers(
Expand Down
98 changes: 98 additions & 0 deletions client/gc_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2026 TiKV Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pd

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/pingcap/kvproto/pkg/pdpb"
)

func TestPBToGCStatePreservesKeyspaceLevelGC(t *testing.T) {
requestStart := time.Unix(100, 0)
for _, testCase := range []struct {
name string
isKeyspaceLevelGC bool
excludeBarriers bool
}{
{name: "keyspace-level-with-barriers", isKeyspaceLevelGC: true},
{
name: "keyspace-level-without-barriers",
isKeyspaceLevelGC: true,
excludeBarriers: true,
},
{name: "unified-with-barriers", isKeyspaceLevelGC: false},
{
name: "unified-without-barriers",
isKeyspaceLevelGC: false,
excludeBarriers: true,
},
} {
t.Run(testCase.name, func(t *testing.T) {
pbState := &pdpb.GCState{
KeyspaceScope: wrapKeyspaceScope(42),
IsKeyspaceLevelGc: testCase.isKeyspaceLevelGC,
TxnSafePoint: 100,
GcSafePoint: 90,
GcBarriers: []*pdpb.GCBarrierInfo{
{BarrierId: "backup", BarrierTs: 95, TtlSeconds: 60},
},
}

state := pbToGCState(
pbState,
requestStart,
testCase.excludeBarriers,
)
require.Equal(t, testCase.isKeyspaceLevelGC,
state.IsKeyspaceLevelGC)
require.Equal(t, uint32(42), state.KeyspaceID)
require.Equal(t, uint64(100), state.TxnSafePoint)
require.Equal(t, uint64(90), state.GCSafePoint)
require.Equal(t, !testCase.excludeBarriers,
state.HasGCBarriers())
})
}
}

func TestPBToGCStateWithGlobalBarriersPreservesKeyspaceLevelGC(t *testing.T) {
requestStart := time.Unix(100, 0)
state := pbToGCStateWithGlobalGCBarriers(
&pdpb.GCState{
KeyspaceScope: wrapKeyspaceScope(42),
IsKeyspaceLevelGc: true,
TxnSafePoint: 100,
GcSafePoint: 90,
},
&pdpb.GlobalGCBarriersInfo{
Barriers: []*pdpb.GlobalGCBarrierInfo{
{BarrierId: "snapshot", BarrierTs: 95, TtlSeconds: 60},
},
},
requestStart,
true,
)

require.True(t, state.IsKeyspaceLevelGC)
require.False(t, state.HasGCBarriers())
require.True(t, state.HasGlobalGCBarriers())
barriers, err := state.GetGlobalGCBarriers()
require.NoError(t, err)
require.Len(t, barriers, 1)
require.Equal(t, "snapshot", barriers[0].BarrierID)
}
1 change: 1 addition & 0 deletions pkg/storage/endpoint/cluster_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestMain(m *testing.M) {
}

func TestInitClusterID(t *testing.T) {
t.Cleanup(keypath.ResetClusterID)
re := require.New(t)
_, client, clean := etcdutil.NewTestEtcdCluster(t, 1, nil)
defer clean()
Expand Down
11 changes: 9 additions & 2 deletions tests/integrations/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2872,9 +2872,16 @@ func (s *clientStatefulTestSuite) TestGetAllKeyspaceGCStates() {
re.NoError(err)
res, err = cli.GetAllKeyspacesGCStates(ctx, gc.ExcludeGCBarriers(false), gc.ExcludeGlobalGCBarriers(false))
re.NoError(err)
state, ok = res.GCStates[2]
state1, ok := res.GCStates[1]
re.True(ok)
gcBarriers, err = state.GetGCBarriers()
re.True(state1.IsKeyspaceLevelGC)
state2, ok := res.GCStates[2]
re.True(ok)
re.True(state2.IsKeyspaceLevelGC)
state3, ok := res.GCStates[3]
re.True(ok)
re.False(state3.IsKeyspaceLevelGC)
gcBarriers, err = state2.GetGCBarriers()
re.NoError(err)
re.Equal("b4", gcBarriers[0].BarrierID)
re.Equal(uint64(14), gcBarriers[0].BarrierTS)
Expand Down
94 changes: 94 additions & 0 deletions tools/pd-ctl/README.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The markdown in this document has strange, premature line breaks. I don't think they're necessary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed

Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,97 @@
## Usage

The details about how to use `pd-ctl` can be found in [PD Control User Guide](https://docs.pingcap.com/tidb/dev/pd-control).

## GC state troubleshooting

Use `gc-state` to inspect the safe points and barriers that can block GC. The command is read-only and emits deterministic JSON for scripts and diffs.

The default `keyspace` and `all` views request global barriers. They omit barriers that PD returns with a zero TTL because those barriers normally represent expired barriers awaiting lazy deletion. Add `--include-expired` to include those barriers in the existing `gc_barriers` or `global_gc_barriers` array with `ttl_seconds` set to `0`. An empty `global_gc_barriers` array means PD returned no global barriers.

For example, inspect one keyspace and include zero-TTL barriers:

```bash
pd-ctl gc-state keyspace 42 --include-expired
```

Use `keyspace` when diagnosing one GC scope. Inspect a keyspace by its decimal ID:

```bash
pd-ctl gc-state keyspace 42
```

```json
{
"requested_keyspace_id": 42,
"effective_keyspace_id": 4294967295,
"is_keyspace_level_gc": false,
"txn_safe_point": 465000000000000000,
"gc_safe_point": 464900000000000000,
"gc_barriers": [
{
"barrier_id": "br",
"barrier_ts": 464950000000000000,
"ttl_seconds": 3600
}
],
"global_gc_barriers": [
{
"barrier_id": "native_br",
"barrier_ts": 464940000000000000,
"ttl_seconds": 9223372036854775807
}
]
}
```

The response contains both `requested_keyspace_id` and `effective_keyspace_id`. They are equal for keyspace-level GC. A keyspace that uses unified GC returns `4294967295`, the NullKeyspace ID, as its effective scope. The local and global barriers come from one `GetGCState` read. The same global list applies to every keyspace.

Use `all` only when you need every effective GC scope because it enumerates all keyspaces. Inspect every effective GC scope together with cluster-wide state:

```bash
pd-ctl gc-state all
```

```json
{
"gc_states": [
{
"keyspace_id": 4294967295,
"is_keyspace_level_gc": false,
"txn_safe_point": 465000000000000000,
"gc_safe_point": 464900000000000000,
"gc_barriers": [
{
"barrier_id": "br",
"barrier_ts": 464950000000000000,
"ttl_seconds": 3600
}
]
}
],
"global_gc_barriers": [
{
"barrier_id": "native_br",
"barrier_ts": 464940000000000000,
"ttl_seconds": 9223372036854775807
}
]
}
```

The combined response sorts effective `gc_states` by `keyspace_id` and reports cluster-wide barriers once in the top-level `global_gc_barriers` array. Unified GC keyspaces share the NullKeyspace scope, so their marker records are not reported as separate states. The real NullKeyspace state appears once with its safe points and local barriers. When no local or global barriers exist, the corresponding arrays are encoded as `[]`. Barrier TTLs use remaining seconds, and `9223372036854775807` means that a barrier never expires. Because PD rounds remaining TTLs down to whole seconds, a zero TTL can also represent a barrier with less than one second remaining.

GC state RPCs time out after 30 seconds by default. The timeout applies to both subcommands and must be a positive duration. If a large cluster takes longer to enumerate, increase it with `--timeout` using Go duration syntax:

```bash
pd-ctl gc-state all --timeout 2m
```

Use `--exclude-global-barriers` to skip the global-barrier read and remove the `global_gc_barriers` field from the JSON output. The flag applies to both subcommands:

```bash
pd-ctl gc-state keyspace 42 --exclude-global-barriers
pd-ctl gc-state all --exclude-global-barriers --include-expired
```

When combined with `--include-expired`, exclusion wins for global barriers, while expired local barriers remain visible.
Loading
Loading