Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: invalidate persisted cache #635

Merged
merged 19 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/di/init.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* © 2022-2024 Snyk Limited
* © 2024 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
37 changes: 32 additions & 5 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package server
import (
"context"
"fmt"
"github.com/snyk/snyk-ls/domain/snyk/persistence"
"os"
"runtime"
"strings"
Expand Down Expand Up @@ -187,8 +188,13 @@ func workspaceDidChangeWorkspaceFoldersHandler(srv *jrpc2.Server) jrpc2.Handler

logger.Info().Msg("RECEIVING")
defer logger.Info().Msg("SENDING")
workspace.Get().ChangeWorkspaceFolders(bgCtx, params)
command.HandleFolders(bgCtx, srv, di.Notifier())
changedFolders := workspace.Get().ChangeWorkspaceFolders(params)
command.HandleFolders(bgCtx, srv, di.Notifier(), di.ScanPersister())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i moved the persister init logic to the folder handlers
This refactoring here was to make sure that the persister is initialized before a new f.ScanFolder is triggered for the new added folder

if config.CurrentConfig().IsAutoScanEnabled() {
for _, f := range changedFolders {
f.ScanFolder(ctx)
}
}
return nil, nil
})
}
Expand Down Expand Up @@ -386,6 +392,12 @@ func initializedHandler(srv *jrpc2.Server) handler.Func {
logger.Error().Err(err).Msg("Scan initialization error, canceling scan")
return nil, nil
}
command.HandleFolders(context.Background(), srv, di.Notifier(), di.ScanPersister())

// Check once for expired cache in same thread before triggering a scan.
// Start a periodic go routine to check for the expired cache afterwards
deleteExpiredCache()
go periodicallyCheckForExpiredCache()

autoScanEnabled := c.IsAutoScanEnabled()
if autoScanEnabled {
Expand All @@ -400,11 +412,26 @@ func initializedHandler(srv *jrpc2.Server) handler.Func {
}

logger.Debug().Msg("trying to get trusted status for untrusted folders")
go command.HandleFolders(context.Background(), srv, di.Notifier())
return nil, nil
})
}

func deleteExpiredCache() {
w := workspace.Get()
var folderList []string
for _, f := range w.Folders() {
folderList = append(folderList, f.Path())
}
di.ScanPersister().Clear(folderList, true)
}

func periodicallyCheckForExpiredCache() {
for {
deleteExpiredCache()
time.Sleep(time.Duration(persistence.ExpirationInSeconds) * time.Second)
}
}

