Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upstream processed exec implementation #20

Merged
merged 11 commits into from
Jun 15, 2023
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