Skip to content

Commit aa7bb9f

Browse files
committed
Ref #34841: add cpu and memory limit/request for docker
1 parent cf43465 commit aa7bb9f

6 files changed

Lines changed: 100 additions & 8 deletions

File tree

src/main/kotlin/eu/openanalytics/shinyproxyoperator/controller/ShinyProxyController.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import eu.openanalytics.shinyproxyoperator.event.ShinyProxyEventType
2929
import eu.openanalytics.shinyproxyoperator.logPrefix
3030
import eu.openanalytics.shinyproxyoperator.model.ShinyProxy
3131
import eu.openanalytics.shinyproxyoperator.model.ShinyProxyInstance
32+
import eu.openanalytics.shinyproxyoperator.prettyMessage
3233
import io.fabric8.kubernetes.client.KubernetesClientException
3334
import io.github.oshai.kotlinlogging.KotlinLogging
3435
import kotlinx.coroutines.CancellationException
@@ -242,11 +243,11 @@ class ShinyProxyController(
242243
if (e.status != null && e.status.message != null) {
243244
eventController.createInstanceFailed(shinyProxyInstance, "KubernetesClientException: " + e.status.message)
244245
} else {
245-
eventController.createInstanceFailed(shinyProxyInstance, e.message)
246+
eventController.createInstanceFailed(shinyProxyInstance, e.prettyMessage())
246247
}
247248
throw e
248249
} catch (e: Exception) {
249-
eventController.createInstanceFailed(shinyProxyInstance, e.message)
250+
eventController.createInstanceFailed(shinyProxyInstance, e.prettyMessage())
250251
throw e
251252
}
252253
}

src/main/kotlin/eu/openanalytics/shinyproxyoperator/extensions.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,22 @@ fun JsonNode.getSubPath(): String {
5454
}
5555
return "/"
5656
}
57+
58+
fun JsonNode.getTextValueOrNull(key: String): String? {
59+
if (get(key)?.isTextual == true) {
60+
return get(key).textValue()
61+
}
62+
if (get(key)?.isNumber == true) {
63+
return get(key).numberValue().toString()
64+
}
65+
return null
66+
}
67+
68+
fun Exception.prettyMessage(): String? {
69+
val name = javaClass.simpleName
70+
if (listOf("Exception", "RuntimeException", "IllegalArgumentException", "IllegalStateException", "IOExecption").contains(name)) {
71+
// don't include name of exception if it's too generic
72+
return message
73+
}
74+
return "$name: $message"
75+
}

src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/docker/DockerOrchestrator.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import eu.openanalytics.shinyproxyoperator.logPrefix
3939
import eu.openanalytics.shinyproxyoperator.model.ShinyProxy
4040
import eu.openanalytics.shinyproxyoperator.model.ShinyProxyInstance
4141
import eu.openanalytics.shinyproxyoperator.model.ShinyProxyStatus
42+
import eu.openanalytics.shinyproxyoperator.prettyMessage
4243
import io.github.oshai.kotlinlogging.KotlinLogging
4344
import kotlinx.coroutines.Dispatchers
4445
import kotlinx.coroutines.channels.Channel
@@ -54,6 +55,8 @@ import java.nio.file.Path
5455
import java.time.OffsetDateTime
5556
import java.time.ZoneOffset
5657
import java.time.temporal.ChronoUnit
58+
import java.util.*
59+
import java.util.regex.Pattern
5760

