Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
128 changes: 128 additions & 0 deletions tools/pd-gc-barrier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# pd-gc-barrier

`pd-gc-barrier` is a standalone command-line tool for inspecting, creating, updating, and deleting keyspace GC barriers through PD's gRPC APIs. It operates on an explicitly selected keyspace with keyspace-level GC enabled.

## Build

From the repository root:

```sh
(
cd tools || exit 1
CGO_ENABLED=0 go build -tags nextgen -o ../bin/pd-gc-barrier ./pd-gc-barrier
)
./bin/pd-gc-barrier --help
```

Use Go 1.25 or newer for the repository's development environment. Set `GOOS` and `GOARCH` when building for another platform. Run the binary on a machine that can reach the PD endpoints.

## Commands

```text
pd-gc-barrier --pd <endpoints> --keyspace-id <id> show
pd-gc-barrier --pd <endpoints> --keyspace-id <id> set <barrier-id> <tso-or-rfc3339-time> --ttl <duration|never>
pd-gc-barrier --pd <endpoints> --keyspace-id <id> delete <barrier-id> [--execute]
```

Both `--pd` and `--keyspace-id` are required for every command. The examples below use placeholder values; replace them with the actual endpoints, an existing keyspace ID, and an appropriate target timestamp.

```sh
PD='http://127.0.0.1:2379'
KEYSPACE_ID='123'
BARRIER_ID='example-barrier'
TARGET='<target TSO or RFC3339 time>'
```

### Show GC state

```sh
./bin/pd-gc-barrier --pd "$PD" --keyspace-id "$KEYSPACE_ID" show
```

Returns `keyspace_id`, `txn_safe_point`, `gc_safe_point`, and `barriers`. Barriers are sorted by timestamp, then by ID. Each barrier includes its ID, timestamp, and TTL.

The result includes only barriers in the selected keyspace. Global barriers are not listed. Safe points describe GC boundaries; they do not indicate that physical data reclamation or compaction has completed.

### Create or update a barrier

```sh
./bin/pd-gc-barrier --pd "$PD" --keyspace-id "$KEYSPACE_ID" \
set "$BARRIER_ID" "$TARGET" --ttl 2h
```

If the ID does not exist in the selected keyspace, `set` creates it. Otherwise, it replaces both its timestamp and TTL. Every call requires both values; to change only one, supply the desired unchanged value for the other.

The target timestamp must be greater than or equal to the current transaction safe point. PD enforces this constraint when it writes the barrier. The API does not provide compare-and-swap or require the new timestamp to be greater than the barrier's previous timestamp. Coordinate writers that share a barrier ID.

Use `never` for a barrier that should remain until explicitly deleted:

```sh
./bin/pd-gc-barrier --pd "$PD" --keyspace-id "$KEYSPACE_ID" \
set "$BARRIER_ID" "$TARGET" --ttl never
```

A finite TTL must be positive, such as `30m` or `2h`. PD rounds it up to whole seconds and resets the expiration time on each successful `set`. In `show` results, a finite TTL is the remaining lifetime reported at query time; expired entries may remain visible until cleaned up. `never` denotes no expiration.

### Delete a barrier

```sh
./bin/pd-gc-barrier --pd "$PD" --keyspace-id "$KEYSPACE_ID" delete "$BARRIER_ID"
```

By default, `delete` only reads and validates the current GC state. It does not send a deletion request. The JSON preview includes `dry_run: true`, `operation: "delete"`, the PD endpoints in `pd`, `keyspace_id`, `barrier_id`, `txn_safe_point`, `gc_safe_point`, and `current_barrier`. The current barrier includes its ID, timestamp, and remaining TTL (`never` for no expiration), or is `null` if the ID does not exist. The tool prints a reminder to stderr to add `--execute`; if the barrier does not exist, it reports that no changes were made.

After checking the preview, explicitly execute the deletion:

```sh
./bin/pd-gc-barrier --pd "$PD" --keyspace-id "$KEYSPACE_ID" delete "$BARRIER_ID" --execute
```

`--execute` reads and validates the current GC state again, then returns the deleted barrier's information. Deleting an ID that does not exist succeeds with `deleted_barrier: null`.

A preview does not lock the barrier or reserve its state: it can change before execution, and the API does not support conditional deletion. `--execute` applies only to `delete`; `set` continues to write immediately.

Deleting or expiring a barrier removes its retention constraint. Increasing its timestamp can also permit GC to advance. Neither operation reverses GC that has already occurred. The tool performs the requested operation without automatically installing a replacement barrier.

## Timestamp input

The timestamp argument accepts a positive decimal `uint64` TSO or an RFC3339 date and time with an explicit timezone.

| Input | Example | Interpretation |
| --- | --- | --- |
| Decimal TSO | `262144001` | Preserves the exact physical and logical components |
| Date and time with offset | `2025-10-01T00:10:00+08:00` | Uses the specified UTC offset |
| Date and time with milliseconds | `2025-10-01T00:10:00.123+08:00` | Preserves millisecond precision |
| UTC date and time | `2025-09-30T16:10:00Z` | Equivalent to the offset example above |

Date and time input uses `T` as the separator and requires `Z` or an explicit offset. Date-only input and timestamps such as `2025-10-01 00:10:00` are rejected. The time must be after the Unix epoch and fit within the TSO range.

Use at most three fractional digits. Inputs with more than nine fractional digits may currently be truncated before precision validation. Date and time input sets the TSO's logical counter to zero while preserving its physical date, time, and milliseconds. To retain an existing TSO exactly, copy its complete decimal value.

