go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/cfg/provider.go (about)

     1  // Copyright 2023 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 cfg
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"math/rand"
    21  	"sync/atomic"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/common/logging"
    26  )
    27  
    28  // Provider knows how to load preprocessed configs from the datastore and
    29  // convert them into instances of Config.
    30  //
    31  // Provider is a long-living object that keeps a reference to the most recent
    32  // config, periodically reloading it from the datastore.
    33  type Provider struct {
    34  	cur atomic.Value
    35  }
    36  
    37  // NewProvider initializes a Provider by fetching the initial copy of configs.
    38  //
    39  // If there are no configs stored in the datastore (happens when bootstrapping
    40  // a new service), uses some default empty config.
    41  func NewProvider(ctx context.Context) (*Provider, error) {
    42  	cur, err := fetchFromDatastore(ctx, nil)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	p := &Provider{}
    47  	p.cur.Store(cur)
    48  	return p, nil
    49  }
    50  
    51  // Config returns an immutable snapshot of the most recent config.
    52  //
    53  // Note that multiple sequential calls to Config() may return different
    54  // snapshots if the config changes between them. For that reason it is better to
    55  // get the snapshot once at the beginning of an RPC handler, and use it
    56  // throughout.
    57  func (p *Provider) Config(ctx context.Context) *Config {
    58  	return p.cur.Load().(*Config)
    59  }
    60  
    61  // RefreshPeriodically runs a loop that periodically refetches the config from
    62  // the datastore.
    63  func (p *Provider) RefreshPeriodically(ctx context.Context) {
    64  	for {
    65  		jitter := time.Duration(rand.Int63n(int64(10 * time.Second)))
    66  		if r := <-clock.After(ctx, 30*time.Second+jitter); r.Err != nil {
    67  			return // the context is canceled
    68  		}
    69  		if err := p.refresh(ctx); err != nil {
    70  			// Don't log the error if the server is shutting down.
    71  			if !errors.Is(err, context.Canceled) {
    72  				logging.Warningf(ctx, "Failed to refresh local copy of the config: %s", err)
    73  			}
    74  		}
    75  	}
    76  }
    77  
    78  // refresh fetches the new config from the datastore if it is available.
    79  func (p *Provider) refresh(ctx context.Context) error {
    80  	cur := p.cur.Load().(*Config)
    81  	new, err := fetchFromDatastore(ctx, cur)
    82  	if err != nil {
    83  		return err
    84  	}
    85  	p.cur.Store(new)
    86  	return nil
    87  }