blob: cf667663d1a7ba49c44d67e2a22119fd791a4adf [file] [log] [blame]
// Copyright 2018 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 notify
import (
"context"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/common/data/caching/lru"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/server/caching"
"go.chromium.org/luci/luci_notify/config"
"go.chromium.org/luci/luci_notify/mailtmpl"
)
// bundle is a wrapper around mailtmpl.Bundle to provide extra info
// relevant only on server.
type bundle struct {
*mailtmpl.Bundle
revision string
}
// bundleCache is a in-process cache of email template bundles.
var bundleCache = caching.RegisterLRUCache(128)
// getBundle returns a bundle of all email templates for the given project.
// The returned bundle is cached in the process memory, do not modify it.
//
// Returns an error only on transient failures.
//
// Ignores an existing Datastore transaction in c, if any.
func getBundle(c context.Context, projectID string) (*bundle, error) {
// Untie c from the current transaction.
// What we do here has nothing to do with a possible current transaction in c.
c = datastore.WithoutTransaction(c)
// Fetch current revision of the project config.
project := &config.Project{Name: projectID}
if err := datastore.Get(c, project); err != nil {
return nil, errors.Annotate(err, "failed to fetch project").Err()
}
// Lookup an existing bundle in the process cache.
// If not available, make one and cache it.
var transientErr error
value, ok := bundleCache.LRU(c).Mutate(c, projectID, func(it *lru.Item) *lru.Item {
if it != nil && it.Value.(*bundle).revision == project.Revision {
return it // Cache hit.
}
// Cache miss. Either no cached value or revision mismatch.
// Fetch all templates from the Datastore transactionally with the project.
// On a transient error, return it and do not purge cache.
var templateEntities []*config.EmailTemplate
transientErr = datastore.RunInTransaction(c, func(c context.Context) error {
templateEntities = templateEntities[:0] // txn may be retried
if err := datastore.Get(c, project); err != nil {
return err
}
q := datastore.NewQuery("EmailTemplate").Ancestor(datastore.KeyForObj(c, project))
return datastore.GetAll(c, q, &templateEntities)
}, nil)
if transientErr != nil {
return it
}
logging.Infof(c, "bundleCache: fetched %d email templates of project %q", len(templateEntities), projectID)
templates := make([]*mailtmpl.Template, len(templateEntities))
for i, t := range templateEntities {
templates[i] = t.Template()
}
// Bundle all fetched templates. If bundling/parsing fails, cache the error,
// so we don't recompile bad templates over and over.
b := &bundle{
revision: project.Revision,
Bundle: mailtmpl.NewBundle(templates),
}
// Cache without expiration.
return &lru.Item{Value: b}
})
switch {
case transientErr != nil:
return nil, transientErr
case !ok:
panic("impossible: no cached value and no error")
default:
return value.(*bundle), nil
}
}