## Connection options

| Option | Description |
| --- | --- |
| `--pd` | Required. Comma-separated PD endpoints; the client discovers the leader. |
| `--keyspace-id` | Required. An existing keyspace ID in the inclusive range `0..16777215`. |
| `--timeout` | Positive timeout for the entire command, including connection initialization. Defaults to `30s`. |
| `--cacert` | Path to the trusted CA certificate file. |
| `--cert` | Path to the client certificate file. |
| `--key` | Path to the client private key file. |

Keyspace `0` is the `DEFAULT` keyspace. `NullKeyspaceID` (`4294967295`), unified GC, and global barrier management are unsupported. Before each operation, the tool reads GC state and rejects a returned scope that differs from the requested keyspace. The reserved barrier ID `gc_worker` cannot be set or deleted.

For TLS connections, provide the certificate files with the command:

```sh
./bin/pd-gc-barrier --pd 'https://pd.example.com:2379' --keyspace-id "$KEYSPACE_ID" \
--cacert /path/to/ca.pem --cert /path/to/client.pem --key /path/to/client-key.pem \
show
```

This version of the PD client requires a client certificate and private key to enable TLS. The tool rejects a CA file supplied without the certificate and key, as well as HTTPS endpoints supplied without the client certificate and key.

## Output and errors

Successful commands write JSON to stdout. Diagnostics go to stderr, and failures return a nonzero exit status. Timestamp objects contain a decimal `tso` string and a UTC `time` string. Keeping the TSO as a string avoids precision loss in JSON consumers that use floating-point numbers.

If `set` or `delete --execute` times out or loses its connection, the write may already have succeeded. Run `show` to inspect the actual state before retrying. Exiting the tool does not delete a barrier or reset its TTL.
101 changes: 101 additions & 0 deletions tools/pd-gc-barrier/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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.

//go:build integration

package main

import (
"bytes"
"context"
"strconv"
"testing"

"github.com/stretchr/testify/require"

pd "github.com/tikv/pd/client"
"github.com/tikv/pd/client/clients/gc"
"github.com/tikv/pd/pkg/keyspace"
"github.com/tikv/pd/server/config"
"github.com/tikv/pd/tests"
)

// TestBarrierControlsGC verifies the command against the actual release's PD
// server and client, including persistence after each command's client closes.
func TestBarrierControlsGC(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cluster, err := tests.NewTestCluster(ctx, 1, func(conf *config.Config, _ string) {
conf.Keyspace.WaitRegionSplit = false
})
require.NoError(t, err)
defer cluster.Destroy()
require.NoError(t, cluster.RunInitialServers())
require.NotEmpty(t, cluster.WaitLeader())
server := cluster.GetLeaderServer()
require.NoError(t, server.BootstrapCluster())
ks, err := server.GetKeyspaceManager().CreateKeyspace(&keyspace.CreateKeyspaceRequest{
Name: "barrier-recovery",
Config: map[string]string{keyspace.GCManagementType: keyspace.KeyspaceLevelGC},
})
require.NoError(t, err)
other, err := server.GetKeyspaceManager().CreateKeyspace(&keyspace.CreateKeyspaceRequest{
Name: "unaffected",
Config: map[string]string{keyspace.GCManagementType: keyspace.KeyspaceLevelGC},
})
require.NoError(t, err)
client, err := pd.NewClientWithContext(ctx, "barrier-tool-test", []string{server.GetAddr()}, pd.SecurityOption{})
require.NoError(t, err)
defer client.Close()
for _, id := range []uint32{ks.GetId(), other.GetId()} {
_, err = client.GetGCStatesClient(id).SetGCBarrier(ctx, "ticdc-old", 100, gc.TTLNeverExpire)
require.NoError(t, err)
}
gcClient := client.GetGCStatesClient(ks.GetId())
controller := client.GetGCInternalController(ks.GetId())
assertLimit := func(want uint64) {
result, err := controller.AdvanceTxnSafePoint(ctx, 1000)
require.NoError(t, err)
require.Equal(t, want, result.NewTxnSafePoint)
}
run := func(args ...string) string {
cmd := newCommand(connectPD)
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetArgs(append([]string{"--pd", server.GetAddr(), "--keyspace-id", strconv.FormatUint(uint64(ks.GetId()), 10)}, args...))
require.NoError(t, cmd.ExecuteContext(ctx))
return out.String()
}
assertLimit(100)
run("set", "recovery", "200", "--ttl=never")
assertLimit(100)
require.Contains(t, run("delete", "ticdc-old"), `"dry_run": true`)
assertLimit(100)
run("delete", "ticdc-old", "--execute")
assertLimit(200)
run("set", "recovery", "300", "--ttl=never")
assertLimit(300)
state, err := gcClient.GetGCState(ctx)
require.NoError(t, err)
require.Len(t, state.GCBarriers, 1)
require.Equal(t, gc.TTLNeverExpire, state.GCBarriers[0].TTL)
require.Contains(t, run("show"), `"tso": "300"`)
run("delete", "recovery", "--execute")
assertLimit(1000)
state, err = client.GetGCStatesClient(other.GetId()).GetGCState(ctx)
require.NoError(t, err)
require.Len(t, state.GCBarriers, 1)
require.Equal(t, "ticdc-old", state.GCBarriers[0].BarrierID)
require.Equal(t, uint64(100), state.GCBarriers[0].BarrierTS)
}
Loading
Loading