blob: 5203b5adab0eb3e56b33ec16196527e1a8fc1e02 [file] [log] [blame]
// Copyright 2017 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 gaemiddleware
import (
"context"
"net/http"
"sync"
"go.chromium.org/luci/gae/filter/dscache"
"go.chromium.org/luci/gae/filter/featureBreaker"
"go.chromium.org/luci/gae/filter/readonly"
"go.chromium.org/luci/gae/filter/txndefer"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/common/data/caching/cacheContext"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/server/caching"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/secrets"
"go.chromium.org/luci/server/settings"
"go.chromium.org/luci/server/warmup"
"go.chromium.org/luci/appengine/gaesecrets"
"go.chromium.org/luci/appengine/gaesettings"
)
// errSimulatedMemcacheOutage is returned by all memcache calls if
// SimulateMemcacheOutage setting is enabled.
var errSimulatedMemcacheOutage = errors.New("simulated memcache outage")
// Environment is a middleware environment. Its parameters define how the
// middleware is applied, and which services are enlisted.
//
// This is low-level API. Use 'gaemiddeware/standard' instead.
type Environment struct {
// MemcacheAvailable is true if the environment has working memcache.
//
// If false, also implies disabled datastore caching layer.
MemcacheAvailable bool
// DSReadOnly, if true, causes a read-only datastore layer to be imposed,
// preventing datastore writes.
//
// For any given datastore instance, at most one caching layer may be used.
// All other instances must be ReadOnly to prevent errant writes from breaking
// the assumptions of that caching layer. For example, if a Flex VM is being
// used in conjunction with a non-read-only Classic AppEngine instance.
DSReadOnly bool
// DSReadOnlyPredicate returns true for keys that must not be mutated.
//
// Effective only when DSReadOnly is true. If nil, all keys are considered
// read-only.
DSReadOnlyPredicate readonly.Predicate
// Prepare will be called once after init() time, but before serving requests.
//
// The given context is very bare, use it only for logging and deadlines and
// stuff like that. It has no other services installed.
Prepare func(context.Context)
// WithInitialRequest is called at the very beginning of the handler. It
// contains a reference to the handler's HTTP request.
//
// This should install basic services into the Context, including:
// - Logging
// - luci/GAE services.
WithInitialRequest func(context.Context, *http.Request) context.Context
// WithConfig is called during service setup to install the "gaeconfig" layer.
//
// If nil, no config layer will be installed.
WithConfig func(context.Context) context.Context
// WithAuth is called during service setup to install the "gaeauth" layer.
//
// If nil, no auth layer will be installed.
WithAuth func(context.Context) context.Context
// ExtraMiddleware, if not nil, is additional middleware chain to append to
// the end of the Base middleware chain to perform per-request monitoring.
ExtraMiddleware router.MiddlewareChain
// ExtraHandlers, if not nil, is used to install additional handlers when
// InstallHandlers is called.
ExtraHandlers func(r *router.Router, base router.MiddlewareChain)
prepareOnce sync.Once
// processCacheData holds all global LRU caches.
processCacheData *caching.ProcessCacheData
// globalSettings holds global app settings lazily updated from the datastore.
globalSettings *settings.Settings
}
// ensurePrepared is called before handling requests to initialize global state.
func (e *Environment) ensurePrepared(ctx context.Context) {
e.prepareOnce.Do(func() {
e.processCacheData = caching.NewProcessCacheData()
e.globalSettings = settings.New(gaesettings.Storage{})
if e.Prepare != nil {
e.Prepare(ctx)
}
})
}
// InstallHandlers installs handlers for an Environment's framework routes.
//
// See InstallHandlersWithMiddleware for more information.
func (e *Environment) InstallHandlers(r *router.Router) {
e.InstallHandlersWithMiddleware(r, e.Base())
}
// InstallHandlersWithMiddleware installs handlers for an Environment's
// framework routes.
//
// In addition to Environment-specific handlers, InstallHandlersWithMiddleware
// installs:
// * Warmup Handler (warmup)
func (e *Environment) InstallHandlersWithMiddleware(r *router.Router, base router.MiddlewareChain) {
warmup.InstallHandlersDeprecated(r, base)
if e.ExtraHandlers != nil {
e.ExtraHandlers(r, base)
}
}
// With adds various production GAE LUCI services to the context.
//
// Basically, it installs GAE-specific backends and caches for various
// subsystems to make them work in GAE environment.
//
// One example is a backend for Logging: go.chromium.org/luci/common/logging.
// Logs emitted through a WithProd() context go to GAE logs.
//
// 'Production' here means the services will use real GAE APIs (not mocks or
// stubs), so With should never be used from unit tests.
func (e *Environment) With(ctx context.Context, req *http.Request) context.Context {
// Set an initial logging level. We'll configure this to be more specific
// later once we can load settings.
ctx = logging.SetLevel(ctx, logging.Debug)
// Ensure one-time initialization happened.
e.ensurePrepared(ctx)
// Install process and request LRU caches.
ctx = caching.WithProcessCacheData(ctx, e.processCacheData)
ctx = caching.WithRequestCache(ctx)
ctx = e.WithInitialRequest(ctx, req)
// A previous layer must have installed a "luci/gae" datastore.
if datastore.Raw(ctx) == nil {
panic("no luci/gae datastore is installed")
}
// The global cache depends on luci/gae's memcache installed.
if e.MemcacheAvailable {
ctx = caching.WithGlobalCache(ctx, blobCacheProvider)
}
// These are needed to use fetchCachedSettings.
ctx = settings.Use(ctx, e.globalSettings)
// Fetch and apply configuration stored in the datastore.
cachedSettings := fetchCachedSettings(ctx)
ctx = logging.SetLevel(ctx, cachedSettings.LoggingLevel)
if e.MemcacheAvailable {
if bool(cachedSettings.SimulateMemcacheOutage) {
logging.Warningf(ctx, "Memcache outage simulation is enabled")
var fb featureBreaker.FeatureBreaker
ctx, fb = featureBreaker.FilterMC(ctx, errSimulatedMemcacheOutage)
fb.BreakFeatures(nil,
"GetMulti", "AddMulti", "SetMulti", "DeleteMulti",
"CompareAndSwapMulti", "Flush", "Stats")
}
if !bool(cachedSettings.DisableDSCache) {
ctx = dscache.FilterRDS(ctx, nil)
}
}
if e.DSReadOnly {
ctx = readonly.FilterRDS(ctx, e.DSReadOnlyPredicate)
}
ctx = txndefer.FilterRDS(ctx)
// The rest of the service may use applied configuration.
if e.WithConfig != nil {
ctx = e.WithConfig(ctx)
}
ctx = secrets.Use(ctx, gaesecrets.New(nil))
if e.WithAuth != nil {
ctx = e.WithAuth(ctx)
}
// Wrap this in a cache context so that lookups for any of the aforementioned
// items are fast.
return cacheContext.Wrap(ctx)
}
// Base returns a middleware chain to use for all GAE environment
// requests.
//
// Base DOES NOT install "luci/gae" services. To install appropriate services,
// use methods from a sub-package. This is done so that a given AppEngine
// environment doesn't need to include the superset of packages across all
// supported environments.
//
// This middleware chain installs prod GAE services into the request context
// (via With), and wraps the request with a panic catcher and monitoring
// hooks.
func (e *Environment) Base() router.MiddlewareChain {
// addServices is a middleware that installs the set of standard production
// AppEngine services by calling With.
addServices := func(c *router.Context, next router.Handler) {
// Create a cancelable Context that cancels when this request finishes.
//
// We do this because Contexts will leak resources and/or goroutines if they
// have timers or can be canceled, but aren't. Predictably canceling the
// parent will ensure that any such resource leaks are cleaned up.
var cancelFunc context.CancelFunc
c.Context, cancelFunc = context.WithCancel(c.Context)
defer cancelFunc()
// Apply production settings.
c.Context = e.With(c.Context, c.Request)
next(c)
}
mw := router.NewMiddlewareChain(addServices)
mw = mw.Extend(e.ExtraMiddleware...)
return mw
}