Skip to content

Commit

Permalink
Upstream processed exec implementation (#20)
Browse files Browse the repository at this point in the history
* Switch to Path for projectDirOption

* Initial port of processed exec

* Make issues fully configurable

* Standardize envs a bit

* Comment a bug

* Make JSON more canonical

* Add a bugsnag artifact upload failures

* Add build scan server to config + test

* Fix exit code not falling through

* Use duration in message

* Spotless
  • Loading branch information
ZacSweers committed Jun 15, 2023
1 parent 7b5bd0e commit e39d674
Show file tree
Hide file tree
Showing 15 changed files with 958 additions and 5 deletions.
33 changes: 33 additions & 0 deletions api/kotlin-cli-util.api
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,36 @@ public final class slack/cli/Toml {
public final fun parseVersion (Ljava/io/File;)Ljava/util/Map;
}

public final class slack/cli/exec/IssueJsonAdapter : com/squareup/moshi/JsonAdapter {
public fun <init> (Lcom/squareup/moshi/Moshi;)V
public fun fromJson (Lcom/squareup/moshi/JsonReader;)Ljava/lang/Object;
public fun toJson (Lcom/squareup/moshi/JsonWriter;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String;
}

public final class slack/cli/exec/ProcessedExecCli : com/github/ajalt/clikt/core/CliktCommand {
public fun <init> ()V
public fun run ()V
}

public final class slack/cli/exec/ProcessedExecConfigJsonAdapter : com/squareup/moshi/JsonAdapter {
public fun <init> (Lcom/squareup/moshi/Moshi;)V
public fun fromJson (Lcom/squareup/moshi/JsonReader;)Ljava/lang/Object;
public fun toJson (Lcom/squareup/moshi/JsonWriter;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String;
}

public final class slack/cli/exec/RetrySignalJsonAdapter : com/squareup/moshi/JsonAdapter {
public fun <init> (Lcom/squareup/moshi/Moshi;)V
public fun fromJson (Lcom/squareup/moshi/JsonReader;)Ljava/lang/Object;
public fun toJson (Lcom/squareup/moshi/JsonWriter;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String;
}

public final class slack/cli/exec/RetrySignal_RetryDelayedJsonAdapter : com/squareup/moshi/JsonAdapter {
public fun <init> (Lcom/squareup/moshi/Moshi;)V
public fun fromJson (Lcom/squareup/moshi/JsonReader;)Ljava/lang/Object;
public fun toJson (Lcom/squareup/moshi/JsonWriter;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String;
}

10 changes: 10 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ plugins {
alias(libs.plugins.mavenPublish)
alias(libs.plugins.spotless)
alias(libs.plugins.binaryCompatibilityValidator)
alias(libs.plugins.moshix)
}

spotless {
Expand Down Expand Up @@ -77,10 +78,19 @@ tasks.withType<DokkaTask>().configureEach {

kotlin { explicitApi() }

moshi {
enableSealed.set(true)
generateProguardRules.set(false)
}

dependencies {
api(libs.clikt)
implementation(libs.kotlinShell)
implementation(libs.okio)
implementation(libs.okhttp)
implementation(libs.bugsnag)
implementation(libs.moshi)
implementation(libs.kotlin.reflect)
testImplementation(libs.junit)
testImplementation(libs.truth)
}
7 changes: 7 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@
kotlin = "1.8.22"
ktfmt = "0.43"
jvmTarget = "17"
moshix = "0.22.1"
moshi = "1.15.0"

[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.0" }
dokka = { id = "org.jetbrains.dokka", version = "1.8.20" }
lint = { id = "com.android.lint", version = "8.0.2" }
mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.25.2" }
moshix = { id = "dev.zacsweers.moshix", version.ref = "moshix" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
spotless = { id = "com.diffplug.spotless", version = "6.19.0" }
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.2" }

[libraries]
bugsnag = "com.bugsnag:bugsnag:3.6.4"
clikt = "com.github.ajalt.clikt:clikt:3.5.2"
kotlinShell = "eu.jrie.jetbrains:kotlin-shell-core:0.2.1"
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
okhttp = "com.squareup.okhttp3:okhttp:4.11.0"
okio = "com.squareup.okio:okio:3.3.0"
junit = "junit:junit:4.13.2"
truth = "com.google.truth:truth:1.1.4"
10 changes: 5 additions & 5 deletions src/main/kotlin/slack/cli/CliktExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import com.github.ajalt.clikt.parameters.options.OptionDelegate
import com.github.ajalt.clikt.parameters.options.defaultLazy
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.file
import java.io.File
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path
import java.nio.file.Paths

/** A dry run option for [clikt commands][CliktCommand]. */
Expand All @@ -41,7 +41,7 @@ public fun CliktCommand.dryRunOption(
public fun CliktCommand.projectDirOption(
vararg names: String = arrayOf("--project-dir"),
help: String = "The project directory. Defaults to the current working directory."
): OptionDelegate<File> =
option(names = names, help = help).file(mustExist = true, canBeFile = false).defaultLazy {
Paths.get("").toFile().canonicalFile
): OptionDelegate<Path> =
option(names = names, help = help).path(mustExist = true, canBeFile = false).defaultLazy {
Paths.get("").toAbsolutePath()
}
65 changes: 65 additions & 0 deletions src/main/kotlin/slack/cli/exec/Issue.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package slack.cli.exec

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* An issue that can be reported to Bugsnag.
*
* @property message the message shown in the bugsnag report message. Should be human-readable.
* @property logMessage the message shown in the CI log when [matchingText] is found. Should be
* human-readable.
* @property matchingText the matching text to look for in the log.
* @property groupingHash grouping hash for reporting to bugsnag. This should usually be unique, but
* can also be reused across issues that are part of the same general issue.
*/
@JsonClass(generateAdapter = true)
internal data class Issue(
val message: String,
@Json(name = "log_message") val logMessage: String,
@Json(name = "matching_text") val matchingText: String,
@Json(name = "grouping_hash") val groupingHash: String,
@Json(name = "retry_signal") val retrySignal: RetrySignal
) {

private fun List<String>.checkContains(errorText: String): Boolean {
return any { it.contains(errorText, ignoreCase = true) }
}

/** Checks the log for this issue and returns a [RetrySignal] if it should be retried. */
fun check(lines: List<String>, log: (String) -> Unit): RetrySignal {
return if (lines.checkContains(matchingText)) {
log(logMessage)
retrySignal
} else {
RetrySignal.Unknown
}
}
}

/**
* Base class for an issue that can be reported to Bugsnag. This is a [Throwable] for BugSnag
* purposes but doesn't fill in a stacktrace.
*/
internal class IssueThrowable(issue: Issue) : Throwable(issue.message) {

override fun fillInStackTrace(): Throwable {
// Do nothing, the stacktrace isn't relevant for these!
return this
}
}
97 changes: 97 additions & 0 deletions src/main/kotlin/slack/cli/exec/KnownIssues.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2023 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package slack.cli.exec

import kotlin.time.Duration.Companion.minutes

private const val OOM_GROUPING_HASH = "oom"

/** A set of known issues. */
@Suppress("unused") // We look these up reflectively at runtime
internal object KnownIssues {
// A simple fake checker for testing this script
val fakeFailure =
Issue(
message = "Fake failure",
logMessage = "Detected fake failure. Beep boop.",
matchingText = "FAKE FAILURE NOT REAL",
groupingHash = "fake-failure",
retrySignal = RetrySignal.Ack
)

val ftlRateLimit =
Issue(
message = "FTL rate limit",
matchingText = "429 Too Many Requests",
logMessage = "Detected FTL rate limit. Retrying in 1 minute.",
groupingHash = "ftl-rate-limit",
retrySignal = RetrySignal.RetryDelayed(1.minutes)
)

val oom =
Issue(
message = "Generic OOM",
matchingText = "Java heap space",
logMessage = "Detected OOM. Retrying immediately.",
groupingHash = OOM_GROUPING_HASH,
retrySignal = RetrySignal.RetryImmediately
)

val ftlInfrastructureFailure =
Issue(
message = "Inconclusive FTL infrastructure failure",
matchingText = "Infrastructure failure",
logMessage = "Detected inconclusive FTL infrastructure failure. Retrying immediately.",
groupingHash = "ftl-infrastructure-failure",
retrySignal = RetrySignal.RetryImmediately
)

val flankTimeout =
Issue(
message = "Flank timeout",
groupingHash = "flank-timeout",
matchingText = "Canceling flank due to timeout",
logMessage = "Detected a flank timeout. Retrying immediately.",
retrySignal = RetrySignal.RetryImmediately
)

val r8Oom =
Issue(
message = "R8 OOM",
matchingText = "Out of space in CodeCache",
logMessage = "Detected a OOM in R8. Retrying immediately.",
groupingHash = OOM_GROUPING_HASH,
retrySignal = RetrySignal.RetryImmediately
)

val oomKilledByKernel =
Issue(
message = "OOM killed by kernel",
groupingHash = OOM_GROUPING_HASH,
matchingText = "Gradle build daemon disappeared unexpectedly",
logMessage = "Detected a OOM that was killed by the kernel. Retrying immediately.",
retrySignal = RetrySignal.RetryImmediately
)

val bugsnagUploadFailed =
Issue(
message = "Bugsnag artifact upload failure",
groupingHash = "bugsnag-upload-failure",
matchingText = "Bugsnag request failed to complete",
logMessage = "Detected bugsnag failed to upload. Retrying immediately.",
retrySignal = RetrySignal.RetryImmediately
)
}
88 changes: 88 additions & 0 deletions src/main/kotlin/slack/cli/exec/OkHttpSyncHttpDelivery.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2023 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package slack.cli.exec

import com.bugsnag.delivery.HttpDelivery
import com.bugsnag.serialization.Serializer
import java.io.IOException
import java.net.Proxy
import java.util.concurrent.TimeUnit
import okhttp3.Authenticator
import okhttp3.Headers.Companion.toHeaders
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okio.BufferedSink
import org.slf4j.LoggerFactory

/** An [OkHttpClient]-based implementation of Bugsnag's [HttpDelivery]. */
internal object OkHttpSyncHttpDelivery : HttpDelivery {
private val LOGGER = LoggerFactory.getLogger(OkHttpSyncHttpDelivery::class.java)
private const val DEFAULT_TIMEOUT = 5000L
private const val ENDPOINT = "https://notify.bugsnag.com"

override fun deliver(serializer: Serializer, payload: Any, headers: Map<String, String>) {
val client =
OkHttpClient.Builder()
.callTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
.proxyAuthenticator(Authenticator.JAVA_NET_AUTHENTICATOR)
.build()
val request =
Request.Builder()
.url(ENDPOINT)
.headers(headers.toHeaders())
.post(
object : RequestBody() {
override fun contentType() = "application/json".toMediaType()

override fun writeTo(sink: BufferedSink) {
sink.outputStream().use { serializer.writeToStream(it, payload) }
}
}
)
.build()

try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
LOGGER.warn(
"Error not reported to Bugsnag - got non-200 response code: {}",
response.code
)
}
}
} catch (ex: IOException) {
LOGGER.warn("Error not reported to Bugsnag - exception when making request", ex)
}
}

override fun close() {
// Nothing to do here.
}

override fun setEndpoint(endpoint: String) {
// Unsupported here
}

override fun setTimeout(timeout: Int) {
// Unsupported here
}

override fun setProxy(proxy: Proxy) {
// Unsupported here
}
}
Loading

0 comments on commit e39d674

Please sign in to comment.