5861
class DockerOrchestrator(channel: Channel<ShinyProxyEvent>,
5962
config: Config,
@@ -171,6 +174,20 @@ class DockerOrchestrator(channel: Channel<ShinyProxyEvent>,
171174
0
172175
}
173176

177+
var cpuPeriod: Long? = null
178+
var cpuQuota: Long? = null
179+
if (shinyProxy.cpuLimit != null) {
180+
cpuPeriod = 100_000
181+
try {
182+
cpuQuota = getCpuQuota(cpuPeriod, shinyProxy.cpuLimit!!)
183+
} catch (e: Exception) {
184+
throw RuntimeException("Invalid cpu limit: " + e.prettyMessage(), e)
185+
}
186+
}
187+
if (shinyProxy.cpuRequest != null) {
188+
logger.warn { "Realm '${shinyProxy.realmId}' has 'cpuRequest' configured: this is not supported in Docker and is ignored." }
189+
}
190+
174191
repeat(shinyProxy.replicas - containers.size) {
175192
val suffix = RandomStringUtils.randomAlphanumeric(10)
176193
val containerName = "sp-${shinyProxyInstance.realmId}-${shinyProxyInstance.hashOfSpec}-${shinyProxyInstance.revision}-${suffix}"
@@ -224,6 +241,10 @@ class DockerOrchestrator(channel: Channel<ShinyProxyEvent>,
224241
)
225242
.groupAdd(dockerGID.toString())
226243
.restartPolicy(HostConfig.RestartPolicy.always())
244+
.memoryReservation(memoryToBytes(shinyProxy.memoryRequest))
245+
.memory(memoryToBytes(shinyProxy.memoryLimit))
246+
.cpuPeriod(cpuPeriod)
247+
.cpuQuota(cpuQuota)
227248
.build()
228249

229250
val containerConfig = ContainerConfig.builder()
@@ -432,4 +453,29 @@ class DockerOrchestrator(channel: Channel<ShinyProxyEvent>,
432453
return objectMapper.writeValueAsString(config)
433454
}
434455

456+
private fun memoryToBytes(memory: String?): Long? {
457+
if (memory.isNullOrEmpty()) return null
458+
val matcher = Pattern.compile("(\\d+)([bkmg]?)i?").matcher(memory.lowercase(Locale.getDefault()))
459+
if (!matcher.matches()) {
460+
throw IllegalArgumentException("Invalid memory argument: $memory")
461+
}
462+
val mem = matcher.group(1).toLong()
463+
val unit = matcher.group(2)
464+
return when (unit) {
465+
"k" -> mem * 1024
466+
"m" -> mem * (1024 * 1024).toLong()
467+
"g" -> mem * (1024 * 1024 * 1024).toLong()
468+
else -> throw IllegalArgumentException("Invalid memory argument: $memory")
469+
}
470+
}
471+
472+
private fun getCpuQuota(cpuPeriod: Long, cpu: String): Long {
473+
val converted = if (cpu.endsWith("m")) {
474+
cpu.dropLast(1).toDouble() / 1_000
475+
} else {
476+
cpu.toDouble()
477+
}
478+
return (cpuPeriod.toDouble() * converted).toLong()
479+
}
480+
435481
}

src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/kubernetes/ShinyProxy.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
2424
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
2525
import com.fasterxml.jackson.datatype.jsr353.JSR353Module
2626
import eu.openanalytics.shinyproxyoperator.model.ShinyProxy
27+
import eu.openanalytics.shinyproxyoperator.prettyMessage
2728
import io.github.oshai.kotlinlogging.KotlinLogging
2829
import javax.json.JsonPatch
2930

@@ -38,7 +39,7 @@ fun ShinyProxy.getParsedKubernetesPodTemplateSpecPatches(): JsonPatch? {
3839
return yamlReader.readValue(getSpec().get("kubernetesPodTemplateSpecPatches").textValue(), JsonPatch::class.java)
3940
} catch (exception: Exception) {
4041
logger.warn(exception) { "Error while parsing 'kubernetesPodTemplateSpecPatches" }
41-
throw RuntimeException("Error while parsing 'kubernetesPodTemplateSpecPatches': " + exception.javaClass.simpleName + ": " + exception.message)
42+
throw RuntimeException("Error while parsing 'kubernetesPodTemplateSpecPatches': " + exception.prettyMessage())
4243
}
4344
}
4445
return null
@@ -74,7 +75,7 @@ fun ShinyProxy.getParsedServicePatches(): JsonPatch? {
7475
return yamlReader.readValue(getSpec().get("kubernetesServicePatches").textValue(), JsonPatch::class.java)
7576
} catch (exception: Exception) {
7677
logger.warn(exception) { "Error while parsing 'kubernetesServicePatches" }
77-
throw RuntimeException("Error while parsing 'kubernetesServicePatches': " + exception.javaClass.simpleName + ": " + exception.message)
78+
throw RuntimeException("Error while parsing 'kubernetesServicePatches': " + exception.prettyMessage())
7879
}
7980
}
8081
return null
@@ -90,7 +91,7 @@ fun ShinyProxy.getParsedIngressPatches(): JsonPatch? {
9091
return yamlReader.readValue(getSpec().get("kubernetesIngressPatches").textValue(), JsonPatch::class.java)
9192
} catch (exception: Exception) {
9293
logger.warn(exception) { "Error while parsing 'kubernetesIngressPatches" }
93-
throw RuntimeException("Error while parsing 'kubernetesServicePatches': " + exception.javaClass.simpleName + ": " + exception.message)
94+
throw RuntimeException("Error while parsing 'kubernetesServicePatches': " + exception.prettyMessage())
9495
}
9596
}
9697
return null

