This repository has been archived by the owner on Sep 1, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bafa1a4
commit 464669f
Showing
5 changed files
with
372 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,7 @@ | ||
# java-memory-assistant-cleanup | ||
Utility to clean up old heap dumps in the Java Buildpack | ||
# Java Memory Assistant Tools | ||
|
||
This repository contains tools that enable the integration of the [Java Memory Assistant](https://github.com/SAP/java-memory-assistant) in other projects. | ||
|
||
## Cleanup | ||
|
||
This is a small utility to clean up heap dumps created by the Java Memory Assistant in the Cloud Foundry community [Java Buildpack](https://github.com/cloudfoundry/java-buildpack). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. | ||
* This file is licensed under the Apache Software License, v. 2 except as noted | ||
* otherwise in the LICENSE file at the root of the repository. | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"sort" | ||
"strings" | ||
|
||
"robpike.io/filter" | ||
|
||
"github.com/caarlos0/env" | ||
"github.com/spf13/afero" | ||
) | ||
|
||
func main() { | ||
cfg := Config{} | ||
err := env.Parse(&cfg) | ||
|
||
if err != nil { | ||
log.Fatalf("%+v\n", err) | ||
} | ||
|
||
deletedFiles, err := CleanUp(afero.NewOsFs(), cfg) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
for _, deletedFile := range deletedFiles { | ||
fmt.Printf("Heap dump '%v' deleted\n", deletedFile) | ||
} | ||
} | ||
|
||
type byName []os.FileInfo | ||
|
||
// Config visible for testing | ||
type Config struct { | ||
HeapDumpFolder string `env:"JMA_HEAP_DUMP_FOLDER"` | ||
MaxDumpCount int `env:"JMA_MAX_DUMP_COUNT" envDefault:"0"` | ||
} | ||
|
||
func (f byName) Len() int { | ||
return len(f) | ||
} | ||
|
||
func (f byName) Less(i, j int) bool { | ||
return f[i].Name() < f[j].Name() | ||
} | ||
|
||
func (f byName) Swap(i, j int) { | ||
f[i], f[j] = f[j], f[i] | ||
} | ||
|
||
// CleanUp visible for testing | ||
func CleanUp(fs afero.Fs, cfg Config) ([]string, error) { | ||
if cfg.HeapDumpFolder == "" { | ||
return nil, fmt.Errorf("The environment variable 'JMA_HEAP_DUMP_FOLDER' is not set") | ||
} | ||
|
||
heapDumpFolder := cfg.HeapDumpFolder | ||
|
||
maxDumpCount := cfg.MaxDumpCount | ||
if maxDumpCount < 0 { | ||
return nil, fmt.Errorf("The value of the 'JMA_MAX_DUMP_COUNT' environment variable contains a negative number: %v", maxDumpCount) | ||
} | ||
|
||
file, err := fs.Stat(heapDumpFolder) | ||
if os.IsNotExist(err) { | ||
return nil, fmt.Errorf("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory '%v': does not exist", heapDumpFolder) | ||
} | ||
|
||
mode := file.Mode() | ||
if !mode.IsDir() { | ||
return nil, fmt.Errorf("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory '%v': not a directory (mode: %v)", heapDumpFolder, mode) | ||
} | ||
|
||
files, err := afero.ReadDir(fs, heapDumpFolder) | ||
if err != nil { | ||
return nil, fmt.Errorf("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory '%v': %v", heapDumpFolder, err) | ||
} | ||
|
||
isHeapDumpFile := func(file os.FileInfo) bool { | ||
return strings.HasSuffix(file.Name(), ".hprof") | ||
} | ||
|
||
heapDumpFiles := filter.Choose(files, isHeapDumpFile).([]os.FileInfo) | ||
|
||
if len(heapDumpFiles) < maxDumpCount || maxDumpCount < 1 { | ||
return []string{}, nil | ||
} | ||
|
||
var deletedFiles []string | ||
sort.Sort(sort.Reverse(byName(heapDumpFiles))) | ||
|
||
for _, file := range heapDumpFiles[maxDumpCount-1:] { | ||
path := heapDumpFolder + "/" + file.Name() | ||
var err = fs.Remove(path) | ||
if err != nil { | ||
return nil, fmt.Errorf("Cannot delete heap dump file '"+path+"': %v", err) | ||
} | ||
|
||
deletedFiles = append(deletedFiles, path) | ||
} | ||
|
||
return deletedFiles, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. | ||
* This file is licensed under the Apache Software License, v. 2 except as noted | ||
* otherwise in the LICENSE file at the root of the repository. | ||
*/ | ||
|
||
package main_test | ||
|
||
import ( | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
|
||
"testing" | ||
) | ||
|
||
func TestCleanUp(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "JavaMemoryAssistant CleanUp Suite") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
/* | ||
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. | ||
* This file is licensed under the Apache Software License, v. 2 except as noted | ||
* otherwise in the LICENSE file at the root of the repository. | ||
*/ | ||
|
||
package main_test | ||
|
||
import ( | ||
"os" | ||
|
||
"github.com/spf13/afero" | ||
|
||
. "github.com/SAP/java-memory-assistant/cleanup" | ||
. "github.com/SAP/java-memory-assistant/cleanup/matchers" | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("Run clean_up", func() { | ||
|
||
var fs afero.Fs | ||
|
||
BeforeEach(func() { | ||
fs = afero.NewMemMapFs() | ||
}) | ||
|
||
Context("without 'JMA_HEAP_DUMP_FOLDER' environment variable set", func() { | ||
|
||
It("fails", func() { | ||
deletedFiles, err := CleanUp(fs, Config{ | ||
MaxDumpCount: 1, | ||
}) | ||
|
||
Expect(err.Error()).To(Equal("The environment variable 'JMA_HEAP_DUMP_FOLDER' is not set")) | ||
Expect(deletedFiles).To(BeEmpty()) | ||
}) | ||
|
||
}) | ||
|
||
Context("with the 'JMA_HEAP_DUMP_FOLDER' pointing to a non-existing folder", func() { | ||
|
||
It("fails", func() { | ||
deletedFiles, err := CleanUp(fs, Config{ | ||
MaxDumpCount: 1, | ||
HeapDumpFolder: "nope", | ||
}) | ||
|
||
Expect(err.Error()).To(ContainSubstring("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory 'nope': does not exist")) | ||
Expect(deletedFiles).To(BeEmpty()) | ||
}) | ||
|
||
}) | ||
|
||
Context("with the 'JMA_HEAP_DUMP_FOLDER' pointing to a regular file", func() { | ||
|
||
It("fails", func() { | ||
fs.Create("dumps") | ||
|
||
deletedFiles, err := CleanUp(fs, Config{ | ||
MaxDumpCount: 1, | ||
HeapDumpFolder: "dumps", | ||
}) | ||
|
||
Expect(err.Error()).To(Equal("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory 'dumps': not a directory (mode: T---------)")) | ||
Expect(deletedFiles).To(BeEmpty()) | ||
}) | ||
|
||
}) | ||
|
||
Context("with 3 heap dump files", func() { | ||
|
||
BeforeEach(func() { | ||
fs.MkdirAll("dumps", os.ModeDir) | ||
fs.Create("dumps/1.hprof") | ||
fs.Create("dumps/2.hprof") | ||
fs.Create("dumps/3.hprof") | ||
fs.Create("dumps/not.a.dump") | ||
}) | ||
|
||
AfterEach(func() { | ||
if _, err := fs.Stat("dumps/not.a.dump"); os.IsNotExist(err) { | ||
Fail("A non-dump file has been deleted") | ||
} | ||
}) | ||
|
||
It("with max one heap dump, it deletes all three files", func() { | ||
deletedFiles, err := CleanUp(fs, Config{ | ||
MaxDumpCount: 1, | ||
HeapDumpFolder: "dumps"}) | ||
|
||
Expect(err).To(BeNil()) | ||
Expect(deletedFiles).To(ConsistOf("dumps/1.hprof", "dumps/2.hprof", "dumps/3.hprof")) | ||
|
||
Expect(fs).ToNot(HaveFile("dumps/1.hprof")) | ||
Expect(fs).ToNot(HaveFile("dumps/2.hprof")) | ||
Expect(fs).ToNot(HaveFile("dumps/3.hprof")) | ||
}) | ||
|
||
It("with max two heap dump, it deletes the first two files", func() { | ||
deletedFiles, err := CleanUp(fs, Config{ | ||
MaxDumpCount: 2, | ||
HeapDumpFolder: "dumps"}) | ||
|
||
Expect(err).To(BeNil()) | ||
Expect(deletedFiles).To(ConsistOf("dumps/1.hprof", "dumps/2.hprof")) | ||
|
||
Expect(fs).ToNot(HaveFile("dumps/1.hprof")) | ||
Expect(fs).ToNot(HaveFile("dumps/2.hprof")) | ||
Expect(fs).To(HaveFile("dumps/3.hprof")) | ||
}) | ||
}) | ||
|
||
Context("with repeated invocations", func() { | ||
|
||
BeforeEach(func() { | ||
fs.MkdirAll("dumps", os.ModeDir) | ||
fs.Create("dumps/1.hprof") | ||
fs.Create("dumps/2.hprof") | ||
fs.Create("dumps/not.a.dump") | ||
}) | ||
|
||
AfterEach(func() { | ||
if _, err := fs.Stat("dumps/not.a.dump"); os.IsNotExist(err) { | ||
Fail("A non-dump file has been deleted") | ||
} | ||
}) | ||
|
||
It("is idempotent", func() { | ||
deletedFiles_1, err_1 := CleanUp(fs, Config{ | ||
MaxDumpCount: 2, | ||
HeapDumpFolder: "dumps", | ||
}) | ||
|
||
Expect(err_1).To(BeNil()) | ||
Expect(deletedFiles_1).To(ConsistOf("dumps/1.hprof")) | ||
|
||
Expect(fs).ToNot(HaveFile("dumps/1.hprof")) | ||
Expect(fs).To(HaveFile("dumps/2.hprof")) | ||
|
||
// Test repeated invocations | ||
deletedFiles_2, err_2 := CleanUp(fs, Config{ | ||
MaxDumpCount: 2, | ||
HeapDumpFolder: "dumps", | ||
}) | ||
|
||
Expect(err_2).To(BeNil()) | ||
Expect(deletedFiles_2).To(BeEmpty()) | ||
|
||
Expect(fs).To(HaveFile("dumps/2.hprof")) | ||
}) | ||
|
||
}) | ||
|
||
Context("with no heap dump files", func() { | ||
|
||
BeforeEach(func() { | ||
fs.MkdirAll("dumps", os.ModeDir) | ||
fs.Create("dumps/not.a.dump") | ||
}) | ||
|
||
AfterEach(func() { | ||
if _, err := fs.Stat("dumps/not.a.dump"); os.IsNotExist(err) { | ||
Fail("A non-dump file has been deleted") | ||
} | ||
}) | ||
|
||
It("with max one heap dump, it deletes no files", func() { | ||
deletedFiles, err := CleanUp(fs, Config{ | ||
MaxDumpCount: 3, | ||
HeapDumpFolder: "dumps"}) | ||
|
||
Expect(err).To(BeNil()) | ||
Expect(deletedFiles).To(BeEmpty()) | ||
}) | ||
}) | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. | ||
* This file is licensed under the Apache Software License, v. 2 except as noted | ||
* otherwise in the LICENSE file at the root of the repository. | ||
*/ | ||
|
||
package matchers | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
|
||
"github.com/onsi/gomega/types" | ||
"github.com/spf13/afero" | ||
) | ||
|
||
// HaveFile checks if the afero FS has a given file in it | ||
func HaveFile(expected interface{}) types.GomegaMatcher { | ||
return &hasFile{ | ||
expected: expected, | ||
} | ||
} | ||
|
||
type hasFile struct { | ||
expected interface{} | ||
} | ||
|
||
func (matcher *hasFile) Match(actual interface{}) (success bool, err error) { | ||
fs, ok := actual.(afero.Fs) | ||
if !ok { | ||
return false, fmt.Errorf("HaveFile matcher expects an afero.Fs as 'actual'") | ||
} | ||
|
||
fileName, ok := matcher.expected.(string) | ||
if !ok { | ||
return false, fmt.Errorf("HaveFile matcher expects a string as 'expected'") | ||
} | ||
|
||
if _, err := fs.Stat(fileName); err != nil { | ||
if os.IsNotExist(err) { | ||
return false, nil | ||
} | ||
|
||
return false, fmt.Errorf("Cannot open file '%v': %s", fileName, err.Error()) | ||
} | ||
|
||
return true, nil | ||
} | ||
|
||
func (matcher *hasFile) FailureMessage(actual interface{}) (message string) { | ||
return fmt.Sprintf("Expected\n\t%#v\nto contain a file named\n\t%#v", actual, matcher.expected) | ||
} | ||
|
||
func (matcher *hasFile) NegatedFailureMessage(actual interface{}) (message string) { | ||
return fmt.Sprintf("Expected\n\t%#v\nnot to contain a file named\n\t%#v", actual, matcher.expected) | ||
} |