|
6 | 6 | package cmn |
7 | 7 |
|
8 | 8 | import ( |
| 9 | + "reflect" |
9 | 10 | "sync" |
10 | 11 | ratomic "sync/atomic" |
11 | 12 |
|
12 | 13 | "github.com/NVIDIA/aistore/api/apc" |
13 | 14 | "github.com/NVIDIA/aistore/cmn/cos" |
| 15 | + "github.com/NVIDIA/aistore/cmn/debug" |
14 | 16 | ) |
15 | 17 |
|
16 | 18 | // GCO (Global Config Owner) is responsible for updating and notifying |
@@ -70,86 +72,117 @@ func (gco *gco) SetLocalFSPaths(toUpdate *ConfigToSet) (overrideConfig *ConfigTo |
70 | 72 | return |
71 | 73 | } |
72 | 74 |
|
73 | | -// NOTE [backward compatibility] and future steps: ================================================================= |
| 75 | +// NOTE [backward compatibility] ================================================================================== |
74 | 76 | // |
75 | 77 | // When pointerizing additional sections: |
76 | 78 | // - update the corresponding <section>.Validate() to normalize zero/unset fields to their canonical defaults; |
77 | 79 | // - review/skip fields where zero has an intentional user-visible meaning such as "disabled" or "" (for "none", etc). |
| 80 | +// - implement `defaultOmittable()` interface |
78 | 81 | // |
79 | 82 | // Release notes for the intervening TBD releases (v5.0, v5.1) must carry a disclaimer. |
80 | 83 | // |
81 | | -// Phase-2: keep ensureDefaults() (in-memory sections stay non-nil); add normalizeToNil() |
82 | | -// to strip pointerized sections containing only canonical defaults at marshal time. |
83 | | -// Caveats: |
84 | | -// - `Config` embeds `ClusterConfig` inline, so a MarshalJSON on the latter gets promoted |
85 | | -// to the former and would truncate it - the strip belongs at the encode sites; |
86 | | -// - the persisted/metasynced clone is *not* validated (ais/gconfig.go `_runPre` validates |
87 | | -// a separate copy), so "all-defaults" cannot be assumed of the value being marshaled. |
| 84 | +// Note that ensureDefaults() keeps in-memory sections non-nil; PruneOmittables() |
| 85 | +// strips all-default sections at encode time - see ais/gconfig.go `_encode`. |
| 86 | +// Join/handshake `cluMeta` and apc.WhatNodeConfig/apc.WhatClusterConfig queries stay fully populated (not sparse). |
88 | 87 | // ================================================================================================================= |
89 | 88 |
|
90 | | -// CopyStruct is a shallow copy. Mutable pointer-backed sections are |
91 | | -// deep-copied explicitly below; read-only FSPaths and BackendConf are shared. |
92 | | -// NOTE: |
93 | | -// every new pointer section in ClusterConfig must be added here |
94 | 89 | func (gco *gco) Clone() *Config { |
95 | 90 | src := gco.Get() |
96 | 91 | dst := &Config{} |
97 | | - cos.CopyStruct(dst, src) // shallow |
| 92 | + cos.CopyStruct(dst, src) // shallow-copy |
98 | 93 |
|
99 | 94 | // clone assorted pointers to structs |
100 | 95 | src.Auth.CopyTo(&dst.Auth) |
101 | 96 |
|
102 | | - dst.clonePtrs() |
| 97 | + dst.clonePtrs() // deep-copy |
103 | 98 |
|
104 | 99 | return dst |
105 | 100 | } |
106 | 101 |
|
107 | 102 | // deep-copy pointerized sections (to break aliasing) |
108 | 103 | func (c *ClusterConfig) clonePtrs() { |
| 104 | + // optional configuration |
109 | 105 | if c.Tracing != nil { |
110 | 106 | v := *c.Tracing |
111 | 107 | c.Tracing = &v |
112 | 108 | } |
113 | | - if c.TCB != nil { |
114 | | - v := *c.TCB |
115 | | - c.TCB = &v |
116 | | - } |
117 | | - if c.TCO != nil { |
118 | | - v := *c.TCO |
119 | | - c.TCO = &v |
120 | | - } |
121 | | - if c.Arch != nil { |
122 | | - v := *c.Arch |
123 | | - c.Arch = &v |
124 | | - } |
125 | 109 | if pub := c.Net.HTTP.Pub; pub != nil { |
126 | 110 | v := *pub |
127 | 111 | c.Net.HTTP.Pub = &v |
128 | 112 | } |
129 | | - if c.Lso != nil { |
130 | | - v := *c.Lso |
131 | | - c.Lso = &v |
| 113 | + |
| 114 | + // default-omittable sections |
| 115 | + c.rangeDefaultOmittable(func(field reflect.Value) { |
| 116 | + if field.IsNil() { |
| 117 | + return |
| 118 | + } |
| 119 | + clone := reflect.New(field.Type().Elem()) |
| 120 | + clone.Elem().Set(field.Elem()) |
| 121 | + field.Set(clone) |
| 122 | + }) |
| 123 | +} |
| 124 | + |
| 125 | +func (c *ClusterConfig) rangeDefaultOmittable(visit func(reflect.Value)) { |
| 126 | + v := reflect.ValueOf(c).Elem() |
| 127 | + |
| 128 | + for i := range v.NumField() { |
| 129 | + field := v.Field(i) |
| 130 | + if field.Kind() != reflect.Pointer || !field.CanInterface() { |
| 131 | + continue |
| 132 | + } |
| 133 | + if _, ok := field.Interface().(defaultOmittable); ok { |
| 134 | + visit(field) |
| 135 | + } |
132 | 136 | } |
133 | 137 | } |
134 | 138 |
|
135 | | -// from `nil` to canonical defaults via subsequent config-section.Validate() |
136 | | -// NOTE: |
137 | | -// pointerized sections are always materialized in memory - consumers dereference |
138 | | -// them unconditionally (e.g. config.Lso.WalkBuffer). Not *persisting* all-default |
139 | | -// sections is a separate, marshal-time concern - see phase-2 below. |
| 139 | +// Materialize default-omittable sections before section validation. |
| 140 | +// Runtime consumers may therefore dereference them unconditionally. |
140 | 141 | func (c *ClusterConfig) ensureDefaults() { |
141 | | - if c.TCB == nil { |
142 | | - c.TCB = &TCBConf{} |
143 | | - } |
144 | | - if c.TCO == nil { |
145 | | - c.TCO = &TCOConf{} |
146 | | - } |
147 | | - if c.Arch == nil { |
148 | | - c.Arch = &ArchConf{} |
149 | | - } |
150 | | - if c.Lso == nil { |
151 | | - c.Lso = &LsoConf{} |
152 | | - } |
| 142 | + c.rangeDefaultOmittable(func(field reflect.Value) { |
| 143 | + if field.IsNil() { |
| 144 | + field.Set(reflect.New(field.Type().Elem())) |
| 145 | + } |
| 146 | + }) |
| 147 | +} |
| 148 | + |
| 149 | +// PruneOmittables converts cluster config to its persistence/metasync sparse form. |
| 150 | +// The caller must invoke it only on a private copy (shallow is fine), never on the live config. |
| 151 | +// |
| 152 | +// A default-omittable section is removed when its validated value equals the |
| 153 | +// canonical defaults produced by validating a zero value. Any non-default |
| 154 | +// section remains unchanged, including its zero-valued fields. |
| 155 | +// |
| 156 | +// Configuration GET APIs (apc.WhatClusterConfig, apc.WhatNodeConfig) |
| 157 | +// deliberately return the fully materialized effective configuration, so |
| 158 | +// clients need not resolve server-side defaults. The asymmetry is intentional: |
| 159 | +// do not make those paths sparse. |
| 160 | +// |
| 161 | +// See also: ensureDefaults and clonePtrs; all three are driven by defaultOmittable |
| 162 | +// marker interface. |
| 163 | +func (c *ClusterConfig) PruneOmittables() { |
| 164 | + c.rangeDefaultOmittable(func(field reflect.Value) { |
| 165 | + if field.IsNil() { |
| 166 | + return |
| 167 | + } |
| 168 | + |
| 169 | + curr := reflect.New(field.Type().Elem()) |
| 170 | + curr.Elem().Set(field.Elem()) |
| 171 | + if err := curr.Interface().(defaultOmittable).Validate(); err != nil { |
| 172 | + debug.AssertNoErr(err) |
| 173 | + return |
| 174 | + } |
| 175 | + |
| 176 | + dflt := reflect.New(field.Type().Elem()) |
| 177 | + if err := dflt.Interface().(defaultOmittable).Validate(); err != nil { |
| 178 | + debug.AssertNoErr(err) |
| 179 | + return |
| 180 | + } |
| 181 | + |
| 182 | + if reflect.DeepEqual(curr.Elem().Interface(), dflt.Elem().Interface()) { |
| 183 | + field.SetZero() |
| 184 | + } |
| 185 | + }) |
153 | 186 | } |
154 | 187 |
|
155 | 188 | // When updating we need to make sure that the update is transaction and no |
|
0 commit comments