From 8668dc8193d8181934f123e351a6f5177d1aa50c Mon Sep 17 00:00:00 2001 From: vicdenz Date: Fri, 16 Oct 2026 19:23:16 -0400 Subject: [PATCH 1/3] Keep optimized Temporal worker opt-in and restore Valkey access --- .github/workflows/optimized-verify.yml | 15 +++++++++++++++ infra/optimized/runtime/compose.yaml | 1 + infra/optimized/runtime/indus-optimized.service | 2 +- infra/optimized/runtime/valkey.conf | 3 ++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/optimized-verify.yml b/.github/workflows/optimized-verify.yml index e905b64..ad2ada9 100644 --- a/.github/workflows/optimized-verify.yml +++ b/.github/workflows/optimized-verify.yml @@ -48,3 +48,18 @@ jobs: docker compose --env-file infra/optimized/runtime/release.env.example -f infra/optimized/runtime/compose.yaml config >/dev/null docker run --rm --env INDUS_HOSTNAME=optimized.example.com --volume "$PWD/infra/optimized/runtime/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2.10.2-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile bash -n scripts/optimized/install-release.sh infra/optimized/runtime/bootstrap.sh + - name: Check optional Temporal worker and Valkey access + shell: bash + run: | + set -euo pipefail + compose=(docker compose --env-file infra/optimized/runtime/release.env.example -f infra/optimized/runtime/compose.yaml) + default_services="$(env -u COMPOSE_PROFILES "${compose[@]}" config --services)" + if grep -qx research-worker <<< "$default_services"; then + echo "Temporal worker must stay disabled by default" >&2 + exit 1 + fi + COMPOSE_PROFILES=temporal "${compose[@]}" config --services | grep -qx research-worker + "${compose[@]}" config --format json | jq -e '(.services.valkey.ports // []) | length == 0' >/dev/null + trap '"${compose[@]}" down --volumes' EXIT + "${compose[@]}" up --detach --wait valkey + [[ "$("${compose[@]}" run --rm --no-deps --entrypoint valkey-cli valkey -h valkey ping)" == PONG ]] diff --git a/infra/optimized/runtime/compose.yaml b/infra/optimized/runtime/compose.yaml index 51b6bcd..be5721f 100644 --- a/infra/optimized/runtime/compose.yaml +++ b/infra/optimized/runtime/compose.yaml @@ -82,6 +82,7 @@ services: restart: unless-stopped research-worker: + profiles: [temporal] image: ${RESEARCH_WORKER_IMAGE:?Set RESEARCH_WORKER_IMAGE to a digest-pinned image} command: ["./bin/research-worker"] env_file: diff --git a/infra/optimized/runtime/indus-optimized.service b/infra/optimized/runtime/indus-optimized.service index 13f6147..6d586d6 100644 --- a/infra/optimized/runtime/indus-optimized.service +++ b/infra/optimized/runtime/indus-optimized.service @@ -10,7 +10,7 @@ WorkingDirectory=/opt/indus-optimized EnvironmentFile=/opt/indus-optimized/host.env ExecStartPre=/opt/indus-optimized/install-release.sh /opt/indus-optimized/release.env --refresh-secrets ExecStart=/usr/bin/docker compose --env-file /opt/indus-optimized/release.env up --detach --remove-orphans -ExecStop=/usr/bin/docker compose --env-file /opt/indus-optimized/release.env down +ExecStop=/usr/bin/docker compose --profile temporal --env-file /opt/indus-optimized/release.env down --remove-orphans TimeoutStartSec=10min [Install] diff --git a/infra/optimized/runtime/valkey.conf b/infra/optimized/runtime/valkey.conf index 5c0136b..54c6d80 100644 --- a/infra/optimized/runtime/valkey.conf +++ b/infra/optimized/runtime/valkey.conf @@ -3,6 +3,7 @@ appendfsync everysec save 60 1 maxmemory 384mb maxmemory-policy noeviction -protected-mode yes +# Docker Compose does not publish this service's port outside its application network. +protected-mode no bind 0.0.0.0 port 6379 From 3ebb9bb96b7984819d8e06835ee9a2dbc8a6ec1c Mon Sep 17 00:00:00 2001 From: vicdenz Date: Fri, 16 Oct 2026 19:56:21 -0400 Subject: [PATCH 2/3] Prepare optimized host for a reviewed 2 GiB resize --- infra/optimized/COSTS.md | 2 +- infra/optimized/README.md | 8 ++--- infra/optimized/RUNBOOK.md | 29 +++++++++++++++---- .../environment/tests/optimized.tftest.hcl | 4 +-- .../terraform/environment/variables.tf | 2 +- scripts/optimized/install-release.sh | 2 +- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/infra/optimized/COSTS.md b/infra/optimized/COSTS.md index d92e6a4..222f3de 100644 --- a/infra/optimized/COSTS.md +++ b/infra/optimized/COSTS.md @@ -5,7 +5,7 @@ reviewing spend. It is an inventory, not an estimate or billing claim. | Component | Usage input to collect | Notes | | --- | --- | --- | -| EC2 | `t3a.medium` instance-hours and CPU burst credits | Includes the Docker host; 2 GiB has not been proven for the full process set. | +| EC2 | `t3a.small` instance-hours and CPU burst credits | The 2 GiB target requires a separate, reviewed resize after memory verification. | | EBS and public IPv4 | gp3 GiB, IOPS/throughput if changed, snapshots, IPv4 hours | Valkey data occupies host EBS. | | RDS | `db.t4g.micro` hours, gp3 GiB, backups, I/O if applicable | Single-AZ is not a standby. | | S3 | artifact GiB-months, requests, retrieval/transfer | Versioning and lifecycle affect retained bytes. | diff --git a/infra/optimized/README.md b/infra/optimized/README.md index e117e11..7fcd055 100644 --- a/infra/optimized/README.md +++ b/infra/optimized/README.md @@ -9,16 +9,16 @@ repositories, VPC, EC2 host, standard RDS PostgreSQL database, S3 artifact bucket, Cognito pool, secrets, and deployment workflow. It never uses a Terraform remote-state reference to main. -The runtime is deliberately single-host and single-AZ: one `t3a.medium` +The runtime is deliberately single-host and single-AZ: one `t3a.small` EC2 host, local persistent Valkey, and one `db.t4g.micro` RDS instance. The database subnet group includes a second empty subnet in a second AZ because RDS requires it; it does not create a standby. There is no EKS, Aurora, RDS Proxy, MSK, ElastiCache, ALB, CloudFront, or NAT gateway. -The host keeps 4 GiB of memory for the complete multi-process container set. -Do not reduce it to `t3a.small` without measured memory headroom and an -approved load test. +The host target is 2 GiB. Apply the resize only after deploying an application +release with working Valkey access, completing a report, and measuring host and +container memory for 24 hours as described in the [runbook](./RUNBOOK.md). The host retains an Elastic IP because the production apex record points directly to it. There is no separate `optimized.tryindus.ca` hosted zone or diff --git a/infra/optimized/RUNBOOK.md b/infra/optimized/RUNBOOK.md index 7bbde12..ecd1794 100644 --- a/infra/optimized/RUNBOOK.md +++ b/infra/optimized/RUNBOOK.md @@ -189,11 +189,30 @@ backups and reconcile any writes made after the recovery point. ## Capacity, cost, and monitoring -The configured `t3a.medium` and `db.t4g.micro` sizes require capacity monitoring. -Measure memory, CPU credits, disk, database connections, latency, market -ingestion, and Sidekiq backlog under representative load. The host retains -4 GiB because the full process set has not been demonstrated within 2 GiB; -resize only after measuring headroom. +The target `t3a.small` host has 2 GiB of memory; the database remains +`db.t4g.micro`. This repository change does not resize a running instance. +First deploy an application release with working Valkey access. Confirm that +Sidekiq stays running and a research report finishes. Then sample host +`MemTotal` and `MemAvailable`, container memory, swap use, and OOM events +through Systems Manager for at least 24 hours, including a report run. Keep +the current 4 GiB host if peak `MemTotal - MemAvailable` exceeds 1.5 GiB, +any process is OOM killed, or Sidekiq fails to drain. This preserves at least +512 MiB of estimated headroom on a 2 GiB host. Check CPU credits, disk, +database connections, market ingestion, and health endpoints as well. + +Only after those checks, dispatch `optimized-infrastructure` with `plan` from +`main`. Verify the saved plan changes `aws_instance.host.instance_type` in +place from `t3a.medium` to `t3a.small` and has no unrelated changes. A private +`terraform.tfvars` value for `instance_type` overrides the repository default; +update the private optimized Terraform variable file and its GitHub +Environment secret if it still pins `t3a.medium`, then plan again. Dispatch +`apply` and review its saved plan before approving the production environment. +The instance type update stops and starts the EC2 host, causing downtime; +the Elastic IP and EBS-backed Valkey volume remain attached to the host. +After it starts, verify the instance type, systemd service, containers, report +creation, and health endpoints. If memory pressure or OOM kills appear, +restore `t3a.medium` in the optimized Terraform configuration and apply a +reviewed rollback plan. The same stop/start downtime applies to rollback. Budget for EC2 and any burst credits, EBS, public IPv4, RDS compute/storage/ backups/I/O, S3, Cognito and optional SES, ECR, KMS/Secrets Manager, Route 53, diff --git a/infra/optimized/terraform/environment/tests/optimized.tftest.hcl b/infra/optimized/terraform/environment/tests/optimized.tftest.hcl index bc1f410..20ef5a3 100644 --- a/infra/optimized/terraform/environment/tests/optimized.tftest.hcl +++ b/infra/optimized/terraform/environment/tests/optimized.tftest.hcl @@ -57,8 +57,8 @@ run "single_host_cost_profile" { error_message = "The DB subnet group needs two AZ subnets without a standby." } assert { - condition = aws_instance.host.instance_type == "t3a.medium" && aws_db_instance.this.instance_class == "db.t4g.micro" && aws_db_instance.this.apply_immediately == true - error_message = "The minimal profile must retain a 4-GiB host and apply its db.t4g.micro resize immediately." + condition = aws_instance.host.instance_type == "t3a.small" && aws_db_instance.this.instance_class == "db.t4g.micro" && aws_db_instance.this.apply_immediately == true + error_message = "The minimal profile must use a 2-GiB host and apply its db.t4g.micro resize immediately." } assert { condition = aws_db_instance.this.publicly_accessible == false && aws_db_instance.this.storage_encrypted == true diff --git a/infra/optimized/terraform/environment/variables.tf b/infra/optimized/terraform/environment/variables.tf index 41663a9..169b068 100644 --- a/infra/optimized/terraform/environment/variables.tf +++ b/infra/optimized/terraform/environment/variables.tf @@ -29,7 +29,7 @@ variable "alert_email_addresses" { } variable "instance_type" { type = string - default = "t3a.medium" + default = "t3a.small" } variable "root_volume_size_gib" { type = number diff --git a/scripts/optimized/install-release.sh b/scripts/optimized/install-release.sh index 8e39267..084447c 100755 --- a/scripts/optimized/install-release.sh +++ b/scripts/optimized/install-release.sh @@ -46,7 +46,7 @@ install -d -m 0700 "$runtime_dir" available_kib="$(df -Pk "$release_dir" | awk 'NR == 2 { print $4 }')" [[ "$available_kib" =~ ^[0-9]+$ && "$available_kib" -ge 5242880 ]] || { echo "need at least 5 GiB free disk" >&2; exit 1; } memory_kib="$(awk '/MemTotal:/ { print $2 }' /proc/meminfo)" -[[ "$memory_kib" =~ ^[0-9]+$ && "$memory_kib" -ge 3670016 ]] || { echo "need at least 3.5 GiB memory" >&2; exit 1; } +[[ "$memory_kib" =~ ^[0-9]+$ && "$memory_kib" -ge 1835008 ]] || { echo "need at least 1.75 GiB memory" >&2; exit 1; } refresh_secrets() { for secret in platform-api market-data research-worker; do arn_file="$release_dir/${secret}.secret-arn" From bace6c1c1b2d8ae2e19f97f5230b9bfe87c5944a Mon Sep 17 00:00:00 2001 From: vicdenz Date: Fri, 16 Oct 2026 20:16:24 -0400 Subject: [PATCH 3/3] Patch legacy Next.js cache poisoning advisories --- bun.lock | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bun.lock b/bun.lock index 6dbba61..145ce7b 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ "date-fns": "^4.1.0", "lightweight-charts": "^5.2.0", "lucide-react": "^0.525.0", - "next": "15.5.24", + "next": "15.5.27", "next-themes": "^0.4.6", "pdf-lib": "1.17.1", "react": "^19.1.0", @@ -229,23 +229,23 @@ "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], - "@next/env": ["@next/env@15.5.24", "", {}, "sha512-mBDF7T0XKZjs9SpUAl0buizVO+O02ULjOvWX8o/AZo/5AGw/UAS1Zzcylmd4pqbftzmKQi+L/nB4jgBYKEAl5Q=="], + "@next/env": ["@next/env@15.5.27", "", {}, "sha512-WC5hvVqiuFOcRyz6gKPeJkiVvSMf3scNeAV49CdWtVqxmUlzh8UKR6YHt9yiNkh8LngtzqSlVz07/Ma3ZAzchA=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AGdNLvxZNY6eR2iSnV+6wUa8CiHTMr4F7g3uHH7fT4ICIJBE00R9u4tzN/Vuwsw0cOi8MTD2HJcTCb6siMH88Q=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.27", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fBA9QurmK4+uj4wNhmRTuO5zMVRaBHCvp6H93zTG/5i6Yi6TewbvbesXB8YQNj8T99Cwrow87hHiFV6wL/hyMA=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.27", "", { "os": "darwin", "cpu": "x64" }, "sha512-FGZwJwry8qZNHxD/nfwrCwCHHrqx6epqItkkxbYcBQEQ2uw5S43bY83fQB5zbAsZE7TELMjoYkbhsQ4oKM7wwg=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-GQulAaJYNF1RYuatlsw9/uzbKz4if2V7Ic9nVO+P0ljYPm4yBRTFIl6g5YkKTw40lMLbpy9edLoH44g3jSHMYA=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-URFFsUmu8Pi0/1qdnUI7tx7keZw1Mt5zHfrIX1RfDG9Nih8DYxK54WZsDJUX2ZTlii1jgotL7qcbwKnSAES6xQ=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.24", "", { "os": "linux", "cpu": "x64" }, "sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.27", "", { "os": "linux", "cpu": "x64" }, "sha512-aJp74z7uvU5jQhyH7lyk+4qSRcTdKbKT9NQE3jiO9kWYkoHZjKi0zzHDbCoZ+v2sJDEiJ/z58R0UlWMGWNF3HA=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.24", "", { "os": "linux", "cpu": "x64" }, "sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.27", "", { "os": "linux", "cpu": "x64" }, "sha512-ILl2bQ8MbpvyMpY8AXljb3BXs0MJSC8HXZ6QV5AWpVS7ZELqNPtrHvhUk+SNarvjvICXX8bCS287TCi+gUFVYg=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.24", "", { "os": "win32", "cpu": "arm64" }, "sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.27", "", { "os": "win32", "cpu": "arm64" }, "sha512-1MB9Rh5y8TOPN2x7naNTJNkCZQav9c70AI7EBYXxsEewwOffC4DjjwBwKf20NBvcA+u+5jDv7nVmHxunlP2d7Q=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.24", "", { "os": "win32", "cpu": "x64" }, "sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.27", "", { "os": "win32", "cpu": "x64" }, "sha512-XtYgpGOswgnyBWXzOEG6q9RhvnSw0CEZ0MbWqCsl5PISgt9EzvJvNW5mU5n1PsphX/lnamzxjgQXH2GUJL8nqg=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -637,7 +637,7 @@ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "next": ["next@15.5.24", "", { "dependencies": { "@next/env": "15.5.24", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.24", "@next/swc-darwin-x64": "15.5.24", "@next/swc-linux-arm64-gnu": "15.5.24", "@next/swc-linux-arm64-musl": "15.5.24", "@next/swc-linux-x64-gnu": "15.5.24", "@next/swc-linux-x64-musl": "15.5.24", "@next/swc-win32-arm64-msvc": "15.5.24", "@next/swc-win32-x64-msvc": "15.5.24", "sharp": "^0.34.3 || ^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Y+xn8EQCoC3ZbsFPyzE+tE8XOdrWeUdUF7NeXbmg9DsgAxl5UYxlsrvgVESHTyTGigoTa1bCUrxn70F5bqt0Gw=="], + "next": ["next@15.5.27", "", { "dependencies": { "@next/env": "15.5.27", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.27", "@next/swc-darwin-x64": "15.5.27", "@next/swc-linux-arm64-gnu": "15.5.27", "@next/swc-linux-arm64-musl": "15.5.27", "@next/swc-linux-x64-gnu": "15.5.27", "@next/swc-linux-x64-musl": "15.5.27", "@next/swc-win32-arm64-msvc": "15.5.27", "@next/swc-win32-x64-msvc": "15.5.27", "sharp": "^0.34.3 || ^0.35.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-F82CrlPZ8GRxBH9RIIoR3qU1t5RT3dv1K8moe8SjlDEquiFipvEI13R1Om90TaUOEwShkWCMlg8+gvTNzfMhvg=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], diff --git a/package.json b/package.json index fa3ed10..7452c82 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "date-fns": "^4.1.0", "lightweight-charts": "^5.2.0", "lucide-react": "^0.525.0", - "next": "15.5.24", + "next": "15.5.27", "next-themes": "^0.4.6", "pdf-lib": "1.17.1", "react": "^19.1.0",