Skip to content

Commit bce3722

Browse files
authored
Merge pull request #912 from cqse/ts/38628_kotlin_refactor
TS-38628 Codebase Cleanup
2 parents 90da159 + e65caea commit bce3722

47 files changed

Lines changed: 500 additions & 594 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

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

77
# Next version
8+
- [breaking] _report-generator_: `RevisionInfo` is now a sealed class with polymorphic Jackson serialization (`@JsonTypeInfo` / `@JsonSubTypes`). The JSON representation now uses "COMMIT" and "REVISION" as type discriminator values instead of the previous `ERevisionType` enum names.
89
- [feature] _agent_: Added official support for Java 26 and experimental support for Java 27 (via JaCoCo 0.8.15)
910

1011
# 36.5.2

agent/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ dependencies {
6060

6161
implementation(libs.jackson.databind)
6262
implementation(libs.jetbrains.annotations)
63+
implementation(libs.coroutines.core)
6364

6465
testImplementation(project(":tia-client"))
6566
testImplementation(libs.retrofit.converter.jackson)

agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBas
9999
override fun initResourceConfig(): ResourceConfig? {
100100
val resourceConfig = ResourceConfig()
101101
resourceConfig.property(ServerProperties.WADL_FEATURE_DISABLE, true.toString())
102-
AgentResource.setAgent(this)
103-
return resourceConfig.register(AgentResource::class.java).register(GenericExceptionMapper::class.java)
102+
return resourceConfig
103+
.register(AgentResource(this))
104+
.register(GenericExceptionMapper::class.java)
104105
}
105106

106107
override fun prepareShutdown() {

agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import javax.ws.rs.core.Response
88
* The resource of the Jersey + Jetty http server holding all the endpoints specific for the [Agent].
99
*/
1010
@Path("/")
11-
class AgentResource : ResourceBase() {
11+
class AgentResource(private val agent: Agent) : ResourceBase(agent) {
1212
/** Handles dumping a XML coverage report for coverage collected until now. */
1313
@POST
1414
@Path("/dump")
@@ -26,16 +26,4 @@ class AgentResource : ResourceBase() {
2626
agent.controller.reset()
2727
return Response.noContent().build()
2828
}
29-
30-
companion object {
31-
private lateinit var agent: Agent
32-
33-
/**
34-
* Static setter to inject the [Agent] to the resource.
35-
*/
36-
fun setAgent(agent: Agent) {
37-
Companion.agent = agent
38-
agentBase = agent
39-
}
40-
}
41-
}
29+
}

agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import java.lang.instrument.IllegalClassFormatException
99
import java.security.ProtectionDomain
1010

1111
/**
12-
* A class file transformer which delegates to the JaCoCo [org.jacoco.agent.rt.internal_bac9136.CoverageTransformer] to do the actual instrumentation,
12+
* A class file transformer which delegates to the JaCoCo [CoverageTransformer] to do the actual instrumentation,
1313
* but treats instrumentation errors e.g. due to unsupported class file versions more lenient by only logging them, but
1414
* not bailing out completely. Those unsupported classes will not be instrumented and will therefore not be contained in
1515
* the collected coverage report.
@@ -19,8 +19,7 @@ class LenientCoverageTransformer(
1919
options: AgentOptions,
2020
private val logger: Logger
2121
) : CoverageTransformer(
22-
runtime,
23-
options,
22+
runtime, options,
2423
// The coverage transformer only uses the logger to print an error when the instrumentation fails.
2524
// We want to show our more specific error message instead, so we only log this for debugging at trace.
2625
IExceptionLogger { logger.trace(it.message, it) }
@@ -48,4 +47,4 @@ class LenientCoverageTransformer(
4847
private fun getRootCauseMessage(e: Throwable): String? =
4948
e.cause?.let { getRootCauseMessage(it) } ?: e.message
5049
}
51-
}
50+
}

agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,10 @@ import javax.ws.rs.core.Response
1313
/**
1414
* The resource of the Jersey + Jetty http server holding all the endpoints specific for the [AgentBase].
1515
*/
16-
abstract class ResourceBase {
16+
abstract class ResourceBase(protected val agentBase: AgentBase) {
1717
/** The logger. */
1818
protected val logger: Logger = LoggingUtils.getLogger(this)
1919

20-
companion object {
21-
/**
22-
* The agentBase inject via [AgentResource.setAgent] or
23-
* [com.teamscale.jacoco.agent.testimpact.TestwiseCoverageResource.setAgent].
24-
*/
25-
@JvmStatic
26-
protected lateinit var agentBase: AgentBase
27-
}
28-
2920
@get:Path("/partition")
3021
@get:GET
3122
val partition: String
@@ -119,7 +110,7 @@ abstract class ResourceBase {
119110
/** Returns revision information for the Teamscale upload. */
120111
get() {
121112
val server = agentBase.options.teamscaleServer
122-
return RevisionInfo(server.commit, server.revision)
113+
return RevisionInfo.of(server.commit, server.revision)
123114
}
124115

125116
/**
@@ -131,4 +122,4 @@ abstract class ResourceBase {
131122
logger.error(message)
132123
throw BadRequestException(message)
133124
}
134-
}
125+
}

agent/src/main/kotlin/com/teamscale/jacoco/agent/commit_resolution/git_properties/GitPropertiesLocatingTransformer.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.teamscale.report.util.ClasspathWildcardIncludeFilter
55
import java.io.File
66
import java.lang.instrument.ClassFileTransformer
77
import java.security.ProtectionDomain
8+
import java.util.concurrent.ConcurrentHashMap
89
import java.util.concurrent.ConcurrentSkipListSet
910

1011
/**
@@ -17,6 +18,7 @@ class GitPropertiesLocatingTransformer(
1718
) : ClassFileTransformer {
1819
private val logger = getLogger(this)
1920
private val seenJars = ConcurrentSkipListSet<String>()
21+
private val classIncludedCache = ConcurrentHashMap<String, Boolean>()
2022

2123
override fun transform(
2224
classLoader: ClassLoader?,
@@ -30,8 +32,14 @@ class GitPropertiesLocatingTransformer(
3032
return null
3133
}
3234

33-
if (className.isNullOrEmpty() || !locationIncludeFilter.isIncluded(className)) {
34-
// only search in jar files of included classes
35+
if (className.isNullOrEmpty()) {
36+
return null
37+
}
38+
39+
val isIncluded = classIncludedCache.computeIfAbsent(className) {
40+
locationIncludeFilter.isIncluded(className)
41+
}
42+
if (!isIncluded) {
3543
return null
3644
}
3745

agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt

Lines changed: 88 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,132 +8,108 @@ import ch.qos.logback.core.status.ErrorStatus
88
import com.teamscale.client.ITeamscaleService
99
import com.teamscale.client.ProfilerLogEntry
1010
import com.teamscale.jacoco.agent.options.AgentOptions
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.Job
14+
import kotlinx.coroutines.SupervisorJob
15+
import kotlinx.coroutines.cancel
16+
import kotlinx.coroutines.channels.BufferOverflow
17+
import kotlinx.coroutines.channels.Channel
18+
import kotlinx.coroutines.currentCoroutineContext
19+
import kotlinx.coroutines.isActive
20+
import kotlinx.coroutines.launch
21+
import kotlinx.coroutines.runBlocking
22+
import kotlinx.coroutines.time.delay
23+
import kotlinx.coroutines.time.withTimeoutOrNull
1124
import java.net.ConnectException
1225
import java.time.Duration
13-
import java.util.*
14-
import java.util.concurrent.CompletableFuture
15-
import java.util.concurrent.Executors
16-
import java.util.concurrent.ScheduledExecutorService
17-
import java.util.concurrent.TimeUnit
18-
import java.util.concurrent.atomic.AtomicBoolean
19-
import java.util.function.BiConsumer
2026

2127
/**
22-
* Custom log appender that sends logs to Teamscale; it buffers log that were not sent due to connection issues and
23-
* sends them later.
28+
* Custom log appender that sends logs to Teamscale; it buffers logs that were not sent due to connection issues and
29+
* sends them later. Uses a [Channel] as a lock-free producer-consumer buffer with a coroutine collector for batching.
2430
*/
2531
class LogToTeamscaleAppender : AppenderBase<ILoggingEvent>() {
26-
/** The unique ID of the profiler */
32+
/** The unique ID of the profiler. */
2733
private var profilerId: String? = null
2834

29-
/**
30-
* Buffer for unsent logs. We use a set here to allow for removing entries fast after sending them to Teamscale was
31-
* successful.
32-
*/
33-
private val logBuffer = LinkedHashSet<ProfilerLogEntry>()
34-
35-
/** Scheduler for sending logs after the configured time interval */
36-
private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1) { r ->
37-
// Make the thread a daemon so that it does not prevent the JVM from terminating.
38-
val t = Executors.defaultThreadFactory().newThread(r)
39-
t.setDaemon(true)
40-
t
41-
}
35+
/** Lock-free channel for log entries. [Channel.trySend] is called from Logback threads, [Channel.receive] from the collector coroutine. */
36+
private val logChannel = Channel<ProfilerLogEntry>(capacity = BUFFER_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST)
4237

43-
/** Active log flushing threads */
44-
private val activeLogFlushes: MutableSet<CompletableFuture<Void>> =
45-
Collections.newSetFromMap(IdentityHashMap())
38+
/** Structured concurrency scope backing the collector coroutine. */
39+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
4640

47-
/** Is there a flush going on right now? */
48-
private val isFlusing = AtomicBoolean(false)
41+
/** The collector coroutine job, tracked so [stop] can wait for it to finish. */
42+
private var collectorJob: Job? = null
4943

5044
override fun start() {
5145
super.start()
52-
scheduler.scheduleAtFixedRate({
53-
synchronized(activeLogFlushes) {
54-
activeLogFlushes.removeIf { it.isDone }
55-
if (activeLogFlushes.isEmpty()) flush()
56-
}
57-
}, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS)
46+
collectorJob = scope.launch { collectAndSend() }
5847
}
5948

6049
override fun append(eventObject: ILoggingEvent) {
61-
synchronized(logBuffer) {
62-
logBuffer.add(formatLog(eventObject))
63-
if (logBuffer.size >= BATCH_SIZE) flush()
64-
}
50+
logChannel.trySend(formatLog(eventObject))
6551
}
6652

67-
private fun formatLog(eventObject: ILoggingEvent): ProfilerLogEntry {
68-
val trace = LoggingUtils.getStackTraceFromEvent(eventObject)
69-
val timestamp = eventObject.timeStamp
70-
val message = eventObject.formattedMessage
71-
val severity = eventObject.level.toString()
72-
return ProfilerLogEntry(timestamp, message, trace, severity)
73-
}
53+
private fun formatLog(eventObject: ILoggingEvent) = ProfilerLogEntry(
54+
eventObject.timeStamp,
55+
eventObject.formattedMessage,
56+
LoggingUtils.getStackTraceFromEvent(eventObject),
57+
eventObject.level.toString()
58+
)
7459

75-
private fun flush() {
76-
sendLogs()
77-
}
60+
/**
61+
* Collector coroutine: drains [logChannel] into batches, sends them to Teamscale, and retries with backoff on
62+
* failure.
63+
*/
64+
private suspend fun collectAndSend() {
65+
val batch = mutableListOf<ProfilerLogEntry>()
66+
67+
while (currentCoroutineContext().isActive) {
68+
if (batch.isEmpty()) {
69+
val receiveResult = withTimeoutOrNull(FLUSH_INTERVAL) {
70+
logChannel.receiveCatching()
71+
} ?: continue
72+
val entry = receiveResult.getOrNull() ?: break
73+
batch.add(entry)
74+
}
7875

79-
/** Send logs in a separate thread */
80-
private fun sendLogs() {
81-
synchronized(activeLogFlushes) {
82-
activeLogFlushes.add(CompletableFuture.runAsync {
83-
if (isFlusing.compareAndSet(false, true)) {
84-
try {
85-
val client = teamscaleClient ?: return@runAsync // There might be no connection configured.
86-
87-
val logsToSend: MutableList<ProfilerLogEntry>
88-
synchronized(logBuffer) {
89-
logsToSend = logBuffer.toMutableList()
90-
}
91-
92-
val call = client.postProfilerLog(profilerId!!, logsToSend)
93-
val response = call.execute()
94-
check(response.isSuccessful) { "Failed to send log: HTTP error code : ${response.code()}" }
95-
96-
synchronized(logBuffer) {
97-
// Removing the logs that have been sent after the fact.
98-
// This handles problems with lost network connections.
99-
logBuffer.removeAll(logsToSend.toSet())
100-
}
101-
} catch (e: Exception) {
102-
// We do not report on exceptions here.
103-
if (e !is ConnectException) {
104-
addStatus(ErrorStatus("Sending logs to Teamscale failed: ${e.message}", this, e))
105-
}
106-
} finally {
107-
isFlusing.set(false)
108-
}
109-
}
110-
}.whenComplete(BiConsumer { _, _ ->
111-
synchronized(activeLogFlushes) {
112-
activeLogFlushes.removeIf { it.isDone }
76+
while (batch.size < BATCH_SIZE) {
77+
logChannel.tryReceive().getOrNull()?.let { batch.add(it) } ?: break
78+
}
79+
80+
if (batch.isNotEmpty()) {
81+
if (sendBatch(batch)) {
82+
batch.clear()
83+
} else {
84+
delay(RETRY_BACKOFF)
11385
}
114-
}))
86+
}
11587
}
11688
}
11789

118-
override fun stop() {
119-
// Already flush here once to make sure that we do not miss too much.
120-
flush()
121-
122-
scheduler.shutdown()
123-
try {
124-
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
125-
scheduler.shutdownNow()
90+
/**
91+
* Posts the given [batch] to Teamscale.
92+
*
93+
* @return `true` if the batch was sent successfully, `false` if it should be retried.
94+
*/
95+
private fun sendBatch(batch: List<ProfilerLogEntry>): Boolean {
96+
val client = teamscaleClient ?: return true
97+
return try {
98+
val response = client.postProfilerLog(profilerId!!, batch).execute()
99+
check(response.isSuccessful) { "Failed to send log: HTTP error code : ${response.code()}" }
100+
true
101+
} catch (e: Exception) {
102+
if (e !is ConnectException) {
103+
addStatus(ErrorStatus("Sending logs to Teamscale failed: ${e.message}", this, e))
126104
}
127-
} catch (_: InterruptedException) {
128-
scheduler.shutdownNow()
105+
false
129106
}
107+
}
130108

131-
// A final flush after the scheduler has been shut down.
132-
flush()
133-
134-
// Block until all flushes are done
135-
CompletableFuture.allOf(*activeLogFlushes.toTypedArray()).join()
136-
109+
override fun stop() {
110+
logChannel.close()
111+
runBlocking { withTimeoutOrNull(SHUTDOWN_TIMEOUT) { collectorJob?.join() } }
112+
scope.cancel()
137113
super.stop()
138114
}
139115

@@ -146,13 +122,22 @@ class LogToTeamscaleAppender : AppenderBase<ILoggingEvent>() {
146122
}
147123

148124
companion object {
149-
/** Flush the logs after N elements are in the queue */
125+
/** Maximum number of log entries held in memory. Older entries are dropped on overflow. */
126+
private const val BUFFER_CAPACITY = 10_000
127+
128+
/** Flush the logs after N elements are in the queue. */
150129
private const val BATCH_SIZE = 50
151130

152-
/** Flush the logs in the given time interval */
131+
/** Flush the logs in the given time interval. */
153132
private val FLUSH_INTERVAL: Duration = Duration.ofSeconds(3)
154133

155-
/** The service client for sending logs to Teamscale */
134+
/** Backoff duration before retrying a failed batch. */
135+
private val RETRY_BACKOFF: Duration = Duration.ofSeconds(5)
136+
137+
/** Maximum time to wait for the collector to drain during shutdown. */
138+
private val SHUTDOWN_TIMEOUT: Duration = Duration.ofSeconds(3)
139+
140+
/** The service client for sending logs to Teamscale. */
156141
private var teamscaleClient: ITeamscaleService? = null
157142

158143
/**
@@ -178,4 +163,4 @@ class LogToTeamscaleAppender : AppenderBase<ILoggingEvent>() {
178163
return true
179164
}
180165
}
181-
}
166+
}

0 commit comments

Comments
 (0)