diff --git a/.actionlint.yml b/.actionlint.yml new file mode 100644 index 000000000..0247c95da --- /dev/null +++ b/.actionlint.yml @@ -0,0 +1,4 @@ +self-hosted-runner: + labels: + - arm-runner + - blacksmith-4vcpu-ubuntu-2404-arm diff --git a/.github/workflows/beacon_tests.yml b/.github/workflows/beacon_tests.yml index 50cc85f82..3d1a5a6c5 100644 --- a/.github/workflows/beacon_tests.yml +++ b/.github/workflows/beacon_tests.yml @@ -34,7 +34,7 @@ jobs: otp-version: 27.x # Define the OTP version [required] elixir-version: 1.18.x # Define the elixir version [required] - name: Cache Mix - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 with: path: | beacon/deps diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6f93db363..7539261d3 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -5,16 +5,78 @@ on: branches: - main +env: + POSTGRES_IMAGE: supabase/postgres:17.6.1.074 + jobs: build: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout code uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: useblacksmith/setup-docker-builder@v1 - name: Build Docker image - run: docker build . + uses: useblacksmith/build-push-action@v2 + with: + push: false + + build-burrito: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - burrito_target: linux_amd64 + runner: blacksmith-4vcpu-ubuntu-2404 + - burrito_target: linux_arm64 + runner: blacksmith-4vcpu-ubuntu-2404-arm + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: useblacksmith/setup-docker-builder@v1 + + - name: Cache Docker images + uses: actions/cache@v5 + id: docker-cache + with: + path: /tmp/docker-images + key: docker-images-zstd-${{ env.POSTGRES_IMAGE }} + - name: Load Docker images from cache + if: steps.docker-cache.outputs.cache-hit == 'true' + run: zstd -d --stdout /tmp/docker-images/postgres.tar.zst | docker image load + - name: Pull and save Docker images + if: steps.docker-cache.outputs.cache-hit != 'true' + run: | + docker pull ${{ env.POSTGRES_IMAGE }} + mkdir -p /tmp/docker-images + docker image save ${{ env.POSTGRES_IMAGE }} | zstd -T0 -o /tmp/docker-images/postgres.tar.zst + + - name: Build realtime image with cache + uses: useblacksmith/build-push-action@v2 + with: + load: true + build-args: BURRITO_TARGET=${{ matrix.burrito_target }} + tags: realtime-realtime:latest + + - name: Build and start Burrito binary + env: + BURRITO_TARGET: ${{ matrix.burrito_target }} + run: docker compose -p realtime -f docker-compose.burrito.yml up --wait + + - name: Test healthcheck endpoint + run: curl -f http://localhost:4001/healthcheck + + - name: Print logs on failure + if: failure() + run: docker compose -p realtime -f docker-compose.burrito.yml logs + + - name: Tear down + if: always() + run: docker compose -p realtime -f docker-compose.burrito.yml down diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 14f3f093c..c836d94f9 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -33,32 +33,5 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Cache Docker images - uses: actions/cache@v5 - id: docker-cache - with: - path: /tmp/docker-images - key: docker-images-integration-zstd-${{ env.POSTGRES_IMAGE }}-${{ env.DENO_IMAGE }} - - name: Load Docker images from cache - if: steps.docker-cache.outputs.cache-hit == 'true' - run: | - zstd -d --stdout /tmp/docker-images/postgres.tar.zst | docker image load & - PID1=$! - zstd -d --stdout /tmp/docker-images/deno.tar.zst | docker image load & - PID2=$! - wait $PID1 || exit $? - wait $PID2 || exit $? - - name: Pull and save Docker images - if: steps.docker-cache.outputs.cache-hit != 'true' - run: | - docker pull ${{ env.POSTGRES_IMAGE }} & - PID1=$! - docker pull ${{ env.DENO_IMAGE }} & - PID2=$! - wait $PID1 || exit $? - wait $PID2 || exit $? - mkdir -p /tmp/docker-images - docker image save ${{ env.POSTGRES_IMAGE }} | zstd -T0 -o /tmp/docker-images/postgres.tar.zst - docker image save ${{ env.DENO_IMAGE }} | zstd -T0 -o /tmp/docker-images/deno.tar.zst - name: Run integration test run: docker compose -f docker-compose.tests.yml up --abort-on-container-exit --exit-code-from test-runner diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6ea3dcb98..90d78dbc9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -35,7 +35,7 @@ jobs: otp-version: 27.x # Define the OTP version [required] elixir-version: 1.18.x # Define the elixir version [required] - name: Cache Mix - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 with: path: | deps @@ -59,7 +59,7 @@ jobs: - name: Run sobelow run: mix sobelow --config .sobelow-conf - name: Retrieve PLT Cache - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 id: plt-cache with: path: priv/plts diff --git a/.github/workflows/prod_build.yml b/.github/workflows/prod_build.yml index 8ef2f2769..ff9e025da 100644 --- a/.github/workflows/prod_build.yml +++ b/.github/workflows/prod_build.yml @@ -35,6 +35,104 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACTION }} + binary_linux_amd64: + needs: release + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: needs.release.outputs.published == 'true' + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + with: + ref: v${{ needs.release.outputs.version }} + + - uses: erlef/setup-beam@v1 + with: + otp-version: 27.x + elixir-version: 1.18.x + + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl + + - run: mix deps.get --only prod + + - name: Build Burrito binary + env: + MIX_ENV: prod + BURRITO_TARGET: linux_amd64 + SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} + run: mix release + + - name: Upload binary to GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACTION }} + VERSION: ${{ needs.release.outputs.version }} + run: gh release upload "v${VERSION}" burrito_out/realtime_linux_amd64 --clobber + + binary_linux_arm64: + needs: release + runs-on: arm-runner + if: needs.release.outputs.published == 'true' + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + with: + ref: v${{ needs.release.outputs.version }} + + - uses: erlef/setup-beam@v1 + with: + otp-version: 27.x + elixir-version: 1.18.x + + - uses: dtolnay/rust-toolchain@stable + + - run: mix deps.get --only prod + + - name: Build Burrito binary + env: + MIX_ENV: prod + BURRITO_TARGET: linux_arm64 + SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} + run: mix release + + - name: Upload binary to GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACTION }} + VERSION: ${{ needs.release.outputs.version }} + run: gh release upload "v${VERSION}" burrito_out/realtime_linux_arm64 --clobber + + binary_macos_arm64: + needs: release + runs-on: macos-15 + if: needs.release.outputs.published == 'true' + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + with: + ref: v${{ needs.release.outputs.version }} + + - uses: erlef/setup-beam@v1 + with: + otp-version: 27.x + elixir-version: 1.18.x + + - uses: dtolnay/rust-toolchain@stable + + - run: mix deps.get --only prod + + - name: Build Burrito binary + env: + MIX_ENV: prod + BURRITO_TARGET: macos_arm64 + SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }} + run: mix release + + - name: Upload binary to GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PROJECT_ACTION }} + VERSION: ${{ needs.release.outputs.version }} + run: gh release upload "v${VERSION}" burrito_out/realtime_macos_arm64 --clobber + docker_x86_release: needs: release runs-on: blacksmith-4vcpu-ubuntu-2404 diff --git a/.github/workflows/prod_linter.yml b/.github/workflows/prod_linter.yml index 2046f8d75..5edf1f4f4 100644 --- a/.github/workflows/prod_linter.yml +++ b/.github/workflows/prod_linter.yml @@ -18,7 +18,7 @@ jobs: otp-version: 27.x # Define the OTP version [required] elixir-version: 1.18.x # Define the elixir version [required] - name: Cache Mix - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -36,7 +36,7 @@ jobs: - name: Credo checks run: mix credo --strict --mute-exit-status - name: Retrieve PLT Cache - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 id: plt-cache with: path: priv/plts diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e482effec..c7e2dc643 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,7 +32,7 @@ jobs: components: rustfmt, clippy - name: Cache Cargo - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 with: path: | ~/.cargo/registry/index diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index daf1c976d..a47399a8e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,7 +43,7 @@ jobs: otp-version: 27.x # Define the OTP version [required] elixir-version: 1.18.x # Define the elixir version [required] - name: Cache Mix - uses: actions/cache@v5 + uses: useblacksmith/cache@v5 with: path: | deps diff --git a/.gitignore b/.gitignore index b061cba99..12db4bde3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ demo/.env # Rust/Rustler build artifacts /native/*/target/ -/priv/native/ \ No newline at end of file +/priv/native/ + +# Burrito binary output +/burrito_out/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f44ad7305..1ed999f0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,18 +3,21 @@ ARG OTP_VERSION=27.3 ARG DEBIAN_VERSION=bookworm-20250929-slim ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" +ARG ZIG_VERSION=0.15.2 FROM ${BUILDER_IMAGE} AS builder -ENV MIX_ENV="prod" +ARG ZIG_VERSION +ARG BURRITO_TARGET="" -RUN apt-get update -y \ - && apt-get install curl -y \ - && apt-get install -y build-essential git \ - && apt-get clean +ENV MIX_ENV="prod" +ENV RUSTUP_HOME=/usr/local/rustup +ENV CARGO_HOME=/usr/local/cargo +ENV PATH="/usr/local/cargo/bin:/usr/local/zig:${PATH}" -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -ENV PATH="/root/.cargo/bin:${PATH}" +RUN apt-get update -y && apt-get install -y \ + build-essential git curl xz-utils \ + && apt-get clean && rm -rf /var/lib/apt/lists/* RUN set -uex; \ apt-get update; \ @@ -28,45 +31,51 @@ RUN set -uex; \ apt-get -qy update; \ apt-get -qy install nodejs; -# prepare build dir +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --default-toolchain stable + +RUN if [ -n "${BURRITO_TARGET}" ]; then \ + ARCH=$(uname -m) && \ + curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ARCH}-linux-${ZIG_VERSION}.tar.xz" \ + | tar -xJ -C /usr/local/ && \ + mv /usr/local/zig-${ARCH}-linux-${ZIG_VERSION} /usr/local/zig && \ + rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu && \ + cargo install cargo-zigbuild; \ + fi + WORKDIR /app -# install hex + rebar -RUN mix local.hex --force && \ - mix local.rebar --force +RUN mix local.hex --force && mix local.rebar --force -# install mix dependencies COPY mix.exs mix.lock ./ COPY beacon beacon RUN mix deps.get --only $MIX_ENV RUN mkdir config -# copy compile-time config files before we compile dependencies -# to ensure any relevant config change will trigger the dependencies -# to be re-compiled. COPY config/config.exs config/${MIX_ENV}.exs config/ RUN mix deps.compile + COPY priv priv COPY lib lib COPY native native COPY assets assets -# compile assets with esbuild and npm -RUN cd assets \ - && npm install \ - && cd .. \ - && mix assets.deploy +RUN cd assets && npm install && cd .. && mix assets.deploy -# Compile the release RUN mix compile -# Changes to config/runtime.exs don't require recompiling the code COPY config/runtime.exs config/ COPY rel rel -RUN mix release -# start a new build stage so that the final image will only contain -# the compiled release and other runtime necessities +RUN mkdir -p /app/release && \ + if [ -n "${BURRITO_TARGET}" ]; then \ + BURRITO_TARGET=${BURRITO_TARGET} mix release && \ + cp burrito_out/realtime_${BURRITO_TARGET} /app/release/realtime; \ + else \ + mix release && \ + cp -r _build/prod/rel/realtime/. /app/release/; \ + fi + FROM ${RUNNER_IMAGE} ARG SLOT_NAME_SUFFIX @@ -76,21 +85,22 @@ ENV SLOT_NAME_SUFFIX="${SLOT_NAME_SUFFIX}" \ LC_ALL="en_US.UTF-8" \ MIX_ENV="prod" \ ECTO_IPV6="true" \ - ERL_AFLAGS="-proto_dist inet6_tcp" + ERL_AFLAGS="-proto_dist inet6_tcp" \ + BURRITO_CACHE_DIR=/tmp/burrito_cache RUN apt-get update -y && \ apt-get install -y libstdc++6 openssl libncurses5 locales iptables sudo tini curl awscli jq && \ apt-get clean && rm -f /var/lib/apt/lists/*_* -# Set the locale RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen WORKDIR "/app" RUN chown nobody /app -COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/realtime ./ +COPY --from=builder --chown=nobody:root /app/release ./ COPY run.sh run.sh RUN ls -la /app + ENTRYPOINT ["/usr/bin/tini", "-s", "-g", "--", "/app/run.sh"] CMD ["/app/bin/server"] diff --git a/Makefile b/Makefile index 1259a1335..018afa86a 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,20 @@ seed: ## Seed the database prod: ## Start a server with a MIX_ENV=prod ELIXIR_ERL_OPTIONS="+hmax 1000000000" SLOT_NAME_SUFFIX=some_sha MIX_ENV=prod FLY_APP_NAME=realtime-local API_KEY=dev SECURE_CHANNELS=true API_JWT_SECRET=dev METRICS_JWT_SECRET=dev FLY_REGION=fra FLY_ALLOC_ID=123e4567-e89b-12d3-a456-426614174000 DB_ENC_KEY="1234567890123456" SECRET_KEY_BASE=M+55t7f6L9VWyhH03R5N7cIhrdRlZaMDfTE6Udz0eZS7gCbnoLQ8PImxwhEyao6D DASHBOARD_USER=realtime_local DASHBOARD_PASSWORD=password ERL_AFLAGS="-kernel shell_history enabled" iex -S mix phx.server +release.%: ## Build a single binary for a target. e.g. release.linux_amd64 + MIX_ENV=prod BURRITO_TARGET=$* mix release + +release.all: ## Build single binaries for all targets + MIX_ENV=prod BURRITO_TARGET=macos_arm64 mix release + MIX_ENV=prod BURRITO_TARGET=linux_amd64 mix release + MIX_ENV=prod BURRITO_TARGET=linux_arm64 mix release + +burrito.up: ## Start the Burrito binary with Docker Compose + docker compose -f docker-compose.burrito.yml up + +burrito.build: ## Build the Burrito Docker image (BURRITO_TARGET defaults to linux_arm64) + docker compose -f docker-compose.burrito.yml build + bench.%: ## Run benchmark with a specific file. e.g. bench.secrets ELIXIR_ERL_OPTIONS="+hmax 1000000000" SLOT_NAME_SUFFIX=some_sha MIX_ENV=dev SECURE_CHANNELS=true API_JWT_SECRET=dev METRICS_JWT_SECRET=dev FLY_REGION=fra FLY_ALLOC_ID=123e4567-e89b-12d3-a456-426614174000 DB_ENC_KEY="1234567890123456" ERL_AFLAGS="-kernel shell_history enabled" mix run bench/$* diff --git a/assets/package-lock.json b/assets/package-lock.json index 84debc7bd..e17fd551a 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -5,125 +5,139 @@ "packages": { "": { "dependencies": { - "@supabase/supabase-js": "^2.50.0" + "@supabase/supabase-js": "^2.85.0" } }, "node_modules/@supabase/auth-js": { - "version": "2.70.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz", - "integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==", + "version": "2.98.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.98.0.tgz", + "integrity": "sha512-GBH361T0peHU91AQNzOlIrjUZw9TZbB9YDRiyFgk/3Kvr3/Z1NWUZ2athWTfHhwNNi8IrW00foyFxQD9IO/Trg==", + "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/functions-js": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", - "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", - "dependencies": { - "@supabase/node-fetch": "^2.6.14" - } - }, - "node_modules/@supabase/node-fetch": { - "version": "2.6.15", - "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", - "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "version": "2.98.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.98.0.tgz", + "integrity": "sha512-N/xEyiNU5Org+d+PNCpv+TWniAXRzxIURxDYsS/m2I/sfAB/HcM9aM2Dmf5edj5oWb9GxID1OBaZ8NMmPXL+Lg==", + "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "tslib": "2.8.1" }, "engines": { - "node": "4.x || >=6.0.0" + "node": ">=20.0.0" } }, "node_modules/@supabase/postgrest-js": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", - "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "version": "2.98.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.98.0.tgz", + "integrity": "sha512-v6e9WeZuJijzUut8HyXu6gMqWFepIbaeaMIm1uKzei4yLg9bC9OtEW9O14LE/9ezqNbSAnSLO5GtOLFdm7Bpkg==", + "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/realtime-js": { - "version": "2.11.10", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz", - "integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==", + "version": "2.98.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.98.0.tgz", + "integrity": "sha512-rOWt28uGyFipWOSd+n0WVMr9kUXiWaa7J4hvyLCIHjRFqWm1z9CaaKAoYyfYMC1Exn3WT8WePCgiVhlAtWC2yw==", + "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.13", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", + "tslib": "2.8.1", "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/storage-js": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", - "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "version": "2.98.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.98.0.tgz", + "integrity": "sha512-tzr2mG+v7ILSAZSfZMSL9OPyIH4z1ikgQ8EcQTKfMRz4EwmlFt3UnJaGzSOxyvF5b+fc9So7qdSUWTqGgeLokQ==", + "license": "MIT", "dependencies": { - "@supabase/node-fetch": "^2.6.14" + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@supabase/supabase-js": { - "version": "2.50.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz", - "integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==", + "version": "2.98.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz", + "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==", + "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.70.0", - "@supabase/functions-js": "2.4.4", - "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "1.19.4", - "@supabase/realtime-js": "2.11.10", - "@supabase/storage-js": "2.7.1" + "@supabase/auth-js": "2.98.0", + "@supabase/functions-js": "2.98.0", + "@supabase/postgrest-js": "2.98.0", + "@supabase/realtime-js": "2.98.0", + "@supabase/storage-js": "2.98.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@types/node": { - "version": "22.15.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", - "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/phoenix": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", - "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/config/config.exs b/config/config.exs index 8550e0d72..ea944d23a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -86,6 +86,8 @@ config :gen_rpc, config :prom_ex, :storage_adapter, Realtime.PromEx.Store config :realtime, Realtime.PromEx, ets_flush_interval: 90_000 config :realtime, Realtime.TenantPromEx, ets_flush_interval: 90_000 +config :realtime, :dev_mode, false +config :realtime, :api_url, "https://{tenant}.supabase.co/realtime/v1" # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/config/dev.exs b/config/dev.exs index 9694bb350..2f5d963c4 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,5 +1,8 @@ import Config +config :realtime, :dev_mode, true +config :realtime, :api_url, "http://{tenant}.localhost:4000/" + # For development, we disable any cache and enable # debugging and code reloading. # diff --git a/docker-compose.burrito.yml b/docker-compose.burrito.yml new file mode 100644 index 000000000..5411c5274 --- /dev/null +++ b/docker-compose.burrito.yml @@ -0,0 +1,53 @@ +services: + db: + image: ${POSTGRES_IMAGE:-supabase/postgres:17.6.1.074} + container_name: burrito-db + ports: + - "5434:5432" + volumes: + - ./dev/postgres/00-supabase-schema.sql:/docker-entrypoint-initdb.d/00-supabase-schema.sql + command: postgres -c config_file=/etc/postgresql/postgresql.conf + environment: + POSTGRES_HOST: /var/run/postgresql + POSTGRES_PASSWORD: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 10 + + realtime: + depends_on: + db: + condition: service_healthy + build: + context: . + args: + BURRITO_TARGET: ${BURRITO_TARGET:-linux_arm64} + container_name: burrito-realtime + ports: + - "4001:4000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4000/healthcheck"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 30s + environment: + PORT: 4000 + APP_NAME: realtime + DB_HOST: db + DB_PORT: 5432 + DB_USER: supabase_admin + DB_PASSWORD: postgres + DB_NAME: postgres + DB_ENC_KEY: supabaserealtime + DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" + API_JWT_SECRET: dev-jwt-secret-for-testing + METRICS_JWT_SECRET: dev-metrics-secret + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1u2oq + ERL_AFLAGS: "-proto_dist inet_tcp" + CLUSTER_STRATEGIES: EPMD + DNS_NODES: "''" + SEED_SELF_HOST: "true" + LOG_LEVEL: info diff --git a/lib/realtime/monitoring/os_metrics.ex b/lib/realtime/monitoring/os_metrics.ex index b2623d2f0..1eaeb6bcd 100644 --- a/lib/realtime/monitoring/os_metrics.ex +++ b/lib/realtime/monitoring/os_metrics.ex @@ -6,7 +6,13 @@ defmodule Realtime.OsMetrics do @spec ram_usage() :: float() def ram_usage do mem = :memsup.get_system_memory_data() - free_mem = if Mix.env() in [:dev, :test], do: mem[:free_memory], else: mem[:available_memory] + + free_mem = + case Keyword.fetch(mem, :available_memory) do + {:ok, value} -> value + :error -> mem[:free_memory] + end + 100 - free_mem / mem[:total_memory] * 100 end diff --git a/lib/realtime/signal_handler.ex b/lib/realtime/signal_handler.ex index 46908cc67..b22562cc1 100644 --- a/lib/realtime/signal_handler.ex +++ b/lib/realtime/signal_handler.ex @@ -3,28 +3,36 @@ defmodule Realtime.SignalHandler do @behaviour :gen_event require Logger - @spec shutdown_in_progress? :: :ok | {:error, :shutdown_in_progress} + @spec shutdown_in_progress?() :: :ok | {:error, :shutdown_in_progress} def shutdown_in_progress? do - case !!Application.get_env(:realtime, :shutdown_in_progress) do - true -> {:error, :shutdown_in_progress} - false -> :ok - end + if Application.get_env(:realtime, :shutdown_in_progress), + do: {:error, :shutdown_in_progress}, + else: :ok end @impl true def init({%{handler_mod: _} = args, :ok}) do - {:ok, args} + {:ok, Map.put_new(args, :shutdown_fn, fn -> System.stop(0) end)} end @impl true def handle_event(signal, %{handler_mod: handler_mod} = state) do - Logger.error("#{__MODULE__}: #{inspect(signal)} received") + case signal do + :sigterm -> + Logger.warning("#{__MODULE__}: :sigterm received") + Application.put_env(:realtime, :shutdown_in_progress, true) + handler_mod.handle_event(signal, state) - if signal == :sigterm do - Application.put_env(:realtime, :shutdown_in_progress, true) - end + :sigint -> + Application.put_env(:realtime, :shutdown_in_progress, true) + Logger.notice("#{__MODULE__}: SIGINT received - shutting down") + Task.start(state.shutdown_fn) + {:ok, state} - handler_mod.handle_event(signal, state) + _ -> + Logger.error("#{__MODULE__}: unexpected signal #{inspect(signal)} received") + handler_mod.handle_event(signal, state) + end end @impl true diff --git a/lib/realtime_web/api_spec.ex b/lib/realtime_web/api_spec.ex index 2c84a4a65..f027827d8 100644 --- a/lib/realtime_web/api_spec.ex +++ b/lib/realtime_web/api_spec.ex @@ -13,13 +13,11 @@ defmodule RealtimeWeb.ApiSpec do @behaviour OpenApi + @api_url Application.compile_env(:realtime, :api_url, "https://{tenant}.supabase.co/realtime/v1") + @impl OpenApi def spec do - url = - case Mix.env() do - :prod -> "https://{tenant}.supabase.co/realtime/v1" - _ -> "http://{tenant}.localhost:4000/" - end + url = @api_url %OpenApi{ servers: [ diff --git a/lib/realtime_web/live/ping_live.ex b/lib/realtime_web/live/ping_live.ex index 6cc4a0cd2..a89495742 100644 --- a/lib/realtime_web/live/ping_live.ex +++ b/lib/realtime_web/live/ping_live.ex @@ -30,8 +30,9 @@ defmodule RealtimeWeb.PingLive do {:noreply, assign(socket, :ping, pong <> " ms")} end + @ping_timer if Application.compile_env(:realtime, :dev_mode, false), do: 60_000, else: 1_000 + defp ping do - timer = if Mix.env() == :dev, do: 60_000, else: 1_000 - Process.send_after(self(), :ping, timer) + Process.send_after(self(), :ping, @ping_timer) end end diff --git a/lib/realtime_web/live/time_live.ex b/lib/realtime_web/live/time_live.ex index c8554c1a7..36dc26139 100644 --- a/lib/realtime_web/live/time_live.ex +++ b/lib/realtime_web/live/time_live.ex @@ -15,8 +15,10 @@ defmodule RealtimeWeb.TimeLive do {:noreply, assign_time(socket)} end + @time_timer if Application.compile_env(:realtime, :dev_mode, false), do: 60_000, else: 100 + defp assign_time(socket) do - timer = if Mix.env() == :dev, do: 60_000, else: 100 + timer = @time_timer Process.send_after(self(), :time, timer) now = DateTime.utc_now() |> DateTime.to_string() diff --git a/mix.exs b/mix.exs index f4e80743a..b864b92d5 100644 --- a/mix.exs +++ b/mix.exs @@ -14,10 +14,17 @@ defmodule Realtime.MixProject do test_coverage: [tool: ExCoveralls], releases: [ realtime: [ - # This will ensure that if opentelemetry terminates, even abnormally, our application will not be terminated. applications: [ opentelemetry_exporter: :permanent, opentelemetry: :temporary + ], + steps: release_steps(), + burrito: [ + targets: [ + linux_amd64: [os: :linux, cpu: :x86_64, skip_nifs: true], + linux_arm64: [os: :linux, cpu: :aarch64, skip_nifs: true], + macos_arm64: [os: :darwin, cpu: :aarch64, skip_nifs: true] + ] ] ] ] @@ -40,7 +47,7 @@ defmodule Realtime.MixProject do def application do [ mod: {Realtime.Application, []}, - extra_applications: [:logger, :runtime_tools, :prom_ex, :mix, :os_mon] + extra_applications: [:logger, :runtime_tools, :prom_ex, :os_mon] ] end @@ -108,7 +115,8 @@ defmodule Realtime.MixProject do {:dialyxir, "~> 1.4", only: :dev, runtime: false}, {:poolboy, "~> 1.5", only: :test}, {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false}, - {:rustler, "~> 0.37", runtime: false} + {:rustler, "~> 0.37", runtime: false}, + {:burrito, "~> 1.5"} ] end @@ -118,6 +126,69 @@ defmodule Realtime.MixProject do # $ mix setup # # See the documentation for `Mix` for more info on aliases. + defp release_steps do + if System.get_env("BURRITO_TARGET") not in [nil, ""] do + [:assemble, &compile_assets/1, &cross_compile_nif/1, &Burrito.wrap/1] + else + [:assemble] + end + end + + defp compile_assets(%Mix.Release{} = release) do + Mix.shell().info("Compiling assets for Burrito release") + Mix.Task.run("cmd", ["--cd", "assets", "npm", "install"]) + Mix.Task.run("assets.deploy") + release + end + + defp cross_compile_nif(%Mix.Release{} = release) do + burrito_target = System.get_env("BURRITO_TARGET") |> then(&if(&1 == "", do: nil, else: &1)) + + if burrito_target != nil and burrito_target != host_platform() do + {rust_target, src_filename} = nif_rust_target(burrito_target) + crate_dir = Path.join([File.cwd!(), "native", "prometheus_remote_write"]) + + Mix.shell().info("Cross-compiling NIF for #{burrito_target} (#{rust_target})") + + {output, code} = + System.cmd("cargo", ["zigbuild", "--release", "--target", rust_target], + cd: crate_dir, + stderr_to_stdout: true + ) + + if code != 0, do: Mix.raise("NIF cross-compilation failed:\n#{output}") + + src = Path.join([crate_dir, "target", rust_target, "release", src_filename]) + + dst = + Path.join([release.path, "lib", "realtime-#{release.version}", "priv", "native", src_filename]) + + File.mkdir_p!(Path.dirname(dst)) + File.cp!(src, dst) + Mix.shell().info("NIF installed at #{dst}") + else + Mix.shell().info("NIF: native build, skipping cross-compilation") + end + + release + end + + defp host_platform do + arch = :erlang.system_info(:system_architecture) |> List.to_string() + {_family, os} = :os.type() + + cond do + os == :darwin and String.contains?(arch, "aarch64") -> "macos_arm64" + os == :linux and String.contains?(arch, "aarch64") -> "linux_arm64" + os == :linux and String.contains?(arch, "x86_64") -> "linux_amd64" + true -> "unknown" + end + end + + defp nif_rust_target("linux_amd64"), do: {"x86_64-unknown-linux-gnu", "libprometheus_remote_write.so"} + defp nif_rust_target("linux_arm64"), do: {"aarch64-unknown-linux-gnu", "libprometheus_remote_write.so"} + defp nif_rust_target("macos_arm64"), do: {"aarch64-apple-darwin", "libprometheus_remote_write.dylib"} + defp aliases do [ setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"], diff --git a/mix.lock b/mix.lock index a98c823bb..11927d110 100644 --- a/mix.lock +++ b/mix.lock @@ -3,8 +3,9 @@ "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "bertex": {:hex, :bertex, "1.3.0", "0ad0df9159b5110d9d2b6654f72fbf42a54884ef43b6b651e6224c0af30ba3cb", [:mix], [], "hexpm", "0a5d5e478bb5764b7b7bae37cae1ca491200e58b089df121a2fe1c223d8ee57a"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "burrito": {:hex, :burrito, "1.5.0", "d68ec01df2871f1d5bc603b883a78546c75761ac73c1bec1b7ae2cc74790fcd1", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "3861abda7bffa733862b48da3e03df0b4cd41abf6fd24b91745f5c16d971e5fa"}, "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, - "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"}, "corsica": {:hex, :corsica, "2.1.3", "dccd094ffce38178acead9ae743180cdaffa388f35f0461ba1e8151d32e190e6", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "616c08f61a345780c2cf662ff226816f04d8868e12054e68963e95285b5be8bc"}, "cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"}, @@ -27,7 +28,7 @@ "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, + "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gen_rpc": {:git, "https://github.com/supabase/gen_rpc.git", "5382a0f2689a4cb8838873a2173928281dbe5002", [ref: "5382a0f2689a4cb8838873a2173928281dbe5002"]}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, @@ -76,7 +77,7 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, @@ -85,7 +86,7 @@ "prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, - "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, + "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rustler": {:hex, :rustler, "0.37.3", "5f4e6634d43b26f0a69834dd1d3ed4e1710b022a053bf4a670220c9540c92602", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a6872c6f53dcf00486d1e7f9e046e20e01bf1654bdacc4193016c2e8002b32a2"}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "snabbkaffe": {:git, "https://github.com/kafka4beam/snabbkaffe", "b59298334ed349556f63405d1353184c63c66534", [tag: "1.0.10"]}, diff --git a/native/prometheus_remote_write/src/lib.rs b/native/prometheus_remote_write/src/lib.rs index 2b2941042..e2b3057f8 100644 --- a/native/prometheus_remote_write/src/lib.rs +++ b/native/prometheus_remote_write/src/lib.rs @@ -216,7 +216,12 @@ fn decode<'a>(env: Env<'a>, bytes: Binary<'a>) -> Term<'a> { (atoms::ok(), series.encode(env)).encode(env) } -rustler::init!("Elixir.Realtime.PrometheusRemoteWrite"); +#[allow(dead_code)] +fn on_upgrade(_env: rustler::Env, _old_code: rustler::Term) -> bool { + true +} + +rustler::init!("Elixir.Realtime.PrometheusRemoteWrite", on_upgrade = on_upgrade); #[cfg(test)] mod tests { diff --git a/priv/static/cache_manifest.json b/priv/static/cache_manifest.json new file mode 100644 index 000000000..c61565d10 --- /dev/null +++ b/priv/static/cache_manifest.json @@ -0,0 +1,6 @@ +{ + "!comment!":"This file was auto-generated by `mix phx.digest`. Remove it and all generated artefacts with `mix phx.digest.clean --all`", + "version":1, + "latest":{"assets/app.css":"assets/app-7099925af14a23bf11734ed5e61fb829.css","assets/app.js":"assets/app-b8b903ed8c6ba26d214bd1052abf66d5.js","favicon.svg":"favicon-357c209cd8c6fa1c20761fe5486aac22.svg","robots.txt":"robots-9e2c81b0855bbff2baa8371bc4a78186.txt","worker.js":"worker-800f90ec3bbf81e19ce35042e7690c7c.js"}, + "digests":{"assets/app-7099925af14a23bf11734ed5e61fb829.css":{"size":30057,"sha512":"jtRPjXIM+UKbkuTob+Plqj4xY66zU8TnmL36hKpRq/5Ym++Fud6pXu70bGmNTlwDcMZRcYLRbq+PdNE9lCnOQw==","mtime":63939844542,"digest":"7099925af14a23bf11734ed5e61fb829","logical_path":"assets/app.css"},"assets/app-b8b903ed8c6ba26d214bd1052abf66d5.js":{"size":292160,"sha512":"UeCR6V5vEmIYOR7eRm5ZA6Bbfv1zQkBIek5JjPwLq6XYij8P/Mvpzzel6XWETryY80rQ7sZCymabuBMAYvwUGw==","mtime":63939844542,"digest":"b8b903ed8c6ba26d214bd1052abf66d5","logical_path":"assets/app.js"},"favicon-357c209cd8c6fa1c20761fe5486aac22.svg":{"size":1107,"sha512":"0DzZx5UBYqDeultsH6bOLWDn3iKD0GsJv6EUvlLngQzSgs+gMwgSDHMAr3K+Sh8J0NWqLIx8Uo5Dttw+U6uo9g==","mtime":63939844542,"digest":"357c209cd8c6fa1c20761fe5486aac22","logical_path":"favicon.svg"},"robots-9e2c81b0855bbff2baa8371bc4a78186.txt":{"size":203,"sha512":"xbItKTfrkHgEWxk3S1qNTCr+mlz5yzeyGC2rcGhs1oKdf4N+D4T/WWt+qBnOnT4gMuKQ+PkgLFffla3w8rPBaQ==","mtime":63939844542,"digest":"9e2c81b0855bbff2baa8371bc4a78186","logical_path":"robots.txt"},"worker-800f90ec3bbf81e19ce35042e7690c7c.js":{"size":156,"sha512":"skMASsFwRPZx7IPz4FO9NR4Ysqmw/c0uMZNIf4m80ej7ZE5X93ejMypOFOmXc2TBFDF1daAbYYK1hp13zzfThA==","mtime":63939844542,"digest":"800f90ec3bbf81e19ce35042e7690c7c","logical_path":"worker.js"}} +} diff --git a/priv/static/favicon-357c209cd8c6fa1c20761fe5486aac22.svg b/priv/static/favicon-357c209cd8c6fa1c20761fe5486aac22.svg new file mode 100644 index 000000000..ad802ac16 --- /dev/null +++ b/priv/static/favicon-357c209cd8c6fa1c20761fe5486aac22.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/priv/static/favicon-357c209cd8c6fa1c20761fe5486aac22.svg.gz b/priv/static/favicon-357c209cd8c6fa1c20761fe5486aac22.svg.gz new file mode 100644 index 000000000..084e8995d Binary files /dev/null and b/priv/static/favicon-357c209cd8c6fa1c20761fe5486aac22.svg.gz differ diff --git a/priv/static/favicon.svg.gz b/priv/static/favicon.svg.gz new file mode 100644 index 000000000..084e8995d Binary files /dev/null and b/priv/static/favicon.svg.gz differ diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt new file mode 100644 index 000000000..26e06b5f1 --- /dev/null +++ b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz new file mode 100644 index 000000000..a1d6ca87a Binary files /dev/null and b/priv/static/robots-9e2c81b0855bbff2baa8371bc4a78186.txt.gz differ diff --git a/priv/static/robots.txt.gz b/priv/static/robots.txt.gz new file mode 100644 index 000000000..a1d6ca87a Binary files /dev/null and b/priv/static/robots.txt.gz differ diff --git a/priv/static/worker-800f90ec3bbf81e19ce35042e7690c7c.js b/priv/static/worker-800f90ec3bbf81e19ce35042e7690c7c.js new file mode 100644 index 000000000..426ef38f0 --- /dev/null +++ b/priv/static/worker-800f90ec3bbf81e19ce35042e7690c7c.js @@ -0,0 +1,5 @@ +addEventListener("message", (e) => { + if (e.data.event === "start") { + setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval); + } +}); diff --git a/priv/static/worker-800f90ec3bbf81e19ce35042e7690c7c.js.gz b/priv/static/worker-800f90ec3bbf81e19ce35042e7690c7c.js.gz new file mode 100644 index 000000000..88ac9f99b Binary files /dev/null and b/priv/static/worker-800f90ec3bbf81e19ce35042e7690c7c.js.gz differ diff --git a/priv/static/worker.js.gz b/priv/static/worker.js.gz new file mode 100644 index 000000000..88ac9f99b Binary files /dev/null and b/priv/static/worker.js.gz differ diff --git a/run.sh b/run.sh index ae4d48e33..6d96f008c 100755 --- a/run.sh +++ b/run.sh @@ -9,6 +9,7 @@ if [ ! -z "${RLIMIT_NOFILE:-}" ]; then fi export ERL_CRASH_DUMP=/tmp/erl_crash.dump +export BURRITO_CACHE_DIR="${BURRITO_CACHE_DIR:-/tmp/burrito_cache}" upload_crash_dump_to_s3() { EXIT_CODE=${?:-0} @@ -95,13 +96,29 @@ if [[ -n "${GENERATE_CLUSTER_CERTS:-}" ]] ; then fi echo "Running migrations" -sudo -E -u nobody /app/bin/migrate -if [ "${SEED_SELF_HOST-}" = true ]; then - echo "Seeding selfhosted Realtime" - sudo -E -u nobody /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' -fi +if [ -f "/app/bin/realtime" ]; then + # Traditional Mix release + sudo -E -u nobody /app/bin/migrate -echo "Starting Realtime" -ulimit -n -exec "$@" + if [ "${SEED_SELF_HOST-}" = true ]; then + echo "Seeding selfhosted Realtime" + sudo -E -u nobody /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' + fi + + echo "Starting Realtime" + ulimit -n + exec "$@" +else + # Burrito single binary + /app/realtime eval "Realtime.Release.migrate()" + + if [ "${SEED_SELF_HOST:-}" = "true" ]; then + echo "Seeding selfhosted Realtime" + /app/realtime eval "Realtime.Release.seeds(Realtime.Repo)" + fi + + echo "Starting Realtime" + ulimit -n + exec /app/realtime start +fi diff --git a/test/realtime/metrics_pusher_test.exs b/test/realtime/metrics_pusher_test.exs index 216e6c7e7..445404e12 100644 --- a/test/realtime/metrics_pusher_test.exs +++ b/test/realtime/metrics_pusher_test.exs @@ -156,7 +156,6 @@ defmodule Realtime.MetricsPusherTest do assert conn.port == 8428 assert conn.request_path == "/api/v1/write" assert Conn.get_req_header(conn, "authorization") == ["Basic #{Base.encode64("realtime:secret")}"] - assert Conn.get_req_header(conn, "content-encoding") == ["snappy"] assert Conn.get_req_header(conn, "content-type") == ["application/x-protobuf"] assert Conn.get_req_header(conn, "x-prometheus-remote-write-version") == ["0.1.0"] @@ -183,7 +182,6 @@ defmodule Realtime.MetricsPusherTest do assert byte_size(body) > 0 assert Conn.get_req_header(conn, "authorization") == [] assert Conn.get_req_header(conn, "content-type") == ["application/x-protobuf"] - assert Conn.get_req_header(conn, "content-encoding") == ["snappy"] send(parent, :req_called) Req.Test.text(conn, "") diff --git a/test/realtime/monitoring/gen_rpc_metrics_test.exs b/test/realtime/monitoring/gen_rpc_metrics_test.exs index 1f7269705..f7089239f 100644 --- a/test/realtime/monitoring/gen_rpc_metrics_test.exs +++ b/test/realtime/monitoring/gen_rpc_metrics_test.exs @@ -4,6 +4,8 @@ defmodule Realtime.GenRpcMetricsTest do alias Realtime.GenRpcMetrics + @metrics_byte_tolerance 200 + setup_all do {:ok, node} = Clustered.start() %{node: node} @@ -63,8 +65,8 @@ defmodule Realtime.GenRpcMetricsTest do assert_in_delta local_metrics[:send_avg], remote_metrics[:recv_avg], 200 assert_in_delta local_metrics[:recv_avg], remote_metrics[:send_avg], 200 - assert local_metrics[:send_oct] == remote_metrics[:recv_oct] - assert local_metrics[:recv_oct] == remote_metrics[:send_oct] + assert_in_delta local_metrics[:send_oct], remote_metrics[:recv_oct], @metrics_byte_tolerance + assert_in_delta local_metrics[:recv_oct], remote_metrics[:send_oct], @metrics_byte_tolerance assert local_metrics[:send_cnt] == remote_metrics[:recv_cnt] assert local_metrics[:recv_cnt] == remote_metrics[:send_cnt] diff --git a/test/realtime/signal_handler_test.exs b/test/realtime/signal_handler_test.exs index fe2e5f902..af36962da 100644 --- a/test/realtime/signal_handler_test.exs +++ b/test/realtime/signal_handler_test.exs @@ -1,10 +1,13 @@ defmodule Realtime.SignalHandlerTest do - use ExUnit.Case + use ExUnit.Case, async: false import ExUnit.CaptureLog alias Realtime.SignalHandler defmodule FakeHandler do - def handle_event(signal, _state), do: send(self(), signal) + def handle_event(signal, state) do + send(self(), {:signal_received, signal}) + {:ok, state} + end end setup do @@ -20,36 +23,68 @@ defmodule Realtime.SignalHandlerTest do assert capture_log(fn -> SignalHandler.handle_event(:sigterm, state) end) =~ "SignalHandler: :sigterm received" - assert_receive :sigterm + assert_receive {:signal_received, :sigterm} end - test "sets shutdown_in_progress on sigterm" do + test "logs error for unexpected signals" do {:ok, state} = SignalHandler.init({%{handler_mod: FakeHandler}, :ok}) - capture_log(fn -> SignalHandler.handle_event(:sigterm, state) end) + assert capture_log(fn -> SignalHandler.handle_event(:sigusr1, state) end) =~ + "unexpected signal :sigusr1" + end + test "sets shutdown_in_progress on sigterm" do + {:ok, state} = SignalHandler.init({%{handler_mod: FakeHandler}, :ok}) + capture_log(fn -> SignalHandler.handle_event(:sigterm, state) end) assert Application.get_env(:realtime, :shutdown_in_progress) == true end test "does not set shutdown_in_progress on non-sigterm signals" do Application.put_env(:realtime, :shutdown_in_progress, false) {:ok, state} = SignalHandler.init({%{handler_mod: FakeHandler}, :ok}) - capture_log(fn -> SignalHandler.handle_event(:sigusr1, state) end) - refute Application.get_env(:realtime, :shutdown_in_progress) end + + test "sigint sets shutdown_in_progress, logs, returns state, does not delegate" do + shutdown_called = self() + {:ok, state} = SignalHandler.init({%{handler_mod: FakeHandler, shutdown_fn: fn -> send(shutdown_called, :shutdown_called) end}, :ok}) + + log = + capture_log(fn -> + assert {:ok, ^state} = SignalHandler.handle_event(:sigint, state) + end) + + assert Application.get_env(:realtime, :shutdown_in_progress) == true + assert log =~ "SIGINT received - shutting down" + assert_receive :shutdown_called + refute_receive {:signal_received, :sigint} + end end describe "shutdown_in_progress?/1" do - test "shutdown_in_progress? returns error when shutdown is in progress" do + test "returns error when shutdown is in progress" do Application.put_env(:realtime, :shutdown_in_progress, true) assert SignalHandler.shutdown_in_progress?() == {:error, :shutdown_in_progress} end - test "shutdown_in_progress? returns ok when no shutdown in progress" do + test "returns ok when no shutdown in progress" do Application.put_env(:realtime, :shutdown_in_progress, false) assert SignalHandler.shutdown_in_progress?() == :ok end end + + describe "SIGINT shutdown path" do + @describetag :integration + test "peer node shuts down when sigint is handled by SignalHandler" do + {:ok, peer_pid, node} = Clustered.start_disconnected() + + true = Node.connect(node) + Node.monitor(node, true) + + :peer.cast(peer_pid, :gen_event, :notify, [:erl_signal_server, :sigint]) + + assert_receive {:nodedown, ^node}, 10_000 + end + end end