Skip to content

Commit 6e54140

Browse files
committed
Integrate memory calculator v4 into runtime startup command
Implement Ruby buildpack parity by invoking memory calculator at container runtime to calculate optimal JVM memory settings based on available memory. Key changes: - Add MemoryCalculatorCommand() interface method to all JRE implementations to return shell command snippet for runtime memory calculation - Update memory calculator to use v4.x double-dash flag format: --total-memory, --loaded-class-count, --thread-count, --head-room, --jvm-options (replacing v3.x single-dash format) - Remove --pool-type flag (not used in v4.x) - Prepend memory calculator command to container startup in release.yml - Add base JAVA_OPTS: -Djava.io.tmpdir, -XX:ActiveProcessorCount, -Djava.ext.dirs (Ruby buildpack parity) - Escape startup command with single quotes in YAML to preserve shell special characters ($, quotes, etc.) - Use default class count (6,300) when class counting fails or returns 0 (v4 calculator requires this parameter) Integration test updates: - Reduce memory settings to fit within 1G container limit used by tests - Memory calculator v4 has stricter defaults (240M code cache, 250 threads) compared to v3.x used by Ruby buildpack - Updated 8 integration tests with reduced heap (-Xmx384m or -Xmx256m), smaller code cache (-XX:ReservedCodeCacheSize=120M), and reduced thread stack (-Xss512k) The memory calculator now runs inline in startup command (after profile.d scripts assemble JAVA_OPTS) to read runtime $MEMORY_LIMIT and calculate optimal memory flags, matching Ruby buildpack behavior. All 99 integration tests pass.
1 parent 654a1ad commit 6e54140

File tree

13 files changed

+217
-72
lines changed

13 files changed

+217
-72
lines changed

