Skip to content

Commit 464669f

Browse files
Initial import
1 parent bafa1a4 commit 464669f

File tree

5 files changed

+372
-2
lines changed

5 files changed

+372
-2
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1-
# java-memory-assistant-cleanup
2-
Utility to clean up old heap dumps in the Java Buildpack
1+
# Java Memory Assistant Tools
2+
3+
This repository contains tools that enable the integration of the [Java Memory Assistant](https://github.com/SAP/java-memory-assistant) in other projects.
4+
5+
## Cleanup
6+
7+
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).

cleanup/cleanup.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved.
3+
* This file is licensed under the Apache Software License, v. 2 except as noted
4+
* otherwise in the LICENSE file at the root of the repository.
5+
*/
6+
7+
package main
8+
9+
import (
10+
"fmt"
11+
"log"
12+
"os"
13+
"sort"
14+
"strings"
15+
16+
"robpike.io/filter"
17+
18+
"github.com/caarlos0/env"
19+
"github.com/spf13/afero"
20+
)
21+
22+
func main() {
23+
cfg := Config{}
24+
err := env.Parse(&cfg)
25+
26+
if err != nil {
27+
log.Fatalf("%+v\n", err)
28+
}
29+
30+
deletedFiles, err := CleanUp(afero.NewOsFs(), cfg)
31+
if err != nil {
32+
log.Fatal(err)
33+
}
34+
35+
for _, deletedFile := range deletedFiles {
36+
fmt.Printf("Heap dump '%v' deleted\n", deletedFile)
37+
}
38+
}
39+
40+
type byName []os.FileInfo
41+
42+
// Config visible for testing
43+
type Config struct {
44+
HeapDumpFolder string `env:"JMA_HEAP_DUMP_FOLDER"`
45+
MaxDumpCount int `env:"JMA_MAX_DUMP_COUNT" envDefault:"0"`
46+
}
47+
48+
func (f byName) Len() int {
49+
return len(f)
50+
}
51+
52+
func (f byName) Less(i, j int) bool {
53+
return f[i].Name() < f[j].Name()
54+
}
55+
56+
func (f byName) Swap(i, j int) {
57+
f[i], f[j] = f[j], f[i]
58+
}
59+
60+
// CleanUp visible for testing
61+
func CleanUp(fs afero.Fs, cfg Config) ([]string, error) {
62+
if cfg.HeapDumpFolder == "" {
63+
return nil, fmt.Errorf("The environment variable 'JMA_HEAP_DUMP_FOLDER' is not set")
64+
}
65+
66+
heapDumpFolder := cfg.HeapDumpFolder
67+
68+
maxDumpCount := cfg.MaxDumpCount
69+
if maxDumpCount < 0 {
70+
return nil, fmt.Errorf("The value of the 'JMA_MAX_DUMP_COUNT' environment variable contains a negative number: %v", maxDumpCount)
71+
}
72+
73+
file, err := fs.Stat(heapDumpFolder)
74+
if os.IsNotExist(err) {
75+
return nil, fmt.Errorf("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory '%v': does not exist", heapDumpFolder)
76+
}
77+
78+
mode := file.Mode()
79+
if !mode.IsDir() {
80+
return nil, fmt.Errorf("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory '%v': not a directory (mode: %v)", heapDumpFolder, mode)
81+
}
82+
83+
files, err := afero.ReadDir(fs, heapDumpFolder)
84+
if err != nil {
85+
return nil, fmt.Errorf("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory '%v': %v", heapDumpFolder, err)
86+
}
87+
88+
isHeapDumpFile := func(file os.FileInfo) bool {
89+
return strings.HasSuffix(file.Name(), ".hprof")
90+
}
91+
92+
heapDumpFiles := filter.Choose(files, isHeapDumpFile).([]os.FileInfo)
93+
94+
if len(heapDumpFiles) < maxDumpCount || maxDumpCount < 1 {
95+
return []string{}, nil
96+
}
97+
98+
var deletedFiles []string
99+
sort.Sort(sort.Reverse(byName(heapDumpFiles)))
100+
101+
for _, file := range heapDumpFiles[maxDumpCount-1:] {
102+
path := heapDumpFolder + "/" + file.Name()
103+
var err = fs.Remove(path)
104+
if err != nil {
105+
return nil, fmt.Errorf("Cannot delete heap dump file '"+path+"': %v", err)
106+
}
107+
108+
deletedFiles = append(deletedFiles, path)
109+
}
110+
111+
return deletedFiles, nil
112+
}

