diff --git a/.github/workflows/build-kind.yaml b/.github/workflows/build-kind.yaml new file mode 100644 index 0000000..6416634 --- /dev/null +++ b/.github/workflows/build-kind.yaml @@ -0,0 +1,131 @@ +# Copyright 2025 Oracle Corporation and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. + +# --------------------------------------------------------------------------- +# Coherence Go Client GitHub Actions - Kind Tests +# --------------------------------------------------------------------------- +name: CI Kind Tests + +on: + push: + branches: + - '*' + workflow_dispatch: + inputs: + go-version: + description: "Go version (comma-separated for matrix)" + required: false + default: "1.23.x,1.24.x" + max-iterations: + description: "Maximum number of iterations" + required: false + default: "10" + +jobs: + build: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + fail-fast: false + matrix: + go-version: + - 1.24.x + +# Checkout the source, we need a depth of zero to fetch all of the history otherwise +# the copyright check cannot work out the date of the files from Git. + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache Go Modules + # if: github.event_name == 'workflow_dispatch' + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-mods-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-mods- + + - name: Set up Go + # if: github.event_name == 'workflow_dispatch' + uses: actions/setup-go@v5 + with: + go-version: '${{ matrix.go-version }}' + + - name: Create Kind Cluster + # if: github.event_name == 'workflow_dispatch' + shell: bash + run: | + make kind + make create-namespace deploy-operator + make deploy-coherence + make build-go-client + make load-schools + mkdir -p build/_output/test-logs + + - name: Run Tests + # if: github.event_name == 'workflow_dispatch' + shell: bash + env: + MAX_ITERATIONS: ${{ github.event.inputs.max-iterations }} + run: | + export MAX_ITERATIONS + NAMESPACE=coherence-perf + kubectl exec -it -n $NAMESPACE perf-cluster-0 -c coherence -- /coherence-operator/utils/cohctl get caches -o wide + make deploy-test-schools + JOB_NAME=perf-go-client + echo "Waiting for pod from job $JOB_NAME to start running..." + for i in {1..30}; do + POD=$(kubectl get pods -n "$NAMESPACE" -l job-name="$JOB_NAME" -o jsonpath='{.items[0].metadata.name}') + STATUS=$(kubectl get pod "$POD" -n "$NAMESPACE" -o jsonpath='{.status.phase}') + echo "Pod: $POD, Status: $STATUS" + if [[ "$STATUS" == "Running" ]]; then + echo "Pod is running" + break + elif [[ "$STATUS" == "Pending" || "$STATUS" == "ContainerCreating" ]]; then + sleep 2 + else + echo "Pod entered unexpected state: $STATUS" + kubectl describe pod "$POD" -n "$NAMESPACE" + exit 1 + fi + done + # Put the tail into the background + POD=$(kubectl get pods -n $NAMESPACE | grep perf-go-client | awk '{print $1}') + kubectl logs $POD -n $NAMESPACE -f > build/_output/test-logs/perf-go-client.log 2>&1 & + TAIL_PID=$! + # port forward the http port + echo "Pod: $POD" + kubectl port-forward -n $NAMESPACE pod/${POD} 8080:8080 > build/_output/test-logs/port-forward.log 2>&1 & + PORT_FORWARD_PID=$! + echo "Sleep 10..." + sleep 10 + # run curl requests + while : ; do curl -s -v http://127.0.0.1:8080/api/schools?q=[1-2] | jq length || true ; sleep 0.5; done > build/_output/test-logs/curl.log 2>&1 & + CURL_PID=$! + # Start the rolling restart + ./scripts/kind/roll-cluster.sh | tee build/_output/test-logs/rolling-restart.log + kill -9 $CURL_PID || true + sleep 5 && kill -9 $PORT_FORWARD_PID || true + kill -9 $TAIL_PID || true + + - name: Shutdown + # if: github.event_name == 'workflow_dispatch' + shell: bash + run: | + make kind-stop + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-output-${{ matrix.go-version }}-failure-logs + path: build/_output/test-logs + + - uses: actions/upload-artifact@v4 + with: + name: test-output-${{ matrix.go-version }}-test-logs + path: build/_output/test-logs + diff --git a/.gitignore b/.gitignore index 42b03a9..473be54 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ certs/ release/ etc/ coherence/coherence.test +runner diff --git a/Makefile b/Makefile index 102370a..a22fca0 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ # ---------------------------------------------------------------------------------------------------------------------- # This is the version of the coherence-go-client -VERSION ?=2.3.0-rc1 +VERSION ?=2.3.0-rc2 CURRDIR := $(shell pwd) USER_ID := $(shell echo "`id -u`:`id -g`") @@ -178,6 +178,7 @@ copyright: getcopyright ## Check copyright headers -X proto/ \ -X /Dockerfile \ -X .Dockerfile \ + -X runner \ -X go.sum \ -X HEADER.txt \ -X .iml \ @@ -277,6 +278,111 @@ show-docs: ## Show the Documentation trivy-scan: gettrivy ## Scan the CLI using trivy $(TOOLS_BIN)/trivy fs --cache-dir ${TRIVY_CACHE} --exit-code 1 --skip-dirs "./java" . +# ====================================================================================================================== +# Targets related to running KinD clusters for testing +# ====================================================================================================================== +##@ KinD + +KIND_CLUSTER ?= go-client +KIND_IMAGE ?= "kindest/node:v1.33.0@sha256:91e9ed777db80279c22d1d1068c091b899b2078506e4a0f797fbf6e397c0b0b2" +KIND_SCRIPTS := ./scripts/kind +NAMESPACE ?= coherence-perf +OPERATOR_VERSION ?= v3.5.0 +COHERENCE_IMAGE ?= ghcr.io/oracle/coherence-ce:14.1.2-0-2-java17 +INITIAL_HEAP ?= 1g +GO_CLIENT_ARCH ?= amd64 +GO_IMAGE ?= perf-go-client:1.0.0 + +# ---------------------------------------------------------------------------------------------------------------------- +# Start a Kind cluster +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: kind +kind: ## Run a default KinD cluster + kind create cluster --name $(KIND_CLUSTER) --wait 10m --config $(KIND_SCRIPTS)/kind-config.yaml --image $(KIND_IMAGE) + $(KIND_SCRIPTS)/kind-label-node.sh + +# ---------------------------------------------------------------------------------------------------------------------- +# Stop and delete the Kind cluster +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: kind-stop +kind-stop: ## Stop and delete the KinD cluster + kind delete cluster --name $(KIND_CLUSTER) + +# ---------------------------------------------------------------------------------------------------------------------- +# Deploy Coherence Operator +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: deploy-operator +deploy-operator: ## Deploy the Coherence Operator + kubectl apply -f https://github.com/oracle/coherence-operator/releases/download/$(OPERATOR_VERSION)/coherence-operator.yaml + kubectl -n coherence wait --timeout=300s --for condition=available deployment/coherence-operator-controller-manager + +# ---------------------------------------------------------------------------------------------------------------------- +# UnDeploy Coherence Operator +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: undeploy-operator +undeploy-operator: ## UnDeploy the Coherence Operator + kubectl delete -f https://github.com/oracle/coherence-operator/releases/download/$(OPERATOR_VERSION)/coherence-operator.yaml || true + +# ---------------------------------------------------------------------------------------------------------------------- +# Deploy Coherence Cluster +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: deploy-coherence +deploy-coherence: ## Deploy the Coherence Cluster + envsubst < $(KIND_SCRIPTS)/coherence-cluster.yaml | kubectl apply -n $(NAMESPACE) -f - + sleep 5 + kubectl -n $(NAMESPACE) wait --timeout=300s --for condition=Ready coherence/perf-cluster + +#----------------------------------------------------------------------------------------------------------------------- +# Deploy Coherence Cluster +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: build-go-client +build-go-client: ## Make Go Client + cd test/e2e/kind && CGO_ENABLED=0 GOOS=linux GOARCH=$(GO_CLIENT_ARCH) GO111MODULE=on go build -trimpath -o runner . && docker build --no-cache -t $(GO_IMAGE) . + kind --name $(KIND_CLUSTER) load docker-image $(GO_IMAGE) + +#----------------------------------------------------------------------------------------------------------------------- +# Load Schools Data +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: load-schools +load-schools: ## Load Schools + kubectl -n $(NAMESPACE) apply -f $(KIND_SCRIPTS)/load-schools.yaml + kubectl wait -n $(NAMESPACE) --timeout=1200s --for condition=Complete job/go-perf-load-schools + kubectl -n $(NAMESPACE) delete -f $(KIND_SCRIPTS)/load-schools.yaml || true + +#----------------------------------------------------------------------------------------------------------------------- +# Test Schools +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: deploy-test-schools +deploy-test-schools: ## Deploy Test Schools + kubectl -n $(NAMESPACE) apply -f $(KIND_SCRIPTS)/test-schools.yaml + +#----------------------------------------------------------------------------------------------------------------------- +# Stop Schools Test +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: undeploy-test-schools +undeploy-test-schools: ## Undeploy Test Schools + kubectl -n $(NAMESPACE) delete -f $(KIND_SCRIPTS)/test-schools.yaml + +# ---------------------------------------------------------------------------------------------------------------------- +# UnDeploy Coherence Cluster +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: undeploy-coherence +undeploy-coherence: ## UnDeploy the Coherence Cluster + kubectl delete -n $(NAMESPACE) -f $(KIND_SCRIPTS)/coherence-cluster.yaml || true + +# ---------------------------------------------------------------------------------------------------------------------- +# Create Perf namespace +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: create-namespace +create-namespace: ## Create the perf test namespace + kubectl create namespace $(NAMESPACE) + +# ---------------------------------------------------------------------------------------------------------------------- +# Delete Perf namespace +# ---------------------------------------------------------------------------------------------------------------------- +.PHONY: delete-namespace +delete-namespace: ## Create the perf test namespace + kubectl delete namespace $(NAMESPACE) || true # ====================================================================================================================== # Test targets @@ -344,7 +450,6 @@ test-v1-base: test-clean test gotestsum $(BUILD_PROPS) ## Run e2e tests with Coh test-examples: test-clean gotestsum $(BUILD_PROPS) ## Run examples tests with Coherence ./scripts/run-test-examples.sh - # ---------------------------------------------------------------------------------------------------------------------- # Startup cluster members via docker compose # ---------------------------------------------------------------------------------------------------------------------- diff --git a/coherence/common.go b/coherence/common.go index 09e86d7..2919a90 100644 --- a/coherence/common.go +++ b/coherence/common.go @@ -57,6 +57,8 @@ const ( integerMaxValue = 2147483647 ListenAll InvalidationStrategyType = 0 + + panicWarning = "recovered from panic, possible connection closed while received channel data: %v" ) var ( @@ -636,6 +638,14 @@ func executeGetAll[K comparable, V any](ctx context.Context, bc *baseClient[K, V } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + if cancel != nil { defer cancel() } @@ -1185,6 +1195,14 @@ func executeInvokeAllFilterOrKeys[K comparable, V any, R any](ctx context.Contex } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + if cancel != nil { defer cancel() } @@ -1264,10 +1282,18 @@ func executeKeySet[K comparable, V any](ctx context.Context, bc *baseClient[K, V } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + for { result, err1 := iter.Next() - if err1 == ErrDone { + if errors.Is(err1, ErrDone) { close(ch) return } else if err1 != nil { @@ -1307,6 +1333,14 @@ func executeKeySetFilter[K comparable, V any](ctx context.Context, bc *baseClien } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + if cancel != nil { defer cancel() } @@ -1840,10 +1874,18 @@ func executeEntrySet[K comparable, V any](ctx context.Context, bc *baseClient[K, } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + for { result, err1 := iter.Next() - if err1 == ErrDone { + if errors.Is(err1, ErrDone) { close(ch) return } else if err1 != nil { @@ -1892,6 +1934,14 @@ func executeEntrySetFilter[K comparable, V any, E any](ctx context.Context, bc * } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + if cancel != nil { defer cancel() } @@ -1986,6 +2036,14 @@ func executeValues[K comparable, V any, E any](ctx context.Context, bc *baseClie } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + if cancel != nil { defer cancel() } @@ -2056,10 +2114,18 @@ func executeValuesNoFilter[K comparable, V any](ctx context.Context, bc *baseCli } go func() { + defer func() { + // catch panic of closed channel read in rare circumstances + if r := recover(); r != nil { + logMessage(WARNING, panicWarning, r) + return + } + }() + for { result, err1 := iter.Next() - if err1 == ErrDone { + if errors.Is(err1, ErrDone) { close(ch) return } else if err1 != nil { diff --git a/coherence/session.go b/coherence/session.go index e4bcaca..b153fd0 100644 --- a/coherence/session.go +++ b/coherence/session.go @@ -664,6 +664,7 @@ func waitForReady(s *Session) error { // try to connect up until timeout, then throw err if not available timeout := time.Now().Add(readyTimeout) + for { if time.Now().After(timeout) { return fmt.Errorf("unable to connect to %s after ready timeout of %v", s.sessOpts.Address, readyTimeout) @@ -677,6 +678,7 @@ func waitForReady(s *Session) error { if state == connectivity.Ready { return nil } + if !messageLogged { logMessage(INFO, "Session [%s] State is %v, waiting until ready timeout of %v for valid connection", s.sessionID, state, readyTimeout) diff --git a/scripts/kind/coherence-cluster.yaml b/scripts/kind/coherence-cluster.yaml new file mode 100644 index 0000000..90dd83a --- /dev/null +++ b/scripts/kind/coherence-cluster.yaml @@ -0,0 +1,26 @@ +# Copyright 2025 Oracle Corporation and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +apiVersion: coherence.oracle.com/v1 +kind: Coherence +metadata: + name: perf-cluster +spec: + jvm: + memory: + initialHeapSize: $INITIAL_HEAP + maxHeapSize: 1g + replicas: 3 + readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + suspendServicesOnShutdown: false + image: "ghcr.io/oracle/coherence-ce:14.1.2-0-2-java17" + imagePullPolicy: IfNotPresent + coherence: + management: + enabled: true + ports: + - name: management + - name: grpc + port: 1408 \ No newline at end of file diff --git a/scripts/kind/kind-config.yaml b/scripts/kind/kind-config.yaml new file mode 100644 index 0000000..65fe126 --- /dev/null +++ b/scripts/kind/kind-config.yaml @@ -0,0 +1,10 @@ +# Copyright 2025 Oracle Corporation and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + - role: worker + - role: worker + - role: worker diff --git a/scripts/kind/kind-label-node.sh b/scripts/kind/kind-label-node.sh new file mode 100755 index 0000000..30a12f5 --- /dev/null +++ b/scripts/kind/kind-label-node.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# +# Copyright (c) 2020, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# + +PREFIX=go-client + +kubectl label node ${PREFIX}-worker topology.kubernetes.io/zone=zone-one --overwrite +kubectl label node ${PREFIX}-worker topology.kubernetes.io/region=one --overwrite +kubectl label node ${PREFIX}-worker oci.oraclecloud.com/fault-domain=fd-one --overwrite +kubectl label node ${PREFIX}-worker coherence.oracle.com/site=site-one --overwrite +kubectl label node ${PREFIX}-worker coherence.oracle.com/rack=rack-one --overwrite +kubectl label node ${PREFIX}-worker2 topology.kubernetes.io/zone=zone-two --overwrite || true +kubectl label node ${PREFIX}-worker2 topology.kubernetes.io/region=two --overwrite || true +kubectl label node ${PREFIX}-worker2 oci.oraclecloud.com/fault-domain=fd-two --overwrite || true +kubectl label node ${PREFIX}-worker2 coherence.oracle.com/site=site-two --overwrite || true +kubectl label node ${PREFIX}-worker2 coherence.oracle.com/rack=rack-two --overwrite || true +kubectl label node ${PREFIX}-worker3 topology.kubernetes.io/zone=zone-three --overwrite || true +kubectl label node ${PREFIX}-worker3 topology.kubernetes.io/region=three --overwrite || true +kubectl label node ${PREFIX}-worker3 oci.oraclecloud.com/fault-domain=fd-three --overwrite || true +kubectl label node ${PREFIX}-worker3 coherence.oracle.com/site=site-three --overwrite || true +kubectl label node ${PREFIX}-worker3 coherence.oracle.com/rack=rack-three --overwrite || true + diff --git a/scripts/kind/kind.sh b/scripts/kind/kind.sh new file mode 100755 index 0000000..b512202 --- /dev/null +++ b/scripts/kind/kind.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# +# Copyright (c) 2020, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# + +set -o errexit + +# desired cluster name; default is "kind" +KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-coherence-go-client}" + +kind create cluster --name "${KIND_CLUSTER_NAME}" $@ diff --git a/scripts/kind/load-schools.yaml b/scripts/kind/load-schools.yaml new file mode 100644 index 0000000..6849a44 --- /dev/null +++ b/scripts/kind/load-schools.yaml @@ -0,0 +1,32 @@ +# Copyright 2025 Oracle Corporation and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +apiVersion: batch/v1 +kind: Job +metadata: + name: go-perf-load-schools +spec: + completions: 1 # total times to run + parallelism: 1 # how many pods to run at the same time + template: + metadata: + labels: + app: go-client + spec: + restartPolicy: Never + containers: + - name: perf-go-client + image: "perf-go-client:1.0.0" + imagePullPolicy: IfNotPresent + env: + - name: COHERENCE_SERVER_ADDRESS + value: "perf-cluster-grpc:1408" + - name: TEST_TYPE + value: "load" + - name: CACHE_COUNT + value: "250000" + resources: + requests: + memory: "1024Mi" + limits: + memory: "1024Mi" \ No newline at end of file diff --git a/scripts/kind/roll-cluster.sh b/scripts/kind/roll-cluster.sh new file mode 100755 index 0000000..afd1122 --- /dev/null +++ b/scripts/kind/roll-cluster.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# +# Copyright (c) 2020, 2025, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# + +set -o errexit +maxIterations=10 + +if [ ! -z "$MAX_ITERATIONS" ]; then + maxIterations=$MAX_ITERATIONS +fi + +if [ -z "$NAMESPACE" ]; then + NAMESPACE=coherence-perf +fi + +echo "`date`: Starting rolling restart, iterations=$maxIterations" + +for i in $(seq 1 $maxIterations); do + + echo "`date`: Iteration $i of $maxIterations" + + if [ $((i%2)) -eq 0 ]; then + INITIAL_HEAP=1000m + else + INITIAL_HEAP=1001m + fi + + export INITIAL_HEAP + + envsubst < ./scripts/kind/coherence-cluster.yaml | kubectl -n $NAMESPACE apply -f - + + echo "`date`: Waiting for cluster to be updated..." + kubectl rollout status statefulset/perf-cluster -n $NAMESPACE --timeout=300s + + echo "`date`: Sleeping 10..." + sleep 10 +done + + diff --git a/scripts/kind/test-schools.yaml b/scripts/kind/test-schools.yaml new file mode 100644 index 0000000..2c41751 --- /dev/null +++ b/scripts/kind/test-schools.yaml @@ -0,0 +1,35 @@ +# Copyright 2025 Oracle Corporation and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. +apiVersion: batch/v1 +kind: Job +metadata: + name: perf-go-client +spec: + backoffLimit: 0 + template: + metadata: + labels: + app: perf-go-client + spec: + restartPolicy: Never + containers: + - name: go-client + image: "perf-go-client:1.0.0" + imagePullPolicy: IfNotPresent + env: + - name: COHERENCE_SERVER_ADDRESS + value: "perf-cluster-grpc:1408" + - name: COHERENCE_READY_TIMEOUT + value: "30000" + - name: TEST_TYPE + value: "runTest" + - name: COHERENCE_LOG_LEVEL + value: "DEBUG" + resources: + requests: + memory: "512Mi" + limits: + memory: "512Mi" + ports: + - containerPort: 8080 \ No newline at end of file diff --git a/test/e2e/kind/Dockerfile b/test/e2e/kind/Dockerfile new file mode 100644 index 0000000..f5330cb --- /dev/null +++ b/test/e2e/kind/Dockerfile @@ -0,0 +1,21 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. + +FROM alpine AS builder + +COPY runner /runner +RUN chmod 0555 /runner + +FROM scratch + +COPY --chown=1000:1000 --from=builder /runner /files/runner + +USER 1000:1000 + +EXPOSE 8080 + +ENTRYPOINT ["/files/runner"] +CMD ["-h"] \ No newline at end of file diff --git a/test/e2e/kind/main.go b/test/e2e/kind/main.go new file mode 100644 index 0000000..94c1fb4 --- /dev/null +++ b/test/e2e/kind/main.go @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ + +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/oracle/coherence-go-client/v2/coherence" + "github.com/oracle/coherence-go-client/v2/coherence/extractors" + "github.com/oracle/coherence-go-client/v2/coherence/filters" + "log" + "math/rand/v2" + "net/http" + "os" + "strconv" + "time" +) + +type School struct { + ID int `json:"id"` + City string `json:"city"` + State string `json:"state"` + MarketSegment string `json:"marketSegment"` + CountryCode string `json:"countryCode"` + SchoolName string `json:"schoolName"` + LastModifiedDate string `json:"lastModifiedDate"` // "2006-01-02 15:04:05" +} + +var ( + session *coherence.Session + schoolsCache coherence.NamedCache[int, School] + + marketSegmentExtractor = extractors.Extract[string]("marketSegment") + marketSegmentFilter = filters.Equal[string](marketSegmentExtractor, "PRIVATE") + + rnd = rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), uint64(time.Now().UnixNano()))) //nolint:gosec // this is a test +) + +func main() { + var ( + count = 250_000 + testType = "runSchoolsTest" + err error + ) + if err = initializeCoherence(); err != nil { + log.Fatalf("failed to initialize coherence: %v", err) + } + + countOverride := os.Getenv("CACHE_COUNT") + if countOverride != "" { + count, _ = strconv.Atoi(countOverride) + } + + if value := os.Getenv("TEST_TYPE"); value != "" { + testType = value + } + + if testType == "load" { + err = populateSchoolsCache(count) + } else { + err = runSchoolsTest() + } + if err != nil { + log.Fatalf("failed to run %s: %v", testType, err) + } +} + +func initializeCoherence() error { + var err error + session, err = coherence.NewSession(context.Background(), coherence.WithPlainText()) + if err != nil { + return err + } + + // add a reconnect listener + listener := coherence.NewSessionLifecycleListener(). + OnDisconnected(func(e coherence.SessionLifecycleEvent) { + log.Printf("Session [%s] disconnected\n", e.Type()) + }) + + session.AddSessionLifecycleListener(listener) + + schoolsCache, err = coherence.GetNamedCache[int, School](session, "schools") + + return err +} + +func runSchoolsTest() error { + http.HandleFunc("/api/schools", schoolsHandler) + + fmt.Println("Server running on port 8080") + log.Fatal(http.ListenAndServe(":8080", nil)) //nolint:gosec // G404: weak RNG + + return nil +} + +func schoolsHandler(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + defer func() { + if session.IsClosed() { + log.Printf("Session [%s] is closed, calling initializeCoherence()", session.ID()) + err := initializeCoherence() + if err != nil { + log.Fatalf("failed to initialize coherence: %v", err) + } + } + }() + + switch r.Method { + + case http.MethodGet: + var ( + count int + start = time.Now() + ) + + defer func() { + duration := time.Since(start) + log.Printf("Retrieved %d entries in %s", count, duration) + }() + + var people = make([]School, 0) + + ready, err := schoolsCache.IsReady(ctx) + if err != nil || !ready { + session.Close() + log.Printf("failed to do health check: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for ch := range schoolsCache.ValuesFilter(ctx, marketSegmentFilter) { + if ch.Err != nil { + http.Error(w, ch.Err.Error(), http.StatusInternalServerError) + return + } + people = append(people, ch.Value) + count++ + } + _ = json.NewEncoder(w).Encode(people) + return + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func populateSchoolsCache(count int) error { + var ( + buffer = make(map[int]School) + err error + batchSize = 10_000 + marketSegments = []string{"K-12", "MAGNET", "CHARTER", "VIRTUAL", "RELIGIOUS", "BOARDING", "PRIVATE"} + ctx = context.Background() + ) + + if err = schoolsCache.Clear(ctx); err != nil { + return err + } + + if err = coherence.AddIndex[int, School](ctx, schoolsCache, marketSegmentExtractor, true); err != nil { + return err + } + + for i := 1; i <= count; i++ { + modifiedDate := time.Now().UTC().Format(time.DateTime) + + randomCity := randomUSCity() + buffer[i] = School{ + ID: i, + City: randomCity.City, + State: randomCity.State, + MarketSegment: randomize(marketSegments), + CountryCode: "US", + SchoolName: fmt.Sprintf("School Name %d", i), + LastModifiedDate: modifiedDate, + } + if i%batchSize == 0 { + err = schoolsCache.PutAll(ctx, buffer) + if err != nil { + return err + } + buffer = make(map[int]School) + log.Println("PutAll", i) + } + } + if len(buffer) > 0 { + return schoolsCache.PutAll(ctx, buffer) + } + + return nil +} + +func randomize(arr []string) string { + if len(arr) == 0 { + return "" + } + return arr[rnd.IntN(len(arr))] +} + +type USCity struct { + City string + State string +} + +var cities = []USCity{ + // California (CA) + {"Los Angeles", "CA"}, {"San Diego", "CA"}, {"San Jose", "CA"}, {"San Francisco", "CA"}, {"Fresno", "CA"}, + {"Sacramento", "CA"}, {"Long Beach", "CA"}, {"Oakland", "CA"}, {"Bakersfield", "CA"}, {"Anaheim", "CA"}, + {"Santa Ana", "CA"}, {"Riverside", "CA"}, {"Stockton", "CA"}, {"Irvine", "CA"}, {"Chula Vista", "CA"}, + {"Fremont", "CA"}, {"San Bernardino", "CA"}, {"Modesto", "CA"}, {"Oxnard", "CA"}, {"Fontana", "CA"}, + + // Texas (TX) + {"Houston", "TX"}, {"San Antonio", "TX"}, {"Dallas", "TX"}, {"Austin", "TX"}, {"Fort Worth", "TX"}, + {"El Paso", "TX"}, {"Arlington", "TX"}, {"Corpus Christi", "TX"}, {"Plano", "TX"}, {"Laredo", "TX"}, + {"Lubbock", "TX"}, {"Garland", "TX"}, {"Irving", "TX"}, {"Amarillo", "TX"}, {"Grand Prairie", "TX"}, + {"McKinney", "TX"}, {"Frisco", "TX"}, {"Brownsville", "TX"}, {"Pasadena", "TX"}, {"Killeen", "TX"}, + + // Florida (FL) + {"Jacksonville", "FL"}, {"Miami", "FL"}, {"Tampa", "FL"}, {"Orlando", "FL"}, {"St. Petersburg", "FL"}, + {"Hialeah", "FL"}, {"Tallahassee", "FL"}, {"Fort Lauderdale", "FL"}, {"Port St. Lucie", "FL"}, {"Cape Coral", "FL"}, + {"Pembroke Pines", "FL"}, {"Hollywood", "FL"}, {"Miramar", "FL"}, {"Gainesville", "FL"}, {"Coral Springs", "FL"}, + {"Clearwater", "FL"}, {"Palm Bay", "FL"}, {"Pompano Beach", "FL"}, {"West Palm Beach", "FL"}, {"Lakeland", "FL"}, + + // New York (NY) + {"New York", "NY"}, {"Buffalo", "NY"}, {"Rochester", "NY"}, {"Yonkers", "NY"}, {"Syracuse", "NY"}, + {"Albany", "NY"}, {"New Rochelle", "NY"}, {"Mount Vernon", "NY"}, {"Schenectady", "NY"}, {"Utica", "NY"}, + {"White Plains", "NY"}, {"Troy", "NY"}, {"Niagara Falls", "NY"}, {"Binghamton", "NY"}, {"Freeport", "NY"}, + {"Valley Stream", "NY"}, {"Hempstead", "NY"}, {"Levittown", "NY"}, {"Brentwood", "NY"}, {"Irondequoit", "NY"}, + + // Illinois (IL) + {"Chicago", "IL"}, {"Aurora", "IL"}, {"Naperville", "IL"}, {"Joliet", "IL"}, {"Rockford", "IL"}, + {"Springfield", "IL"}, {"Elgin", "IL"}, {"Peoria", "IL"}, {"Champaign", "IL"}, {"Waukegan", "IL"}, + {"Cicero", "IL"}, {"Bloomington", "IL"}, {"Arlington Heights", "IL"}, {"Evanston", "IL"}, {"Decatur", "IL"}, + {"Schaumburg", "IL"}, {"Bolingbrook", "IL"}, {"Palatine", "IL"}, {"Skokie", "IL"}, {"Des Plaines", "IL"}, + + // Pennsylvania (PA) + {"Philadelphia", "PA"}, {"Pittsburgh", "PA"}, {"Allentown", "PA"}, {"Erie", "PA"}, {"Reading", "PA"}, + {"Scranton", "PA"}, {"Bethlehem", "PA"}, {"Lancaster", "PA"}, {"Harrisburg", "PA"}, {"Altoona", "PA"}, + {"York", "PA"}, {"State College", "PA"}, {"Wilkes-Barre", "PA"}, {"Norristown", "PA"}, {"Chester", "PA"}, + {"Bethel Park", "PA"}, {"Monroeville", "PA"}, {"Plum", "PA"}, {"Easton", "PA"}, {"Lebanon", "PA"}, + + // Ohio (OH) + {"Columbus", "OH"}, {"Cleveland", "OH"}, {"Cincinnati", "OH"}, {"Toledo", "OH"}, {"Akron", "OH"}, + {"Dayton", "OH"}, {"Parma", "OH"}, {"Canton", "OH"}, {"Youngstown", "OH"}, {"Lorain", "OH"}, + {"Hamilton", "OH"}, {"Springfield", "OH"}, {"Kettering", "OH"}, {"Elyria", "OH"}, {"Lakewood", "OH"}, + {"Cuyahoga Falls", "OH"}, {"Middletown", "OH"}, {"Euclid", "OH"}, {"Newark", "OH"}, {"Mansfield", "OH"}, + + // Georgia (GA) + {"Atlanta", "GA"}, {"Augusta", "GA"}, {"Columbus", "GA"}, {"Macon", "GA"}, {"Savannah", "GA"}, + {"Athens", "GA"}, {"Sandy Springs", "GA"}, {"Roswell", "GA"}, {"Johns Creek", "GA"}, {"Warner Robins", "GA"}, + {"Albany", "GA"}, {"Alpharetta", "GA"}, {"Marietta", "GA"}, {"Valdosta", "GA"}, {"Smyrna", "GA"}, + {"Dunwoody", "GA"}, {"Rome", "GA"}, {"Peachtree City", "GA"}, {"Gainesville", "GA"}, {"Brookhaven", "GA"}, + + // North Carolina (NC) + {"Charlotte", "NC"}, {"Raleigh", "NC"}, {"Greensboro", "NC"}, {"Durham", "NC"}, {"Winston-Salem", "NC"}, + {"Fayetteville", "NC"}, {"Cary", "NC"}, {"Wilmington", "NC"}, {"High Point", "NC"}, {"Greenville", "NC"}, + {"Asheville", "NC"}, {"Concord", "NC"}, {"Gastonia", "NC"}, {"Jacksonville", "NC"}, {"Chapel Hill", "NC"}, + {"Rocky Mount", "NC"}, {"Huntersville", "NC"}, {"Burlington", "NC"}, {"Wilson", "NC"}, {"Kannapolis", "NC"}, + + // Michigan (MI) + {"Detroit", "MI"}, {"Grand Rapids", "MI"}, {"Warren", "MI"}, {"Sterling Heights", "MI"}, {"Ann Arbor", "MI"}, + {"Lansing", "MI"}, {"Flint", "MI"}, {"Dearborn", "MI"}, {"Livonia", "MI"}, {"Westland", "MI"}, + {"Troy", "MI"}, {"Farmington Hills", "MI"}, {"Kalamazoo", "MI"}, {"Wyoming", "MI"}, {"Southfield", "MI"}, + {"Rochester Hills", "MI"}, {"Taylor", "MI"}, {"Pontiac", "MI"}, {"St. Clair Shores", "MI"}, {"Royal Oak", "MI"}, +} + +func randomUSCity() USCity { + return cities[rand.IntN(len(cities))] //nolint:gosec // this is a test, +}