Skip to content

Commit c0d9835

Browse files
committed
TS-45056 Rework
1 parent fd1ac13 commit c0d9835

4 files changed

Lines changed: 98 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ We use [semantic versioning](http://semver.org/):
66

77
# Next version
88
- [fix] _agent_: Fixed `IllegalStateException: Can't add different class with same name` when application servers like JBoss perform a reload without full restart or when duplicate class files exist across multiple archives
9+
- [fix] _agent_: The profiler no longer aborts the profiled application when its options are malformed or startup fails for any other reason. The startup failure is logged and the application continues to run without coverage collection
910
- [feature] _teamscale-gradle-plugin_: Annotated tasks with `@DisableCachingByDefault` where caching can't be applied
1011

1112
# 36.4.0

agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@
3535

3636
import static com.teamscale.jacoco.agent.logging.LoggingUtils.getLoggerContext;
3737

38-
/** Container class for the premain entry point for the agent. */
38+
/**
39+
* Container class for the premain entry point for the agent.
40+
* <p>
41+
* <b>Design decision: the agent must never crash the profiled application.</b>
42+
* Even when the agent is misconfigured, startup must not propagate any exception out of
43+
* {@link #premain(String, Instrumentation)}. An exception escaping {@code premain} would be re-thrown by the JVM as
44+
* {@code IllegalAgentException}, which aborts startup of the profiled application. The profiler is an observational
45+
* tool, so a failure to start up the profiler must degrade to "no coverage is collected" rather than taking down the
46+
* application. All failure paths in {@code premain} are therefore logged and swallowed; see the top-level
47+
* {@code try}/{@code catch} in {@link #premain(String, Instrumentation)}.
48+
*/
3949
public class PreMain {
4050

4151
private static LoggingUtils.LoggingResources loggingResources = null;
@@ -60,8 +70,26 @@ public class PreMain {
6070

6171
/**
6272
* Entry point for the agent, called by the JVM.
73+
* <p>
74+
* This method never throws. Any exception raised during startup is logged (to the file log and, if configured,
75+
* to Teamscale) and swallowed, and the JVM continues without coverage collection. See the class-level Javadoc for
76+
* the rationale.
6377
*/
64-
public static void premain(String options, Instrumentation instrumentation) throws Exception {
78+
public static void premain(String options, Instrumentation instrumentation) {
79+
try {
80+
startProfiler(options, instrumentation);
81+
} catch (Throwable t) {
82+
// Top-level safety net: an exception escaping premain would be re-thrown by the JVM as
83+
// IllegalAgentException and abort the profiled application. See class-level Javadoc.
84+
logStartupFailure(t);
85+
}
86+
}
87+
88+
/**
89+
* Starts the profiler. Contains all logic that may legitimately throw during startup; every caller must route
90+
* through {@link #premain(String, Instrumentation)} which catches these exceptions.
91+
*/
92+
private static void startProfiler(String options, Instrumentation instrumentation) throws Exception {
6593
if (System.getProperty(LOCKING_SYSTEM_PROPERTY) != null) {
6694
return;
6795
}
@@ -81,8 +109,8 @@ public static void premain(String options, Instrumentation instrumentation) thro
81109
environmentConfigFile);
82110
agentOptions = parseResult.getFirst();
83111

84-
// After parsing everything and configuring logging, we now
85-
// can throw the caught exceptions.
112+
// After parsing everything and configuring logging, we now can throw the caught exceptions. They are
113+
// handled locally below and never propagate out of premain (see class-level Javadoc).
86114
for (Exception exception : parseResult.getSecond()) {
87115
throw exception;
88116
}
@@ -97,7 +125,9 @@ public static void premain(String options, Instrumentation instrumentation) thro
97125
agentOptions.configurationViaTeamscale.unregisterProfiler();
98126
}
99127

100-
throw e;
128+
// Previously rethrown. We now swallow the exception so a misconfigured agent cannot abort the profiled
129+
// application. The error has been logged above.
130+
return;
101131
} catch (AgentOptionReceiveException e) {
102132
// When Teamscale is not available, we don't want to fail hard to still allow for testing even if no
103133
// coverage is collected (see TS-33237)
@@ -118,6 +148,29 @@ public static void premain(String options, Instrumentation instrumentation) thro
118148
agent.registerShutdownHook();
119149
}
120150

151+
/**
152+
* Logs a startup failure on a best-effort basis. Must not throw: even logging failures are swallowed so that
153+
* {@link #premain(String, Instrumentation)} keeps its no-throw contract.
154+
*/
155+
private static void logStartupFailure(Throwable t) {
156+
try {
157+
LoggingUtils.getLogger(PreMain.class).error(
158+
"Teamscale Java Profiler failed to start up. The profiled application will continue to run "
159+
+ "without coverage collection.", t);
160+
} catch (Throwable loggingFailure) {
161+
// The logger may not be initialized yet or may itself be broken. Fall back to stderr so the failure is
162+
// at least visible somewhere, then give up silently.
163+
try {
164+
System.err.println(
165+
"[Teamscale Java Profiler] Failed to start up and will not collect coverage: " + t);
166+
loggingFailure.addSuppressed(t);
167+
loggingFailure.printStackTrace();
168+
} catch (Throwable ignored) {
169+
// Nothing we can do; the alternative is to crash the profiled application, which we must not do.
170+
}
171+
}
172+
}
173+
121174
private static Pair<AgentOptions, List<Exception>> getAndApplyAgentOptions(String options,
122175
String environmentConfigId,
123176
String environmentConfigFile) throws AgentOptionParseException, IOException, AgentOptionReceiveException {
@@ -204,9 +257,14 @@ private static void initializeLogging(AgentOptions agentOptions, DelayedLogger l
204257
}
205258
}
206259

207-
/** Closes the opened logging contexts. */
260+
/**
261+
* Closes the opened logging contexts. Safe to call before logging has been initialized, in which case this is a
262+
* no-op.
263+
*/
208264
static void closeLoggingResources() {
209-
loggingResources.close();
265+
if (loggingResources != null) {
266+
loggingResources.close();
267+
}
210268
}
211269

212270
/**
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.teamscale.jacoco.agent;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.lang.instrument.Instrumentation;
6+
7+
import static org.assertj.core.api.Assertions.assertThatCode;
8+
9+
/**
10+
* Verifies the cross-cutting design invariant documented on {@link PreMain}: a failure to start up the profiler
11+
* (misconfiguration, missing files, environment issues, ...) must never propagate an exception out of
12+
* {@link PreMain#premain(String, Instrumentation)}, because the JVM would turn that into an
13+
* {@code IllegalAgentException} and abort the profiled application.
14+
*/
15+
class PreMainTest {
16+
17+
/**
18+
* Invalid agent options produce an {@code AgentOptionParseException} internally. The agent must swallow it so the
19+
* profiled application keeps running.
20+
*/
21+
@Test
22+
void premainDoesNotThrowOnInvalidOptions() {
23+
assertThatCode(() -> PreMain.premain("this=is=not=a=valid=option,bogus-key=value", null))
24+
.doesNotThrowAnyException();
25+
}
26+
}

report-generator/src/main/kotlin/com/teamscale/report/EDuplicateClassFileBehavior.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ package com.teamscale.report
22

33
/**
44
* Behavior when two non-identical class files with the same package name are found.
5+
*
6+
* Note for use inside the agent: the agent must never crash the profiled application (see
7+
* `PreMain` class Javadoc). [FAIL] therefore only aborts the individual dump; `Agent.dumpReport`
8+
* catches the resulting exception and logs it. Outside the agent (for example, in the CLI
9+
* converter or the Gradle plugin) [FAIL] surfaces as an exception to the caller as usual.
510
*/
611
enum class EDuplicateClassFileBehavior {
712
/** Completely ignores it. */
@@ -10,6 +15,6 @@ enum class EDuplicateClassFileBehavior {
1015
/** Prints a warning to the logger. */
1116
WARN,
1217

13-
/** Fails and stops further processing. */
18+
/** Fails and aborts the current report generation. See class-level note on agent semantics. */
1419
FAIL
1520
}

0 commit comments

Comments
 (0)