Skip to content

Commit b4c54ab

Browse files
authored
[Java17] Add --add-export settings to restore JDK17 compatibility (#13825)
* [Java17] Add `--add-export` settings to restore JDK17 compatibility After #13700 updated google-java-format dependency, it is now required to add a number of `--add-export` flags in order to run on JDK17. This commit adds these flags to the jvm options for a running logstash, and to the tests running on gradle to enable tests to still work * Move mandatory JVM options out of `jvm.options` and into JvmOptionsParser Certain values for the JVM are mandatory for Logstash to function correctly. Rather than leave them in config/jvm.options where they can be updated, and can cause upgrade issues for users when we add new mandatory options, move them into code where they cannot be changed
1 parent 7177207 commit b4c54ab

File tree

4 files changed

+142
-89
lines changed

4 files changed

+142
-89
lines changed

build.gradle

+11-3
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,17 @@ allprojects {
8181
delete "${projectDir}/out/"
8282
}
8383

84-
//https://stackoverflow.com/questions/3963708/gradle-how-to-display-test-results-in-the-console-in-real-time
8584
tasks.withType(Test) {
86-
testLogging {
85+
// Add Exports to enable tests to run in JDK17
86+
jvmArgs = [
87+
"--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
88+
"--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
89+
"--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
90+
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
91+
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
92+
]
93+
//https://stackoverflow.com/questions/3963708/gradle-how-to-display-test-results-in-the-console-in-real-time
94+
testLogging {
8795
// set options for log level LIFECYCLE
8896
events "passed", "skipped", "failed", "standardOut"
8997
showExceptions true
@@ -893,4 +901,4 @@ if (System.getenv('OSS') != 'true') {
893901
tasks.register("runXPackIntegrationTests"){
894902
dependsOn copyPluginTestAlias
895903
dependsOn ":logstash-xpack:rubyIntegrationTests"
896-
}
904+
}

config/jvm.options

+1-9
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@
4949
-Djruby.compile.invokedynamic=true
5050
# Force Compilation
5151
-Djruby.jit.threshold=0
52-
# Make sure joni regexp interruptability is enabled
53-
-Djruby.regexp.interruptible=true
5452

5553
## heap dumps
5654

@@ -73,10 +71,4 @@
7371
-Djava.security.egd=file:/dev/urandom
7472

7573
# Copy the logging context from parent threads to children
76-
-Dlog4j2.isThreadContextMapInheritable=true
77-
78-
11-:--add-opens=java.base/java.security=ALL-UNNAMED
79-
11-:--add-opens=java.base/java.io=ALL-UNNAMED
80-
11-:--add-opens=java.base/java.nio.channels=ALL-UNNAMED
81-
11-:--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
82-
11-:--add-opens=java.management/sun.management=ALL-UNNAMED
74+
-Dlog4j2.isThreadContextMapInheritable=true

logstash-core/src/main/java/org/logstash/launchers/JvmOptionsParser.java

+108-76
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,40 @@
1313
import java.nio.file.Paths;
1414
import java.util.ArrayList;
1515
import java.util.Arrays;
16+
import java.util.Collection;
1617
import java.util.Collections;
18+
import java.util.LinkedHashSet;
1719
import java.util.List;
1820
import java.util.Locale;
1921
import java.util.Map;
2022
import java.util.Optional;
23+
import java.util.Set;
2124
import java.util.SortedMap;
2225
import java.util.TreeMap;
2326
import java.util.regex.Matcher;
2427
import java.util.regex.Pattern;
28+
import java.util.stream.Collectors;
2529

2630

2731
/**
2832
* Parse jvm.options file applying version conditional logic. Heavily inspired by same functionality in Elasticsearch.
2933
* */
3034
public class JvmOptionsParser {
3135

36+
private static final String[] MANDATORY_JVM_OPTIONS = new String[]{
37+
"-Djruby.regexp.interruptible=true",
38+
"16-:--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
39+
"16-:--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
40+
"16-:--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
41+
"16-:--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
42+
"16-:--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
43+
"11-:--add-opens=java.base/java.security=ALL-UNNAMED",
44+
"11-:--add-opens=java.base/java.io=ALL-UNNAMED",
45+
"11-:--add-opens=java.base/java.nio.channels=ALL-UNNAMED",
46+
"11-:--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
47+
"11-:--add-opens=java.management/sun.management=ALL-UNNAMED"
48+
};
49+
3250
static class JvmOptionsFileParserException extends Exception {
3351

3452
private static final long serialVersionUID = 2446165130736962758L;
@@ -71,8 +89,7 @@ public static void main(final String[] args) throws InterruptedException, IOExce
7189
);
7290
}
7391
bailOnOldJava();
74-
final String lsJavaOpts = System.getenv("LS_JAVA_OPTS");
75-
handleJvmOptions(args, lsJavaOpts);
92+
handleJvmOptions(args, System.getenv("LS_JAVA_OPTS"));
7693
}
7794

7895
static void bailOnOldJava(){
@@ -93,7 +110,7 @@ static void handleJvmOptions(String[] args, String lsJavaOpts) {
93110
final String jvmOpts = args.length == 2 ? args[1] : null;
94111
try {
95112
Optional<Path> jvmOptions = parser.lookupJvmOptionsFile(jvmOpts);
96-
parser.parseAndInjectEnvironment(jvmOptions, lsJavaOpts);
113+
parser.handleJvmOptions(jvmOptions, lsJavaOpts);
97114
} catch (JvmOptionsFileParserException pex) {
98115
System.err.printf(Locale.ROOT,
99116
"encountered [%d] error%s parsing [%s]",
@@ -128,20 +145,38 @@ private Optional<Path> lookupJvmOptionsFile(String jvmOpts) {
128145
.findFirst();
129146
}
130147

131-
private void parseAndInjectEnvironment(Optional<Path> jvmOptionsFile, String lsJavaOpts) throws IOException, JvmOptionsFileParserException {
132-
final List<String> jvmOptionsContent = new ArrayList<>(parseJvmOptions(jvmOptionsFile));
148+
private void handleJvmOptions(Optional<Path> jvmOptionsFile, String lsJavaOpts) throws IOException, JvmOptionsFileParserException {
149+
int javaMajorVersion = javaMajorVersion();
150+
151+
// Add JVM Options from config/jvm.options
152+
final Set<String> jvmOptionsContent = new LinkedHashSet<>(getJvmOptionsFromFile(jvmOptionsFile, javaMajorVersion));
133153

154+
// Add JVM Options from LS_JAVA_OPTS
134155
if (lsJavaOpts != null && !lsJavaOpts.isEmpty()) {
135156
if (isDebugEnabled()) {
136157
System.err.println("Appending jvm options from environment LS_JAVA_OPTS");
137158
}
138159
jvmOptionsContent.add(lsJavaOpts);
139160
}
161+
// Set mandatory JVM options
162+
jvmOptionsContent.addAll(getMandatoryJvmOptions(javaMajorVersion));
140163

141164
System.out.println(String.join(" ", jvmOptionsContent));
142165
}
143166

144-
private List<String> parseJvmOptions(Optional<Path> jvmOptionsFile) throws IOException, JvmOptionsFileParserException {
167+
/**
168+
* Returns the list of mandatory JVM options for the given version of Java.
169+
* @param javaMajorVersion
170+
* @return Collection of mandatory options
171+
*/
172+
static Collection<String> getMandatoryJvmOptions(int javaMajorVersion){
173+
return Arrays.stream(MANDATORY_JVM_OPTIONS)
174+
.map(option -> jvmOptionFromLine(javaMajorVersion, option))
175+
.flatMap(Optional::stream)
176+
.collect(Collectors.toUnmodifiableList());
177+
}
178+
179+
private List<String> getJvmOptionsFromFile(final Optional<Path> jvmOptionsFile, final int javaMajorVersion) throws IOException, JvmOptionsFileParserException {
145180
if (!jvmOptionsFile.isPresent()) {
146181
System.err.println("Warning: no jvm.options file found.");
147182
return Collections.emptyList();
@@ -155,13 +190,11 @@ private List<String> parseJvmOptions(Optional<Path> jvmOptionsFile) throws IOExc
155190
if (isDebugEnabled()) {
156191
System.err.format("Processing jvm.options file at `%s`\n", optionsFilePath);
157192
}
158-
final int majorJavaVersion = javaMajorVersion();
159-
160193
try (InputStream is = Files.newInputStream(optionsFilePath);
161194
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
162195
BufferedReader br = new BufferedReader(reader)
163196
) {
164-
final ParseResult parseResults = parse(majorJavaVersion, br);
197+
final ParseResult parseResults = parse(javaMajorVersion, br);
165198
if (parseResults.hasErrors()) {
166199
throw new JvmOptionsFileParserException(optionsFilePath, parseResults.getInvalidLines());
167200
}
@@ -209,7 +242,36 @@ public List<String> getJvmOptions() {
209242
private static final Pattern OPTION_DEFINITION = Pattern.compile("((?<start>\\d+)(?<range>-)?(?<end>\\d+)?:)?(?<option>-.*)$");
210243

211244
/**
212-
* Parse the line-delimited JVM options from the specified buffered reader for the specified Java major version.
245+
*
246+
* If the version syntax specified on a line matches the specified JVM options, the JVM option callback will be invoked with the JVM
247+
* option. If the line does not match the specified syntax for the JVM options, the invalid line callback will be invoked with the
248+
* contents of the entire line.
249+
*
250+
* @param javaMajorVersion the Java major version to match JVM options against
251+
* @param br the buffered reader to read line-delimited JVM options from
252+
* @return the admitted options lines respecting the javaMajorVersion and the error lines
253+
* @throws IOException if an I/O exception occurs reading from the buffered reader
254+
*/
255+
static ParseResult parse(final int javaMajorVersion, final BufferedReader br) throws IOException {
256+
final ParseResult result = new ParseResult();
257+
int lineNumber = 0;
258+
while (true) {
259+
final String line = br.readLine();
260+
lineNumber++;
261+
if (line == null) {
262+
break;
263+
}
264+
try{
265+
jvmOptionFromLine(javaMajorVersion, line).ifPresent(result::appendOption);
266+
} catch (IllegalArgumentException e){
267+
result.appendError(lineNumber, line);
268+
};
269+
}
270+
return result;
271+
}
272+
273+
/**
274+
* Parse the line-delimited JVM options from the specified string for the specified Java major version.
213275
* Valid JVM options are:
214276
* <ul>
215277
* <li>
@@ -256,82 +318,52 @@ public List<String> getJvmOptions() {
256318
* {@code 9-10:-Xlog:age*=trace,gc*,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m}
257319
* </li>
258320
* </ul>
259-
*
260-
* If the version syntax specified on a line matches the specified JVM options, the JVM option callback will be invoked with the JVM
261-
* option. If the line does not match the specified syntax for the JVM options, the invalid line callback will be invoked with the
262-
* contents of the entire line.
263-
*
264-
* @param javaMajorVersion the Java major version to match JVM options against
265-
* @param br the buffered reader to read line-delimited JVM options from
266-
* @return the admitted options lines respecting the javaMajorVersion and the error lines
267-
* @throws IOException if an I/O exception occurs reading from the buffered reader
321+
322+
* @param javaMajorVersion
323+
* @param line
324+
* @return Returns an Optional containing a string if the line contains a valid option for the specified Java
325+
* version and empty otherwise
268326
*/
269-
static ParseResult parse(final int javaMajorVersion, final BufferedReader br) throws IOException {
270-
final ParseResult result = new ParseResult();
271-
int lineNumber = 0;
272-
while (true) {
273-
final String line = br.readLine();
274-
lineNumber++;
275-
if (line == null) {
276-
break;
277-
}
278-
if (line.startsWith("#")) {
279-
// lines beginning with "#" are treated as comments
280-
continue;
281-
}
282-
if (line.matches("\\s*")) {
283-
// skip blank lines
284-
continue;
285-
}
286-
final Matcher matcher = OPTION_DEFINITION.matcher(line);
287-
if (matcher.matches()) {
288-
final String start = matcher.group("start");
289-
final String end = matcher.group("end");
290-
if (start == null) {
291-
// no range present, unconditionally apply the JVM option
292-
result.appendOption(line);
327+
private static Optional<String> jvmOptionFromLine(final int javaMajorVersion, final String line){
328+
if (line.startsWith("#") || line.matches("\\s*")) {
329+
// Skip comments and blank lines
330+
return Optional.empty();
331+
}
332+
final Matcher matcher = OPTION_DEFINITION.matcher(line);
333+
if (matcher.matches()) {
334+
final String start = matcher.group("start");
335+
final String end = matcher.group("end");
336+
if (start == null) {
337+
// no range present, unconditionally apply the JVM option
338+
return Optional.of(line);
339+
} else {
340+
final int lower = Integer.parseInt(start);
341+
final int upper;
342+
if (matcher.group("range") == null) {
343+
// no range is present, apply the JVM option to the specified major version only
344+
upper = lower;
345+
} else if (end == null) {
346+
// a range of the form \\d+- is present, apply the JVM option to all major versions larger than the specified one
347+
upper = Integer.MAX_VALUE;
293348
} else {
294-
final int lower;
295-
try {
296-
lower = Integer.parseInt(start);
297-
} catch (final NumberFormatException e) {
298-
result.appendError(lineNumber, line);
299-
continue;
300-
}
301-
final int upper;
302-
if (matcher.group("range") == null) {
303-
// no range is present, apply the JVM option to the specified major version only
304-
upper = lower;
305-
} else if (end == null) {
306-
// a range of the form \\d+- is present, apply the JVM option to all major versions larger than the specified one
307-
upper = Integer.MAX_VALUE;
308-
} else {
309-
// a range of the form \\d+-\\d+ is present, apply the JVM option to the specified range of major versions
310-
try {
311-
upper = Integer.parseInt(end);
312-
} catch (final NumberFormatException e) {
313-
result.appendError(lineNumber, line);
314-
continue;
315-
}
316-
if (upper < lower) {
317-
result.appendError(lineNumber, line);
318-
continue;
319-
}
320-
}
321-
if (lower <= javaMajorVersion && javaMajorVersion <= upper) {
322-
result.appendOption(matcher.group("option"));
349+
upper = Integer.parseInt(end);
350+
if (upper < lower) {
351+
throw new IllegalArgumentException("Upper bound must be greater than lower bound");
323352
}
324353
}
325-
} else {
326-
result.appendError(lineNumber, line);
354+
if (lower <= javaMajorVersion && javaMajorVersion <= upper) {
355+
return Optional.of(matcher.group("option"));
356+
}
327357
}
358+
} else {
359+
throw new IllegalArgumentException("Illegal JVM Option");
328360
}
329-
return result;
361+
return Optional.empty();
330362
}
331363

332364
private static final Pattern JAVA_VERSION = Pattern.compile("^(?:1\\.)?(?<javaMajorVersion>\\d+)(?:\\.\\d+)?$");
333365

334-
private int javaMajorVersion() {
366+
private static int javaMajorVersion() {
335367
final String specVersion = System.getProperty("java.specification.version");
336368
final Matcher specVersionMatcher = JAVA_VERSION.matcher(specVersion);
337369
if (!specVersionMatcher.matches()) {

logstash-core/src/test/java/org/logstash/launchers/JvmOptionsParserTest.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void test_LS_JAVA_OPTS_isUsedWhenNoJvmOptionsIsAvailable() throws IOExcep
4040

4141
// Verify
4242
final String output = outputStreamCaptor.toString();
43-
assertEquals("Output MUST contains the options present in LS_JAVA_OPTS", "-Xblabla" + System.lineSeparator(), output);
43+
assertTrue("Output MUST contains the options present in LS_JAVA_OPTS", output.contains("-Xblabla"));
4444
}
4545

4646
@SuppressWarnings({ "unchecked" })
@@ -89,6 +89,27 @@ public void testParseOptionVersionRange() throws IOException {
8989
assertTrue("No option match outside the range [10-11]", res.getJvmOptions().isEmpty());
9090
}
9191

92+
@Test
93+
public void testMandatoryJvmOptionApplicableJvmPresent() throws IOException{
94+
assertTrue("Contains add-exports value for Java 17",
95+
JvmOptionsParser.getMandatoryJvmOptions(17).contains("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"));
96+
}
97+
98+
@Test
99+
public void testMandatoryJvmOptionNonApplicableJvmNotPresent() throws IOException{
100+
assertFalse("Does not contains add-exports value for Java 11",
101+
JvmOptionsParser.getMandatoryJvmOptions(11).contains("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"));
102+
}
103+
104+
@Test
105+
public void testAlwaysMandatoryJvmPresent() {
106+
assertTrue("Contains regexp interruptible for Java 11",
107+
JvmOptionsParser.getMandatoryJvmOptions(11).contains("-Djruby.regexp.interruptible=true"));
108+
assertTrue("Contains regexp interruptible for Java 17",
109+
JvmOptionsParser.getMandatoryJvmOptions(17).contains("-Djruby.regexp.interruptible=true"));
110+
111+
}
112+
92113
@Test
93114
public void testErrorLinesAreReportedCorrectly() throws IOException {
94115
final String jvmOptionsContent = "10-11:-XX:+UseConcMarkSweepGC" + System.lineSeparator() +

0 commit comments

Comments
 (0)