go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaesettings/gaesettings.go (about)

     1  // Copyright 2015 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 gaesettings implements settings.Storage interface on top of GAE
    16  // datastore.
    17  //
    18  // By default, gaesettings must have its handlers installed into the "default"
    19  // AppEngine module, and must be running on an instance with read/write
    20  // datastore access.
    21  //
    22  // See go.chromium.org/luci/server/settings for more details.
    23  //
    24  // Deprecated: either use command line flags for infrequently changing
    25  // configuration or use the LUCI Config service via
    26  // go.chromium.org/luci/config/server/cfgmodule module to fetch configs
    27  // dynamically from the LUCI Config.
    28  package gaesettings
    29  
    30  import (
    31  	"context"
    32  	"encoding/json"
    33  	"strconv"
    34  	"time"
    35  
    36  	"go.chromium.org/luci/common/clock"
    37  	"go.chromium.org/luci/common/logging"
    38  	"go.chromium.org/luci/common/retry/transient"
    39  	"go.chromium.org/luci/gae/filter/dscache"
    40  	ds "go.chromium.org/luci/gae/service/datastore"
    41  	"go.chromium.org/luci/gae/service/info"
    42  	"go.chromium.org/luci/server/settings"
    43  )
    44  
    45  // Storage knows how to store JSON blobs with settings in the datastore.
    46  //
    47  // It implements server/settings.EventualConsistentStorage interface.
    48  type Storage struct{}
    49  
    50  // settingsEntity is used to store all settings as JSON blob. Latest settings
    51  // are stored under key (gaesettings.Settings, latest). The historical log is
    52  // stored using exact same entity under keys (gaesettings.SettingsLog, version),
    53  // with parent being (gaesettings.Settings, latest). Version is monotonically
    54  // increasing integer starting from 1.
    55  type settingsEntity struct {
    56  	Kind    string    `gae:"$kind"`
    57  	ID      string    `gae:"$id"`
    58  	Parent  *ds.Key   `gae:"$parent"`
    59  	Version int       `gae:",noindex"`
    60  	Value   string    `gae:",noindex"`
    61  	When    time.Time `gae:",noindex"`
    62  
    63  	_extra ds.PropertyMap `gae:"-,extra"`
    64  }
    65  
    66  // defaultContext returns datastore interface configured to use default
    67  // namespace, escape any current transaction, and don't use dscache (since it
    68  // may not be available when modifying settings).
    69  func defaultContext(ctx context.Context) context.Context {
    70  	ctx = ds.WithoutTransaction(info.MustNamespace(ctx, ""))
    71  	return dscache.AddShardFunctions(ctx, func(*ds.Key) (shards int, ok bool) {
    72  		return 0, true
    73  	})
    74  }
    75  
    76  // latestSettings returns settingsEntity with prefilled key pointing to latest
    77  // settings.
    78  func latestSettings() settingsEntity {
    79  	return settingsEntity{Kind: "gaesettings.Settings", ID: "latest"}
    80  }
    81  
    82  // expirationDuration returns how long to hold settings in memory cache.
    83  //
    84  // One minute in prod, one second on dev server (since long expiration time on
    85  // dev server is very annoying).
    86  func (s Storage) expirationDuration(ctx context.Context) time.Duration {
    87  	if info.IsDevAppServer(ctx) {
    88  		return time.Second
    89  	}
    90  	return time.Minute
    91  }
    92  
    93  // FetchAllSettings fetches all latest settings at once.
    94  func (s Storage) FetchAllSettings(ctx context.Context) (*settings.Bundle, time.Duration, error) {
    95  	ctx = defaultContext(ctx)
    96  	logging.Debugf(ctx, "Fetching app settings from the datastore")
    97  
    98  	latest := latestSettings()
    99  	switch err := ds.Get(ctx, &latest); {
   100  	case err == ds.ErrNoSuchEntity:
   101  		break
   102  	case err != nil:
   103  		return nil, 0, transient.Tag.Apply(err)
   104  	}
   105  
   106  	pairs := map[string]*json.RawMessage{}
   107  	if latest.Value != "" {
   108  		if err := json.Unmarshal([]byte(latest.Value), &pairs); err != nil {
   109  			return nil, 0, err
   110  		}
   111  	}
   112  	return &settings.Bundle{Values: pairs}, s.expirationDuration(ctx), nil
   113  }
   114  
   115  // UpdateSetting updates a setting at the given key.
   116  func (s Storage) UpdateSetting(ctx context.Context, key string, value json.RawMessage) error {
   117  	ctx = defaultContext(ctx)
   118  
   119  	var fatalFail error // set in transaction on fatal errors
   120  	err := ds.RunInTransaction(ctx, func(ctx context.Context) error {
   121  		// Fetch the most recent values.
   122  		latest := latestSettings()
   123  		if err := ds.Get(ctx, &latest); err != nil && err != ds.ErrNoSuchEntity {
   124  			return err
   125  		}
   126  
   127  		// Update the value.
   128  		pairs := map[string]*json.RawMessage{}
   129  		if len(latest.Value) != 0 {
   130  			if err := json.Unmarshal([]byte(latest.Value), &pairs); err != nil {
   131  				fatalFail = err
   132  				return err
   133  			}
   134  		}
   135  		pairs[key] = &value
   136  
   137  		// Store the previous one in the log.
   138  		auditCopy := latest
   139  		auditCopy.Kind = "gaesettings.SettingsLog"
   140  		auditCopy.ID = strconv.Itoa(latest.Version)
   141  		auditCopy.Parent = ds.KeyForObj(ctx, &latest)
   142  
   143  		// Prepare a new version.
   144  		buf, err := json.MarshalIndent(pairs, "", "  ")
   145  		if err != nil {
   146  			fatalFail = err
   147  			return err
   148  		}
   149  		latest.Version++
   150  		latest.Value = string(buf)
   151  		latest.When = clock.Now(ctx).UTC()
   152  
   153  		// Skip update if no changes at all.
   154  		if latest.Value == auditCopy.Value {
   155  			return nil
   156  		}
   157  
   158  		// Don't store copy of "no settings at all", it's useless.
   159  		if latest.Version == 1 {
   160  			return ds.Put(ctx, &latest)
   161  		}
   162  		return ds.Put(ctx, &latest, &auditCopy)
   163  	}, nil)
   164  
   165  	if fatalFail != nil {
   166  		return fatalFail
   167  	}
   168  	return transient.Tag.Apply(err)
   169  }
   170  
   171  // GetConsistencyTime returns "last modification time" + "expiration period".
   172  //
   173  // It indicates moment in time when last setting change is fully propagated to
   174  // all instances.
   175  //
   176  // Returns zero time if there are no settings stored.
   177  func (s Storage) GetConsistencyTime(ctx context.Context) (time.Time, error) {
   178  	ctx = defaultContext(ctx)
   179  	latest := latestSettings()
   180  	switch err := ds.Get(ctx, &latest); err {
   181  	case nil:
   182  		return latest.When.Add(s.expirationDuration(ctx)), nil
   183  	case ds.ErrNoSuchEntity:
   184  		return time.Time{}, nil
   185  	default:
   186  		return time.Time{}, transient.Tag.Apply(err)
   187  	}
   188  }