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  }