blob: 4fc616f24378bd83f72484ed43c21788e5045f04 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package settings
import (
"bytes"
"context"
"encoding/json"
"io"
"sync/atomic"
"time"
"go.chromium.org/luci/common/logging"
)
// ExternalStorage implements Storage interface using an externally supplied
// io.Reader with JSON.
//
// This is read-only storage (it doesn't implement MutableStorage interface),
// meaning settings it stores can't be change via UpdateSetting (or admin UI).
//
// They still can be dynamically reloaded via Load() though.
type ExternalStorage struct {
values atomic.Value // settings as map[string]*json.RawMessage, immutable
}
// Load loads (or reloads) settings from a given reader.
//
// The reader should supply a JSON document with a dict. Keys are strings
// matching various settings pages, and values are corresponding settings
// (usually also dicts). After settings are loaded (and the context is properly
// configured), calling settings.Get(c, <key>, &output) deserializes the value
// supplied for the corresponding key into 'output'.
//
// On success logs what has changed (if anything) and eventually starts serving
// new settings (see comments for FetchAllSettings regarding the caching).
//
// On failure returns an error without any logging or without changing what is
// currently being served.
func (s *ExternalStorage) Load(c context.Context, r io.Reader) error {
var newS map[string]*json.RawMessage
if err := json.NewDecoder(r).Decode(&newS); err != nil {
return err
}
// Diff old and new settings, emit difference into the log. Note that we do it
// even on the first Load (when old settings are empty), to log initial
// settings.
type change struct {
Old *json.RawMessage `json:"old,omitempty"`
New *json.RawMessage `json:"new,omitempty"`
}
added := map[string]*json.RawMessage{} // key => new added value
removed := map[string]*json.RawMessage{} // key => old removed value
changed := map[string]change{} // key => old and new values
oldS, _ := s.values.Load().(map[string]*json.RawMessage)
addedKeys, removedKeys, sameKeys := diffKeys(newS, oldS)
for _, k := range addedKeys {
added[k] = newS[k]
}
for _, k := range removedKeys {
removed[k] = oldS[k]
}
for _, k := range sameKeys {
oldV := oldS[k]
newV := newS[k]
if !bytes.Equal(*oldV, *newV) {
changed[k] = change{Old: oldV, New: newV}
}
}
if len(added) != 0 || len(removed) != 0 || len(changed) != 0 {
(logging.Fields{
"added": added,
"removed": removed,
"changed": changed,
}).Warningf(c, "Settings updated")
}
s.values.Store(newS)
return nil
}
// FetchAllSettings is part of Storage interface, it returns a bundle with all
// latest settings and its expiration time.
//
// The bundle has expiration time of 10 sec, so that Settings{...}
// implementation doesn't go through slower cache-miss code path all the time
// a setting is read (which happens multiple times per request). In practice it
// means changes loaded via Load() apply at most 10 sec later.
func (s *ExternalStorage) FetchAllSettings(context.Context) (*Bundle, time.Duration, error) {
values, _ := s.values.Load().(map[string]*json.RawMessage)
return &Bundle{Values: values}, 10 * time.Second, nil
}
// diffKeys returns 3 sets of keys: a-b, b-a and a&b.
func diffKeys(a, b map[string]*json.RawMessage) (aMb, bMa, aAb []string) {
for k := range a {
if _, ok := b[k]; ok {
aAb = append(aAb, k)
} else {
aMb = append(aMb, k)
}
}
for k := range b {
if _, ok := a[k]; !ok {
bMa = append(bMa, k)
}
}
return
}