diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bcb802a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,68 @@ +# .dockerignore for go-cart-actor +# +# Goal: Keep Docker build context lean & reproducible. +# Adjust as project structure evolves. + +# Version control & CI metadata +.git +.git/ +.gitignore +.github + +# Local tooling / editors +.vscode +.idea +*.iml + +# Build artifacts / outputs +bin/ +build/ +dist/ +out/ +coverage/ +*.coverprofile + +# Temporary files +*.tmp +*.log +tmp/ +.tmp/ + +# Dependency/vendor caches (not used; rely on go modules download) +vendor/ + +# Examples / scripts (adjust if you actually need them in build context) +examples/ +scripts/ + +# Docs (retain README.md explicitly) +docs/ +CHANGELOG* +**/*.md +!README.md + +# Tests (not needed for production build) +**/*_test.go + +# Node / frontend artifacts (if any future addition) +node_modules/ + +# Docker / container metadata not needed inside image +Dockerfile + +# Editor swap/backup files +*~ +*.swp + +# Go race / profiling outputs +*.pprof + +# Security / secret placeholders (ensure real secrets never copied) +*.secret +*.key +*.pem + +# Keep proto and generated code (do NOT ignore proto/) +!proto/ + +# End of file diff --git a/Dockerfile b/Dockerfile index 2c7abc3..a53e683 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,72 @@ -# syntax=docker/dockerfile:1 +# syntax=docker/dockerfile:1.7 +# +# Multi-stage build: +# 1. Build static binary with pinned Go version (matching go.mod). +# 2. Copy into distroless static nonroot runtime image. +# +# Build args (optional): +# VERSION - semantic/app version (default: dev) +# GIT_COMMIT - git SHA (default: unknown) +# BUILD_DATE - RFC3339 build timestamp +# +# Example build: +# docker build \ +# --build-arg VERSION=$(git describe --tags --always) \ +# --build-arg GIT_COMMIT=$(git rev-parse HEAD) \ +# --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ +# -t go-cart-actor:dev . +# +# If you add subpackages or directories, no Dockerfile change needed (COPY . .). +# Ensure a .dockerignore exists to keep context lean. -FROM golang:alpine AS build-stage -WORKDIR /app +############################ +# Build Stage +############################ +FROM golang:1.25-alpine AS build +WORKDIR /src + +# Build metadata (can be overridden at build time) +ARG VERSION=dev +ARG GIT_COMMIT=unknown +ARG BUILD_DATE=unknown + +# Ensure reproducible static build +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 + +# Dependency caching COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download -COPY proto ./proto -COPY *.go ./ +# Copy full source (relay on .dockerignore to prune) +COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o /go-cart-actor +# (Optional) If you do NOT check in generated protobuf code, uncomment generation: +# RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ +# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest && \ +# protoc --go_out=. --go_opt=paths=source_relative \ +# --go-grpc_out=. --go-grpc_opt=paths=source_relative \ +# proto/*.proto -FROM gcr.io/distroless/base-debian11 +# Build with minimal binary size and embedded metadata +RUN --mount=type=cache,target=/go/build-cache \ + go build -trimpath -ldflags="-s -w \ + -X main.Version=${VERSION} \ + -X main.GitCommit=${GIT_COMMIT} \ + -X main.BuildDate=${BUILD_DATE}" \ + -o /out/go-cart-actor . + +############################ +# Runtime Stage +############################ +# Using distroless static (nonroot) for minimal surface area. +FROM gcr.io/distroless/static-debian12:nonroot AS runtime WORKDIR / -COPY --from=build-stage /go-cart-actor /go-cart-actor -ENTRYPOINT ["/go-cart-actor"] \ No newline at end of file +COPY --from=build /out/go-cart-actor /go-cart-actor + +# Document (not expose forcibly) typical ports: 8080 (HTTP), 1337 (gRPC) +EXPOSE 8080 1337 + +USER nonroot:nonroot +ENTRYPOINT ["/go-cart-actor"] diff --git a/Makefile b/Makefile index dafaef0..4cf839b 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ help: @echo " protogen Generate protobuf & gRPC code" @echo " clean_proto Remove generated *.pb.go files in $(PROTO_DIR)" @echo " verify_proto Ensure no root-level *.pb.go files (old layout)" + @echo " tidy Run go mod tidy" @echo " build Build the module" @echo " test Run tests (verbose)" @@ -89,6 +90,18 @@ verify_proto: fi @echo "$(GREEN)Proto layout OK (no root-level *.pb.go files).$(RESET)" + + + + + + + + + + + + tidy: @echo "$(YELLOW)Running go mod tidy...$(RESET)" $(GO) mod tidy diff --git a/deployment/deployment-no.yaml b/deployment/deployment-no.yaml deleted file mode 100644 index 18bf8c9..0000000 --- a/deployment/deployment-no.yaml +++ /dev/null @@ -1,276 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: klarna-api-credentials -data: - username: ZjQzZDY3YjEtNzA2Yy00NTk2LTliNTgtYjg1YjU2NDEwZTUw - password: a2xhcm5hX3Rlc3RfYXBpX0trUWhWVE5yYVZsV2FsTnhTRVp3Y1ZSSFF5UkVNRmxyY25Kd1AxSndQMGdzWmpRelpEWTNZakV0TnpBMll5MDBOVGsyTFRsaU5UZ3RZamcxWWpVMk5ERXdaVFV3TERFc2JUUkNjRFpWU1RsTllsSk1aMlEyVEc4MmRVODNZMkozUlRaaFdEZDViV3AwYkhGV1JqTjVNQzlaYXow -type: Opaque ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: cart-actor - arch: amd64 - name: cart-actor-x86 -spec: - replicas: 0 - selector: - matchLabels: - app: cart-actor - arch: amd64 - template: - metadata: - labels: - app: cart-actor - actor-pool: cart - arch: amd64 - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/arch - operator: NotIn - values: - - arm64 - volumes: - - name: data - nfs: - path: /i-data/7a8af061/nfs/cart-actor-no - server: 10.10.1.10 - imagePullSecrets: - - name: regcred - serviceAccountName: default - containers: - - image: registry.knatofs.se/go-cart-actor-amd64:latest - name: cart-actor-amd64 - imagePullPolicy: Always - lifecycle: - preStop: - exec: - command: ["sleep", "15"] - ports: - - containerPort: 8080 - name: web - - containerPort: 1234 - name: echo - - containerPort: 1337 - name: rpc - - containerPort: 1338 - name: quorum - livenessProbe: - httpGet: - path: /livez - port: web - failureThreshold: 1 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /readyz - port: web - failureThreshold: 2 - initialDelaySeconds: 2 - periodSeconds: 10 - volumeMounts: - - mountPath: "/data" - name: data - resources: - limits: - memory: "768Mi" - requests: - memory: "70Mi" - cpu: "1200m" - env: - - name: TZ - value: "Europe/Stockholm" - - name: KLARNA_API_USERNAME - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: username - - name: KLARNA_API_PASSWORD - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: password - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: AMQP_URL - value: "amqp://admin:12bananer@rabbitmq.dev:5672/" - - name: BASE_URL - value: "https://s10n-no.tornberg.me" - - name: CART_BASE_URL - value: "https://cart-no.tornberg.me" - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: cart-actor - arch: arm64 - name: cart-actor-arm64 -spec: - replicas: 3 - selector: - matchLabels: - app: cart-actor - arch: arm64 - template: - metadata: - labels: - app: cart-actor - actor-pool: cart - arch: arm64 - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: NotIn - values: - - masterpi - - key: kubernetes.io/arch - operator: In - values: - - arm64 - volumes: - - name: data - nfs: - path: /i-data/7a8af061/nfs/cart-actor-no - server: 10.10.1.10 - imagePullSecrets: - - name: regcred - serviceAccountName: default - containers: - - image: registry.knatofs.se/go-cart-actor:latest - name: cart-actor-arm64 - imagePullPolicy: Always - lifecycle: - preStop: - exec: - command: ["sleep", "15"] - ports: - - containerPort: 8080 - name: web - - containerPort: 1234 - name: echo - - containerPort: 1337 - name: rpc - - containerPort: 1338 - name: quorum - livenessProbe: - httpGet: - path: /livez - port: web - failureThreshold: 1 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /readyz - port: web - failureThreshold: 2 - initialDelaySeconds: 2 - periodSeconds: 10 - volumeMounts: - - mountPath: "/data" - name: data - resources: - limits: - memory: "768Mi" - requests: - memory: "70Mi" - cpu: "1200m" - env: - - name: TZ - value: "Europe/Stockholm" - - name: KLARNA_API_USERNAME - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: username - - name: KLARNA_API_PASSWORD - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: password - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: AMQP_URL - value: "amqp://admin:12bananer@rabbitmq.dev:5672/" - - name: BASE_URL - value: "https://s10n-no.tornberg.me" - - name: CART_BASE_URL - value: "https://cart-no.tornberg.me" - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name ---- -kind: Service -apiVersion: v1 -metadata: - name: cart-echo -spec: - selector: - app: cart-actor - type: LoadBalancer - ports: - - name: echo - port: 1234 ---- -kind: Service -apiVersion: v1 -metadata: - name: cart-actor - annotations: - prometheus.io/port: "8080" - prometheus.io/scrape: "true" - prometheus.io/path: "/metrics" -spec: - selector: - app: cart-actor - ports: - - name: web - port: 8080 ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: cart-ingress - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - # nginx.ingress.kubernetes.io/affinity: "cookie" - # nginx.ingress.kubernetes.io/session-cookie-name: "cart-affinity" - # nginx.ingress.kubernetes.io/session-cookie-expires: "172800" - # nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" - nginx.ingress.kubernetes.io/proxy-body-size: 4m -spec: - ingressClassName: nginx - tls: - - hosts: - - cart-no.tornberg.me - secretName: cart-actor-no-tls-secret - rules: - - host: cart-no.tornberg.me - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: cart-actor - port: - number: 8080 \ No newline at end of file diff --git a/deployment/deployment.yaml b/deployment/deployment.yaml index a2eb933..40bd0e2 100644 --- a/deployment/deployment.yaml +++ b/deployment/deployment.yaml @@ -1,272 +1,252 @@ apiVersion: v1 kind: Secret metadata: - name: klarna-api-credentials + name: klarna-api-credentials data: - username: ZjQzZDY3YjEtNzA2Yy00NTk2LTliNTgtYjg1YjU2NDEwZTUw - password: a2xhcm5hX3Rlc3RfYXBpX0trUWhWVE5yYVZsV2FsTnhTRVp3Y1ZSSFF5UkVNRmxyY25Kd1AxSndQMGdzWmpRelpEWTNZakV0TnpBMll5MDBOVGsyTFRsaU5UZ3RZamcxWWpVMk5ERXdaVFV3TERFc2JUUkNjRFpWU1RsTllsSk1aMlEyVEc4MmRVODNZMkozUlRaaFdEZDViV3AwYkhGV1JqTjVNQzlaYXow + username: ZjQzZDY3YjEtNzA2Yy00NTk2LTliNTgtYjg1YjU2NDEwZTUw + password: a2xhcm5hX3Rlc3RfYXBpX0trUWhWVE5yYVZsV2FsTnhTRVp3Y1ZSSFF5UkVNRmxyY25Kd1AxSndQMGdzWmpRelpEWTNZakV0TnpBMll5MDBOVGsyTFRsaU5UZ3RZamcxWWpVMk5ERXdaVFV3TERFc2JUUkNjRFpWU1RsTllsSk1aMlEyVEc4MmRVODNZMkozUlRaaFdEZDViV3AwYkhGV1JqTjVNQzlaYXow type: Opaque --- apiVersion: apps/v1 kind: Deployment metadata: - labels: - app: cart-actor - arch: amd64 - name: cart-actor-x86 -spec: - replicas: 0 - selector: - matchLabels: - app: cart-actor - arch: amd64 - template: - metadata: - labels: + labels: app: cart-actor - actor-pool: cart arch: amd64 - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/arch - operator: NotIn - values: - - arm64 - volumes: - - name: data - nfs: - path: /i-data/7a8af061/nfs/cart-actor - server: 10.10.1.10 - imagePullSecrets: - - name: regcred - serviceAccountName: default - containers: - - image: registry.knatofs.se/go-cart-actor-amd64:latest - name: cart-actor-amd64 - imagePullPolicy: Always - lifecycle: - preStop: - exec: - command: ["sleep", "15"] - ports: - - containerPort: 8080 - name: web - - containerPort: 1234 - name: echo - - containerPort: 1337 - name: rpc - - containerPort: 1338 - name: quorum - livenessProbe: - httpGet: - path: /livez - port: web - failureThreshold: 1 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /readyz - port: web - failureThreshold: 2 - initialDelaySeconds: 2 - periodSeconds: 10 - volumeMounts: - - mountPath: "/data" - name: data - resources: - limits: - memory: "768Mi" - requests: - memory: "70Mi" - cpu: "1200m" - env: - - name: TZ - value: "Europe/Stockholm" - - name: KLARNA_API_USERNAME - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: username - - name: KLARNA_API_PASSWORD - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: password - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: AMQP_URL - value: "amqp://admin:12bananer@rabbitmq.dev:5672/" - # - name: BASE_URL - # value: "https://s10n-no.tornberg.me" - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name + name: cart-actor-x86 +spec: + replicas: 0 + selector: + matchLabels: + app: cart-actor + arch: amd64 + template: + metadata: + labels: + app: cart-actor + actor-pool: cart + arch: amd64 + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: NotIn + values: + - arm64 + volumes: + - name: data + nfs: + path: /i-data/7a8af061/nfs/cart-actor + server: 10.10.1.10 + imagePullSecrets: + - name: regcred + serviceAccountName: default + containers: + - image: registry.knatofs.se/go-cart-actor-amd64:latest + name: cart-actor-amd64 + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: ["sleep", "15"] + ports: + - containerPort: 8080 + name: web + - containerPort: 1337 + name: rpc + livenessProbe: + httpGet: + path: /livez + port: web + failureThreshold: 1 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readyz + port: web + failureThreshold: 2 + initialDelaySeconds: 2 + periodSeconds: 10 + volumeMounts: + - mountPath: "/data" + name: data + resources: + limits: + memory: "768Mi" + requests: + memory: "70Mi" + cpu: "1200m" + env: + - name: TZ + value: "Europe/Stockholm" + - name: KLARNA_API_USERNAME + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: username + - name: KLARNA_API_PASSWORD + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: password + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: AMQP_URL + value: "amqp://admin:12bananer@rabbitmq.dev:5672/" + # - name: BASE_URL + # value: "https://s10n-no.tornberg.me" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name --- apiVersion: apps/v1 kind: Deployment metadata: - labels: - app: cart-actor - arch: arm64 - name: cart-actor-arm64 -spec: - replicas: 0 - selector: - matchLabels: - app: cart-actor - arch: arm64 - template: - metadata: - labels: + labels: app: cart-actor - actor-pool: cart arch: arm64 - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: NotIn - values: - - masterpi - - key: kubernetes.io/arch - operator: In - values: - - arm64 - volumes: - - name: data - nfs: - path: /i-data/7a8af061/nfs/cart-actor - server: 10.10.1.10 - imagePullSecrets: - - name: regcred - serviceAccountName: default - containers: - - image: registry.knatofs.se/go-cart-actor:latest - name: cart-actor-arm64 - imagePullPolicy: Always - lifecycle: - preStop: - exec: - command: ["sleep", "15"] - ports: - - containerPort: 8080 - name: web - - containerPort: 1234 - name: echo - - containerPort: 1337 - name: rpc - - containerPort: 1338 - name: quorum - livenessProbe: - httpGet: - path: /livez - port: web - failureThreshold: 1 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /readyz - port: web - failureThreshold: 2 - initialDelaySeconds: 2 - periodSeconds: 10 - volumeMounts: - - mountPath: "/data" - name: data - resources: - limits: - memory: "768Mi" - requests: - memory: "70Mi" - cpu: "1200m" - env: - - name: TZ - value: "Europe/Stockholm" - - name: KLARNA_API_USERNAME - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: username - - name: KLARNA_API_PASSWORD - valueFrom: - secretKeyRef: - name: klarna-api-credentials - key: password - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: AMQP_URL - value: "amqp://admin:12bananer@rabbitmq.dev:5672/" - # - name: BASE_URL - # value: "https://s10n-no.tornberg.me" - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name + name: cart-actor-arm64 +spec: + replicas: 0 + selector: + matchLabels: + app: cart-actor + arch: arm64 + template: + metadata: + labels: + app: cart-actor + actor-pool: cart + arch: arm64 + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: NotIn + values: + - masterpi + - key: kubernetes.io/arch + operator: In + values: + - arm64 + volumes: + - name: data + nfs: + path: /i-data/7a8af061/nfs/cart-actor + server: 10.10.1.10 + imagePullSecrets: + - name: regcred + serviceAccountName: default + containers: + - image: registry.knatofs.se/go-cart-actor:latest + name: cart-actor-arm64 + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: ["sleep", "15"] + ports: + - containerPort: 8080 + name: web + - containerPort: 1337 + name: rpc + livenessProbe: + httpGet: + path: /livez + port: web + failureThreshold: 1 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /readyz + port: web + failureThreshold: 2 + initialDelaySeconds: 2 + periodSeconds: 10 + volumeMounts: + - mountPath: "/data" + name: data + resources: + limits: + memory: "768Mi" + requests: + memory: "70Mi" + cpu: "1200m" + env: + - name: TZ + value: "Europe/Stockholm" + - name: KLARNA_API_USERNAME + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: username + - name: KLARNA_API_PASSWORD + valueFrom: + secretKeyRef: + name: klarna-api-credentials + key: password + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: AMQP_URL + value: "amqp://admin:12bananer@rabbitmq.dev:5672/" + # - name: BASE_URL + # value: "https://s10n-no.tornberg.me" + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name --- kind: Service apiVersion: v1 metadata: - name: cart-echo + name: cart-actor + annotations: + prometheus.io/port: "8080" + prometheus.io/scrape: "true" + prometheus.io/path: "/metrics" spec: - selector: - app: cart-actor - type: LoadBalancer - ports: - - name: echo - port: 1234 ---- -kind: Service -apiVersion: v1 -metadata: - name: cart-actor - annotations: - prometheus.io/port: "8080" - prometheus.io/scrape: "true" - prometheus.io/path: "/metrics" -spec: - selector: - app: cart-actor - ports: - - name: web - port: 8080 + selector: + app: cart-actor + ports: + - name: web + port: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: cart-ingress - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - # nginx.ingress.kubernetes.io/affinity: "cookie" - # nginx.ingress.kubernetes.io/session-cookie-name: "cart-affinity" - # nginx.ingress.kubernetes.io/session-cookie-expires: "172800" - # nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" - nginx.ingress.kubernetes.io/proxy-body-size: 4m + name: cart-ingress + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + # nginx.ingress.kubernetes.io/affinity: "cookie" + # nginx.ingress.kubernetes.io/session-cookie-name: "cart-affinity" + # nginx.ingress.kubernetes.io/session-cookie-expires: "172800" + # nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" + nginx.ingress.kubernetes.io/proxy-body-size: 4m spec: - ingressClassName: nginx - tls: - - hosts: - - cart.tornberg.me - secretName: cart-actor-tls-secret - rules: - - host: cart.tornberg.me - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: cart-actor - port: - number: 8080 \ No newline at end of file + ingressClassName: nginx + tls: + - hosts: + - cart.tornberg.me + secretName: cart-actor-tls-secret + rules: + - host: cart.tornberg.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: cart-actor + port: + number: 8080 diff --git a/deployment/scaling.yaml b/deployment/scaling.yaml index 6024cad..ea7b32c 100644 --- a/deployment/scaling.yaml +++ b/deployment/scaling.yaml @@ -1,25 +1,101 @@ -apiVersion: autoscaling/v1 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: cart-scaler-amd + name: cart-scaler-amd spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: cart-actor-x86 - minReplicas: 3 - maxReplicas: 9 - targetCPUUtilizationPercentage: 30 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: cart-actor-x86 + minReplicas: 3 + maxReplicas: 9 + behavior: + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Percent + value: 100 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 180 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + # Future custom metric (example): + # - type: Pods + # pods: + # metric: + # name: cart_mutations_per_second + # target: + # type: AverageValue + # averageValue: "15" + # - type: Object + # object: + # describedObject: + # apiVersion: networking.k8s.io/v1 + # kind: Ingress + # name: cart-ingress + # metric: + # name: http_requests_per_second + # target: + # type: Value + # value: "100" --- -apiVersion: autoscaling/v1 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: cart-scaler-arm + name: cart-scaler-arm spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: cart-actor-arm64 - minReplicas: 3 - maxReplicas: 9 - targetCPUUtilizationPercentage: 30 \ No newline at end of file + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: cart-actor-arm64 + minReplicas: 3 + maxReplicas: 9 + behavior: + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Percent + value: 100 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 180 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + # Future custom metric (example): + # - type: Pods + # pods: + # metric: + # name: cart_mutations_per_second + # target: + # type: AverageValue + # averageValue: "15" + # - type: Object + # object: + # describedObject: + # apiVersion: networking.k8s.io/v1 + # kind: Ingress + # name: cart-ingress + # metric: + # name: http_requests_per_second + # target: + # type: Value + # value: "100" diff --git a/proto/control_plane.pb.go b/proto/control_plane.pb.go index c494e4a..ad533c2 100644 --- a/proto/control_plane.pb.go +++ b/proto/control_plane.pb.go @@ -246,60 +246,7 @@ func (x *CartIdsReply) GetCartIds() []string { return nil } -// OwnerChangeRequest notifies peers that ownership of a cart moved (or is moving) to new_host. -type OwnerChangeRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"` - NewHost string `protobuf:"bytes,2,opt,name=new_host,json=newHost,proto3" json:"new_host,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *OwnerChangeRequest) Reset() { - *x = OwnerChangeRequest{} - mi := &file_control_plane_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *OwnerChangeRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OwnerChangeRequest) ProtoMessage() {} - -func (x *OwnerChangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_control_plane_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use OwnerChangeRequest.ProtoReflect.Descriptor instead. -func (*OwnerChangeRequest) Descriptor() ([]byte, []int) { - return file_control_plane_proto_rawDescGZIP(), []int{5} -} - -func (x *OwnerChangeRequest) GetCartId() string { - if x != nil { - return x.CartId - } - return "" -} - -func (x *OwnerChangeRequest) GetNewHost() string { - if x != nil { - return x.NewHost - } - return "" -} - -// OwnerChangeAck indicates acceptance or rejection of an ownership change. +// OwnerChangeAck retained as response type for Closing RPC (ConfirmOwner removed). type OwnerChangeAck struct { state protoimpl.MessageState `protogen:"open.v1"` Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` @@ -310,7 +257,7 @@ type OwnerChangeAck struct { func (x *OwnerChangeAck) Reset() { *x = OwnerChangeAck{} - mi := &file_control_plane_proto_msgTypes[6] + mi := &file_control_plane_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -322,7 +269,7 @@ func (x *OwnerChangeAck) String() string { func (*OwnerChangeAck) ProtoMessage() {} func (x *OwnerChangeAck) ProtoReflect() protoreflect.Message { - mi := &file_control_plane_proto_msgTypes[6] + mi := &file_control_plane_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -335,7 +282,7 @@ func (x *OwnerChangeAck) ProtoReflect() protoreflect.Message { // Deprecated: Use OwnerChangeAck.ProtoReflect.Descriptor instead. func (*OwnerChangeAck) Descriptor() ([]byte, []int) { - return file_control_plane_proto_rawDescGZIP(), []int{6} + return file_control_plane_proto_rawDescGZIP(), []int{5} } func (x *OwnerChangeAck) GetAccepted() bool { @@ -362,7 +309,7 @@ type ClosingNotice struct { func (x *ClosingNotice) Reset() { *x = ClosingNotice{} - mi := &file_control_plane_proto_msgTypes[7] + mi := &file_control_plane_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -374,7 +321,7 @@ func (x *ClosingNotice) String() string { func (*ClosingNotice) ProtoMessage() {} func (x *ClosingNotice) ProtoReflect() protoreflect.Message { - mi := &file_control_plane_proto_msgTypes[7] + mi := &file_control_plane_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -387,7 +334,7 @@ func (x *ClosingNotice) ProtoReflect() protoreflect.Message { // Deprecated: Use ClosingNotice.ProtoReflect.Descriptor instead. func (*ClosingNotice) Descriptor() ([]byte, []int) { - return file_control_plane_proto_rawDescGZIP(), []int{7} + return file_control_plane_proto_rawDescGZIP(), []int{6} } func (x *ClosingNotice) GetHost() string { @@ -412,10 +359,7 @@ const file_control_plane_proto_rawDesc = "" + "\x0eNegotiateReply\x12\x14\n" + "\x05hosts\x18\x01 \x03(\tR\x05hosts\")\n" + "\fCartIdsReply\x12\x19\n" + - "\bcart_ids\x18\x01 \x03(\tR\acartIds\"H\n" + - "\x12OwnerChangeRequest\x12\x17\n" + - "\acart_id\x18\x01 \x01(\tR\x06cartId\x12\x19\n" + - "\bnew_host\x18\x02 \x01(\tR\anewHost\"F\n" + + "\bcart_ids\x18\x01 \x03(\tR\acartIds\"F\n" + "\x0eOwnerChangeAck\x12\x1a\n" + "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\"#\n" + @@ -440,26 +384,25 @@ func file_control_plane_proto_rawDescGZIP() []byte { return file_control_plane_proto_rawDescData } -var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_control_plane_proto_goTypes = []any{ - (*Empty)(nil), // 0: messages.Empty - (*PingReply)(nil), // 1: messages.PingReply - (*NegotiateRequest)(nil), // 2: messages.NegotiateRequest - (*NegotiateReply)(nil), // 3: messages.NegotiateReply - (*CartIdsReply)(nil), // 4: messages.CartIdsReply - (*OwnerChangeRequest)(nil), // 5: messages.OwnerChangeRequest - (*OwnerChangeAck)(nil), // 6: messages.OwnerChangeAck - (*ClosingNotice)(nil), // 7: messages.ClosingNotice + (*Empty)(nil), // 0: messages.Empty + (*PingReply)(nil), // 1: messages.PingReply + (*NegotiateRequest)(nil), // 2: messages.NegotiateRequest + (*NegotiateReply)(nil), // 3: messages.NegotiateReply + (*CartIdsReply)(nil), // 4: messages.CartIdsReply + (*OwnerChangeAck)(nil), // 5: messages.OwnerChangeAck + (*ClosingNotice)(nil), // 6: messages.ClosingNotice } var file_control_plane_proto_depIdxs = []int32{ 0, // 0: messages.ControlPlane.Ping:input_type -> messages.Empty 2, // 1: messages.ControlPlane.Negotiate:input_type -> messages.NegotiateRequest 0, // 2: messages.ControlPlane.GetCartIds:input_type -> messages.Empty - 7, // 3: messages.ControlPlane.Closing:input_type -> messages.ClosingNotice + 6, // 3: messages.ControlPlane.Closing:input_type -> messages.ClosingNotice 1, // 4: messages.ControlPlane.Ping:output_type -> messages.PingReply 3, // 5: messages.ControlPlane.Negotiate:output_type -> messages.NegotiateReply 4, // 6: messages.ControlPlane.GetCartIds:output_type -> messages.CartIdsReply - 6, // 7: messages.ControlPlane.Closing:output_type -> messages.OwnerChangeAck + 5, // 7: messages.ControlPlane.Closing:output_type -> messages.OwnerChangeAck 4, // [4:8] is the sub-list for method output_type 0, // [0:4] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -478,7 +421,7 @@ func file_control_plane_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/synced-pool.go b/synced-pool.go index a320392..7406f82 100644 --- a/synced-pool.go +++ b/synced-pool.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "reflect" "sync" "time" @@ -105,6 +106,27 @@ var ( Name: "cart_ring_host_share", Help: "Fractional share of ring vnodes per host", }, []string{"host"}) + + cartMutationsTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cart_mutations_total", + Help: "Total number of cart state mutations applied (local + remote routed).", + }) + + cartMutationFailuresTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cart_mutation_failures_total", + Help: "Total number of failed cart state mutations (local apply errors or remote routing failures).", + }) + + cartMutationLatencySeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "cart_mutation_latency_seconds", + Help: "Latency of cart mutations (successful or failed) in seconds.", + Buckets: prometheus.DefBuckets, + }, []string{"mutation"}) + + cartActiveGrains = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "cart_active_grains", + Help: "Number of active (resident) local grains.", + }) ) func NewSyncedPool(local *GrainLocalPool, hostname string, discovery Discovery) (*SyncedPool, error) { @@ -564,7 +586,33 @@ func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error) if err != nil { return nil, err } - return grain.Apply(mutation, false) + start := time.Now() + result, applyErr := grain.Apply(mutation, false) + + // Derive mutation type label (strip pointer) + mutationType := "unknown" + if mutation != nil { + if t := reflect.TypeOf(mutation); t != nil { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Name() != "" { + mutationType = t.Name() + } + } + } + cartMutationLatencySeconds.WithLabelValues(mutationType).Observe(time.Since(start).Seconds()) + + if applyErr == nil && result != nil { + cartMutationsTotal.Inc() + if p.ownerHostFor(id) == p.Hostname { + // Update active grains gauge only for local ownership + cartActiveGrains.Set(float64(p.local.DebugGrainCount())) + } + } else if applyErr != nil { + cartMutationFailuresTotal.Inc() + } + return result, applyErr } // Get returns current state of a grain (local or remote).