src/main/kotlin/eu/openanalytics/shinyproxyoperator/impl/kubernetes/components/Patcher.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule
2525
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
2626
import com.fasterxml.jackson.datatype.jsr353.JSR353Module
2727
import eu.openanalytics.shinyproxyoperator.impl.kubernetes.IntOrStringDeserializer
28+
import eu.openanalytics.shinyproxyoperator.prettyMessage
2829
import io.fabric8.kubernetes.api.model.IntOrString
2930
import io.fabric8.kubernetes.api.model.PodTemplateSpec
3031
import io.fabric8.kubernetes.api.model.Service
@@ -72,7 +73,7 @@ class Patcher {
7273
} catch (e: JsonException) {
7374
throw RuntimeException("Error while patching service (check logs for full objects): " + e.message?.replaceFirst(Regex("'.*' contains"), "'[redacted]' contains"))
7475
} catch (e: Exception) {
75-
throw RuntimeException("Error while patching service (check logs for full objects): " + e.javaClass.simpleName + ": " + e.message)
76+
throw RuntimeException("Error while patching service (check logs for full objects): " + e.prettyMessage())
7677
}
7778
}
7879
/**
@@ -101,7 +102,7 @@ class Patcher {
101102
} catch (e: JsonException) {
102103
throw RuntimeException("Error while patching ingress (check logs for full objects): " + e.message?.replaceFirst(Regex("'.*' contains"), "'[redacted]' contains"))
103104
} catch (e: Exception) {
104-
throw RuntimeException("Error while patching ingress (check logs for full objects): " + e.javaClass.simpleName + ": " + e.message)
105+
throw RuntimeException("Error while patching ingress (check logs for full objects): " + e.prettyMessage())
105106
}
106107
}
107108

@@ -131,7 +132,7 @@ class Patcher {
131132
} catch (e: JsonException) {
132133
throw RuntimeException("Error while patching pod (check logs for full objects): " + e.message?.replaceFirst(Regex("'.*' contains"), "'[redacted]' contains"))
133134
} catch (e: Exception) {
134-
throw RuntimeException("Error while patching pod (check logs for full objects): " + e.javaClass.simpleName + ": " + e.message)
135+
throw RuntimeException("Error while patching pod (check logs for full objects): " + e.prettyMessage())
135136
}
136137
}
137138

src/main/kotlin/eu/openanalytics/shinyproxyoperator/model/ShinyProxy.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.module.kotlin.convertValue
2626
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
2727
import eu.openanalytics.shinyproxyoperator.convertToYamlString
2828
import eu.openanalytics.shinyproxyoperator.getSubPath
29+
import eu.openanalytics.shinyproxyoperator.getTextValueOrNull
2930
import eu.openanalytics.shinyproxyoperator.sha1
3031

3132
class ShinyProxy(private val spec: JsonNode, val name: String, val namespace: String) {
@@ -51,6 +52,9 @@ class ShinyProxy(private val spec: JsonNode, val name: String, val namespace: St
5152
if (spec.get("additionalFqdns")?.isArray == true) {
5253
return@lazy spec.get("additionalFqdns").elements().asSequence().map { it.textValue() }.toList()
5354
}
55+
if (spec.get("additional-fqdns")?.isArray == true) {
56+
return@lazy spec.get("additional-fqdns").elements().asSequence().map { it.textValue() }.toList()
57+
}
5458
return@lazy listOf()
5559
}
5660

@@ -96,6 +100,26 @@ class ShinyProxy(private val spec: JsonNode, val name: String, val namespace: St
96100
return@lazy mapOf()
97101
}
98102

103+
@get:JsonIgnore
104+
val memoryRequest: String? by lazy {
105+
return@lazy spec.getTextValueOrNull("memory-request") ?: spec.getTextValueOrNull("memoryRequest")
106+
}
107+
108+
@get:JsonIgnore
109+
val memoryLimit: String? by lazy {
110+
return@lazy spec.getTextValueOrNull("memory-limit") ?: spec.getTextValueOrNull("memoryLimit")
111+
}
112+
113+
@get:JsonIgnore
114+
val cpuRequest: String? by lazy {
115+
return@lazy spec.getTextValueOrNull("cpu-request") ?: spec.getTextValueOrNull("cpuRequest")
116+
}
117+
118+
@get:JsonIgnore
119+
val cpuLimit: String? by lazy {
120+
return@lazy spec.getTextValueOrNull("cpu-limit") ?: spec.getTextValueOrNull("cpuLimit")
121+
}
122+
99123
fun getSpec(): JsonNode {
100124
return spec
101125
}

0 commit comments

Comments
 (0)