go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaemiddleware/context.go (about)

     1  // Copyright 2017 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 gaemiddleware
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"sync"
    21  
    22  	"go.chromium.org/luci/gae/filter/dscache"
    23  	"go.chromium.org/luci/gae/filter/featureBreaker"
    24  	"go.chromium.org/luci/gae/filter/readonly"
    25  	"go.chromium.org/luci/gae/filter/txndefer"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  
    28  	"go.chromium.org/luci/common/data/caching/cacheContext"
    29  	"go.chromium.org/luci/common/errors"
    30  	"go.chromium.org/luci/common/logging"
    31  
    32  	"go.chromium.org/luci/server/caching"
    33  	"go.chromium.org/luci/server/router"
    34  	"go.chromium.org/luci/server/secrets"
    35  	"go.chromium.org/luci/server/settings"
    36  	"go.chromium.org/luci/server/warmup"
    37  
    38  	"go.chromium.org/luci/appengine/gaesecrets"
    39  	"go.chromium.org/luci/appengine/gaesettings"
    40  )
    41  
    42  // errSimulatedMemcacheOutage is returned by all memcache calls if
    43  // SimulateMemcacheOutage setting is enabled.
    44  var errSimulatedMemcacheOutage = errors.New("simulated memcache outage")
    45  
    46  // Environment is a middleware environment. Its parameters define how the
    47  // middleware is applied, and which services are enlisted.
    48  //
    49  // This is low-level API. Use 'gaemiddeware/standard' instead.
    50  type Environment struct {
    51  	// MemcacheAvailable is true if the environment has working memcache.
    52  	//
    53  	// If false, also implies disabled datastore caching layer.
    54  	MemcacheAvailable bool
    55  
    56  	// DSReadOnly, if true, causes a read-only datastore layer to be imposed,
    57  	// preventing datastore writes.
    58  	//
    59  	// For any given datastore instance, at most one caching layer may be used.
    60  	// All other instances must be ReadOnly to prevent errant writes from breaking
    61  	// the assumptions of that caching layer. For example, if a Flex VM is being
    62  	// used in conjunction with a non-read-only Classic AppEngine instance.
    63  	DSReadOnly bool
    64  
    65  	// DSReadOnlyPredicate returns true for keys that must not be mutated.
    66  	//
    67  	// Effective only when DSReadOnly is true. If nil, all keys are considered
    68  	// read-only.
    69  	DSReadOnlyPredicate readonly.Predicate
    70  
    71  	// Prepare will be called once after init() time, but before serving requests.
    72  	//
    73  	// The given context is very bare, use it only for logging and deadlines and
    74  	// stuff like that. It has no other services installed.
    75  	Prepare func(context.Context)
    76  
    77  	// WithInitialRequest is called at the very beginning of the handler. It
    78  	// contains a reference to the handler's HTTP request.
    79  	//
    80  	// This should install basic services into the Context, including:
    81  	// - Logging
    82  	// - luci/GAE services.
    83  	WithInitialRequest func(context.Context, *http.Request) context.Context
    84  
    85  	// WithConfig is called during service setup to install the "gaeconfig" layer.
    86  	//
    87  	// If nil, no config layer will be installed.
    88  	WithConfig func(context.Context) context.Context
    89  
    90  	// WithAuth is called during service setup to install the "gaeauth" layer.
    91  	//
    92  	// If nil, no auth layer will be installed.
    93  	WithAuth func(context.Context) context.Context
    94  
    95  	// ExtraMiddleware, if not nil, is additional middleware chain to append to
    96  	// the end of the Base middleware chain to perform per-request monitoring.
    97  	ExtraMiddleware router.MiddlewareChain
    98  
    99  	// ExtraHandlers, if not nil, is used to install additional handlers when
   100  	// InstallHandlers is called.
   101  	ExtraHandlers func(r *router.Router, base router.MiddlewareChain)
   102  
   103  	prepareOnce sync.Once
   104  
   105  	// processCacheData holds all global LRU caches.
   106  	processCacheData *caching.ProcessCacheData
   107  	// globalSettings holds global app settings lazily updated from the datastore.
   108  	globalSettings *settings.Settings
   109  }
   110  
   111  // ensurePrepared is called before handling requests to initialize global state.
   112  func (e *Environment) ensurePrepared(ctx context.Context) {
   113  	e.prepareOnce.Do(func() {
   114  		e.processCacheData = caching.NewProcessCacheData()
   115  		e.globalSettings = settings.New(gaesettings.Storage{})
   116  		if e.Prepare != nil {
   117  			e.Prepare(ctx)
   118  		}
   119  	})
   120  }
   121  
   122  // InstallHandlers installs handlers for an Environment's framework routes.
   123  //
   124  // See InstallHandlersWithMiddleware for more information.
   125  func (e *Environment) InstallHandlers(r *router.Router) {
   126  	e.InstallHandlersWithMiddleware(r, e.Base())
   127  }
   128  
   129  // InstallHandlersWithMiddleware installs handlers for an Environment's
   130  // framework routes.
   131  //
   132  // In addition to Environment-specific handlers, InstallHandlersWithMiddleware
   133  // installs:
   134  //   - Warmup Handler (warmup)
   135  func (e *Environment) InstallHandlersWithMiddleware(r *router.Router, base router.MiddlewareChain) {
   136  	warmup.InstallHandlersDeprecated(r, base)
   137  
   138  	if e.ExtraHandlers != nil {
   139  		e.ExtraHandlers(r, base)
   140  	}
   141  }
   142  
   143  // With adds various production GAE LUCI services to the context.
   144  //
   145  // Basically, it installs GAE-specific backends and caches for various
   146  // subsystems to make them work in GAE environment.
   147  //
   148  // One example is a backend for Logging: go.chromium.org/luci/common/logging.
   149  // Logs emitted through a WithProd() context go to GAE logs.
   150  //
   151  // 'Production' here means the services will use real GAE APIs (not mocks or
   152  // stubs), so With should never be used from unit tests.
   153  func (e *Environment) With(ctx context.Context, req *http.Request) context.Context {
   154  	// Set an initial logging level. We'll configure this to be more specific
   155  	// later once we can load settings.
   156  	ctx = logging.SetLevel(ctx, logging.Debug)
   157  
   158  	// Ensure one-time initialization happened.
   159  	e.ensurePrepared(ctx)
   160  
   161  	// Install process and request LRU caches.
   162  	ctx = caching.WithProcessCacheData(ctx, e.processCacheData)
   163  	ctx = caching.WithRequestCache(ctx)
   164  
   165  	ctx = e.WithInitialRequest(ctx, req)
   166  
   167  	// A previous layer must have installed a "luci/gae" datastore.
   168  	if datastore.Raw(ctx) == nil {
   169  		panic("no luci/gae datastore is installed")
   170  	}
   171  
   172  	// The global cache depends on luci/gae's memcache installed.
   173  	if e.MemcacheAvailable {
   174  		ctx = caching.WithGlobalCache(ctx, blobCacheProvider)
   175  	}
   176  
   177  	// These are needed to use fetchCachedSettings.
   178  	ctx = settings.Use(ctx, e.globalSettings)
   179  
   180  	// Fetch and apply configuration stored in the datastore.
   181  	cachedSettings := fetchCachedSettings(ctx)
   182  	ctx = logging.SetLevel(ctx, cachedSettings.LoggingLevel)
   183  	if e.MemcacheAvailable {
   184  		if bool(cachedSettings.SimulateMemcacheOutage) {
   185  			logging.Warningf(ctx, "Memcache outage simulation is enabled")
   186  			var fb featureBreaker.FeatureBreaker
   187  			ctx, fb = featureBreaker.FilterMC(ctx, errSimulatedMemcacheOutage)
   188  			fb.BreakFeatures(nil,
   189  				"GetMulti", "AddMulti", "SetMulti", "DeleteMulti",
   190  				"CompareAndSwapMulti", "Flush", "Stats")
   191  		}
   192  		if !bool(cachedSettings.DisableDSCache) {
   193  			ctx = dscache.FilterRDS(ctx, nil)
   194  		}
   195  	}
   196  	if e.DSReadOnly {
   197  		ctx = readonly.FilterRDS(ctx, e.DSReadOnlyPredicate)
   198  	}
   199  	ctx = txndefer.FilterRDS(ctx)
   200  
   201  	// The rest of the service may use applied configuration.
   202  	if e.WithConfig != nil {
   203  		ctx = e.WithConfig(ctx)
   204  	}
   205  	ctx = secrets.Use(ctx, gaesecrets.New(nil))
   206  	if e.WithAuth != nil {
   207  		ctx = e.WithAuth(ctx)
   208  	}
   209  
   210  	// Wrap this in a cache context so that lookups for any of the aforementioned
   211  	// items are fast.
   212  	return cacheContext.Wrap(ctx)
   213  }
   214  
   215  // Base returns a middleware chain to use for all GAE environment
   216  // requests.
   217  //
   218  // Base DOES NOT install "luci/gae" services. To install appropriate services,
   219  // use methods from a sub-package. This is done so that a given AppEngine
   220  // environment doesn't need to include the superset of packages across all
   221  // supported environments.
   222  //
   223  // This middleware chain installs prod GAE services into the request context
   224  // (via With), and wraps the request with a panic catcher and monitoring
   225  // hooks.
   226  func (e *Environment) Base() router.MiddlewareChain {
   227  	// addServices is a middleware that installs the set of standard production
   228  	// AppEngine services by calling With.
   229  	addServices := func(c *router.Context, next router.Handler) {
   230  		// Create a cancelable Context that cancels when this request finishes.
   231  		//
   232  		// We do this because Contexts will leak resources and/or goroutines if they
   233  		// have timers or can be canceled, but aren't. Predictably canceling the
   234  		// parent will ensure that any such resource leaks are cleaned up.
   235  		ctx, cancelFunc := context.WithCancel(c.Request.Context())
   236  		defer cancelFunc()
   237  
   238  		// Apply production settings.
   239  		c.Request = c.Request.WithContext(e.With(ctx, c.Request))
   240  
   241  		next(c)
   242  	}
   243  
   244  	mw := router.NewMiddlewareChain(addServices)
   245  	mw = mw.Extend(e.ExtraMiddleware...)
   246  	return mw
   247  }