go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/settings/external.go (about) 1 // Copyright 2019 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package settings 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "io" 22 "sync/atomic" 23 "time" 24 25 "go.chromium.org/luci/common/logging" 26 ) 27 28 // ExternalStorage implements Storage interface using an externally supplied 29 // io.Reader with JSON. 30 // 31 // This is read-only storage (it doesn't implement MutableStorage interface), 32 // meaning settings it stores can't be change via UpdateSetting (or admin UI). 33 // 34 // They still can be dynamically reloaded via Load() though. 35 type ExternalStorage struct { 36 values atomic.Value // settings as map[string]*json.RawMessage, immutable 37 } 38 39 // Load loads (or reloads) settings from a given reader. 40 // 41 // The reader should supply a JSON document with a dict. Keys are strings 42 // matching various settings pages, and values are corresponding settings 43 // (usually also dicts). After settings are loaded (and the context is properly 44 // configured), calling settings.Get(c, <key>, &output) deserializes the value 45 // supplied for the corresponding key into 'output'. 46 // 47 // On success logs what has changed (if anything) and eventually starts serving 48 // new settings (see comments for FetchAllSettings regarding the caching). 49 // 50 // On failure returns an error without any logging or without changing what is 51 // currently being served. 52 func (s *ExternalStorage) Load(c context.Context, r io.Reader) error { 53 var newS map[string]*json.RawMessage 54 if err := json.NewDecoder(r).Decode(&newS); err != nil { 55 return err 56 } 57 58 // Diff old and new settings, emit difference into the log. Note that we do it 59 // even on the first Load (when old settings are empty), to log initial 60 // settings. 61 type change struct { 62 Old *json.RawMessage `json:"old,omitempty"` 63 New *json.RawMessage `json:"new,omitempty"` 64 } 65 added := map[string]*json.RawMessage{} // key => new added value 66 removed := map[string]*json.RawMessage{} // key => old removed value 67 changed := map[string]change{} // key => old and new values 68 69 oldS, _ := s.values.Load().(map[string]*json.RawMessage) 70 addedKeys, removedKeys, sameKeys := diffKeys(newS, oldS) 71 72 for _, k := range addedKeys { 73 added[k] = newS[k] 74 } 75 for _, k := range removedKeys { 76 removed[k] = oldS[k] 77 } 78 for _, k := range sameKeys { 79 oldV := oldS[k] 80 newV := newS[k] 81 if !bytes.Equal(*oldV, *newV) { 82 changed[k] = change{Old: oldV, New: newV} 83 } 84 } 85 86 if len(added) != 0 || len(removed) != 0 || len(changed) != 0 { 87 (logging.Fields{ 88 "added": added, 89 "removed": removed, 90 "changed": changed, 91 }).Warningf(c, "Settings updated") 92 } 93 94 s.values.Store(newS) 95 return nil 96 } 97 98 // FetchAllSettings is part of Storage interface, it returns a bundle with all 99 // latest settings and its expiration time. 100 // 101 // The bundle has expiration time of 10 sec, so that Settings{...} 102 // implementation doesn't go through slower cache-miss code path all the time 103 // a setting is read (which happens multiple times per request). In practice it 104 // means changes loaded via Load() apply at most 10 sec later. 105 func (s *ExternalStorage) FetchAllSettings(context.Context) (*Bundle, time.Duration, error) { 106 values, _ := s.values.Load().(map[string]*json.RawMessage) 107 return &Bundle{Values: values}, 10 * time.Second, nil 108 } 109 110 // diffKeys returns 3 sets of keys: a-b, b-a and a&b. 111 func diffKeys(a, b map[string]*json.RawMessage) (aMb, bMa, aAb []string) { 112 for k := range a { 113 if _, ok := b[k]; ok { 114 aAb = append(aAb, k) 115 } else { 116 aMb = append(aMb, k) 117 } 118 } 119 for k := range b { 120 if _, ok := a[k]; !ok { 121 bMa = append(bMa, k) 122 } 123 } 124 return 125 }