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  }