@@ -18,18 +18,28 @@ type Route struct {
1818 End []byte
1919 // GroupID identifies the raft group for the range starting at Start.
2020 GroupID uint64
21+ // Load tracks the number of accesses served by this range.
22+ Load uint64
2123}
2224
2325// Engine holds in-memory metadata of routes and provides timestamp generation.
2426type Engine struct {
25- mu sync.RWMutex
26- routes []Route
27- ts uint64
27+ mu sync.RWMutex
28+ routes []Route
29+ ts uint64
30+ hotspotThreshold uint64
2831}
2932
30- // NewEngine creates an Engine.
33+ // NewEngine creates an Engine with no hotspot splitting .
3134func NewEngine () * Engine {
32- return & Engine {routes : make ([]Route , 0 )}
35+ return NewEngineWithThreshold (0 )
36+ }
37+
38+ // NewEngineWithThreshold creates an Engine and sets a threshold for hotspot
39+ // detection. A non-zero threshold enables automatic range splitting when the
40+ // number of accesses to a range exceeds the threshold.
41+ func NewEngineWithThreshold (threshold uint64 ) * Engine {
42+ return & Engine {routes : make ([]Route , 0 ), hotspotThreshold : threshold }
3343}
3444
3545// UpdateRoute registers or updates a route for the given key range.
@@ -47,25 +57,113 @@ func (e *Engine) UpdateRoute(start, end []byte, group uint64) {
4757func (e * Engine ) GetRoute (key []byte ) (Route , bool ) {
4858 e .mu .RLock ()
4959 defer e .mu .RUnlock ()
50- if len (e .routes ) == 0 {
60+ idx := e .routeIndex (key )
61+ if idx < 0 {
5162 return Route {}, false
5263 }
64+ return e .routes [idx ], true
65+ }
66+
67+ // NextTimestamp returns a monotonic increasing timestamp.
68+ func (e * Engine ) NextTimestamp () uint64 {
69+ return atomic .AddUint64 (& e .ts , 1 )
70+ }
71+
72+ // RecordAccess increases the access counter for the range containing key and
73+ // splits the range if it turns into a hotspot. The load counter is updated
74+ // atomically under a read lock to allow concurrent access recording. If the
75+ // hotspot threshold is exceeded, RecordAccess acquires a full write lock and
76+ // re-checks the condition before splitting to avoid races with concurrent
77+ // splits.
78+ func (e * Engine ) RecordAccess (key []byte ) {
79+ e .mu .RLock ()
80+ idx := e .routeIndex (key )
81+ if idx < 0 {
82+ e .mu .RUnlock ()
83+ return
84+ }
85+ load := atomic .AddUint64 (& e .routes [idx ].Load , 1 )
86+ threshold := e .hotspotThreshold
87+ e .mu .RUnlock ()
88+ if threshold == 0 || load < threshold {
89+ return
90+ }
91+
92+ e .mu .Lock ()
93+ defer e .mu .Unlock ()
94+ idx = e .routeIndex (key )
95+ if idx < 0 {
96+ return
97+ }
98+ if e .routes [idx ].Load >= threshold {
99+ e .splitRange (idx )
100+ }
101+ }
53102
54- // Find the first route with Start > key.
103+ // Stats returns a snapshot of current ranges and their load counters.
104+ func (e * Engine ) Stats () []Route {
105+ e .mu .RLock ()
106+ defer e .mu .RUnlock ()
107+ stats := make ([]Route , len (e .routes ))
108+ for i , r := range e .routes {
109+ stats [i ] = Route {Start : cloneBytes (r .Start ), End : cloneBytes (r .End ), GroupID : r .GroupID , Load : atomic .LoadUint64 (& e .routes [i ].Load )}
110+ }
111+ return stats
112+ }
113+
114+ func (e * Engine ) routeIndex (key []byte ) int {
115+ if len (e .routes ) == 0 {
116+ return - 1
117+ }
55118 i := sort .Search (len (e .routes ), func (i int ) bool {
56119 return bytes .Compare (e .routes [i ].Start , key ) > 0
57120 })
58121 if i == 0 {
59- return Route {}, false
122+ return - 1
60123 }
61- r := e . routes [ i - 1 ]
62- if r . End != nil && bytes .Compare (key , r . End ) >= 0 {
63- return Route {}, false
124+ i --
125+ if end := e . routes [ i ]. End ; end != nil && bytes .Compare (key , end ) >= 0 {
126+ return - 1
64127 }
65- return r , true
128+ return i
66129}
67130
68- // NextTimestamp returns a monotonic increasing timestamp.
69- func (e * Engine ) NextTimestamp () uint64 {
70- return atomic .AddUint64 (& e .ts , 1 )
131+ func (e * Engine ) splitRange (idx int ) {
132+ r := e .routes [idx ]
133+ if r .End == nil {
134+ // cannot split unbounded range; reset load to avoid repeated attempts
135+ e .routes [idx ].Load = 0
136+ return
137+ }
138+ mid := midpoint (r .Start , r .End )
139+ if mid == nil {
140+ // cannot determine midpoint; reset load to avoid repeated attempts
141+ e .routes [idx ].Load = 0
142+ return
143+ }
144+ left := Route {Start : r .Start , End : mid , GroupID : r .GroupID }
145+ right := Route {Start : mid , End : r .End , GroupID : r .GroupID }
146+ // replace the range at idx with left and right in an idiomatic manner
147+ e .routes = append (e .routes [:idx + 1 ], e .routes [idx :]... )
148+ e .routes [idx ] = left
149+ e .routes [idx + 1 ] = right
150+ }
151+
152+ func cloneBytes (b []byte ) []byte {
153+ if b == nil {
154+ return nil
155+ }
156+ out := make ([]byte , len (b ))
157+ copy (out , b )
158+ return out
159+ }
160+
161+ // midpoint returns a key that is lexicographically between a and b. It returns
162+ // nil if such a key cannot be determined (e.g. a and b are too close).
163+ func midpoint (a , b []byte ) []byte {
164+ m := append (cloneBytes (a ), 0 )
165+ if bytes .Compare (m , b ) >= 0 {
166+ return nil
167+ }
168+ return m
71169}
0 commit comments