cleanup/cleanup_suite_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved.
3+
* This file is licensed under the Apache Software License, v. 2 except as noted
4+
* otherwise in the LICENSE file at the root of the repository.
5+
*/
6+
7+
package main_test
8+
9+
import (
10+
. "github.com/onsi/ginkgo"
11+
. "github.com/onsi/gomega"
12+
13+
"testing"
14+
)
15+
16+
func TestCleanUp(t *testing.T) {
17+
RegisterFailHandler(Fail)
18+
RunSpecs(t, "JavaMemoryAssistant CleanUp Suite")
19+
}

cleanup/cleanup_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved.
3+
* This file is licensed under the Apache Software License, v. 2 except as noted
4+
* otherwise in the LICENSE file at the root of the repository.
5+
*/
6+
7+
package main_test
8+
9+
import (
10+
"os"
11+
12+
"github.com/spf13/afero"
13+
14+
. "github.com/SAP/java-memory-assistant/cleanup"
15+
. "github.com/SAP/java-memory-assistant/cleanup/matchers"
16+
. "github.com/onsi/ginkgo"
17+
. "github.com/onsi/gomega"
18+
)
19+
20+
var _ = Describe("Run clean_up", func() {
21+
22+
var fs afero.Fs
23+
24+
BeforeEach(func() {
25+
fs = afero.NewMemMapFs()
26+
})
27+
28+
Context("without 'JMA_HEAP_DUMP_FOLDER' environment variable set", func() {
29+
30+
It("fails", func() {
31+
deletedFiles, err := CleanUp(fs, Config{
32+
MaxDumpCount: 1,
33+
})
34+
35+
Expect(err.Error()).To(Equal("The environment variable 'JMA_HEAP_DUMP_FOLDER' is not set"))
36+
Expect(deletedFiles).To(BeEmpty())
37+
})
38+
39+
})
40+
41+
Context("with the 'JMA_HEAP_DUMP_FOLDER' pointing to a non-existing folder", func() {
42+
43+
It("fails", func() {
44+
deletedFiles, err := CleanUp(fs, Config{
45+
MaxDumpCount: 1,
46+
HeapDumpFolder: "nope",
47+
})
48+
49+
Expect(err.Error()).To(ContainSubstring("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory 'nope': does not exist"))
50+
Expect(deletedFiles).To(BeEmpty())
51+
})
52+
53+
})
54+
55+
Context("with the 'JMA_HEAP_DUMP_FOLDER' pointing to a regular file", func() {
56+
57+
It("fails", func() {
58+
fs.Create("dumps")
59+
60+
deletedFiles, err := CleanUp(fs, Config{
61+
MaxDumpCount: 1,
62+
HeapDumpFolder: "dumps",
63+
})
64+
65+
Expect(err.Error()).To(Equal("Cannot open 'JMA_HEAP_DUMP_FOLDER' directory 'dumps': not a directory (mode: T---------)"))
66+
Expect(deletedFiles).To(BeEmpty())
67+
})
68+
69+
})
70+
71+
Context("with 3 heap dump files", func() {
72+
73+
BeforeEach(func() {
74+
fs.MkdirAll("dumps", os.ModeDir)
75+
fs.Create("dumps/1.hprof")
76+
fs.Create("dumps/2.hprof")
77+
fs.Create("dumps/3.hprof")
78+
fs.Create("dumps/not.a.dump")
79+
})
80+
81+
AfterEach(func() {
82+
if _, err := fs.Stat("dumps/not.a.dump"); os.IsNotExist(err) {
83+
Fail("A non-dump file has been deleted")
84+
}
85+
})
86+
87+
It("with max one heap dump, it deletes all three files", func() {
88+
deletedFiles, err := CleanUp(fs, Config{
89+
MaxDumpCount: 1,
90+
HeapDumpFolder: "dumps"})
91+
92+
Expect(err).To(BeNil())
93+
Expect(deletedFiles).To(ConsistOf("dumps/1.hprof", "dumps/2.hprof", "dumps/3.hprof"))
94+
95+
Expect(fs).ToNot(HaveFile("dumps/1.hprof"))
96+
Expect(fs).ToNot(HaveFile("dumps/2.hprof"))
97+
Expect(fs).ToNot(HaveFile("dumps/3.hprof"))
98+
})
99+
100+
It("with max two heap dump, it deletes the first two files", func() {
101+
deletedFiles, err := CleanUp(fs, Config{
102+
MaxDumpCount: 2,
103+
HeapDumpFolder: "dumps"})
104+
105+
Expect(err).To(BeNil())
106+
Expect(deletedFiles).To(ConsistOf("dumps/1.hprof", "dumps/2.hprof"))
107+
108+
Expect(fs).ToNot(HaveFile("dumps/1.hprof"))
109+
Expect(fs).ToNot(HaveFile("dumps/2.hprof"))
110+
Expect(fs).To(HaveFile("dumps/3.hprof"))
111+
})
112+
})
113+
114+
Context("with repeated invocations", func() {
115+
116+
BeforeEach(func() {
117+
fs.MkdirAll("dumps", os.ModeDir)
118+
fs.Create("dumps/1.hprof")
119+
fs.Create("dumps/2.hprof")
120+
fs.Create("dumps/not.a.dump")
121+
})
122+
123+
AfterEach(func() {
124+
if _, err := fs.Stat("dumps/not.a.dump"); os.IsNotExist(err) {
125+
Fail("A non-dump file has been deleted")
126+
}
127+
})
128+
129+
It("is idempotent", func() {
130+
deletedFiles_1, err_1 := CleanUp(fs, Config{
131+
MaxDumpCount: 2,
132+
HeapDumpFolder: "dumps",
133+
})
134+
135+
Expect(err_1).To(BeNil())
136+
Expect(deletedFiles_1).To(ConsistOf("dumps/1.hprof"))
137+
138+
Expect(fs).ToNot(HaveFile("dumps/1.hprof"))
139+
Expect(fs).To(HaveFile("dumps/2.hprof"))
140+
141+
// Test repeated invocations
142+
deletedFiles_2, err_2 := CleanUp(fs, Config{
143+
MaxDumpCount: 2,
144+
HeapDumpFolder: "dumps",
145+
})
146+
147+
Expect(err_2).To(BeNil())
148+
Expect(deletedFiles_2).To(BeEmpty())
149+
150+
Expect(fs).To(HaveFile("dumps/2.hprof"))
151+
})
152+
153+
})
154+
155+
Context("with no heap dump files", func() {
156+
157+
BeforeEach(func() {
158+
fs.MkdirAll("dumps", os.ModeDir)
159+
fs.Create("dumps/not.a.dump")
160+
})
161+
162+
AfterEach(func() {
163+
if _, err := fs.Stat("dumps/not.a.dump"); os.IsNotExist(err) {
164+
Fail("A non-dump file has been deleted")
165+
}
166+
})
167+
168+
It("with max one heap dump, it deletes no files", func() {
169+
deletedFiles, err := CleanUp(fs, Config{
170+
MaxDumpCount: 3,
171+
HeapDumpFolder: "dumps"})
172+
173+
Expect(err).To(BeNil())
174+
Expect(deletedFiles).To(BeEmpty())
175+
})
176+
})
177+
178+
})

