From ac95f811cec9be5b8ae2e19cc019a58de6d3b634 Mon Sep 17 00:00:00 2001 From: okjiang <819421878@qq.com> Date: Fri, 15 May 2026 12:08:36 +0800 Subject: [PATCH 1/6] server/api: allow follower local region reads Signed-off-by: okjiang <819421878@qq.com> --- server/api/middleware.go | 53 +++++++++++++++++++++++++++++++++ tests/server/api/api_test.go | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/server/api/middleware.go b/server/api/middleware.go index 745a2df71d7..de7ca94baf7 100644 --- a/server/api/middleware.go +++ b/server/api/middleware.go @@ -17,6 +17,7 @@ package api import ( "context" "net/http" + "strings" "time" "github.com/unrolled/render" @@ -26,6 +27,7 @@ import ( "github.com/tikv/pd/pkg/audit" "github.com/tikv/pd/pkg/errs" + "github.com/tikv/pd/pkg/utils/apiutil" "github.com/tikv/pd/pkg/utils/requestutil" "github.com/tikv/pd/server" "github.com/tikv/pd/server/cluster" @@ -93,6 +95,9 @@ func newClusterMiddleware(s *server.Server) clusterMiddleware { func (m clusterMiddleware) middleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rc := m.s.GetRaftCluster() + if rc == nil { + rc = m.getFollowerSyncedCluster(r) + } if rc == nil { m.rd.JSON(w, http.StatusInternalServerError, errs.ErrNotBootstrapped.FastGenByArgs().Error()) return @@ -102,6 +107,54 @@ func (m clusterMiddleware) middleware(h http.Handler) http.Handler { }) } +func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.RaftCluster { + if r.Method != http.MethodGet || + r.Header.Get(apiutil.PDAllowFollowerHandleHeader) == "" || + !isFollowerSyncedRegionPath(r.URL.Path) || + m.s.GetMember().IsServing() { + return nil + } + rc := m.s.DirectlyGetRaftCluster() + if rc == nil || !rc.GetRegionSyncer().IsRunning() { + return nil + } + return rc +} + +func isFollowerSyncedRegionPath(path string) bool { + switch { + case path == "/pd/api/v1/regions", + path == "/pd/api/v1/regions/key", + path == "/pd/api/v1/regions/count", + path == "/pd/api/v1/regions/check/hist-size", + path == "/pd/api/v1/regions/check/hist-keys", + path == "/pd/api/v1/regions/range-holes", + path == "/pd/api/v1/regions/writeflow", + path == "/pd/api/v1/regions/writequery", + path == "/pd/api/v1/regions/readflow", + path == "/pd/api/v1/regions/readquery", + path == "/pd/api/v1/regions/confver", + path == "/pd/api/v1/regions/version", + path == "/pd/api/v1/regions/size", + path == "/pd/api/v1/regions/keys", + path == "/pd/api/v1/regions/cpu": + return true + case hasSinglePathValue(path, "/pd/api/v1/region/id/"), + strings.HasPrefix(path, "/pd/api/v1/region/key/"), + hasSinglePathValue(path, "/pd/api/v1/regions/store/"), + hasSinglePathValue(path, "/pd/api/v1/regions/keyspace/id/"), + hasSinglePathValue(path, "/pd/api/v1/regions/sibling/"): + return true + default: + return false + } +} + +func hasSinglePathValue(path, prefix string) bool { + value, ok := strings.CutPrefix(path, prefix) + return ok && value != "" && !strings.Contains(value, "/") +} + type clusterCtxKey struct{} func getCluster(r *http.Request) *cluster.RaftCluster { diff --git a/tests/server/api/api_test.go b/tests/server/api/api_test.go index 0b8f80b733d..a857e346771 100644 --- a/tests/server/api/api_test.go +++ b/tests/server/api/api_test.go @@ -809,6 +809,63 @@ func (suite *redirectorTestSuite) TestXForwardedFor() { re.NotContains(l, suite.cluster.GetConfig().GetClientURLs()) } +func TestFollowerRegionAPIWithNoForward(t *testing.T) { + re := require.New(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cluster, err := tests.NewTestCluster(ctx, 3, func(conf *config.Config, _ string) { + conf.PDServerCfg.UseRegionStorage = true + conf.TickInterval = typeutil.Duration{Duration: 50 * time.Millisecond} + conf.ElectionInterval = typeutil.Duration{Duration: 250 * time.Millisecond} + }) + re.NoError(err) + defer cluster.Destroy() + + re.NoError(cluster.RunInitialServers()) + re.NotEmpty(cluster.WaitLeader()) + leader := cluster.GetLeaderServer() + re.NoError(leader.BootstrapCluster()) + re.True(cluster.WaitRegionSyncerClientsReady(2)) + + follower := cluster.GetServer(cluster.GetFollower()) + re.NotNil(follower) + testutil.Eventually(re, func() bool { + return follower.GetServer().DirectlyGetRaftCluster().GetRegionSyncer().IsRunning() + }) + + regions := tests.InitRegions(3) + for _, region := range regions { + re.NoError(leader.GetRaftCluster().HandleRegionHeartbeat(region)) + } + testutil.Eventually(re, func() bool { + return len(follower.GetServer().GetBasicCluster().GetRegions()) == len(regions) + }) + + req, err := http.NewRequest(http.MethodGet, follower.GetAddr()+"/pd/api/v1/regions", http.NoBody) + re.NoError(err) + req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") + resp, err := tests.TestDialClient.Do(req) + re.NoError(err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + re.NoError(err) + re.Equal(http.StatusOK, resp.StatusCode, string(body)) + var regionsInfo response.RegionsInfo + re.NoError(json.Unmarshal(body, ®ionsInfo)) + re.Equal(len(regions), regionsInfo.Count) + + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/pd/api/v1/region/id/%d", follower.GetAddr(), regions[0].GetID()), http.NoBody) + re.NoError(err) + req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") + resp, err = tests.TestDialClient.Do(req) + re.NoError(err) + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + re.NoError(err) + re.Equal(http.StatusOK, resp.StatusCode, string(body)) + re.Contains(string(body), fmt.Sprintf(`"id":%d`, regions[0].GetID())) +} + func mustRequestSuccess(re *require.Assertions, s *server.Server) http.Header { resp, err := tests.TestDialClient.Get(s.GetAddr() + "/pd/api/v1/version") re.NoError(err) From fdcedb58a720f0a5a26ae1ab564c051cc7ae13f4 Mon Sep 17 00:00:00 2001 From: okjiang <819421878@qq.com> Date: Mon, 18 May 2026 11:46:40 +0800 Subject: [PATCH 2/6] server/api: add follower region path comments Signed-off-by: okjiang <819421878@qq.com> --- server/api/middleware.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/api/middleware.go b/server/api/middleware.go index de7ca94baf7..c238619c613 100644 --- a/server/api/middleware.go +++ b/server/api/middleware.go @@ -123,6 +123,8 @@ func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.Ra func isFollowerSyncedRegionPath(path string) bool { switch { + // Only allow read-only APIs whose responses are fully backed by the + // region cache synchronized to followers. case path == "/pd/api/v1/regions", path == "/pd/api/v1/regions/key", path == "/pd/api/v1/regions/count", @@ -139,6 +141,11 @@ func isFollowerSyncedRegionPath(path string) bool { path == "/pd/api/v1/regions/keys", path == "/pd/api/v1/regions/cpu": return true + // Keep the dynamic routes to the exact resource shape registered in + // router.go. Other subpaths under these prefixes, such as + // /region/id/{id}/labels, need leader-side data and should keep being + // forwarded to the leader. /region/key/{key} is matched by prefix because + // the encoded key itself may contain slashes. case hasSinglePathValue(path, "/pd/api/v1/region/id/"), strings.HasPrefix(path, "/pd/api/v1/region/key/"), hasSinglePathValue(path, "/pd/api/v1/regions/store/"), From 1bc0a12d7a69e25eff59e3a1296a3e154cbbb3fe Mon Sep 17 00:00:00 2001 From: okjiang <819421878@qq.com> Date: Mon, 18 May 2026 14:46:20 +0800 Subject: [PATCH 3/6] server/api: attach follower reads to routes Signed-off-by: okjiang <819421878@qq.com> --- server/api/middleware.go | 65 ++++++++++-------------------------- server/api/router.go | 43 +++++++++++++----------- tests/server/api/api_test.go | 11 ++++++ 3 files changed, 52 insertions(+), 67 deletions(-) diff --git a/server/api/middleware.go b/server/api/middleware.go index c238619c613..52d8fd4135c 100644 --- a/server/api/middleware.go +++ b/server/api/middleware.go @@ -17,7 +17,6 @@ package api import ( "context" "net/http" - "strings" "time" "github.com/unrolled/render" @@ -81,15 +80,28 @@ func (rm *requestInfoMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques } type clusterMiddleware struct { - s *server.Server - rd *render.Render + s *server.Server + rd *render.Render + allowFollowerSyncedRegion bool } -func newClusterMiddleware(s *server.Server) clusterMiddleware { - return clusterMiddleware{ +type clusterMiddlewareOption func(*clusterMiddleware) + +func withFollowerSyncedRegion() clusterMiddlewareOption { + return func(m *clusterMiddleware) { + m.allowFollowerSyncedRegion = true + } +} + +func newClusterMiddleware(s *server.Server, opts ...clusterMiddlewareOption) clusterMiddleware { + m := clusterMiddleware{ s: s, rd: render.New(render.Options{IndentJSON: true}), } + for _, opt := range opts { + opt(&m) + } + return m } func (m clusterMiddleware) middleware(h http.Handler) http.Handler { @@ -110,7 +122,7 @@ func (m clusterMiddleware) middleware(h http.Handler) http.Handler { func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.RaftCluster { if r.Method != http.MethodGet || r.Header.Get(apiutil.PDAllowFollowerHandleHeader) == "" || - !isFollowerSyncedRegionPath(r.URL.Path) || + !m.allowFollowerSyncedRegion || m.s.GetMember().IsServing() { return nil } @@ -121,47 +133,6 @@ func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.Ra return rc } -func isFollowerSyncedRegionPath(path string) bool { - switch { - // Only allow read-only APIs whose responses are fully backed by the - // region cache synchronized to followers. - case path == "/pd/api/v1/regions", - path == "/pd/api/v1/regions/key", - path == "/pd/api/v1/regions/count", - path == "/pd/api/v1/regions/check/hist-size", - path == "/pd/api/v1/regions/check/hist-keys", - path == "/pd/api/v1/regions/range-holes", - path == "/pd/api/v1/regions/writeflow", - path == "/pd/api/v1/regions/writequery", - path == "/pd/api/v1/regions/readflow", - path == "/pd/api/v1/regions/readquery", - path == "/pd/api/v1/regions/confver", - path == "/pd/api/v1/regions/version", - path == "/pd/api/v1/regions/size", - path == "/pd/api/v1/regions/keys", - path == "/pd/api/v1/regions/cpu": - return true - // Keep the dynamic routes to the exact resource shape registered in - // router.go. Other subpaths under these prefixes, such as - // /region/id/{id}/labels, need leader-side data and should keep being - // forwarded to the leader. /region/key/{key} is matched by prefix because - // the encoded key itself may contain slashes. - case hasSinglePathValue(path, "/pd/api/v1/region/id/"), - strings.HasPrefix(path, "/pd/api/v1/region/key/"), - hasSinglePathValue(path, "/pd/api/v1/regions/store/"), - hasSinglePathValue(path, "/pd/api/v1/regions/keyspace/id/"), - hasSinglePathValue(path, "/pd/api/v1/regions/sibling/"): - return true - default: - return false - } -} - -func hasSinglePathValue(path, prefix string) bool { - value, ok := strings.CutPrefix(path, prefix) - return ok && value != "" && !strings.Contains(value, "/") -} - type clusterCtxKey struct{} func getCluster(r *http.Request) *cluster.RaftCluster { diff --git a/server/api/router.go b/server/api/router.go index 7533fa9e4b0..6618ccaade3 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -130,8 +130,11 @@ func createRouter(prefix string, svr *server.Server) *mux.Router { clusterRouter := apiRouter.NewRoute().Subrouter() clusterRouter.Use(newClusterMiddleware(svr).middleware) + followerSyncedRegionRouter := apiRouter.NewRoute().Subrouter() + followerSyncedRegionRouter.Use(newClusterMiddleware(svr, withFollowerSyncedRegion()).middleware) escapeRouter := clusterRouter.NewRoute().Subrouter().UseEncodedPath() + followerSyncedRegionEscapeRouter := followerSyncedRegionRouter.NewRoute().Subrouter().UseEncodedPath() operatorHandler := newOperatorHandler(handler, rd) registerFunc(apiRouter, "/operators", operatorHandler.GetOperators, setMethods(http.MethodGet), setAuditBackend(prometheus)) @@ -245,27 +248,27 @@ func createRouter(prefix string, svr *server.Server) *mux.Router { registerFunc(apiRouter, "/hotspot/buckets", hotStatusHandler.GetHotBuckets, setMethods(http.MethodGet), setAuditBackend(prometheus)) regionHandler := newRegionHandler(svr, rd) - registerFunc(clusterRouter, "/region/id/{id}", regionHandler.GetRegionByID, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter.UseEncodedPath(), "/region/key/{key}", regionHandler.GetRegion, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/region/id/{id}", regionHandler.GetRegionByID, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionEscapeRouter, "/region/key/{key}", regionHandler.GetRegion, setMethods(http.MethodGet), setAuditBackend(prometheus)) srd := createStreamingRender() regionsAllHandler := newRegionsHandler(svr, srd) - registerFunc(clusterRouter, "/regions", regionsAllHandler.GetRegions, setMethods(http.MethodGet), setAuditBackend(localLog, prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions", regionsAllHandler.GetRegions, setMethods(http.MethodGet), setAuditBackend(localLog, prometheus)) regionsHandler := newRegionsHandler(svr, rd) - registerFunc(clusterRouter, "/regions/key", regionsHandler.ScanRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/count", regionsHandler.GetRegionCount, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/store/{id}", regionsHandler.GetStoreRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/keyspace/id/{id}", regionsHandler.GetKeyspaceRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/writeflow", regionsHandler.GetTopWriteFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/writequery", regionsHandler.GetTopWriteQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/readflow", regionsHandler.GetTopReadFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/readquery", regionsHandler.GetTopReadQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/confver", regionsHandler.GetTopConfVerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/version", regionsHandler.GetTopVersionRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/size", regionsHandler.GetTopSizeRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/keys", regionsHandler.GetTopKeysRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/cpu", regionsHandler.GetTopCPURegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/key", regionsHandler.ScanRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/count", regionsHandler.GetRegionCount, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/store/{id}", regionsHandler.GetStoreRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/keyspace/id/{id}", regionsHandler.GetKeyspaceRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/writeflow", regionsHandler.GetTopWriteFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/writequery", regionsHandler.GetTopWriteQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/readflow", regionsHandler.GetTopReadFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/readquery", regionsHandler.GetTopReadQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/confver", regionsHandler.GetTopConfVerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/version", regionsHandler.GetTopVersionRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/size", regionsHandler.GetTopSizeRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/keys", regionsHandler.GetTopKeysRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/cpu", regionsHandler.GetTopCPURegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/miss-peer", regionsHandler.GetMissPeerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/extra-peer", regionsHandler.GetExtraPeerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/pending-peer", regionsHandler.GetPendingPeerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) @@ -276,14 +279,14 @@ func createRouter(prefix string, svr *server.Server) *mux.Router { registerFunc(clusterRouter, "/regions/check/oversized-region", regionsHandler.GetOverSizedRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/undersized-region", regionsHandler.GetUndersizedRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/check/hist-size", regionsHandler.GetSizeHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/check/hist-keys", regionsHandler.GetKeysHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(clusterRouter, "/regions/sibling/{id}", regionsHandler.GetRegionSiblings, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/check/hist-size", regionsHandler.GetSizeHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/check/hist-keys", regionsHandler.GetKeysHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/sibling/{id}", regionsHandler.GetRegionSiblings, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/accelerate-schedule", regionsHandler.AccelerateRegionsScheduleInRange, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) registerFunc(clusterRouter, "/regions/accelerate-schedule/batch", regionsHandler.AccelerateRegionsScheduleInRanges, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) registerFunc(clusterRouter, "/regions/scatter", regionsHandler.ScatterRegions, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) registerFunc(clusterRouter, "/regions/split", regionsHandler.SplitRegions, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) - registerFunc(clusterRouter, "/regions/range-holes", regionsHandler.GetRangeHoles, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(followerSyncedRegionRouter, "/regions/range-holes", regionsHandler.GetRangeHoles, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/replicated", regionsHandler.CheckRegionsReplicated, setMethods(http.MethodGet), setQueries("startKey", "{startKey}", "endKey", "{endKey}"), setAuditBackend(prometheus)) registerFunc(apiRouter, "/version", newVersionHandler(rd).GetVersion, setMethods(http.MethodGet), setAuditBackend(prometheus)) diff --git a/tests/server/api/api_test.go b/tests/server/api/api_test.go index a857e346771..33e9f65366c 100644 --- a/tests/server/api/api_test.go +++ b/tests/server/api/api_test.go @@ -864,6 +864,17 @@ func TestFollowerRegionAPIWithNoForward(t *testing.T) { re.NoError(err) re.Equal(http.StatusOK, resp.StatusCode, string(body)) re.Contains(string(body), fmt.Sprintf(`"id":%d`, regions[0].GetID())) + + req, err = http.NewRequest(http.MethodGet, follower.GetAddr()+"/pd/api/v1/regions/check/miss-peer", http.NoBody) + re.NoError(err) + req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") + resp, err = tests.TestDialClient.Do(req) + re.NoError(err) + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + re.NoError(err) + re.Equal(http.StatusInternalServerError, resp.StatusCode, string(body)) + re.Contains(string(body), "TiKV cluster not bootstrapped") } func mustRequestSuccess(re *require.Assertions, s *server.Server) http.Header { From 661909bc80dce2dbf6b0efe5fd065c47c75c230d Mon Sep 17 00:00:00 2001 From: okjiang <819421878@qq.com> Date: Tue, 19 May 2026 16:27:10 +0800 Subject: [PATCH 4/6] server/api: address region read router comments Signed-off-by: okjiang <819421878@qq.com> --- server/api/middleware.go | 4 ++-- server/api/router.go | 46 ++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/server/api/middleware.go b/server/api/middleware.go index 52d8fd4135c..acf41c00124 100644 --- a/server/api/middleware.go +++ b/server/api/middleware.go @@ -121,9 +121,9 @@ func (m clusterMiddleware) middleware(h http.Handler) http.Handler { func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.RaftCluster { if r.Method != http.MethodGet || - r.Header.Get(apiutil.PDAllowFollowerHandleHeader) == "" || !m.allowFollowerSyncedRegion || - m.s.GetMember().IsServing() { + m.s.GetMember().IsServing() || + r.Header.Get(apiutil.PDAllowFollowerHandleHeader) == "" { return nil } rc := m.s.DirectlyGetRaftCluster() diff --git a/server/api/router.go b/server/api/router.go index 6618ccaade3..f1593f5e388 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -130,11 +130,11 @@ func createRouter(prefix string, svr *server.Server) *mux.Router { clusterRouter := apiRouter.NewRoute().Subrouter() clusterRouter.Use(newClusterMiddleware(svr).middleware) - followerSyncedRegionRouter := apiRouter.NewRoute().Subrouter() - followerSyncedRegionRouter.Use(newClusterMiddleware(svr, withFollowerSyncedRegion()).middleware) + regionReadRouter := apiRouter.NewRoute().Subrouter() + regionReadRouter.Use(newClusterMiddleware(svr, withFollowerSyncedRegion()).middleware) escapeRouter := clusterRouter.NewRoute().Subrouter().UseEncodedPath() - followerSyncedRegionEscapeRouter := followerSyncedRegionRouter.NewRoute().Subrouter().UseEncodedPath() + regionReadEscapeRouter := regionReadRouter.NewRoute().Subrouter().UseEncodedPath() operatorHandler := newOperatorHandler(handler, rd) registerFunc(apiRouter, "/operators", operatorHandler.GetOperators, setMethods(http.MethodGet), setAuditBackend(prometheus)) @@ -248,27 +248,27 @@ func createRouter(prefix string, svr *server.Server) *mux.Router { registerFunc(apiRouter, "/hotspot/buckets", hotStatusHandler.GetHotBuckets, setMethods(http.MethodGet), setAuditBackend(prometheus)) regionHandler := newRegionHandler(svr, rd) - registerFunc(followerSyncedRegionRouter, "/region/id/{id}", regionHandler.GetRegionByID, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionEscapeRouter, "/region/key/{key}", regionHandler.GetRegion, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/region/id/{id}", regionHandler.GetRegionByID, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadEscapeRouter, "/region/key/{key}", regionHandler.GetRegion, setMethods(http.MethodGet), setAuditBackend(prometheus)) srd := createStreamingRender() regionsAllHandler := newRegionsHandler(svr, srd) - registerFunc(followerSyncedRegionRouter, "/regions", regionsAllHandler.GetRegions, setMethods(http.MethodGet), setAuditBackend(localLog, prometheus)) + registerFunc(regionReadRouter, "/regions", regionsAllHandler.GetRegions, setMethods(http.MethodGet), setAuditBackend(localLog, prometheus)) regionsHandler := newRegionsHandler(svr, rd) - registerFunc(followerSyncedRegionRouter, "/regions/key", regionsHandler.ScanRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/count", regionsHandler.GetRegionCount, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/store/{id}", regionsHandler.GetStoreRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/keyspace/id/{id}", regionsHandler.GetKeyspaceRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/writeflow", regionsHandler.GetTopWriteFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/writequery", regionsHandler.GetTopWriteQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/readflow", regionsHandler.GetTopReadFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/readquery", regionsHandler.GetTopReadQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/confver", regionsHandler.GetTopConfVerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/version", regionsHandler.GetTopVersionRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/size", regionsHandler.GetTopSizeRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/keys", regionsHandler.GetTopKeysRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/cpu", regionsHandler.GetTopCPURegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/key", regionsHandler.ScanRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/count", regionsHandler.GetRegionCount, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/store/{id}", regionsHandler.GetStoreRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/keyspace/id/{id}", regionsHandler.GetKeyspaceRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/writeflow", regionsHandler.GetTopWriteFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/writequery", regionsHandler.GetTopWriteQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/readflow", regionsHandler.GetTopReadFlowRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/readquery", regionsHandler.GetTopReadQueryRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/confver", regionsHandler.GetTopConfVerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/version", regionsHandler.GetTopVersionRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/size", regionsHandler.GetTopSizeRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/keys", regionsHandler.GetTopKeysRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/cpu", regionsHandler.GetTopCPURegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/miss-peer", regionsHandler.GetMissPeerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/extra-peer", regionsHandler.GetExtraPeerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/pending-peer", regionsHandler.GetPendingPeerRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) @@ -279,14 +279,14 @@ func createRouter(prefix string, svr *server.Server) *mux.Router { registerFunc(clusterRouter, "/regions/check/oversized-region", regionsHandler.GetOverSizedRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/check/undersized-region", regionsHandler.GetUndersizedRegions, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/check/hist-size", regionsHandler.GetSizeHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/check/hist-keys", regionsHandler.GetKeysHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/sibling/{id}", regionsHandler.GetRegionSiblings, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/check/hist-size", regionsHandler.GetSizeHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/check/hist-keys", regionsHandler.GetKeysHistogram, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/sibling/{id}", regionsHandler.GetRegionSiblings, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/accelerate-schedule", regionsHandler.AccelerateRegionsScheduleInRange, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) registerFunc(clusterRouter, "/regions/accelerate-schedule/batch", regionsHandler.AccelerateRegionsScheduleInRanges, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) registerFunc(clusterRouter, "/regions/scatter", regionsHandler.ScatterRegions, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) registerFunc(clusterRouter, "/regions/split", regionsHandler.SplitRegions, setMethods(http.MethodPost), setAuditBackend(localLog, prometheus)) - registerFunc(followerSyncedRegionRouter, "/regions/range-holes", regionsHandler.GetRangeHoles, setMethods(http.MethodGet), setAuditBackend(prometheus)) + registerFunc(regionReadRouter, "/regions/range-holes", regionsHandler.GetRangeHoles, setMethods(http.MethodGet), setAuditBackend(prometheus)) registerFunc(clusterRouter, "/regions/replicated", regionsHandler.CheckRegionsReplicated, setMethods(http.MethodGet), setQueries("startKey", "{startKey}", "endKey", "{endKey}"), setAuditBackend(prometheus)) registerFunc(apiRouter, "/version", newVersionHandler(rd).GetVersion, setMethods(http.MethodGet), setAuditBackend(prometheus)) From 50068efb38f3eaecb47bcb7fb511877efe47951b Mon Sep 17 00:00:00 2001 From: okjiang <819421878@qq.com> Date: Wed, 20 May 2026 10:30:56 +0800 Subject: [PATCH 5/6] server/api: require true follower region header Signed-off-by: okjiang <819421878@qq.com> --- server/api/middleware.go | 2 +- server/server.go | 2 +- tests/server/api/api_test.go | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/api/middleware.go b/server/api/middleware.go index acf41c00124..59cbb3089f0 100644 --- a/server/api/middleware.go +++ b/server/api/middleware.go @@ -123,7 +123,7 @@ func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.Ra if r.Method != http.MethodGet || !m.allowFollowerSyncedRegion || m.s.GetMember().IsServing() || - r.Header.Get(apiutil.PDAllowFollowerHandleHeader) == "" { + r.Header.Get(apiutil.PDAllowFollowerHandleHeader) != "true" { return nil } rc := m.s.DirectlyGetRaftCluster() diff --git a/server/server.go b/server/server.go index 74859726bcf..040ae8792dd 100644 --- a/server/server.go +++ b/server/server.go @@ -1472,7 +1472,7 @@ func (s *Server) IsServiceIndependent(name string) bool { } // DirectlyGetRaftCluster returns raft cluster directly. -// Only used for test. +// It bypasses the leader-running check for follower-local paths and tests. func (s *Server) DirectlyGetRaftCluster() *cluster.RaftCluster { return s.cluster } diff --git a/tests/server/api/api_test.go b/tests/server/api/api_test.go index 33e9f65366c..e96dfa2a4a9 100644 --- a/tests/server/api/api_test.go +++ b/tests/server/api/api_test.go @@ -843,12 +843,23 @@ func TestFollowerRegionAPIWithNoForward(t *testing.T) { req, err := http.NewRequest(http.MethodGet, follower.GetAddr()+"/pd/api/v1/regions", http.NoBody) re.NoError(err) - req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") + req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "false") resp, err := tests.TestDialClient.Do(req) re.NoError(err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) re.NoError(err) + re.Equal(http.StatusInternalServerError, resp.StatusCode, string(body)) + re.Contains(string(body), "TiKV cluster not bootstrapped") + + req, err = http.NewRequest(http.MethodGet, follower.GetAddr()+"/pd/api/v1/regions", http.NoBody) + re.NoError(err) + req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") + resp, err = tests.TestDialClient.Do(req) + re.NoError(err) + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + re.NoError(err) re.Equal(http.StatusOK, resp.StatusCode, string(body)) var regionsInfo response.RegionsInfo re.NoError(json.Unmarshal(body, ®ionsInfo)) From 56038cd9d533f6181e6a8f49a72838c6423d238c Mon Sep 17 00:00:00 2001 From: okjiang <819421878@qq.com> Date: Wed, 20 May 2026 11:21:40 +0800 Subject: [PATCH 6/6] server/api: keep follower handle header compatibility Signed-off-by: okjiang <819421878@qq.com> --- server/api/middleware.go | 2 +- tests/server/api/api_test.go | 13 +------------ tools/pd-ctl/tests/region/region_test.go | 3 +-- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/server/api/middleware.go b/server/api/middleware.go index 59cbb3089f0..acf41c00124 100644 --- a/server/api/middleware.go +++ b/server/api/middleware.go @@ -123,7 +123,7 @@ func (m clusterMiddleware) getFollowerSyncedCluster(r *http.Request) *cluster.Ra if r.Method != http.MethodGet || !m.allowFollowerSyncedRegion || m.s.GetMember().IsServing() || - r.Header.Get(apiutil.PDAllowFollowerHandleHeader) != "true" { + r.Header.Get(apiutil.PDAllowFollowerHandleHeader) == "" { return nil } rc := m.s.DirectlyGetRaftCluster() diff --git a/tests/server/api/api_test.go b/tests/server/api/api_test.go index e96dfa2a4a9..33e9f65366c 100644 --- a/tests/server/api/api_test.go +++ b/tests/server/api/api_test.go @@ -843,23 +843,12 @@ func TestFollowerRegionAPIWithNoForward(t *testing.T) { req, err := http.NewRequest(http.MethodGet, follower.GetAddr()+"/pd/api/v1/regions", http.NoBody) re.NoError(err) - req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "false") + req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") resp, err := tests.TestDialClient.Do(req) re.NoError(err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) re.NoError(err) - re.Equal(http.StatusInternalServerError, resp.StatusCode, string(body)) - re.Contains(string(body), "TiKV cluster not bootstrapped") - - req, err = http.NewRequest(http.MethodGet, follower.GetAddr()+"/pd/api/v1/regions", http.NoBody) - re.NoError(err) - req.Header.Set(apiutil.PDAllowFollowerHandleHeader, "true") - resp, err = tests.TestDialClient.Do(req) - re.NoError(err) - defer resp.Body.Close() - body, err = io.ReadAll(resp.Body) - re.NoError(err) re.Equal(http.StatusOK, resp.StatusCode, string(body)) var regionsInfo response.RegionsInfo re.NoError(json.Unmarshal(body, ®ionsInfo)) diff --git a/tools/pd-ctl/tests/region/region_test.go b/tools/pd-ctl/tests/region/region_test.go index 6a9f92e8242..cdbcf442274 100644 --- a/tools/pd-ctl/tests/region/region_test.go +++ b/tools/pd-ctl/tests/region/region_test.go @@ -19,7 +19,6 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "testing" "time" @@ -561,7 +560,7 @@ func (suite *regionTestSuite) followerDirect(cluster *pdTests.TestCluster) { output, err := tests.ExecuteCommand(cmd, "-u", serverAddr, "region", "100", "--no-forward") re.NoError(err) outputStr := string(output) - re.True(strings.Contains(outputStr, "\"id\":100") || strings.Contains(outputStr, "TiKV cluster not bootstrapped"), outputStr) + re.Contains(outputStr, "\"id\":100") } } }