go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/notify/emailgen.go (about) 1 // Copyright 2018 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 notify 16 17 import ( 18 "context" 19 20 "go.chromium.org/luci/common/data/caching/lru" 21 "go.chromium.org/luci/common/errors" 22 "go.chromium.org/luci/common/logging" 23 "go.chromium.org/luci/gae/service/datastore" 24 "go.chromium.org/luci/server/caching" 25 26 "go.chromium.org/luci/luci_notify/config" 27 "go.chromium.org/luci/luci_notify/mailtmpl" 28 ) 29 30 // bundle is a wrapper around mailtmpl.Bundle to provide extra info 31 // relevant only on server. 32 type bundle struct { 33 *mailtmpl.Bundle 34 revision string 35 } 36 37 // bundleCache is a in-process cache of email template bundles. 38 var bundleCache = caching.RegisterLRUCache[string, *bundle](128) 39 40 // getBundle returns a bundle of all email templates for the given project. 41 // The returned bundle is cached in the process memory, do not modify it. 42 // 43 // Returns an error only on transient failures. 44 // 45 // Ignores an existing Datastore transaction in c, if any. 46 func getBundle(c context.Context, projectID string) (*bundle, error) { 47 // Untie c from the current transaction. 48 // What we do here has nothing to do with a possible current transaction in c. 49 c = datastore.WithoutTransaction(c) 50 51 // Fetch current revision of the project config. 52 project := &config.Project{Name: projectID} 53 if err := datastore.Get(c, project); err != nil { 54 return nil, errors.Annotate(err, "failed to fetch project").Err() 55 } 56 57 // Lookup an existing bundle in the process cache. 58 // If not available, make one and cache it. 59 var transientErr error 60 value, ok := bundleCache.LRU(c).Mutate(c, projectID, func(it *lru.Item[*bundle]) *lru.Item[*bundle] { 61 if it != nil && it.Value.revision == project.Revision { 62 return it // Cache hit. 63 } 64 65 // Cache miss. Either no cached value or revision mismatch. 66 67 // Fetch all templates from the Datastore transactionally with the project. 68 // On a transient error, return it and do not purge cache. 69 var templateEntities []*config.EmailTemplate 70 transientErr = datastore.RunInTransaction(c, func(c context.Context) error { 71 templateEntities = templateEntities[:0] // txn may be retried 72 if err := datastore.Get(c, project); err != nil { 73 return err 74 } 75 76 q := datastore.NewQuery("EmailTemplate").Ancestor(datastore.KeyForObj(c, project)) 77 return datastore.GetAll(c, q, &templateEntities) 78 }, nil) 79 if transientErr != nil { 80 return it 81 } 82 logging.Infof(c, "bundleCache: fetched %d email templates of project %q", len(templateEntities), projectID) 83 84 templates := make([]*mailtmpl.Template, len(templateEntities)) 85 for i, t := range templateEntities { 86 templates[i] = t.Template() 87 } 88 89 // Bundle all fetched templates. If bundling/parsing fails, cache the error, 90 // so we don't recompile bad templates over and over. 91 b := &bundle{ 92 revision: project.Revision, 93 Bundle: mailtmpl.NewBundle(templates), 94 } 95 96 // Cache without expiration. 97 return &lru.Item[*bundle]{Value: b} 98 }) 99 100 switch { 101 case transientErr != nil: 102 return nil, transientErr 103 case !ok: 104 panic("impossible: no cached value and no error") 105 default: 106 return value, nil 107 } 108 }