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 }