cleanup/matchers/matchers.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved.
3+
* This file is licensed under the Apache Software License, v. 2 except as noted
4+
* otherwise in the LICENSE file at the root of the repository.
5+
*/
6+
7+
package matchers
8+
9+
import (
10+
"fmt"
11+
"os"
12+
13+
"github.com/onsi/gomega/types"
14+
"github.com/spf13/afero"
15+
)
16+
17+
// HaveFile checks if the afero FS has a given file in it
18+
func HaveFile(expected interface{}) types.GomegaMatcher {
19+
return &hasFile{
20+
expected: expected,
21+
}
22+
}
23+
24+
type hasFile struct {
25+
expected interface{}
26+
}
27+
28+
func (matcher *hasFile) Match(actual interface{}) (success bool, err error) {
29+
fs, ok := actual.(afero.Fs)
30+
if !ok {
31+
return false, fmt.Errorf("HaveFile matcher expects an afero.Fs as 'actual'")
32+
}
33+
34+
fileName, ok := matcher.expected.(string)
35+
if !ok {
36+
return false, fmt.Errorf("HaveFile matcher expects a string as 'expected'")
37+
}
38+
39+
if _, err := fs.Stat(fileName); err != nil {
40+
if os.IsNotExist(err) {
41+
return false, nil
42+
}
43+
44+
return false, fmt.Errorf("Cannot open file '%v': %s", fileName, err.Error())
45+
}
46+
47+
return true, nil
48+
}
49+
50+
func (matcher *hasFile) FailureMessage(actual interface{}) (message string) {
51+
return fmt.Sprintf("Expected\n\t%#v\nto contain a file named\n\t%#v", actual, matcher.expected)
52+
}
53+
54+
func (matcher *hasFile) NegatedFailureMessage(actual interface{}) (message string) {
55+
return fmt.Sprintf("Expected\n\t%#v\nnot to contain a file named\n\t%#v", actual, matcher.expected)
56+
}

0 commit comments

Comments
 (0)