1+ /*
2+ Package cluster
3+ Tellstone Cloud-Native In-Memory Database
4+ File: geo.go
5+ Description: Geo-aware placement (Phase 6, ADR-006). Nodes self-declare their
6+ availability zone at startup and register it in etcd under /tellstone/nodes/<id>.
7+ Operators define a GeoPolicy mapping key prefixes to zones; the PD places new
8+ regions according to the policy and leader election prefers same-zone replicas.
9+ Unmatched keys default to the global zone "*" (replicated everywhere).
10+
11+ Wire format for NodeInfo (compact binary, no protobuf):
12+
13+ [8B ID][2B zone_len][zone][2B addr_len][addr]
14+
15+ Wire format for GeoPolicy:
16+
17+ [8B version][2B rule_count][rule...]
18+ rule: [2B prefix_len][prefix][2B zone_len][zone][8B replicas]
19+
20+ Authors:
21+
22+ Maximilian Hagen
23+ */
24+ package cluster
25+
26+ import (
27+ "encoding/binary"
28+ "sync"
29+ )
30+
31+ // GeoZoneGlobal is the wildcard zone used by rules that replicate a key
32+ // range everywhere. It is also the default for keys that match no rule:
33+ // unknown data is replicated globally rather than silently pinned to one
34+ // zone with cross-ocean latency for readers elsewhere (ADR-006 §Negative).
35+ const GeoZoneGlobal = "*"
36+
37+ // GeoDefaultReplicas is the replica count assumed when a rule does not
38+ // specify one.
39+ const GeoDefaultReplicas = 3
40+
41+ const nodesKeyPrefix = "/tellstone/nodes/"
42+
43+ const geoPolicyKey = "/tellstone/geo/rules"
44+
45+ func nodesKey (id uint64 ) string {
46+ var b [8 ]byte
47+ binary .BigEndian .PutUint64 (b [:], id )
48+ return nodesKeyPrefix + string (b [:])
49+ }
50+
51+ // NodeInfo is the metadata a node self-registers in etcd at startup so every
52+ // other node (and the PD) can resolve node IDs to availability zones.
53+ type NodeInfo struct {
54+ ID uint64
55+ Zone string
56+ Addr string
57+ }
58+
59+ // encodeNodeInfo serializes a NodeInfo into compact binary form.
60+ func encodeNodeInfo (n NodeInfo ) []byte {
61+ buf := make ([]byte , 0 , 12 + len (n .Zone )+ len (n .Addr ))
62+ buf = appendUint64 (buf , n .ID )
63+ buf = appendBytesField (buf , []byte (n .Zone ))
64+ buf = appendBytesField (buf , []byte (n .Addr ))
65+ return buf
66+ }
67+
68+ // decodeNodeInfo parses a NodeInfo value. Returns ok=false on truncated data.
69+ func decodeNodeInfo (b []byte ) (NodeInfo , bool ) {
70+ var n NodeInfo
71+ if len (b ) < 12 { // 8 + 2 + 2
72+ return n , false
73+ }
74+ n .ID = binary .BigEndian .Uint64 (b [0 :8 ])
75+ b = b [8 :]
76+ var ok bool
77+ b , z , ok := readBytesField (b )
78+ if ! ok {
79+ return n , false
80+ }
81+ n .Zone = string (z )
82+ b , a , ok := readBytesField (b )
83+ if ! ok {
84+ return n , false
85+ }
86+ n .Addr = string (a )
87+ return n , true
88+ }
89+
90+ // GeoRule pins keys with a matching prefix to a zone. Zone "*" means the
91+ // range is replicated across all zones; Replicas defaults to
92+ // GeoDefaultReplicas when zero.
93+ type GeoRule struct {
94+ Prefix string
95+ Zone string
96+ Replicas int
97+ }
98+
99+ // GeoPolicy is the operator-defined placement policy. Rules are matched by
100+ // longest prefix; a key with no matching rule defaults to GeoZoneGlobal.
101+ type GeoPolicy struct {
102+ Version uint64
103+ Rules []GeoRule
104+ }
105+
106+ // encodeGeoPolicy serializes a policy into compact binary form.
107+ func encodeGeoPolicy (p GeoPolicy ) []byte {
108+ buf := make ([]byte , 0 , 16 )
109+ buf = appendUint64 (buf , p .Version )
110+ buf = appendUint16 (buf , uint16 (len (p .Rules )))
111+ for _ , r := range p .Rules {
112+ buf = appendBytesField (buf , []byte (r .Prefix ))
113+ buf = appendBytesField (buf , []byte (r .Zone ))
114+ buf = appendUint64 (buf , uint64 (r .Replicas ))
115+ }
116+ return buf
117+ }
118+
119+ // decodeGeoPolicy parses a policy value. Returns ok=false on truncated data.
120+ func decodeGeoPolicy (b []byte ) (GeoPolicy , bool ) {
121+ var p GeoPolicy
122+ if len (b ) < 10 { // 8 + 2
123+ return p , false
124+ }
125+ p .Version = binary .BigEndian .Uint64 (b [0 :8 ])
126+ nRules := binary .BigEndian .Uint16 (b [8 :10 ])
127+ b = b [10 :]
128+ for i := 0 ; i < int (nRules ); i ++ {
129+ var r GeoRule
130+ var prefix , zone []byte
131+ var ok bool
132+ b , prefix , ok = readBytesField (b )
133+ if ! ok {
134+ return p , false
135+ }
136+ r .Prefix = string (prefix )
137+ b , zone , ok = readBytesField (b )
138+ if ! ok {
139+ return p , false
140+ }
141+ r .Zone = string (zone )
142+ if len (b ) < 8 { // replicas
143+ return p , false
144+ }
145+ r .Replicas = int (binary .BigEndian .Uint64 (b [0 :8 ]))
146+ b = b [8 :]
147+ p .Rules = append (p .Rules , r )
148+ }
149+ return p , true
150+ }
151+
152+ // DefaultGeoPolicy returns the bootstrap policy: a single catch-all rule
153+ // replicating everything globally. Applied when no policy exists in etcd.
154+ func DefaultGeoPolicy () GeoPolicy {
155+ return GeoPolicy {
156+ Version : 1 ,
157+ Rules : []GeoRule {
158+ {Prefix : "" , Zone : GeoZoneGlobal , Replicas : GeoDefaultReplicas },
159+ },
160+ }
161+ }
162+
163+ // Match returns the zone a key should be pinned to. Longest matching prefix
164+ // wins; keys matching none default to GeoZoneGlobal.
165+ func (p GeoPolicy ) Match (key []byte ) string {
166+ best := ""
167+ bestLen := - 1
168+ for _ , r := range p .Rules {
169+ if len (r .Prefix ) <= bestLen {
170+ continue
171+ }
172+ if len (r .Prefix ) == 0 {
173+ // Catch-all rule: a candidate, but only wins if nothing longer
174+ // matches (bestLen guards this below).
175+ if bestLen < 0 {
176+ bestLen = 0
177+ best = r .Zone
178+ }
179+ continue
180+ }
181+ if len (key ) >= len (r .Prefix ) && string (key [:len (r .Prefix )]) == r .Prefix {
182+ bestLen = len (r .Prefix )
183+ best = r .Zone
184+ }
185+ }
186+ if bestLen < 0 {
187+ return GeoZoneGlobal
188+ }
189+ return best
190+ }
191+
192+ // ReplicaCount returns the target replica count for a key, or
193+ // GeoDefaultReplicas when no rule (or no requested count) matches.
194+ func (p GeoPolicy ) ReplicaCount (key []byte ) int {
195+ bestPrefix := ""
196+ bestLen := - 1
197+ for _ , r := range p .Rules {
198+ if len (r .Prefix ) <= bestLen {
199+ continue
200+ }
201+ matched := len (r .Prefix ) == 0
202+ if ! matched && len (key ) >= len (r .Prefix ) && string (key [:len (r .Prefix )]) == r .Prefix {
203+ matched = true
204+ }
205+ if matched {
206+ bestLen = len (r .Prefix )
207+ bestPrefix = r .Prefix
208+ }
209+ }
210+ if bestLen < 0 {
211+ return GeoDefaultReplicas
212+ }
213+ for _ , r := range p .Rules {
214+ if r .Prefix == bestPrefix {
215+ if r .Replicas > 0 {
216+ return r .Replicas
217+ }
218+ }
219+ }
220+ return GeoDefaultReplicas
221+ }
222+
223+ // GeoPolicyProvider is implemented by *GeoManager (and test doubles). It
224+ // supplies the current operator policy to placement/leader-election code.
225+ type GeoPolicyProvider interface {
226+ Policy () GeoPolicy
227+ }
228+
229+ // PreferredZoneOf computes the zone a region covering startKey should be
230+ // pinned to. The policy is applied to the region's start key; a range that
231+ // spans multiple rule zones inherits the zone of its lowest key, and future
232+ // splits at rule boundaries refine it. Global ("*") means the replicas are
233+ // spread across all zones (no pinning).
234+ func PreferredZoneOf (policy GeoPolicy , startKey []byte ) string {
235+ return policy .Match (startKey )
236+ }
237+
238+ // PickZonePeers orders candidate node IDs so that nodes in the preferred
239+ // zone come first, followed by all remaining nodes. The global zone "*"
240+ // means no ordering preference (all nodes are treated equally). Nodes not in
241+ // the registry sort after every known node. Used for placement and read
242+ // routing when a region's replicas must be preferred by zone.
243+ func PickZonePeers (zones * NodeZones , preferred string , candidates []uint64 ) []uint64 {
244+ out := make ([]uint64 , 0 , len (candidates ))
245+ if preferred == "" || preferred == GeoZoneGlobal {
246+ // No pinning: preserve the candidate order (all equal).
247+ return candidates
248+ }
249+ preferredSet := make (map [uint64 ]struct {})
250+ for _ , id := range zones .NodesInZone (preferred ) {
251+ preferredSet [id ] = struct {}{}
252+ }
253+ for _ , id := range candidates {
254+ if _ , ok := preferredSet [id ]; ok {
255+ out = append (out , id )
256+ }
257+ }
258+ for _ , id := range candidates {
259+ if _ , ok := preferredSet [id ]; ! ok {
260+ out = append (out , id )
261+ }
262+ }
263+ return out
264+ }
265+
266+ // ZoneOf best-effort resolves a node's zone from the registry for logging and
267+ // latency metrics. Returns "" when unknown.
268+ func ZoneOf (zones * NodeZones , id uint64 ) string {
269+ if zones == nil {
270+ return ""
271+ }
272+ return zones .Zone (id )
273+ }
274+
275+ // NodeZones tracks the node → zone registry watched from
276+ // /tellstone/nodes/<id>. It is a concurrency-safe cache used by the PD for
277+ // placement decisions and by nodes for zone-aware routing.
278+ type NodeZones struct {
279+ mu sync.RWMutex
280+ zones map [uint64 ]string
281+ }
282+
283+ // NewNodeZones returns an empty node→zone registry.
284+ func NewNodeZones () * NodeZones {
285+ return & NodeZones {zones : make (map [uint64 ]string )}
286+ }
287+
288+ // Update upserts a node's zone. Lower-or-equal registrations are ignored so a
289+ // stale watch event cannot roll back a zone change.
290+ func (z * NodeZones ) Update (n NodeInfo ) {
291+ z .mu .Lock ()
292+ defer z .mu .Unlock ()
293+ if cur , ok := z .zones [n .ID ]; ok && cur != "" && n .Zone == "" {
294+ return // never downgrade a known zone to unknown
295+ }
296+ z .zones [n .ID ] = n .Zone
297+ }
298+
299+ // Remove drops a node entry (etcd deletion).
300+ func (z * NodeZones ) Remove (id uint64 ) {
301+ z .mu .Lock ()
302+ defer z .mu .Unlock ()
303+ delete (z .zones , id )
304+ }
305+
306+ // Zone returns the zone for a node ID, or "" when unknown.
307+ func (z * NodeZones ) Zone (id uint64 ) string {
308+ z .mu .RLock ()
309+ defer z .mu .RUnlock ()
310+ return z .zones [id ]
311+ }
312+
313+ // NodesInZone returns the node IDs registered in the given zone.
314+ func (z * NodeZones ) NodesInZone (zone string ) []uint64 {
315+ z .mu .RLock ()
316+ defer z .mu .RUnlock ()
317+ var out []uint64
318+ for id , zz := range z .zones {
319+ if zz == zone {
320+ out = append (out , id )
321+ }
322+ }
323+ return out
324+ }
325+
326+ // Snapshot returns a copy of the registry keyed by node ID.
327+ func (z * NodeZones ) Snapshot () map [uint64 ]string {
328+ z .mu .RLock ()
329+ defer z .mu .RUnlock ()
330+ out := make (map [uint64 ]string , len (z .zones ))
331+ for id , zone := range z .zones {
332+ out [id ] = zone
333+ }
334+ return out
335+ }
0 commit comments