src/integration/frameworks_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,8 @@ func testFrameworks(platform switchblade.Platform, fixtures string) func(*testin
685685
deployment, logs, err := platform.Deploy.
686686
WithEnv(map[string]string{
687687
"BP_JAVA_VERSION": "11",
688-
"JAVA_OPTS": "-Xmx512m -Dcustom.property=test",
688+
// Reduce code cache and thread stack to fit within 1G memory limit (v4 calculator)
689+
"JAVA_OPTS": "-Xmx384m -XX:ReservedCodeCacheSize=120M -Xss512k -Dcustom.property=test",
689690
}).
690691
Execute(name, filepath.Join(fixtures, "apps", "integration_valid"))
691692
Expect(err).NotTo(HaveOccurred(), logs.String)
@@ -698,8 +699,9 @@ func testFrameworks(platform switchblade.Platform, fixtures string) func(*testin
698699
it("applies custom JAVA_OPTS from configuration file", func() {
699700
deployment, logs, err := platform.Deploy.
700701
WithEnv(map[string]string{
701-
"BP_JAVA_VERSION": "11",
702-
"JBP_CONFIG_JAVA_OPTS": "'{java_opts: [\"-Xms256m\", \"-Xmx1024m\"]}'",
702+
"BP_JAVA_VERSION": "11",
703+
// Reduce heap and code cache to fit within 1G memory limit (v4 calculator)
704+
"JBP_CONFIG_JAVA_OPTS": "'{java_opts: [\"-Xms256m\", \"-Xmx384m\", \"-XX:ReservedCodeCacheSize=120M\", \"-Xss512k\"]}'",
703705
}).
704706
Execute(name, filepath.Join(fixtures, "apps", "integration_valid"))
705707
Expect(err).NotTo(HaveOccurred(), logs.String)

src/integration/java_main_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ func testJavaMain(platform switchblade.Platform, fixtures string) func(*testing.
8989
_, logs, err := platform.Deploy.
9090
WithEnv(map[string]string{
9191
"BP_JAVA_VERSION": "11",
92-
"JAVA_OPTS": "-Xmx512m -XX:+UseG1GC",
92+
// Reduce memory settings to fit within 1G limit (v4 calculator)
93+
"JAVA_OPTS": "-Xmx384m -XX:ReservedCodeCacheSize=120M -Xss512k -XX:+UseG1GC",
9394
}).
9495
Execute(name, filepath.Join(fixtures, "containers", "main"))
9596
Expect(err).NotTo(HaveOccurred(), logs.String)

src/integration/spring_boot_test.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,27 +99,29 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin
9999
it("applies configured JAVA_OPTS with from_environment=false and verifies at runtime", func() {
100100
deployment, logs, err := platform.Deploy.
101101
WithEnv(map[string]string{
102-
"BP_JAVA_VERSION": "17",
103-
"JBP_CONFIG_JAVA_OPTS": `[from_environment: false, java_opts: '-Xmx512M -Xms256M -Xss1M -XX:MetaspaceSize=157286K -XX:MaxMetaspaceSize=314572K -DoptionKey=optionValue']`,
102+
"BP_JAVA_VERSION": "17",
103+
// Reduce memory settings to fit within 1G limit (v4 calculator)
104+
"JBP_CONFIG_JAVA_OPTS": `[from_environment: false, java_opts: '-Xmx256M -Xms128M -Xss512k -XX:ReservedCodeCacheSize=120M -XX:MetaspaceSize=78643K -XX:MaxMetaspaceSize=157286K -DoptionKey=optionValue']`,
104105
}).
105106
Execute(name, filepath.Join(fixtures, "containers", "spring_boot_staged"))
106107
Expect(err).NotTo(HaveOccurred(), logs.String)
107108

108109
// Verify buildpack detected and configured Java Opts
109110
Expect(logs.String()).To(ContainSubstring("Java Opts"))
110111
Expect(logs.String()).To(ContainSubstring("Adding configured JAVA_OPTS"))
111-
Expect(logs.String()).To(ContainSubstring("-Xmx512M"))
112+
Expect(logs.String()).To(ContainSubstring("-Xmx256M"))
112113

113114
// Verify Container Security Provider is configured (should add its opts)
114115
Expect(logs.String()).To(ContainSubstring("Container Security Provider"))
115116

116117
// Verify configured opts are actually applied at runtime
117118
Eventually(deployment).Should(matchers.Serve(And(
118-
ContainSubstring("-Xmx512M"),
119-
ContainSubstring("-Xms256M"),
120-
ContainSubstring("-Xss1M"),
121-
ContainSubstring("-XX:MetaspaceSize=157286K"),
122-
ContainSubstring("-XX:MaxMetaspaceSize=314572K"),
119+
ContainSubstring("-Xmx256M"),
120+
ContainSubstring("-Xms128M"),
121+
ContainSubstring("-Xss512k"),
122+
ContainSubstring("-XX:ReservedCodeCacheSize=120M"),
123+
ContainSubstring("-XX:MetaspaceSize=78643K"),
124+
ContainSubstring("-XX:MaxMetaspaceSize=157286K"),
123125
ContainSubstring("optionKey=optionValue"), // Custom system property
124126
)).WithEndpoint("/jvm-args"))
125127

@@ -134,8 +136,9 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin
134136
it("applies configured JAVA_OPTS with from_environment=true and preserves user opts", func() {
135137
deployment, logs, err := platform.Deploy.
136138
WithEnv(map[string]string{
137-
"BP_JAVA_VERSION": "17",
138-
"JBP_CONFIG_JAVA_OPTS": `{from_environment: true, java_opts: ["-Xmx512M", "-DconfiguredProperty=fromConfig"]}`,
139+
"BP_JAVA_VERSION": "17",
140+
// Reduce memory settings to fit within 1G limit (v4 calculator)
141+
"JBP_CONFIG_JAVA_OPTS": `{from_environment: true, java_opts: ["-Xmx384M", "-XX:ReservedCodeCacheSize=120M", "-Xss512k", "-DconfiguredProperty=fromConfig"]}`,
139142
"JAVA_OPTS": "-DuserProperty=fromUser",
140143
}).
141144
Execute(name, filepath.Join(fixtures, "containers", "spring_boot_staged"))
@@ -152,8 +155,9 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin
152155
it("applies only configured JAVA_OPTS with from_environment=false and ignores user opts", func() {
153156
deployment, logs, err := platform.Deploy.
154157
WithEnv(map[string]string{
155-
"BP_JAVA_VERSION": "17",
156-
"JBP_CONFIG_JAVA_OPTS": `{from_environment: false, java_opts: ["-Xmx512M", "-DconfiguredProperty=fromConfig"]}`,
158+
"BP_JAVA_VERSION": "17",
159+
// Reduce memory settings to fit within 1G limit (v4 calculator)
160+
"JBP_CONFIG_JAVA_OPTS": `{from_environment: false, java_opts: ["-Xmx384M", "-XX:ReservedCodeCacheSize=120M", "-Xss512k", "-DconfiguredProperty=fromConfig"]}`,
157161
"JAVA_OPTS": "-DuserProperty=shouldBeIgnored",
158162
}).
159163
Execute(name, filepath.Join(fixtures, "containers", "spring_boot_staged"))
@@ -184,8 +188,9 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin
184188
it("verifies multiple frameworks (4) append JAVA_OPTS without overwriting each other", func() {
185189
deployment, logs, err := platform.Deploy.
186190
WithEnv(map[string]string{
187-
"BP_JAVA_VERSION": "17",
188-
"JBP_CONFIG_JAVA_OPTS": `{from_environment: false, java_opts: ["-Xmx768M", "-DcustomProp=testValue"]}`,
191+
"BP_JAVA_VERSION": "17",
192+
// Reduce memory settings to fit within 1G limit (v4 calculator)
193+
"JBP_CONFIG_JAVA_OPTS": `{from_environment: false, java_opts: ["-Xmx384M", "-XX:ReservedCodeCacheSize=120M", "-Xss512k", "-DcustomProp=testValue"]}`,
189194
"JBP_CONFIG_DEBUG": `{enabled: true}`,
190195
}).
191196
Execute(name, filepath.Join(fixtures, "containers", "spring_boot_multi_framework"))
@@ -204,7 +209,7 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin
204209
// Verify ALL opts from ALL frameworks are present at runtime (none were overwritten)
205210
Eventually(deployment).Should(matchers.Serve(And(
206211
// Framework 1: User-configured opts from JBP_CONFIG_JAVA_OPTS
207-
ContainSubstring("-Xmx768M"),
212+
ContainSubstring("-Xmx384M"),
208213
ContainSubstring("customProp=testValue"),
209214
// Framework 2: Container Security Provider opts
210215
ContainSubstring("-Xbootclasspath/a:"),
@@ -224,8 +229,9 @@ func testSpringBoot(platform switchblade.Platform, fixtures string) func(*testin
224229
it("verifies from_environment=true preserves user JAVA_OPTS with 4 frameworks", func() {
225230
deployment, logs, err := platform.Deploy.
226231
WithEnv(map[string]string{
227-
"BP_JAVA_VERSION": "17",
228-
"JBP_CONFIG_JAVA_OPTS": `{from_environment: true, java_opts: ["-DconfigProp=fromBuildpack"]}`,
232+
"BP_JAVA_VERSION": "17",
233+
// Reduce memory settings to fit within 1G limit (v4 calculator)
234+
"JBP_CONFIG_JAVA_OPTS": `{from_environment: true, java_opts: ["-XX:ReservedCodeCacheSize=120M", "-Xss512k", "-DconfigProp=fromBuildpack"]}`,
229235
"JAVA_OPTS": "-DuserProp=fromEnvironment -Xmx256M",
230236
"JBP_CONFIG_DEBUG": `{enabled: true}`,
231237
}).

src/java/finalize/finalize.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package finalize
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
54
"fmt"
5+
"github.com/cloudfoundry/java-buildpack/src/java/common"
66
"os"
77
"path/filepath"
88

@@ -19,6 +19,7 @@ type Finalizer struct {
1919
Log *libbuildpack.Logger
2020
Command *libbuildpack.Command
2121
Container containers.Container
22+
JRE jres.JRE
2223
}
2324

2425
// Run performs the finalize phase
@@ -53,10 +54,12 @@ func Run(f *Finalizer) error {
5354
f.Container = container
5455

5556
// Finalize JRE (memory calculator, jvmkill, etc.)
56-
if err := f.finalizeJRE(); err != nil {
57+
jre, err := f.finalizeJRE()
58+
if err != nil {
5759
f.Log.Error("Failed to finalize JRE: %s", err.Error())
5860
return err
5961
}
62+
f.JRE = jre
6063

6164
// Finalize frameworks (APM agents, etc.)
6265
if err := f.finalizeFrameworks(); err != nil {
@@ -81,7 +84,8 @@ func Run(f *Finalizer) error {
8184
}
8285

8386
// finalizeJRE finalizes the JRE configuration (memory calculator, jvmkill, etc.)
84-
func (f *Finalizer) finalizeJRE() error {
87+
// Returns the finalized JRE instance for use in command generation
88+
func (f *Finalizer) finalizeJRE() (jres.JRE, error) {
8589
f.Log.BeginStep("Finalizing JRE")
8690

8791
// Create JRE context
@@ -108,7 +112,7 @@ func (f *Finalizer) finalizeJRE() error {
108112
jre, jreName, err := registry.Detect()
109113
if err != nil {
110114
f.Log.Error("Failed to detect JRE: %s", err.Error())
111-
return err
115+
return nil, err
112116
}
113117

114118
f.Log.Info("Finalizing JRE: %s", jreName)
@@ -117,11 +121,11 @@ func (f *Finalizer) finalizeJRE() error {
117121
if err := jre.Finalize(); err != nil {
118122
f.Log.Warning("Failed to finalize JRE: %s (continuing)", err.Error())
119123
// Don't fail the build if JRE finalization fails
120-
return nil
124+
return jre, nil
121125
}
122126

123127
f.Log.Info("JRE finalization complete")
124-
return nil
128+
return jre, nil
125129
}
126130

127131
// finalizeFrameworks finalizes framework components (APM agents, etc.)
@@ -186,24 +190,41 @@ func (f *Finalizer) writeReleaseYaml(container containers.Container) error {
186190
return fmt.Errorf("failed to get container command: %w", err)
187191
}
188192

193+
// Prepend memory calculator command if available (Ruby buildpack parity)
194+
// The memory calculator must run before the Java command to set JAVA_OPTS
195+
var fullCommand string
196+
if f.JRE != nil {
197+
memCalcCmd := f.JRE.MemoryCalculatorCommand()
198+
if memCalcCmd != "" {
199+
// Join with && to ensure memory calculator runs before container command
200+
fullCommand = memCalcCmd + " && " + containerCommand
201+
f.Log.Debug("Prepended memory calculator command to startup")
202+
} else {
203+
fullCommand = containerCommand
204+
}
205+
} else {
206+
fullCommand = containerCommand
207+
}
208+
189209
// Create tmp directory in build dir
190210
tmpDir := filepath.Join(f.Stager.BuildDir(), "tmp")
191211
if err := os.MkdirAll(tmpDir, 0755); err != nil {
192212
return fmt.Errorf("failed to create tmp directory: %w", err)
193213
}
194214

195215
// Write YAML file with release information
216+
// The command must be properly escaped for YAML - use single quotes to preserve special characters
196217
releaseYamlPath := filepath.Join(tmpDir, "java-buildpack-release-step.yml")
197218
yamlContent := fmt.Sprintf(`---
198219
default_process_types:
199-
web: %s
200-
`, containerCommand)
220+
web: '%s'
221+
`, fullCommand)
201222

202223
if err := os.WriteFile(releaseYamlPath, []byte(yamlContent), 0644); err != nil {
203224
return fmt.Errorf("failed to write release YAML: %w", err)
204225
}
205226

206227
f.Log.Info("Release YAML written: %s", releaseYamlPath)
207-
f.Log.Info("Web process command: %s", containerCommand)
228+
f.Log.Info("Web process command: %s", fullCommand)
208229
return nil
209230
}

src/java/jres/graalvm.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package jres
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
54
"fmt"
5+
"github.com/cloudfoundry/java-buildpack/src/java/common"
66
"os"
77
"path/filepath"
88
)
@@ -178,6 +178,14 @@ func (g *GraalVMJRE) Version() string {
178178
return g.installedVersion
179179
}
180180

181+
// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime
182+
func (g *GraalVMJRE) MemoryCalculatorCommand() string {
183+
if g.memoryCalc == nil {
184+
return ""
185+
}
186+
return g.memoryCalc.GetCalculatorCommand()
187+
}
188+
181189
// findJavaHome locates the actual JAVA_HOME directory after extraction
182190
// GraalVM tarballs usually extract to graalvm-* or jdk-* subdirectories
183191
func (g *GraalVMJRE) findJavaHome() (string, error) {

src/java/jres/ibm.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package jres
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
54
"fmt"
5+
"github.com/cloudfoundry/java-buildpack/src/java/common"
66
"os"
77
"path/filepath"
88
)
@@ -190,6 +190,14 @@ func (i *IBMJRE) Version() string {
190190
return i.installedVersion
191191
}
192192

193+
// MemoryCalculatorCommand returns the shell command snippet to run memory calculator at runtime
194+
func (i *IBMJRE) MemoryCalculatorCommand() string {
195+
if i.memoryCalc == nil {
196+
return ""
197+
}
198+
return i.memoryCalc.GetCalculatorCommand()
199+
}
200+
193201
// findJavaHome locates the actual JAVA_HOME directory after extraction
194202
// IBM JRE tarballs usually extract to ibm-java-* or jre subdirectories
195203
func (i *IBMJRE) findJavaHome() (string, error) {

src/java/jres/jre.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package jres
22

33
import (
4-
"github.com/cloudfoundry/java-buildpack/src/java/common"
54
"fmt"
5+
"github.com/cloudfoundry/java-buildpack/src/java/common"
66
"os"
77
"path/filepath"
88
"strings"
@@ -29,6 +29,11 @@ type JRE interface {
2929

3030
// Version returns the installed JRE version
3131
Version() string
32+
33+
// MemoryCalculatorCommand returns the shell command snippet to run memory calculator
34+
// This command is prepended to the container startup command
35+
// Returns empty string if memory calculator is not installed
36+
MemoryCalculatorCommand() string
3237
}
3338

3439
// Context holds shared dependencies for JRE providers
@@ -149,6 +154,7 @@ type BaseComponent struct {
149154
const (
150155
DefaultStackThreads = 250
151156
DefaultHeadroom = 0
157+
DefaultClassCount = 18000 // Default class count when counting fails (after 35% factor: ~6300)
152158
Java9ClassCount = 42215 // Classes in Java 9+ JRE
153159
)
154160

@@ -219,7 +225,6 @@ func normalizeVersionPattern(version string) string {
219225
return version + ".*"
220226
}
221227

222-
223228
// WriteJavaOpts writes JAVA_OPTS to a .opts file for centralized assembly
224229
// JRE components use priority 05 to run early (before frameworks)
225230
func WriteJavaOpts(ctx *common.Context, opts string) error {

0 commit comments

Comments
 (0)