github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/housekeeping/housekeep.go (about)

     1  package housekeeping
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"runtime"
     7  	"time"
     8  
     9  	"github.com/mutagen-io/extstat"
    10  
    11  	"github.com/mutagen-io/mutagen/pkg/agent"
    12  	"github.com/mutagen-io/mutagen/pkg/filesystem"
    13  	"github.com/mutagen-io/mutagen/pkg/platform"
    14  	"github.com/mutagen-io/mutagen/pkg/sidecar"
    15  )
    16  
    17  const (
    18  	// maximumAgentIdlePeriod is the maximum period of time that an agent binary
    19  	// is allowed to sit on disk without being executed before being deleted.
    20  	maximumAgentIdlePeriod = 30 * 24 * time.Hour
    21  	// maximumCacheAge is the maximum allowed cache age.
    22  	maximumCacheAge = 7 * 24 * time.Hour
    23  	// maximumStagingRootAge is the maximum allowed staging root age.
    24  	maximumStagingRootAge = 7 * 24 * time.Hour
    25  )
    26  
    27  // Housekeep invokes housekeeping functions on the Mutagen data directory.
    28  func Housekeep() {
    29  	// Perform housekeeping on agent binaries, but only if we're not in a
    30  	// Mutagen sidecar container. Sidecar containers are particularly
    31  	// susceptible to stale agent access times due to the fact that the agent is
    32  	// baked into the sidecar image and the sidecar image is typically unpacked
    33  	// via OverlayFS on top of ext4 with either relatime or noatime.
    34  	if !sidecar.EnvironmentIsSidecar() {
    35  		housekeepAgents()
    36  	}
    37  
    38  	// Perform housekeeping on caches.
    39  	housekeepCaches()
    40  
    41  	// Perform housekeeping on staging roots.
    42  	housekeepStaging()
    43  }
    44  
    45  // housekeepAgents performs housekeeping of agent binaries.
    46  func housekeepAgents() {
    47  	// Compute the path to the agents directory. If we fail, just abort. We
    48  	// don't attempt to create the directory, because if it doesn't exist, then
    49  	// we don't need to do anything and we'll just bail when we fail to list the
    50  	// agent directory below.
    51  	agentsDirectoryPath, err := filesystem.Mutagen(false, filesystem.MutagenAgentsDirectoryName)
    52  	if err != nil {
    53  		return
    54  	}
    55  
    56  	// Get the list of locally installed agent versions. If we fail, just abort.
    57  	agentDirectoryContents, err := filesystem.DirectoryContentsByPath(agentsDirectoryPath)
    58  	if err != nil {
    59  		return
    60  	}
    61  
    62  	// Compute the name of the agent binary.
    63  	agentName := platform.ExecutableName(agent.BaseName, runtime.GOOS)
    64  
    65  	// Grab the current time.
    66  	now := time.Now()
    67  
    68  	// Loop through each agent version, compute the time it was last launched,
    69  	// and remove it if longer than the maximum allowed period. Skip contents
    70  	// where failures are encountered.
    71  	for _, c := range agentDirectoryContents {
    72  		// TODO: Ensure that the name matches the expected format. Be mindful of
    73  		// the fact that it might contain a tag.
    74  		agentVersion := c.Name()
    75  		if stat, err := extstat.NewFromFileName(filepath.Join(agentsDirectoryPath, agentVersion, agentName)); err != nil {
    76  			continue
    77  		} else if now.Sub(stat.AccessTime) > maximumAgentIdlePeriod {
    78  			os.RemoveAll(filepath.Join(agentsDirectoryPath, agentVersion))
    79  		}
    80  	}
    81  }
    82  
    83  // housekeepCaches performs housekeeping of caches.
    84  func housekeepCaches() {
    85  	// Compute the path to the caches directory. If we fail, just abort. We
    86  	// don't attempt to create the directory, because if it doesn't exist, then
    87  	// we don't need to do anything and we'll just bail when we fail to list the
    88  	// caches directory contents below.
    89  	// TODO: Move this logic into paths.go? Need to keep it in sync with
    90  	// pathForCache.
    91  	cachesDirectoryPath, err := filesystem.Mutagen(false, filesystem.MutagenSynchronizationCachesDirectoryName)
    92  	if err != nil {
    93  		return
    94  	}
    95  
    96  	// Get the list of caches. If we fail, just abort.
    97  	cachesDirectoryContents, err := filesystem.DirectoryContentsByPath(cachesDirectoryPath)
    98  	if err != nil {
    99  		return
   100  	}
   101  
   102  	// Grab the current time.
   103  	now := time.Now()
   104  
   105  	// Loop through each cache and remove those older than a certain age. Ignore
   106  	// any failures.
   107  	for _, c := range cachesDirectoryContents {
   108  		cacheName := c.Name()
   109  		fullPath := filepath.Join(cachesDirectoryPath, cacheName)
   110  		if stat, err := os.Stat(fullPath); err != nil {
   111  			continue
   112  		} else if now.Sub(stat.ModTime()) > maximumCacheAge {
   113  			os.Remove(fullPath)
   114  		}
   115  	}
   116  }
   117  
   118  // housekeepStaging performs housekeeping of staging roots.
   119  func housekeepStaging() {
   120  	// Compute the path to the staging directory (the top-level directory
   121  	// containing all staging roots). If we fail, just abort. We don't attempt
   122  	// to create the directory, because if it doesn't exist, then we don't need
   123  	// to do anything and we'll just bail when we fail to list the staging
   124  	// directory contents below.
   125  	// TODO: Move this logic into paths.go? Need to keep it in sync with
   126  	// pathForStagingRoot and pathForStaging.
   127  	stagingDirectoryPath, err := filesystem.Mutagen(false, filesystem.MutagenSynchronizationStagingDirectoryName)
   128  	if err != nil {
   129  		return
   130  	}
   131  
   132  	// Get the list of staging roots. If we fail, just abort.
   133  	stagingDirectoryContents, err := filesystem.DirectoryContentsByPath(stagingDirectoryPath)
   134  	if err != nil {
   135  		return
   136  	}
   137  
   138  	// Grab the current time.
   139  	now := time.Now()
   140  
   141  	// Loop through each staging root and remove those older than a certain
   142  	// age. Ignore any failures. This is a little bit more cavalier than cache
   143  	// housekeeping because removal is non-atomic and theoretically a given
   144  	// staging root could be in use. However, a session's staging root is wiped
   145  	// on each successful synchronization cycle, so by using a large maximum
   146  	// staging root age, we're only going to run into trouble if the staging
   147  	// portion of a synchronization cycle starts up, after having failed a long
   148  	// time ago, at the precise moment that we're housekeeping. In that case, it
   149  	// would try to use the existing staging directory from the failed
   150  	// synchronization cycle and there might be a conflict. But even in that
   151  	// statistically unlikely case, the worst case scenario would be triggering
   152  	// an additional synchronization cycle.
   153  	for _, c := range stagingDirectoryContents {
   154  		stagingRootName := c.Name()
   155  		fullPath := filepath.Join(stagingDirectoryPath, stagingRootName)
   156  		if stat, err := os.Stat(fullPath); err != nil {
   157  			continue
   158  		} else if now.Sub(stat.ModTime()) > maximumStagingRootAge {
   159  			os.RemoveAll(fullPath)
   160  		}
   161  	}
   162  }