Skip to content

Commit b1af65c

Browse files
committed
TS-45056 Rework
1 parent ce523d6 commit b1af65c

3 files changed

Lines changed: 109 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ We use [semantic versioning](http://semver.org/):
55
- PATCH version when you make backwards compatible bug fixes.
66

77
# Next version
8-
- [fix] _agent_: The profiled application no longer crashes when the profiler configuration is invalid (e.g., missing `teamscale-user`). Instead, the application starts normally without coverage collection. The error message now also lists which specific options are missing.
8+
- [fix] _agent_: The profiled application no longer crashes when the profiler configuration is invalid (e.g., missing `teamscale-user`). Instead, the application starts normally without coverage collection.
99
- [feature] _teamscale-gradle-plugin_: Annotated tasks with `@DisableCachingByDefault` where caching can't be applied
1010

1111
# 36.4.0

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

Lines changed: 60 additions & 8 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;
@@ -59,11 +69,27 @@ public class PreMain {
5969
private static final String ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "TEAMSCALE_ACCESS_TOKEN";
6070

6171
/**
62-
* Entry point for the agent, called by the JVM. If this method throws an exception, the JVM will abort the
63-
* entire application. Therefore, configuration errors must be handled gracefully by returning normally, which
64-
* allows the application to start without coverage collection.
72+
* 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.
6577
*/
66-
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 {
6793
if (System.getProperty(LOCKING_SYSTEM_PROPERTY) != null) {
6894
return;
6995
}
@@ -83,8 +109,8 @@ public static void premain(String options, Instrumentation instrumentation) thro
83109
environmentConfigFile);
84110
agentOptions = parseResult.getFirst();
85111

86-
// After parsing everything and configuring logging, we now
87-
// 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).
88114
for (Exception exception : parseResult.getSecond()) {
89115
throw exception;
90116
}
@@ -120,6 +146,29 @@ public static void premain(String options, Instrumentation instrumentation) thro
120146
agent.registerShutdownHook();
121147
}
122148

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

211-
/** 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+
*/
212264
static void closeLoggingResources() {
213265
if (loggingResources != null) {
214266
loggingResources.close();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.teamscale.jacoco.agent;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.lang.instrument.Instrumentation;
7+
8+
import static org.assertj.core.api.Assertions.assertThatCode;
9+
10+
/**
11+
* Verifies the cross-cutting design invariant documented on {@link PreMain}: a failure to start up the profiler
12+
* (misconfiguration, missing files, environment issues, ...) must never propagate an exception out of
13+
* {@link PreMain#premain(String, Instrumentation)}, because the JVM would turn that into an
14+
* {@code IllegalAgentException} and abort the profiled application.
15+
*/
16+
class PreMainTest {
17+
18+
/**
19+
* {@link PreMain#premain(String, Instrumentation)} sets a system property on its first invocation and early-returns
20+
* on every subsequent one. Clearing it before each test ensures every test actually exercises the startup path
21+
* instead of silently short-circuiting.
22+
*/
23+
@BeforeEach
24+
void resetPremainLock() {
25+
System.clearProperty("TEAMSCALE_JAVA_PROFILER_ATTACHED");
26+
}
27+
28+
/**
29+
* Invalid agent options produce an {@code AgentOptionParseException} internally. The agent must swallow it so the
30+
* profiled application keeps running.
31+
*/
32+
@Test
33+
void premainDoesNotThrowOnInvalidOptions() {
34+
assertThatCode(() -> PreMain.premain("this=is=not=a=valid=option,bogus-key=value", null))
35+
.doesNotThrowAnyException();
36+
}
37+
38+
/**
39+
* Regression test for TS-43260: a {@code config-id} without the required Teamscale credentials used to propagate an
40+
* {@code AgentOptionParseException} out of {@code premain} and abort the profiled application. It must now be
41+
* swallowed.
42+
*/
43+
@Test
44+
void premainDoesNotThrowOnConfigIdWithoutCredentials() {
45+
assertThatCode(() -> PreMain.premain("config-id=some-config", null))
46+
.doesNotThrowAnyException();
47+
}
48+
}

0 commit comments

Comments
 (0)