name: CI

on:
  push:
    branches: ["master"]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: pg_local_cache_ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  source-unit:
    name: source-unit (ASan + UBSan)
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: Check out source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Test the production RESP codec source
        env:
          ASAN_OPTIONS: detect_leaks=1:strict_string_checks=1
          UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
        run: make source-test source-sanitize

      - name: Test the benchmark source
        run: make benchmark-test

  monitoring-config:
    name: monitoring-config (Prometheus + Grafana + Compose)
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: Check out source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Validate source contracts and Grafana JSON
        run: |
          python3 -m unittest -v tests/monitoring_contract_test.py
          python3 -m json.tool \
            monitoring/grafana/dashboards/pg-local-cache.json >/dev/null
          bash -n monitoring/postgres/provision-monitor.sh

      - name: Validate Prometheus config, alerts, and rule tests
        run: |
          docker run --rm \
            --entrypoint /bin/promtool \
            --volume "$PWD/monitoring/prometheus:/etc/prometheus:ro" \
            prom/prometheus:v3.12.0 \
            check config /etc/prometheus/prometheus.yml
          docker run --rm \
            --entrypoint /bin/promtool \
            --volume "$PWD/monitoring/prometheus:/etc/prometheus:ro" \
            prom/prometheus:v3.12.0 \
            test rules /etc/prometheus/alerts.test.yml

      - name: Validate monitoring overlay for RESP and SQL-only profiles
        run: |
          mkdir -p secrets
          touch secrets/postgres_password \
            secrets/pg_local_cache_auth_token \
            secrets/monitor_password \
            secrets/grafana_admin_password
          docker compose \
            --file compose.yaml \
            --file compose.monitoring.yaml \
            config --quiet
          docker compose \
            --file compose.sql-only.yaml \
            --file compose.monitoring.yaml \
            config --quiet

  comparative-docker-smoke:
    name: comparative-docker-smoke
    runs-on: ubuntu-24.04
    timeout-minutes: 40
    steps:
      - name: Check out source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Exercise Compose, identical RESP client, and pgbench
        env:
          PGLC_BENCH_DURATION: "1"
          PGLC_BENCH_WARMUP_SECONDS: "0"
          PGLC_BENCH_REPETITIONS: "1"
          PGLC_BENCH_CONCURRENCY: "4"
          PGLC_BENCH_PIPELINE: "8"
          PGLC_BENCH_KEYS: "128"
          PGLC_BENCH_CACHE_ENTRIES: "256"
          PGLC_BENCH_PG_LOCAL_CACHE_WORKERS: "1"
          PGLC_BENCH_SINGLEFLIGHT_WAIT_MS: "250"
          PGLC_BENCH_SERVER_CPUS: "2"
          PGLC_BENCH_CLIENT_CPUS: "2"
          PGLC_BENCH_SERVER_MEMORY: "1g"
          PGLC_BENCH_ROW_RESP_MIN_OPS: "10000"
          PGLC_BENCH_ROW_SQL_MIN_OPS: "10000"
          PGLC_BENCH_ROW_SQL_IN_KEYS: "32"
          PGLC_BENCH_ROW_SQL_IN_MIN_OPS: "10000"
          PGLC_BENCH_ROW_WIDTH_MIN_OPS: "0"
          PGLC_BENCH_OUTPUT_DIR: ${{ runner.temp }}/comparison-smoke
        run: bash benchmarks/run.sh

      - name: Upload smoke diagnostics
        if: always()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: comparison-smoke
          path: ${{ runner.temp }}/comparison-smoke/
          if-no-files-found: warn
          retention-days: 7

  docker-integration:
    name: docker-integration (${{ matrix.profile }})
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        include:
          - profile: correctness-small-cache-2pc
            database: pg_local_cache_ci
            role: local_cache_ci_worker
            cache_entries: "128"
            max_prepared_transactions: "10"
            require_small_cache: "1"
            require_2pc: "1"
            duration: "1"
            min_ops: "0"
            concurrency: "4"
            pipeline: "8"
            keys: "128"
          - profile: warm-throughput
            database: app
            role: local_cache_worker
            cache_entries: "65536"
            max_prepared_transactions: "0"
            require_small_cache: "0"
            require_2pc: "0"
            duration: "5"
            min_ops: "10000"
            concurrency: "16"
            pipeline: "32"
            keys: "1024"
    steps:
      - name: Check out source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Validate scripts
        run: |
          bash -n docker/entrypoint.sh
          bash -n docker/healthcheck.sh
          bash -n docker/attach-table.sh
          bash -n docker/initdb/010_pg_local_cache.sh
          bash -n tests/docker_smoke.sh
          bash -n tests/docker_sql_only_smoke.sh
          bash -n tests/compatibility_matrix.sh
          bash -n benchmarks/run.sh
          python3 -m py_compile \
            benchmarks/compare.py \
            benchmarks/scenarios.py \
            benchmarks/whole_row.py \
            benchmarks/sql_only.py \
            tests/cache_contract_test.py \
            tests/whole_row_integration.py \
            tests/whole_row_benchmark_test.py \
            tests/oom_monitoring_integration.py \
            tests/pipeline_integration.py \
            tests/sql_fastpath_integration.py \
            tests/sql_api_test.py \
            tests/sql_only_benchmark_test.py

      - name: Build, integrate, and enforce warm-cache throughput
        env:
          PG_LOCAL_CACHE_SMOKE_DATABASE: ${{ matrix.database }}
          PG_LOCAL_CACHE_SMOKE_ROLE: ${{ matrix.role }}
          PG_LOCAL_CACHE_SMOKE_CACHE_ENTRIES: ${{ matrix.cache_entries }}
          PG_LOCAL_CACHE_SMOKE_MAX_PREPARED_TRANSACTIONS: ${{ matrix.max_prepared_transactions }}
          PG_LOCAL_CACHE_SMOKE_REQUIRE_SMALL_CACHE: ${{ matrix.require_small_cache }}
          PG_LOCAL_CACHE_SMOKE_REQUIRE_2PC: ${{ matrix.require_2pc }}
          PG_LOCAL_CACHE_SMOKE_DURATION: ${{ matrix.duration }}
          PG_LOCAL_CACHE_SMOKE_MIN_OPS: ${{ matrix.min_ops }}
          PG_LOCAL_CACHE_SMOKE_CONCURRENCY: ${{ matrix.concurrency }}
          PG_LOCAL_CACHE_SMOKE_PIPELINE: ${{ matrix.pipeline }}
          PG_LOCAL_CACHE_SMOKE_KEYS: ${{ matrix.keys }}
        run: bash tests/docker_smoke.sh

  docker-compatibility:
    name: docker-compatibility (PostgreSQL 14-18 ${{ matrix.variant }})
    runs-on: ubuntu-24.04
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        variant: [bookworm, alpine3.23]
    steps:
      - name: Check out source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Run checked-in PostgreSQL compatibility smoke matrix
        env:
          PGLC_MATRIX_MAJORS: 14,15,16,17,18
          PGLC_MATRIX_VARIANTS: ${{ matrix.variant }}
        run: bash tests/compatibility_matrix.sh

  docker-sql-only:
    name: docker-sql-only (SQL GET/MGET, no RESP secret)
    runs-on: ubuntu-24.04
    timeout-minutes: 30
    steps:
      - name: Check out source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Build and test the tokenless SQL-only profile
        env:
          PGLC_SQL_ONLY_BENCH_DURATION: "5"
          PGLC_SQL_ONLY_BENCH_WARMUP_SECONDS: "2"
          PGLC_SQL_ONLY_BENCH_LATENCY_DURATION: "5"
          PGLC_SQL_ONLY_BENCH_LATENCY_SAMPLE_RATE: "0.10"
          PGLC_SQL_ONLY_BENCH_LATENCY_MIN_SAMPLES: "2000"
          PGLC_SQL_ONLY_BENCH_REPETITIONS: "3"
          PGLC_SQL_ONLY_BENCH_CONCURRENCY: "16"
          PGLC_SQL_ONLY_BENCH_PIPELINE: "32"
          PGLC_SQL_ONLY_BENCH_KEYS: "4096"
          PGLC_SQL_ONLY_BENCH_PAYLOAD_BYTES: "3000"
          PGLC_SQL_ONLY_BENCH_PREPARED_MIN_OPS: "10000"
          PGLC_SQL_ONLY_BENCH_EXTENDED_MIN_OPS: "10000"
          PGLC_SQL_ONLY_BENCH_MIN_CACHED_TO_DIRECT_RATIO: "1.50"
          PGLC_SQL_ONLY_BENCH_MIN_CACHED_TO_STOCK_RATIO: "1.50"
          PGLC_SQL_ONLY_BENCH_SCALING_SNAPSHOT: "true"
          PGLC_SQL_ONLY_BENCH_SCALING_DURATION: "3"
          PGLC_SQL_ONLY_BENCH_SCALING_WARMUP_SECONDS: "1"
          PGLC_SQL_ONLY_BENCH_SCALING_LATENCY_DURATION: "3"
          PGLC_SQL_ONLY_BENCH_SCALING_LATENCY_SAMPLE_RATE: "0.10"
          PGLC_SQL_ONLY_BENCH_SCALING_LATENCY_MIN_SAMPLES: "500"
          PGLC_SQL_ONLY_BENCH_SCALING_REPETITIONS: "1"
          PGLC_SQL_ONLY_BENCH_OUTPUT_DIR: ${{ runner.temp }}/sql-only-benchmark
        run: bash tests/docker_sql_only_smoke.sh

      - name: Upload SQL-only benchmark evidence
        if: always()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: sql-only-benchmark-smoke
          path: ${{ runner.temp }}/sql-only-benchmark/
          if-no-files-found: warn
          retention-days: 30
