github.com/rigado/snapd@v2.42.5-go-mod+incompatible/overlord/snapstate/catalogrefresh.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package snapstate
    21  
    22  import (
    23  	"fmt"
    24  	"math/rand" // seeded elsewhere
    25  	"os"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/advisor"
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/logger"
    33  	"github.com/snapcore/snapd/osutil"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/overlord/state"
    36  	"github.com/snapcore/snapd/store"
    37  	"github.com/snapcore/snapd/timings"
    38  )
    39  
    40  var (
    41  	catalogRefreshDelayBase      = 24 * time.Hour
    42  	catalogRefreshDelayWithDelta = 24*time.Hour + 1 + time.Duration(rand.Int63n(int64(6*time.Hour)))
    43  )
    44  
    45  type catalogRefresh struct {
    46  	state *state.State
    47  
    48  	nextCatalogRefresh time.Time
    49  }
    50  
    51  func newCatalogRefresh(st *state.State) *catalogRefresh {
    52  	return &catalogRefresh{state: st}
    53  }
    54  
    55  // Ensure will ensure that the catalog refresh happens
    56  func (r *catalogRefresh) Ensure() error {
    57  	r.state.Lock()
    58  	defer r.state.Unlock()
    59  
    60  	// sneakily don't do anything if in testing
    61  	if CanAutoRefresh == nil {
    62  		return nil
    63  	}
    64  
    65  	now := time.Now()
    66  	delay := catalogRefreshDelayBase
    67  	if r.nextCatalogRefresh.IsZero() {
    68  		// try to use the timestamp on the sections file
    69  		if st, err := os.Stat(dirs.SnapNamesFile); err == nil && st.ModTime().Before(now) {
    70  			// add the delay with the delta so we spread the load a bit
    71  			r.nextCatalogRefresh = st.ModTime().Add(catalogRefreshDelayWithDelta)
    72  		} else {
    73  			// first time scheduling, add the delta
    74  			delay = catalogRefreshDelayWithDelta
    75  		}
    76  	}
    77  
    78  	theStore := Store(r.state, nil)
    79  	needsRefresh := r.nextCatalogRefresh.IsZero() || r.nextCatalogRefresh.Before(now)
    80  
    81  	if !needsRefresh {
    82  		return nil
    83  	}
    84  
    85  	next := now.Add(delay)
    86  	// catalog refresh does not carry on trying on error
    87  	r.nextCatalogRefresh = next
    88  
    89  	logger.Debugf("Catalog refresh starting now; next scheduled for %s.", next)
    90  
    91  	err := refreshCatalogs(r.state, theStore)
    92  	switch err {
    93  	case nil:
    94  		logger.Debugf("Catalog refresh succeeded.")
    95  	case store.ErrTooManyRequests:
    96  		logger.Debugf("Catalog refresh postponed.")
    97  		err = nil
    98  	default:
    99  		logger.Debugf("Catalog refresh failed: %v.", err)
   100  	}
   101  	return err
   102  }
   103  
   104  var newCmdDB = advisor.Create
   105  
   106  func refreshCatalogs(st *state.State, theStore StoreService) error {
   107  	st.Unlock()
   108  	defer st.Lock()
   109  
   110  	perfTimings := timings.New(map[string]string{"ensure": "refresh-catalogs"})
   111  
   112  	if err := os.MkdirAll(dirs.SnapCacheDir, 0755); err != nil {
   113  		return fmt.Errorf("cannot create directory %q: %v", dirs.SnapCacheDir, err)
   114  	}
   115  
   116  	var sections []string
   117  	var err error
   118  	timings.Run(perfTimings, "get-sections", "query store for sections", func(tm timings.Measurer) {
   119  		sections, err = theStore.Sections(auth.EnsureContextTODO(), nil)
   120  	})
   121  	if err != nil {
   122  		return err
   123  	}
   124  	sort.Strings(sections)
   125  	if err := osutil.AtomicWriteFile(dirs.SnapSectionsFile, []byte(strings.Join(sections, "\n")), 0644, 0); err != nil {
   126  		return err
   127  	}
   128  
   129  	namesFile, err := osutil.NewAtomicFile(dirs.SnapNamesFile, 0644, 0, osutil.NoChown, osutil.NoChown)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	defer namesFile.Cancel()
   134  
   135  	cmdDB, err := newCmdDB()
   136  	if err != nil {
   137  		return err
   138  	}
   139  
   140  	// if all goes well we'll Commit() making this a NOP:
   141  	defer cmdDB.Rollback()
   142  
   143  	timings.Run(perfTimings, "write-catalogs", "query store for catalogs", func(tm timings.Measurer) {
   144  		err = theStore.WriteCatalogs(auth.EnsureContextTODO(), namesFile, cmdDB)
   145  	})
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	err1 := namesFile.Commit()
   151  	err2 := cmdDB.Commit()
   152  
   153  	if err2 != nil {
   154  		return err2
   155  	}
   156  
   157  	st.Lock()
   158  	perfTimings.Save(st)
   159  	st.Unlock()
   160  
   161  	return err1
   162  }