func addWorkspaceFolders(c *config.Config, params types.InitializeParams, w *workspace.Workspace) {
const method = "addWorkspaceFolders"
if len(params.WorkspaceFolders) > 0 {
Expand Down Expand Up @@ -545,8 +572,8 @@ func textDocumentDidOpenHandler() jrpc2.Handler {
di.Notifier().Send(diagnosticParams)
}

if scanner, ok := di.Scanner().(scanner.PackageScanner); ok {
scanner.ScanPackages(context.Background(), config.CurrentConfig(), filePath, "")
if sc, ok := di.Scanner().(scanner.PackageScanner); ok {
sc.ScanPackages(context.Background(), config.CurrentConfig(), filePath, "")
}
return nil, nil
})
Expand Down
17 changes: 16 additions & 1 deletion domain/ide/command/folder_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package command
import (
"context"
"fmt"
"github.com/snyk/snyk-ls/domain/snyk/persistence"
gitconfig "github.com/snyk/snyk-ls/internal/git_config"
noti "github.com/snyk/snyk-ls/internal/notification"

Expand All @@ -32,8 +33,9 @@ import (
const DoTrust = "Trust folders and continue"
const DontTrust = "Don't trust folders"

func HandleFolders(ctx context.Context, srv types.Server, notifier noti.Notifier) {
func HandleFolders(ctx context.Context, srv types.Server, notifier noti.Notifier, persister persistence.ScanSnapshotPersister) {
go sendFolderConfigsNotification(notifier)
initScanPersister(persister)
HandleUntrustedFolders(ctx, srv)
}

Expand All @@ -53,6 +55,19 @@ func sendFolderConfigsNotification(notifier noti.Notifier) {
notifier.Send(folderConfigsParam)
}

func initScanPersister(persister persistence.ScanSnapshotPersister) {
logger := config.CurrentConfig().Logger().With().Str("method", "initScanPersister").Logger()
w := workspace.Get()
var folderList []string
for _, f := range w.Folders() {
folderList = append(folderList, f.Path())
}
err := persister.Init(folderList)
if err != nil {
logger.Error().Err(err).Msg("could not initialize scan persister")
}
}

func HandleUntrustedFolders(ctx context.Context, srv types.Server) {
w := workspace.Get()
// debounce requests from overzealous clients (Eclipse, I'm looking at you)
Expand Down
9 changes: 4 additions & 5 deletions domain/ide/workspace/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,17 @@ func (w *Workspace) ScanWorkspace(ctx context.Context) {

// ChangeWorkspaceFolders clears the "Removed" folders, adds the "New" folders,
// and starts an automatic scan if auto-scans are enabled.
func (w *Workspace) ChangeWorkspaceFolders(ctx context.Context, params types.DidChangeWorkspaceFoldersParams) {
func (w *Workspace) ChangeWorkspaceFolders(params types.DidChangeWorkspaceFoldersParams) []*Folder {
for _, folder := range params.Event.Removed {
w.RemoveFolder(uri.PathFromUri(folder.Uri))
}

var changedWorkspaceFolders []*Folder
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to avoid triggering scan before initializing the persister.
Caller is responsible for calling the scan for the new added folders

for _, folder := range params.Event.Added {
f := NewFolder(w.c, uri.PathFromUri(folder.Uri), folder.Name, w.scanner, w.hoverService, w.scanNotifier, w.notifier, w.scanPersister)
w.AddFolder(f)
if config.CurrentConfig().IsAutoScanEnabled() {
f.ScanFolder(ctx)
}
changedWorkspaceFolders = append(changedWorkspaceFolders, f)
}
return changedWorkspaceFolders
}

func (w *Workspace) Clear() {
Expand Down
10 changes: 3 additions & 7 deletions domain/ide/workspace/workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func Test_TrustFoldersAndScan_shouldAddFoldersToTrustedFoldersAndTriggerScan(t *
}, time.Second, time.Millisecond, "scanner should be called after trust is granted")
}

func Test_AddAndRemoveFoldersAndTriggerScan(t *testing.T) {
func Test_AddAndRemoveFoldersAndReturnFolderList(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you test the extracted trigger scan path now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in server_test Test_workspaceDidChangeWorkspaceFolders_shouldProcessChanges it checks if folder.Scanned()
I think it should be enough, wdyt ?

c := testutil.UnitTest(t)
const trustedDummy = "trustedDummy"
const untrustedDummy = "untrustedDummy"
Expand Down Expand Up @@ -101,14 +101,10 @@ func Test_AddAndRemoveFoldersAndTriggerScan(t *testing.T) {
},
}}

w.ChangeWorkspaceFolders(context.Background(), params)

folderList := w.ChangeWorkspaceFolders(params)
assert.Nil(t, w.GetFolderContaining(toBeRemoved))

// one call for one trusted folder
assert.Eventually(t, func() bool {
return sc.Calls() == 1
}, time.Second, time.Millisecond, "scanner should be called after trust is granted")
assert.Len(t, folderList, 2)
}

func Test_Get(t *testing.T) {
Expand Down
129 changes: 129 additions & 0 deletions domain/snyk/persistence/file_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* © 2024 Snyk Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package persistence

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/internal/product"
"github.com/snyk/snyk-ls/internal/vcs"
)

func (g *GitPersistenceProvider) persistToDisk(cacheDir string, folderHashedPath hashedFolderPath, commitHash string, p product.Product, inputToCache []snyk.Issue) error {
filePath := getLocalFilePath(cacheDir, folderHashedPath, commitHash, p)
data, err := json.Marshal(inputToCache)
if err != nil {
return err
}
g.logger.Debug().Msg("persisting scan results in file " + filePath)
return os.WriteFile(filePath, data, 0644)
}

func (g *GitPersistenceProvider) getPersistedFiles(cacheDir string) (persistedFiles []string, err error) {
entries, err := os.ReadDir(cacheDir)
if err != nil {
return persistedFiles, err
}

for _, entry := range entries {
fileName := entry.Name()
if !strings.HasSuffix(fileName, ".json") {
continue
}
s := strings.Split(fileName, ".")
if len(s) == 5 {
persistedFiles = append(persistedFiles, fileName)
}
}
return persistedFiles, nil
}

func (g *GitPersistenceProvider) ensureCacheDirExists(folderPath string) (string, error) {
g.logger.Debug().Msg("attempting to determine .git folder path")
cacheDir, err := g.snykCacheDir(folderPath)
if err != nil {
return "", err
}

if _, err = os.Stat(cacheDir); os.IsNotExist(err) {
err = os.Mkdir(cacheDir, 0700)
if err != nil {
return "", err
}
}
return cacheDir, nil
}

func (g *GitPersistenceProvider) snykCacheDir(folderPath string) (string, error) {
gitFolder, err := vcs.GitRepoFolderPath(g.logger, folderPath)
if err != nil {
return "", err
}

cacheDir := filepath.Join(gitFolder, CacheFolder)

return cacheDir, nil
}

func getLocalFilePath(cacheDir string, folderPathHash hashedFolderPath, commitHash string, p product.Product) string {
productName := p.ToProductCodename()
return filepath.Join(cacheDir, fmt.Sprintf("%s.%s.%s.%s.json", SchemaVersion, folderPathHash, commitHash, productName))
}

func (g *GitPersistenceProvider) snapshotExistsOnDisk(cacheDir string, hash hashedFolderPath, commitHash string, p product.Product) bool {
filePath := getLocalFilePath(cacheDir, hash, commitHash, p)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
}
return true
}

func (g *GitPersistenceProvider) deleteFile(fullPath string) error {
g.logger.Debug().Msgf("deleting cached scan file %s", fullPath)
err := os.Remove(fullPath)
if err != nil {
return err
}
return nil
}

func (g *GitPersistenceProvider) isExpired(schemaVersion string, fullFilePath string) bool {
// if file has incorrect schema we just delete it
if schemaVersion != SchemaVersion {
return true
}

// Check last modified date
fileInfo, err := os.Stat(fullFilePath)
if err != nil {
g.logger.Error().Err(err).Msg("couldn't stat file " + fullFilePath)
return true
}

// If elapsed time is > ExpirationInSeconds, delete the file
if time.Since(fileInfo.ModTime()) > time.Duration(ExpirationInSeconds)*time.Second {
return true
}

return false
}
Loading
Loading