99 "io/ioutil"
1010 "os"
1111 "path/filepath"
12+ "sort"
1213 "strings"
1314
1415 "github.com/pkg/errors"
@@ -62,73 +63,183 @@ func (key ExecutionCacheKey) Key() (string, error) {
6263}
6364
6465type ExecutionCache interface {
65- Get (ctx context.Context , key ExecutionCacheKey ) (diff string , found bool , err error )
66- Set (ctx context.Context , key ExecutionCacheKey , diff string ) error
66+ Get (ctx context.Context , key ExecutionCacheKey ) (result ExecutionResult , found bool , err error )
67+ Set (ctx context.Context , key ExecutionCacheKey , result ExecutionResult ) error
6768 Clear (ctx context.Context , key ExecutionCacheKey ) error
6869}
6970
7071type ExecutionDiskCache struct {
7172 Dir string
7273}
7374
75+ const cacheFileExt = ".v3.json"
76+
7477func (c ExecutionDiskCache ) cacheFilePath (key ExecutionCacheKey ) (string , error ) {
7578 keyString , err := key .Key ()
7679 if err != nil {
7780 return "" , errors .Wrap (err , "calculating execution cache key" )
7881 }
7982
80- return filepath .Join (c .Dir , keyString + ".diff" ), nil
83+ return filepath .Join (c .Dir , keyString + cacheFileExt ), nil
8184}
8285
83- func (c ExecutionDiskCache ) Get (ctx context.Context , key ExecutionCacheKey ) (string , bool , error ) {
86+ func (c ExecutionDiskCache ) Get (ctx context.Context , key ExecutionCacheKey ) (ExecutionResult , bool , error ) {
87+ var result ExecutionResult
88+
8489 path , err := c .cacheFilePath (key )
8590 if err != nil {
86- return "" , false , err
91+ return result , false , err
8792 }
8893
89- data , err := ioutil .ReadFile (path )
94+ // We try to be backwards compatible and see if we also find older cache
95+ // files.
96+ //
97+ // There are three different cache versions out in the wild and to be
98+ // backwards compatible we read all of them.
99+ //
100+ // In Sourcegraph/src-cli 3.26 we can remove the code here and simply read
101+ // the cache from `path`, since all the old cache files should be deleted
102+ // until then.
103+ globPattern := strings .TrimSuffix (path , cacheFileExt ) + ".*"
104+ matches , err := filepath .Glob (globPattern )
90105 if err != nil {
91- if os .IsNotExist (err ) {
92- err = nil // treat as not-found
106+ return result , false , err
107+ }
108+
109+ switch len (matches ) {
110+ case 0 :
111+ // Nothing found
112+ return result , false , nil
113+ case 1 :
114+ // One cache file found
115+ if err := c .readCacheFile (matches [0 ], & result ); err != nil {
116+ return result , false , err
93117 }
94- return "" , false , err
118+
119+ // If it's an old cache file, we rewrite the cache and delete the old file
120+ if isOldCacheFile (matches [0 ]) {
121+ if err := c .Set (ctx , key , result ); err != nil {
122+ return result , false , errors .Wrap (err , "failed to rewrite cache in new format" )
123+ }
124+ if err := os .Remove (matches [0 ]); err != nil {
125+ return result , false , errors .Wrap (err , "failed to remove old cache file" )
126+ }
127+ }
128+
129+ return result , true , err
130+
131+ default :
132+ // More than one cache file found.
133+ // Sort them so that we'll can possibly read from the one with the most
134+ // current version.
135+ sortCacheFiles (matches )
136+
137+ newest := matches [0 ]
138+ toDelete := matches [1 :]
139+
140+ // Read from newest
141+ if err := c .readCacheFile (newest , & result ); err != nil {
142+ return result , false , err
143+ }
144+
145+ // If the newest was also an older version, we write a new version...
146+ if isOldCacheFile (newest ) {
147+ if err := c .Set (ctx , key , result ); err != nil {
148+ return result , false , errors .Wrap (err , "failed to rewrite cache in new format" )
149+ }
150+ // ... and mark the file also as to-be-deleted
151+ toDelete = append (toDelete , newest )
152+ }
153+
154+ // Now we clean up the old ones
155+ for _ , path := range toDelete {
156+ if err := os .Remove (path ); err != nil {
157+ return result , false , errors .Wrap (err , "failed to remove old cache file" )
158+ }
159+ }
160+
161+ return result , true , nil
95162 }
163+ }
96164
97- // We previously cached complete ChangesetSpecs instead of just the diffs.
98- // To be backwards compatible, we keep reading these:
99- if strings .HasSuffix (path , ".json" ) {
100- var result ChangesetSpec
101- if err := json .Unmarshal (data , & result ); err != nil {
165+ // sortCacheFiles sorts cache file paths by their "version", so that files
166+ // ending in `cacheFileExt` are first.
167+ func sortCacheFiles (paths []string ) {
168+ sort .Slice (paths , func (i , j int ) bool {
169+ return ! isOldCacheFile (paths [i ]) && isOldCacheFile (paths [j ])
170+ })
171+ }
172+
173+ func isOldCacheFile (path string ) bool { return ! strings .HasSuffix (path , cacheFileExt ) }
174+
175+ func (c ExecutionDiskCache ) readCacheFile (path string , result * ExecutionResult ) error {
176+ data , err := ioutil .ReadFile (path )
177+ if err != nil {
178+ return err
179+ }
180+
181+ switch {
182+ case strings .HasSuffix (path , ".v3.json" ):
183+ // v3 of the cache: we cache the diff and the outputs produced by the step.
184+ if err := json .Unmarshal (data , result ); err != nil {
185+ // Delete the invalid data to avoid causing an error for next time.
186+ if err := os .Remove (path ); err != nil {
187+ return errors .Wrap (err , "while deleting cache file with invalid JSON" )
188+ }
189+ return errors .Wrapf (err , "reading cache file %s" , path )
190+ }
191+ return nil
192+
193+ case strings .HasSuffix (path , ".diff" ):
194+ // v2 of the cache: we only cached the diff, since that's the
195+ // only bit of data we were interested in.
196+ result .Diff = string (data )
197+ result .Outputs = map [string ]interface {}{}
198+ // Conversion is lossy, though: we don't populate result.StepChanges.
199+ result .ChangedFiles = & StepChanges {}
200+
201+ return nil
202+
203+ case strings .HasSuffix (path , ".json" ):
204+ // v1 of the cache: we cached the complete ChangesetSpec instead of just the diffs.
205+ var spec ChangesetSpec
206+ if err := json .Unmarshal (data , & spec ); err != nil {
102207 // Delete the invalid data to avoid causing an error for next time.
103208 if err := os .Remove (path ); err != nil {
104- return "" , false , errors .Wrap (err , "while deleting cache file with invalid JSON" )
209+ return errors .Wrap (err , "while deleting cache file with invalid JSON" )
105210 }
106- return "" , false , errors .Wrapf (err , "reading cache file %s" , path )
211+ return errors .Wrapf (err , "reading cache file %s" , path )
107212 }
108- if len (result .Commits ) != 1 {
109- return "" , false , errors .New ("cached result has no commits" )
213+ if len (spec .Commits ) != 1 {
214+ return errors .New ("cached result has no commits" )
110215 }
111- return result .Commits [0 ].Diff , true , nil
112- }
113216
114- if strings .HasSuffix (path , ".diff" ) {
115- return string (data ), true , nil
217+ result .Diff = spec .Commits [0 ].Diff
218+ result .Outputs = map [string ]interface {}{}
219+ result .ChangedFiles = & StepChanges {}
220+
221+ return nil
116222 }
117223
118- return "" , false , fmt .Errorf ("unknown file format for cache file %q" , path )
224+ return fmt .Errorf ("unknown file format for cache file %q" , path )
119225}
120226
121- func (c ExecutionDiskCache ) Set (ctx context.Context , key ExecutionCacheKey , diff string ) error {
227+ func (c ExecutionDiskCache ) Set (ctx context.Context , key ExecutionCacheKey , result ExecutionResult ) error {
122228 path , err := c .cacheFilePath (key )
123229 if err != nil {
124230 return err
125231 }
126232
233+ raw , err := json .Marshal (& result )
234+ if err != nil {
235+ return errors .Wrap (err , "serializing execution result to JSON" )
236+ }
237+
127238 if err := os .MkdirAll (filepath .Dir (path ), 0700 ); err != nil {
128239 return err
129240 }
130241
131- return ioutil .WriteFile (path , [] byte ( diff ) , 0600 )
242+ return ioutil .WriteFile (path , raw , 0600 )
132243}
133244
134245func (c ExecutionDiskCache ) Clear (ctx context.Context , key ExecutionCacheKey ) error {
@@ -148,11 +259,11 @@ func (c ExecutionDiskCache) Clear(ctx context.Context, key ExecutionCacheKey) er
148259// retrieve cache entries.
149260type ExecutionNoOpCache struct {}
150261
151- func (ExecutionNoOpCache ) Get (ctx context.Context , key ExecutionCacheKey ) (diff string , found bool , err error ) {
152- return "" , false , nil
262+ func (ExecutionNoOpCache ) Get (ctx context.Context , key ExecutionCacheKey ) (result ExecutionResult , found bool , err error ) {
263+ return ExecutionResult {} , false , nil
153264}
154265
155- func (ExecutionNoOpCache ) Set (ctx context.Context , key ExecutionCacheKey , diff string ) error {
266+ func (ExecutionNoOpCache ) Set (ctx context.Context , key ExecutionCacheKey , result ExecutionResult ) error {
156267 return nil
157268}
158269
0 commit comments