Skip to content
This repository has been archived by the owner on Sep 1, 2023. It is now read-only.

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
michele-mancioppi committed Jun 24, 2017
1 parent bafa1a4 commit 464669f
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 2 deletions.
9 changes: 7 additions & 2 deletions README.md
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).
112 changes: 112 additions & 0 deletions cleanup/cleanup.go
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
}
19 changes: 19 additions & 0 deletions cleanup/cleanup_suite_test.go
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")
}
178 changes: 178 additions & 0 deletions cleanup/cleanup_test.go
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())
})
})

})
56 changes: 56 additions & 0 deletions cleanup/matchers/matchers.go
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)
}

0 comments on commit 464669f

Please sign in to comment.