/
JvmOptionsParser.java
357 lines (325 loc) · 15.1 KB
/
JvmOptionsParser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.server.cli;
import org.elasticsearch.bootstrap.ServerArgs;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.ProcessInfo;
import org.elasticsearch.cli.UserException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* Parses JVM options from a file and prints a single line with all JVM options to standard output.
*/
public final class JvmOptionsParser {
static class JvmOptionsFileParserException extends Exception {
private final Path jvmOptionsFile;
Path jvmOptionsFile() {
return jvmOptionsFile;
}
private final SortedMap<Integer, String> invalidLines;
SortedMap<Integer, String> invalidLines() {
return invalidLines;
}
JvmOptionsFileParserException(final Path jvmOptionsFile, final SortedMap<Integer, String> invalidLines) {
this.jvmOptionsFile = jvmOptionsFile;
this.invalidLines = invalidLines;
}
}
/**
* Determines the jvm options that should be passed to the Elasticsearch Java process.
*
* <p> This method works by joining the options found in {@link SystemJvmOptions}, the {@code jvm.options} file,
* files in the {@code jvm.options.d} directory, and the options given by the {@code ES_JAVA_OPTS} environment
* variable.
*
* @param args the start-up arguments
* @param processInfo information about the CLI process.
* @param tmpDir the directory that should be passed to {@code -Djava.io.tmpdir}
* @param machineDependentHeap the heap configurator to use
* @return the list of options to put on the Java command line
* @throws InterruptedException if the java subprocess is interrupted
* @throws IOException if there is a problem reading any of the files
* @throws UserException if there is a problem parsing the `jvm.options` file or `jvm.options.d` files
*/
public static List<String> determineJvmOptions(
ServerArgs args,
ProcessInfo processInfo,
Path tmpDir,
MachineDependentHeap machineDependentHeap
) throws InterruptedException, IOException, UserException {
final JvmOptionsParser parser = new JvmOptionsParser();
final Map<String, String> substitutions = new HashMap<>();
substitutions.put("ES_TMPDIR", tmpDir.toString());
substitutions.put("ES_PATH_CONF", args.configDir().toString());
final String envOptions = processInfo.envVars().get("ES_JAVA_OPTS");
try {
return Collections.unmodifiableList(
parser.jvmOptions(args, args.configDir(), tmpDir, envOptions, substitutions, processInfo.sysprops(), machineDependentHeap)
);
} catch (final JvmOptionsFileParserException e) {
final String errorMessage = String.format(
Locale.ROOT,
"encountered [%d] error%s parsing [%s]%s",
e.invalidLines().size(),
e.invalidLines().size() == 1 ? "" : "s",
e.jvmOptionsFile(),
System.lineSeparator()
);
StringBuilder msg = new StringBuilder(errorMessage);
int count = 0;
for (final Map.Entry<Integer, String> entry : e.invalidLines().entrySet()) {
count++;
final String message = String.format(
Locale.ROOT,
"[%d]: encountered improperly formatted JVM option in [%s] on line number [%d]: [%s]%s",
count,
e.jvmOptionsFile(),
entry.getKey(),
entry.getValue(),
System.lineSeparator()
);
msg.append(message);
}
throw new UserException(ExitCodes.CONFIG, msg.toString());
}
}
private List<String> jvmOptions(
ServerArgs args,
final Path config,
Path tmpDir,
final String esJavaOpts,
final Map<String, String> substitutions,
final Map<String, String> cliSysprops,
final MachineDependentHeap machineDependentHeap
) throws InterruptedException, IOException, JvmOptionsFileParserException, UserException {
final List<String> jvmOptions = readJvmOptionsFiles(config);
if (esJavaOpts != null) {
jvmOptions.addAll(Arrays.stream(esJavaOpts.split("\\s+")).filter(Predicate.not(String::isBlank)).toList());
}
final List<String> substitutedJvmOptions = substitutePlaceholders(jvmOptions, Collections.unmodifiableMap(substitutions));
final SystemMemoryInfo memoryInfo = new OverridableSystemMemoryInfo(substitutedJvmOptions, new DefaultSystemMemoryInfo());
substitutedJvmOptions.addAll(machineDependentHeap.determineHeapSettings(args.nodeSettings(), memoryInfo, substitutedJvmOptions));
final List<String> ergonomicJvmOptions = JvmErgonomics.choose(substitutedJvmOptions, args.nodeSettings());
final List<String> systemJvmOptions = SystemJvmOptions.systemJvmOptions(args.nodeSettings(), cliSysprops);
final List<String> apmOptions = APMJvmOptions.apmJvmOptions(args.nodeSettings(), args.secrets(), args.logsDir(), tmpDir);
final List<String> finalJvmOptions = new ArrayList<>(
systemJvmOptions.size() + substitutedJvmOptions.size() + ergonomicJvmOptions.size() + apmOptions.size()
);
finalJvmOptions.addAll(systemJvmOptions); // add the system JVM options first so that they can be overridden
finalJvmOptions.addAll(substitutedJvmOptions);
finalJvmOptions.addAll(ergonomicJvmOptions);
finalJvmOptions.addAll(apmOptions);
return finalJvmOptions;
}
List<String> readJvmOptionsFiles(final Path config) throws IOException, JvmOptionsFileParserException {
final ArrayList<Path> jvmOptionsFiles = new ArrayList<>();
jvmOptionsFiles.add(config.resolve("jvm.options"));
final Path jvmOptionsDirectory = config.resolve("jvm.options.d");
if (Files.isDirectory(jvmOptionsDirectory)) {
try (DirectoryStream<Path> jvmOptionsDirectoryStream = Files.newDirectoryStream(config.resolve("jvm.options.d"), "*.options")) {
// collect the matching JVM options files after sorting them by Path::compareTo
StreamSupport.stream(jvmOptionsDirectoryStream.spliterator(), false).sorted().forEach(jvmOptionsFiles::add);
}
}
final List<String> jvmOptions = new ArrayList<>();
for (final Path jvmOptionsFile : jvmOptionsFiles) {
final SortedMap<Integer, String> invalidLines = new TreeMap<>();
try (
InputStream is = Files.newInputStream(jvmOptionsFile);
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(reader)
) {
parse(Runtime.version().feature(), br, jvmOptions::add, invalidLines::put);
}
if (invalidLines.isEmpty() == false) {
throw new JvmOptionsFileParserException(jvmOptionsFile, invalidLines);
}
}
return jvmOptions;
}
static List<String> substitutePlaceholders(final List<String> jvmOptions, final Map<String, String> substitutions) {
final Map<String, String> placeholderSubstitutions = substitutions.entrySet()
.stream()
.collect(Collectors.toMap(e -> "${" + e.getKey() + "}", Map.Entry::getValue));
return jvmOptions.stream().map(jvmOption -> {
String actualJvmOption = jvmOption;
int start = jvmOption.indexOf("${");
if (start >= 0 && jvmOption.indexOf('}', start) > 0) {
for (final Map.Entry<String, String> placeholderSubstitution : placeholderSubstitutions.entrySet()) {
actualJvmOption = actualJvmOption.replace(placeholderSubstitution.getKey(), placeholderSubstitution.getValue());
}
}
return actualJvmOption;
}).collect(Collectors.toList());
}
/**
* Callback for valid JVM options.
*/
interface JvmOptionConsumer {
/**
* Invoked when a line in the JVM options file matches the specified syntax and the specified major version.
* @param jvmOption the matching JVM option
*/
void accept(String jvmOption);
}
/**
* Callback for invalid lines in the JVM options.
*/
interface InvalidLineConsumer {
/**
* Invoked when a line in the JVM options does not match the specified syntax.
*/
void accept(int lineNumber, String line);
}
private static final Pattern PATTERN = Pattern.compile("((?<start>\\d+)(?<range>-)?(?<end>\\d+)?:)?(?<option>-.*)$");
/**
* Parse the line-delimited JVM options from the specified buffered reader for the specified Java major version.
* Valid JVM options are:
* <ul>
* <li>
* a line starting with a dash is treated as a JVM option that applies to all versions
* </li>
* <li>
* a line starting with a number followed by a colon is treated as a JVM option that applies to the matching Java major version
* only
* </li>
* <li>
* a line starting with a number followed by a dash followed by a colon is treated as a JVM option that applies to the matching
* Java specified major version and all larger Java major versions
* </li>
* <li>
* a line starting with a number followed by a dash followed by a number followed by a colon is treated as a JVM option that
* applies to the specified range of matching Java major versions
* </li>
* </ul>
*
* For example, if the specified Java major version is 17, the following JVM options will be accepted:
* <ul>
* <li>
* {@code -XX:+PrintGCDateStamps}
* </li>
* <li>
* {@code 17:-XX:+PrintGCDateStamps}
* </li>
* <li>
* {@code 17-:-XX:+PrintGCDateStamps}
* </li>
* <li>
* {@code 17-18:-XX:+PrintGCDateStamps}
* </li>
* </ul>
* and the following JVM options will not be accepted:
* <ul>
* <li>
* {@code 18:-Xlog:age*=trace,gc*,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m}
* </li>
* <li>
* {@code 18-:-Xlog:age*=trace,gc*,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m}
* </li>
* <li>
* {@code 18-19:-Xlog:age*=trace,gc*,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m}
* </li>
* </ul>
*
* If the version syntax specified on a line matches the specified JVM options, the JVM option callback will be invoked with the JVM
* option. If the line does not match the specified syntax for the JVM options, the invalid line callback will be invoked with the
* contents of the entire line.
*
* @param javaMajorVersion the Java major version to match JVM options against
* @param br the buffered reader to read line-delimited JVM options from
* @param jvmOptionConsumer the callback that accepts matching JVM options
* @param invalidLineConsumer a callback that accepts invalid JVM options
* @throws IOException if an I/O exception occurs reading from the buffered reader
*/
static void parse(
final int javaMajorVersion,
final BufferedReader br,
final JvmOptionConsumer jvmOptionConsumer,
final InvalidLineConsumer invalidLineConsumer
) throws IOException {
int lineNumber = 0;
while (true) {
final String line = br.readLine();
lineNumber++;
if (line == null) {
break;
}
if (line.startsWith("#")) {
// lines beginning with "#" are treated as comments
continue;
}
if (line.matches("\\s*")) {
// skip blank lines
continue;
}
final Matcher matcher = PATTERN.matcher(line);
if (matcher.matches()) {
final String start = matcher.group("start");
final String end = matcher.group("end");
if (start == null) {
// no range present, unconditionally apply the JVM option
jvmOptionConsumer.accept(line);
} else {
final int lower;
try {
lower = Integer.parseInt(start);
} catch (final NumberFormatException e) {
invalidLineConsumer.accept(lineNumber, line);
continue;
}
final int upper;
if (matcher.group("range") == null) {
// no range is present, apply the JVM option to the specified major version only
upper = lower;
} else if (end == null) {
// a range of the form \\d+- is present, apply the JVM option to all major versions larger than the specified one
upper = Integer.MAX_VALUE;
} else {
// a range of the form \\d+-\\d+ is present, apply the JVM option to the specified range of major versions
try {
upper = Integer.parseInt(end);
} catch (final NumberFormatException e) {
invalidLineConsumer.accept(lineNumber, line);
continue;
}
if (upper < lower) {
invalidLineConsumer.accept(lineNumber, line);
continue;
}
}
if (lower <= javaMajorVersion && javaMajorVersion <= upper) {
jvmOptionConsumer.accept(matcher.group("option"));
}
}
} else {
invalidLineConsumer.accept(lineNumber, line);
}
}
}
}