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 }