Files
2026-07-13 12:24:33 +08:00

223 lines
6.6 KiB
Go

/*
Copyright 2026.
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
http://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 resources
import (
"encoding/json"
"fmt"
"math"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
lmcachev1alpha1 "github.com/LMCache/LMCache/api/v1alpha1"
)
// ComputeResources returns resource requirements, either from overrides or auto-computed from L1 size.
func ComputeResources(spec *lmcachev1alpha1.LMCacheEngineSpec) corev1.ResourceRequirements {
if spec.ResourceOverrides != nil {
return *spec.ResourceOverrides
}
// memoryRequest = ceil(l1.sizeGB + 5) Gi
memReqGi := int64(math.Ceil(spec.L1.SizeGB + 5))
// memoryLimit = ceil(memoryRequest * 1.5) Gi
memLimGi := int64(math.Ceil(float64(memReqGi) * 1.5))
return corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("4"),
corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dGi", memReqGi)),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dGi", memLimGi)),
},
}
}
// BuildContainerArgs maps CRD spec fields to server.py CLI flags.
func BuildContainerArgs(spec *lmcachev1alpha1.LMCacheEngineSpec) []string {
args := []string{
"--host", "0.0.0.0",
"--port", fmt.Sprintf("%d", derefInt32(getServerPort(spec), 5555)),
"--http-port", fmt.Sprintf("%d", getHTTPPort(spec)),
"--l1-size-gb", fmt.Sprintf("%.1f", spec.L1.SizeGB),
"--chunk-size", fmt.Sprintf("%d", getChunkSize(spec)),
"--max-workers", fmt.Sprintf("%d", getMaxWorkers(spec)),
"--hash-algorithm", getHashAlgorithm(spec),
}
// Eviction args
evPolicy := "LRU"
evTrigger := 0.8
evRatio := 0.2
if spec.Eviction != nil {
evPolicy = derefString(spec.Eviction.Policy, "LRU")
evTrigger = derefFloat64(spec.Eviction.TriggerWatermark, 0.8)
evRatio = derefFloat64(spec.Eviction.EvictionRatio, 0.2)
}
args = append(args,
"--eviction-policy", evPolicy,
"--eviction-trigger-watermark", fmt.Sprintf("%.2f", evTrigger),
"--eviction-ratio", fmt.Sprintf("%.2f", evRatio),
)
// Prometheus args
promEnabled := true
if spec.Prometheus != nil {
promEnabled = derefBool(spec.Prometheus.Enabled, true)
}
if !promEnabled {
args = append(args, "--disable-prometheus")
} else {
promPort := int32(9090)
if spec.Prometheus != nil {
promPort = derefInt32(spec.Prometheus.Port, 9090)
}
args = append(args, "--prometheus-port", fmt.Sprintf("%d", promPort))
}
// L2 backend
if spec.L2Backend != nil {
l2JSON := buildL2AdapterJSON(spec.L2Backend)
if l2JSON != "" {
args = append(args, "--l2-adapter", l2JSON)
}
// L2 policies
args = append(args,
"--l2-store-policy", derefString(spec.L2Backend.StorePolicy, "default"),
"--l2-prefetch-policy", derefString(spec.L2Backend.PrefetchPolicy, "default"),
"--l2-prefetch-max-in-flight", fmt.Sprintf("%d", derefInt32(spec.L2Backend.PrefetchMaxInFlight, 8)),
)
}
// Coordinator registration. The URL is resolved by the controller (from a
// coordinator ref or an explicit url) before this builder runs; an empty URL
// means registration is disabled. The advertise IP falls back to the pod IP
// via the LMCACHE_COORDINATOR_ADVERTISE_IP env var injected by the DaemonSet
// builder, so it is only emitted as a flag when explicitly set.
if c := spec.Coordinator; c != nil {
if url := derefString(c.URL, ""); url != "" {
args = append(args, "--coordinator-url", url)
if c.AdvertiseIP != nil && *c.AdvertiseIP != "" {
args = append(args, "--coordinator-advertise-ip", *c.AdvertiseIP)
}
args = append(args,
"--coordinator-heartbeat-interval", formatFloat(derefFloat64(c.HeartbeatInterval, 5.0)),
)
if derefBool(c.L2EventReporting, false) {
args = append(args, "--coordinator-l2-event-reporting")
}
args = append(args,
"--coordinator-l2-event-flush-interval", formatFloat(derefFloat64(c.L2EventFlushInterval, 1.0)),
)
}
}
// User-supplied extra args (appended last so they can override defaults)
args = append(args, spec.ExtraArgs...)
return args
}
// buildL2AdapterJSON serializes an L2BackendSpec into the --l2-adapter JSON string.
// For RESP adapters with authSecretRef, username/password are set to env var
// placeholders that get interpolated by the shell wrapper (see BuildShellCommand).
func buildL2AdapterJSON(backend *lmcachev1alpha1.L2BackendSpec) string {
if backend.RESP != nil {
return buildRESPL2JSON(backend.RESP)
}
if backend.Raw != nil {
return buildRawL2JSON(backend.Raw)
}
return ""
}
func buildRESPL2JSON(resp *lmcachev1alpha1.RESPL2AdapterSpec) string {
flat := map[string]any{
"type": "resp",
"host": resp.Host,
"port": resp.Port,
"num_workers": derefInt32(resp.NumWorkers, 8),
}
if resp.MaxCapacityGB != nil && *resp.MaxCapacityGB > 0 {
flat["max_capacity_gb"] = *resp.MaxCapacityGB
}
// Auth credentials are passed via LMCACHE_RESP_USERNAME /
// LMCACHE_RESP_PASSWORD env vars (injected by the DaemonSet builder),
// not in the JSON config.
b, err := json.Marshal(flat)
if err != nil {
return ""
}
return string(b)
}
func buildRawL2JSON(raw *lmcachev1alpha1.RawL2AdapterSpec) string {
flat := make(map[string]any)
flat["type"] = raw.Type
for k, v := range raw.Config {
var parsed any
if err := json.Unmarshal(v.Raw, &parsed); err != nil {
flat[k] = string(v.Raw)
} else {
flat[k] = parsed
}
}
b, err := json.Marshal(flat)
if err != nil {
return ""
}
return string(b)
}
func getServerPort(spec *lmcachev1alpha1.LMCacheEngineSpec) *int32 {
if spec.Server != nil {
return spec.Server.Port
}
return nil
}
func getHTTPPort(spec *lmcachev1alpha1.LMCacheEngineSpec) int32 {
if spec.Server != nil {
return derefInt32(spec.Server.HTTPPort, 8080)
}
return 8080
}
func getChunkSize(spec *lmcachev1alpha1.LMCacheEngineSpec) int32 {
if spec.Server != nil {
return derefInt32(spec.Server.ChunkSize, 256)
}
return 256
}
func getMaxWorkers(spec *lmcachev1alpha1.LMCacheEngineSpec) int32 {
if spec.Server != nil {
return derefInt32(spec.Server.MaxWorkers, 1)
}
return 1
}
func getHashAlgorithm(spec *lmcachev1alpha1.LMCacheEngineSpec) string {
if spec.Server != nil {
return derefString(spec.Server.HashAlgorithm, "blake3")
}
return "blake3"
}