From fe0f2f708a89aad727ee93556400f4b79ff0f1d3 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 4 May 2024 21:14:23 +0200 Subject: [PATCH 01/24] ci: Use "devel" as the pre-release name instead of the commit title --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 00c30078fe..1a34ad908c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -234,6 +234,8 @@ jobs: gh release delete "${GITHUB_REF_NAME}" || true gh release create "${GITHUB_REF_NAME}" \ + -t "${GITHUB_REF_NAME}" \ + --verify-tag \ -F artifacts/release-changes/CHANGES.md \ ${isprerelease:+"--prerelease"} \ release-bundle/* From 58d8133454fa66dfb97b0f6fe072da41e9407d17 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 4 May 2024 21:14:40 +0200 Subject: [PATCH 02/24] ci: Enable docker push for releases --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1a34ad908c..253204e315 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -247,11 +247,11 @@ jobs: needs: - prepare if: | - vars.DOCKER_USER && + vars.DOCKER_REPO && vars.DOCKER_USER && (success() || needs.prepare.result == 'success') env: - DOCKER_REPO: postgrest - DOCKER_USER: stevechavez + DOCKER_REPO: ${{ vars.DOCKER_REPO }} + DOCKER_USER: ${{ vars.DOCKER_USER }} DOCKER_PASS: ${{ secrets.DOCKER_PASS }} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 From 9fe90bf9a86461b78f2e8cc3d9a525374bbbbb47 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 4 May 2024 21:53:36 +0200 Subject: [PATCH 03/24] ci: Fix pushing of devel-arm docker tag --- .github/scripts/arm/docker-publish.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/scripts/arm/docker-publish.sh b/.github/scripts/arm/docker-publish.sh index 8235e2ea83..9d0bf8df74 100644 --- a/.github/scripts/arm/docker-publish.sh +++ b/.github/scripts/arm/docker-publish.sh @@ -14,7 +14,13 @@ DOCKER_REPO="$2" DOCKER_USER="$3" DOCKER_PASS="$4" SCRIPT_DIR="$5" -PGRST_VERSION="v$6" +PGRST_VERSION="$6" + +if [ "$PGRST_VERSION" == "devel" ]; then + PGRST_TAG="$PGRST_VERSION" +else + PGRST_TAG="v$PGRST_VERSION" +fi DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env" @@ -37,13 +43,13 @@ cd ~/$DOCKER_BUILD_DIR # be added to the manifest if they are not in the registry beforehand. # This image must be manually deleted from Docker Hub at the end of the process. sudo docker buildx build --build-arg PGRST_GITHUB_COMMIT=$PGRST_GITHUB_COMMIT \ - -t $DOCKER_REPO/postgrest:$PGRST_VERSION-arm \ + -t $DOCKER_REPO/postgrest:$PGRST_TAG-arm \ --push . # Add the arm images to the manifest # NOTE: This assumes that there already is a `postgrest:` image # for the amd64 architecture pushed to Docker Hub -sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:$PGRST_VERSION $DOCKER_REPO/postgrest:$PGRST_VERSION-arm -[ "$PGRST_VERSION" != "devel" ] && sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:latest $DOCKER_REPO/postgrest:$PGRST_VERSION-arm +sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:$PGRST_TAG $DOCKER_REPO/postgrest:$PGRST_TAG-arm +[ "$PGRST_VERSION" != "devel" ] && sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:latest $DOCKER_REPO/postgrest:$PGRST_TAG-arm sudo docker logout From eec35d4569b5170f90e3b30c5db590c86b4f2962 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 4 May 2024 21:54:46 +0200 Subject: [PATCH 04/24] ci: Avoid recreating devel release Instead, the existing release is edited, which avoids notifications for pre-releases. --- .github/workflows/ci.yaml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 253204e315..b2f8423254 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -229,17 +229,20 @@ jobs: echo "Releasing version ${GITHUB_REF_NAME} on GitHub..." if [ "${GITHUB_REF_NAME}" == "devel" ]; then - isprerelease=1 + gh release edit "${GITHUB_REF_NAME}" \ + -t "${GITHUB_REF_NAME}" \ + --verify-tag \ + -F artifacts/release-changes/CHANGES.md \ + --prerelease \ + release-bundle/* + else + gh release create "${GITHUB_REF_NAME}" \ + -t "${GITHUB_REF_NAME}" \ + --verify-tag \ + -F artifacts/release-changes/CHANGES.md \ + release-bundle/* fi - gh release delete "${GITHUB_REF_NAME}" || true - gh release create "${GITHUB_REF_NAME}" \ - -t "${GITHUB_REF_NAME}" \ - --verify-tag \ - -F artifacts/release-changes/CHANGES.md \ - ${isprerelease:+"--prerelease"} \ - release-bundle/* - docker: name: Release / Docker Hub From 2f98d837ffd7ec57fba80e12d6e36168b60a17e4 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 4 May 2024 21:55:52 +0200 Subject: [PATCH 05/24] ci: Push docker hub description automatically on main branch --- .github/workflows/ci.yaml | 29 +++++++----- nix/tools/release/default.nix | 47 +------------------ nix/tools/release/docker-hub-description.md | 1 - .../release/docker-hub-full-description.md | 2 + 4 files changed, 20 insertions(+), 59 deletions(-) delete mode 100644 nix/tools/release/docker-hub-description.md diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b2f8423254..9fc6a0d4c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -283,18 +283,23 @@ jobs: else echo "Skipping push to 'latest' tag for pre-release..." fi -# TODO: Enable dockerhub description update again, once a solution for the permission problem is found: -# https://github.com/docker/hub-feedback/issues/1927 -# - name: Update descriptions on Docker Hub -# env: -# DOCKER_PASS: ${{ secrets.DOCKER_PASS }} -# run: | -# if [[ -z "$ISPRERELEASE" ]]; then -# echo "Updating description on Docker Hub..." -# postgrest-release-dockerhub-description -# else -# echo "Skipping updating description for pre-release..." -# fi + + + docker-description: + name: Release / Docker Hub Description + runs-on: ubuntu-22.04 + if: | + vars.DOCKER_REPO && vars.DOCKER_USER && + github.ref == 'refs/tags/devel' + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4.0.0 + with: + username: ${{ vars.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + repository: ${{ vars.DOCKER_REPO }}/postgrest + short-description: ${{ github.event.repository.description }} + readme-filepath: ./nix/tools/release/docker-hub-full-description.md docker-arm: diff --git a/nix/tools/release/default.nix b/nix/tools/release/default.nix index d3b93705d6..104bfb5f27 100644 --- a/nix/tools/release/default.nix +++ b/nix/tools/release/default.nix @@ -1,52 +1,7 @@ { buildToolbox , checkedShellScript -, curl -, jq }: let - dockerHubDescription = - let - description = - ./docker-hub-description.md; - - fullDescription = - ./docker-hub-full-description.md; - in - checkedShellScript - { - name = "postgrest-release-dockerhub-description"; - docs = "Update the repository description on Docker Hub."; - args = [ - "ARG_USE_ENV([DOCKER_USER], [], [DockerHub user name])" - "ARG_USE_ENV([DOCKER_PASS], [], [DockerHub password])" - "ARG_USE_ENV([DOCKER_REPO], [], [DockerHub repository])" - ]; - } - '' - # ARG_USE_ENV only adds defaults or docs for environment variables - # We manually implement a required check here - # See also: https://github.com/matejak/argbash/issues/80 - : "''${DOCKER_USER:?DOCKER_USER is required}" - : "''${DOCKER_PASS:?DOCKER_PASS is required}" - : "''${DOCKER_REPO:?DOCKER_REPO is required}" - - echo "Logging in to Docker Hub to get an auth token..." - token="$( - ${curl}/bin/curl --fail -s \ - --data-urlencode "username=$DOCKER_USER" \ - --data-urlencode "password=$DOCKER_PASS" \ - "https://hub.docker.com/v2/users/login/" \ - | ${jq}/bin/jq -r .token - )" - - repo_url="https://hub.docker.com/v2/repositories/$DOCKER_REPO/postgrest/" - echo "Patching both descriptions at $repo_url ..." - ${curl}/bin/curl --fail -X PATCH "$repo_url" \ - -H "Authorization: JWT $token" \ - --data-urlencode description@${description} \ - --data-urlencode full_description@${fullDescription} - ''; - release = checkedShellScript { @@ -133,5 +88,5 @@ in buildToolbox { name = "postgrest-release"; - tools = { inherit dockerHubDescription release; }; + tools = { inherit release; }; } diff --git a/nix/tools/release/docker-hub-description.md b/nix/tools/release/docker-hub-description.md deleted file mode 100644 index fc44c85423..0000000000 --- a/nix/tools/release/docker-hub-description.md +++ /dev/null @@ -1 +0,0 @@ -REST API for any Postgres database diff --git a/nix/tools/release/docker-hub-full-description.md b/nix/tools/release/docker-hub-full-description.md index 625e2723d7..d38ef33bfe 100644 --- a/nix/tools/release/docker-hub-full-description.md +++ b/nix/tools/release/docker-hub-full-description.md @@ -68,3 +68,5 @@ The image is built from scratch using no commands are listed in the image history. See the [PostgREST respository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for details on the build process and how to inspect the image. + +This does not apply to the arm64 variant, which is based on Ubuntu. From c67f1c398cc9f731ea34267e3e88c59769f61770 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 5 May 2024 13:33:48 +0200 Subject: [PATCH 06/24] ci: Fix docker release job after 2f98d837 --- .github/workflows/ci.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9fc6a0d4c5..3edaf46009 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -258,11 +258,6 @@ jobs: DOCKER_PASS: ${{ secrets.DOCKER_PASS }} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - - name: Setup Nix Environment - uses: ./.github/actions/setup-nix - with: - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - tools: release.dockerHubDescription.bin - name: Download Docker image uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: From 8433f9812b406595685aec8233f121bd55aa6cf9 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 5 May 2024 13:38:58 +0200 Subject: [PATCH 07/24] ci: Replace assets of existing devel release properly --- .github/workflows/ci.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3edaf46009..b4eec5bfdd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -229,12 +229,18 @@ jobs: echo "Releasing version ${GITHUB_REF_NAME} on GitHub..." if [ "${GITHUB_REF_NAME}" == "devel" ]; then - gh release edit "${GITHUB_REF_NAME}" \ - -t "${GITHUB_REF_NAME}" \ + # To replace the existing release, we must first delete the old assets, + # then modify the release, then add the new assets. + gh release view devel --json assets \ + | jq -r '.assets[] | .name' \ + | xargs -n1 \ + gh release delete-asset -y devel + gh release edit devel \ + -t devel \ --verify-tag \ -F artifacts/release-changes/CHANGES.md \ - --prerelease \ - release-bundle/* + --prerelease + gh release upload --clobber devel release-bundle/* else gh release create "${GITHUB_REF_NAME}" \ -t "${GITHUB_REF_NAME}" \ From d9ba9a82087c975d0a7078c4097d3dcab6789016 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 5 May 2024 14:35:40 +0200 Subject: [PATCH 08/24] ci: Avoid devel release failure when no assets to clean up exist --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4eec5bfdd..1ff6a570c1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -233,7 +233,7 @@ jobs: # then modify the release, then add the new assets. gh release view devel --json assets \ | jq -r '.assets[] | .name' \ - | xargs -n1 \ + | xargs -rn1 \ gh release delete-asset -y devel gh release edit devel \ -t devel \ From a57d12b1023a3b6a41b440ebf7ab0c5bdbf96ea7 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 5 May 2024 17:11:22 +0200 Subject: [PATCH 09/24] ci: Use the new DOCKER_ vars for docker-arm job --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ff6a570c1..cbd0a5a02c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -311,8 +311,8 @@ jobs: - docker env: GITHUB_COMMIT: ${{ github.sha }} - DOCKER_REPO: postgrest - DOCKER_USER: stevechavez + DOCKER_REPO: ${{ vars.DOCKER_REPO }} + DOCKER_USER: ${{ vars.DOCKER_USER }} DOCKER_PASS: ${{ secrets.DOCKER_PASS }} steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 From 463dfb552d8852c4eb5995ffcd741aa93f86b4d0 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 5 May 2024 17:48:44 +0200 Subject: [PATCH 10/24] chore(deps): update actions/checkout hash for release/prepare job Accidentally committed an outdated version, this aligns it with all the other references to the same action. --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cbd0a5a02c..66932806c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -151,7 +151,7 @@ jobs: - build - arm steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Check the version to be released run: | cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" From cabe744f0127564fc3f9765f2a0c378ec331975f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 10:22:10 +0000 Subject: [PATCH 11/24] chore(deps): update ubuntu docker tag to v24 --- .github/scripts/arm/docker-env/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/arm/docker-env/Dockerfile b/.github/scripts/arm/docker-env/Dockerfile index 7115ce28a8..d4cdbfed17 100644 --- a/.github/scripts/arm/docker-env/Dockerfile +++ b/.github/scripts/arm/docker-env/Dockerfile @@ -1,6 +1,6 @@ # PostgREST docker hub image -FROM ubuntu:jammy@sha256:a6d2b38300ce017add71440577d5b0a90460d0e57fd7aec21dd0d1b0761bbfb2 AS postgrest +FROM ubuntu:noble@sha256:3f85b7caad41a95462cf5b787d8a04604c8262cdcdf9a472b8c52ef83375fe15 AS postgrest RUN apt-get update -y \ && apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \ From 21bc48ad9a6239f095658efc8265102dda471ef9 Mon Sep 17 00:00:00 2001 From: Taimoor Zaeem Date: Wed, 10 Apr 2024 15:11:05 +0500 Subject: [PATCH 12/24] fix: fix wrong 503 Service Unavailable on pg error 53400 --- CHANGELOG.md | 1 + src/PostgREST/Error.hs | 1 + test/spec/Feature/Query/RpcSpec.hs | 8 ++++++++ test/spec/fixtures/roles.sql | 5 +++-- test/spec/fixtures/schema.sql | 6 ++++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d798cae10..b6636ce3f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla - #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem - #3404, Clarify the `PGRST121` (could not parse RAISE 'PGRST') error message - @laurenceisla + - #3267, Fix wrong `503 Service Unavailable` on pg error `53400` - @taimoorzaeem ### Deprecated diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index eefe579439..28df23a078 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -489,6 +489,7 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError '3':'9':_ -> HTTP.status500 -- external routine invocation '3':'B':_ -> HTTP.status500 -- savepoint exception '4':'0':_ -> HTTP.status500 -- tx rollback + "53400" -> HTTP.status500 -- config limit exceeded '5':'3':_ -> HTTP.status503 -- insufficient resources '5':'4':_ -> HTTP.status413 -- too complex '5':'5':_ -> HTTP.status500 -- obj not on prereq state diff --git a/test/spec/Feature/Query/RpcSpec.hs b/test/spec/Feature/Query/RpcSpec.hs index 60ae3d343c..0e4af838e2 100644 --- a/test/spec/Feature/Query/RpcSpec.hs +++ b/test/spec/Feature/Query/RpcSpec.hs @@ -1478,3 +1478,11 @@ spec actualPgVersion = "details":"DETAIL is missing in the RAISE statement", "hint":"DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."}|] { matchStatus = 500 } + + -- here JWT has the role: postgrest_test_superuser + context "test function temp_file_limit" $ + let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3Rfc3VwZXJ1c2VyIiwiaWQiOiJqZG9lIn0.LQ-qx0ArBnfkwQQhIHKF5cS-lzl0gnTPI8NLoPbL5Fg" in + it "should return http status 500" $ + request methodGet "/rpc/temp_file_limit" [auth] "" `shouldRespondWith` + [json|{"code":"53400","message":"temporary file size exceeds temp_file_limit (1kB)","details":null,"hint":null}|] + { matchStatus = 500 } diff --git a/test/spec/fixtures/roles.sql b/test/spec/fixtures/roles.sql index a628bf1e07..3590c6edbc 100644 --- a/test/spec/fixtures/roles.sql +++ b/test/spec/fixtures/roles.sql @@ -1,6 +1,7 @@ -DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author; +DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author, postgrest_test_superuser; CREATE ROLE postgrest_test_anonymous; CREATE ROLE postgrest_test_default_role; CREATE ROLE postgrest_test_author; +CREATE ROLE postgrest_test_superuser WITH SUPERUSER; -GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO :PGUSER; +GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author, postgrest_test_superuser TO :PGUSER; diff --git a/test/spec/fixtures/schema.sql b/test/spec/fixtures/schema.sql index e8d0f848da..8b7458d676 100644 --- a/test/spec/fixtures/schema.sql +++ b/test/spec/fixtures/schema.sql @@ -3771,3 +3771,9 @@ select * from test.projects; create or replace view test.infinite_recursion as select * from test.infinite_recursion; + + +create or replace function temp_file_limit() +returns bigint as $$ + select COUNT(*) FROM generate_series('-infinity'::TIMESTAMP, 'epoch'::TIMESTAMP, INTERVAL '1 DAY'); +$$ language sql security definer set temp_file_limit to '1kB'; From 7e91e5311d7c4092daefb3af9f05913370df55db Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 3 May 2024 13:48:42 -0500 Subject: [PATCH 13/24] fix: not adding application_name on all URIs --- CHANGELOG.md | 1 + docs/references/observability.rst | 6 ---- src/PostgREST/Config.hs | 47 ++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6636ce3f5..3e4733ade2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem - #3404, Clarify the `PGRST121` (could not parse RAISE 'PGRST') error message - @laurenceisla - #3267, Fix wrong `503 Service Unavailable` on pg error `53400` - @taimoorzaeem + - #2985, Fix not adding `application_name` on all connection strings - @steve-chavez ### Deprecated diff --git a/docs/references/observability.rst b/docs/references/observability.rst index efa389b78a..a0079df66e 100644 --- a/docs/references/observability.rst +++ b/docs/references/observability.rst @@ -197,12 +197,6 @@ When debugging a problem it's important to verify the running PostgREST version. ------------------------------ PostgREST 11.1.0 -.. important:: - - - The server sets the `fallback_application_name `_ to the connection URI for this query to work. To override the value set ``application_name`` on the connection string. - - The version will only be set if it's a valid URI (`RFC 3986 `_). This means any special characters must be urlencoded. - - The version will not be set if the connection string is in `keyword/value format `_. - - The ``stderr`` logs also contain the version, as noted on :ref:`pgrst_logging`. .. _trace_header: diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index e13cbf4997..66d8a45361 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -49,8 +49,7 @@ import Data.List.NonEmpty (fromList, toList) import Data.Maybe (fromJust) import Data.Scientific (floatingOrInteger) import Network.URI (escapeURIString, - isUnescapedInURIComponent, parseURI, - uriQuery) + isUnescapedInURIComponent) import Numeric (readOct, showOct) import System.Environment (getEnvironment) import System.Posix.Types (FileMode) @@ -502,19 +501,47 @@ readPGRSTEnvironment = -- "postgres:///postgres?host=server&port=5432&fallback_application_name=PostgREST%2011%271%260%40%23%24%25%2C.%3A%22%5B%5D%7B%7D%3F%2B%5E%28%29%3Dasdfqwer" -- -- >>> addFallbackAppName ver "postgres://user:invalid_chars[]#@host:5432/postgres" --- "postgres://user:invalid_chars[]#@host:5432/postgres" +-- "postgres://user:invalid_chars[]#@host:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29" -- --- >>> addFallbackAppName ver "invalid_uri1=val1 invalid_uri2=val2" --- "invalid_uri1=val1 invalid_uri2=val2" +-- >>> addFallbackAppName ver "host=localhost port=5432 dbname=postgres" +-- "host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11.1.0 (5a04ec7)'" +-- +-- >>> addFallbackAppName strangeVer "host=localhost port=5432 dbname=postgres" +-- "host=localhost port=5432 dbname=postgres fallback_application_name='PostgREST 11\\'1&0@#$%,.:\"[]{}?+^()=asdfqwer'" +-- +-- works with passwords containing `?` +-- >>> addFallbackAppName ver "postgres://admin2:?pass?special?@localhost:5432/postgres" +-- "postgres://admin2:?pass?special?@localhost:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29" +-- +-- addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass" +-- "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29" +-- +-- addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass" +-- "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29" addFallbackAppName :: ByteString -> Text -> Text addFallbackAppName version dbUri = dbUri <> - case uriQuery <$> parseURI (toS dbUri) of - -- Does not add the application name to key=val connection strings or invalid URIs + case pgConnString dbUri of Nothing -> mempty - Just "" -> "?" <> uriFmt - Just "?" -> uriFmt - _ -> "&" <> uriFmt + Just PGKeyVal -> " " <> keyValFmt + Just PGURI -> case lookAtOptions dbUri of + (_, "") -> "?" <> uriFmt + (_, "?") -> uriFmt + (_, _) -> "&" <> uriFmt where uriFmt = pKeyWord <> toS (escapeURIString isUnescapedInURIComponent $ toS pgrstVer) + keyValFmt = pKeyWord <> "'" <> T.replace "'" "\\'" pgrstVer <> "'" pKeyWord = "fallback_application_name=" pgrstVer = "PostgREST " <> T.decodeUtf8 version + lookAtOptions x = T.breakOn "?" . snd $ T.breakOnEnd "@" x -- start from after `@` to not mess passwords that include `?`, see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS + +data PGConnString = PGURI | PGKeyVal + +-- Uses same logic as libpq recognized_connection_string +-- https://github.com/postgres/postgres/blob/5eafacd2797dc0b04a0bde25fbf26bf79903e7c2/src/interfaces/libpq/fe-connect.c#L5923-L5936 +pgConnString :: Text -> Maybe PGConnString +pgConnString conn | uriDesignator `T.isPrefixOf` conn || shortUriDesignator `T.isPrefixOf` conn = Just PGURI + | "=" `T.isInfixOf` conn = Just PGKeyVal + | otherwise = Nothing + where + uriDesignator = "postgresql://" + shortUriDesignator = "postgres://" From 1d4f31514f74226e151d790b6ca65c9d8fdd6788 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 6 May 2024 08:20:46 -0500 Subject: [PATCH 14/24] changelog: move 3340 from fixed to added Since it was a feature --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4733ade2..14c257107f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #3404, Show extra information in the `PGRST121` (could not parse RAISE 'PGRST') error - @laurenceisla + Shows the failed MESSAGE or DETAIL in the `details` field + Shows the correct JSON format in the `hints` field + - #3340, Log when the LISTEN channel gets a notification - @steve-chavez ### Fixed @@ -32,7 +33,6 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #3237, Dump media handlers and timezones with --dump-schema - @wolfgangwalther - #3323, #3324, Don't hide error on LISTEN channel failure - @steve-chavez - #3330, Incorrect admin server `/ready` response on slow schema cache loads - @steve-chavez - - #3340, Log when the LISTEN channel gets a notification - @steve-chavez - #3345, Fix in-database configuration values not loading for `pgrst.server_trace_header` and `pgrst.server_cors_allowed_origins` - @laurenceisla - #3361, Clarify the `PGRST204` (column not found) error message - @steve-chavez - #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem From b34c00c52281b397eba72d28345ad622915957dc Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 6 May 2024 08:22:30 -0500 Subject: [PATCH 15/24] changelog: add missing entry for 3184 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c257107f..f437ac62ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). + Shows the failed MESSAGE or DETAIL in the `details` field + Shows the correct JSON format in the `hints` field - #3340, Log when the LISTEN channel gets a notification - @steve-chavez + - #3184, Log full pg version to stderr on connection - @steve-chavez ### Fixed From df9b373465172637e8738cc46fecb640383079bc Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 6 May 2024 09:38:48 -0500 Subject: [PATCH 16/24] docs: better place for application_name --- docs/postgrest.dict | 1 - docs/references/connection_pool.rst | 25 ++++++++++++--- docs/references/observability.rst | 50 +++++++---------------------- 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/docs/postgrest.dict b/docs/postgrest.dict index d24804de56..ded55e8982 100644 --- a/docs/postgrest.dict +++ b/docs/postgrest.dict @@ -157,7 +157,6 @@ stateful stdout supervisees SvelteKit -syslog systemd todo todos diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index 16a671af92..13fdf5c4d2 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -7,11 +7,6 @@ A connection pool is a cache of reusable database connections. It allows serving Minimizing connections is paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. -Connection String ------------------ - -For connecting to the database, the pool requires a connection string. You can configure it using :ref:`db-uri`. - .. _pool_growth_limit: .. _dyn_conn_pool: @@ -22,6 +17,26 @@ To conserve system resources, PostgREST uses a dynamic connection pool. This ena - If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it’s pointless to set this higher than the ``max_connections`` setting in your database. - If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released. +- For connecting to the database, the :ref:`authenticator ` role is used. You can configure this using :ref:`db-uri`. + +Connection Application Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PostgREST sets the connection `application_name `_ for all of its used connections. +This is useful for PostgreSQL statistics and logs. + +For example, you can query `pg_stat_activity `_ to get the PostgREST version: + +.. code-block:: postgres + + select distinct usename, application_name + from pg_stat_activity + where usename = 'authenticator'; + + usename | application_name + ---------------+-------------------------- + authenticator | PostgREST 12.1 + Connection lifetime ------------------- diff --git a/docs/references/observability.rst b/docs/references/observability.rst index a0079df66e..04a91a3f72 100644 --- a/docs/references/observability.rst +++ b/docs/references/observability.rst @@ -20,33 +20,20 @@ PostgREST logs basic request information to ``stdout``, including the authentica 127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" 127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0" -For diagnostic information about the server itself, PostgREST logs to ``stderr``. +For diagnostic information about the server itself, PostgREST logs to ``stderr``. It includes the server version and also the version of the connected PostgreSQL. .. code:: - 12/Jun/2021:17:47:39 -0500: Starting PostgREST 11.1.0... - 12/Jun/2021:17:47:39 -0500: Attempting to connect to the database... - 12/Jun/2021:17:47:39 -0500: Listening on port 3000 - 12/Jun/2021:17:47:39 -0500: Connection successful - 12/Jun/2021:17:47:39 -0500: Config re-loaded - 12/Jun/2021:17:47:40 -0500: Schema cache loaded - -.. note:: - - When running it in an SSH session you must detach it from stdout or it will be terminated when the session closes. The easiest technique is redirecting the output to a log file or to the syslog: - - .. code-block:: bash - - ssh foo@example.com \ - 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' - - # another option is to pipe the output into "logger -t postgrest" - -Currently PostgREST doesn't log the SQL commands executed against the underlying database. + 06/May/2024:08:16:11 -0500: Starting PostgREST 12.1... + 06/May/2024:08:16:11 -0500: Attempting to connect to the database... + 06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit + 06/May/2024:08:16:11 -0500: Listening on port 3000 Database Logs ------------- +Currently PostgREST doesn't log the SQL commands executed against the underlying database. + To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below. Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file. @@ -172,12 +159,10 @@ Max pool connections. Traces ====== -Server Version --------------- - -When debugging a problem it's important to verify the running PostgREST version. There are three ways to do this: +Server Version Header +--------------------- -- Look for the :code:`Server` HTTP response header that is returned on every request. +When debugging a problem it's important to verify the running PostgREST version. For this you can look at the :code:`Server` HTTP response header that is returned on every request. .. code:: @@ -185,20 +170,6 @@ When debugging a problem it's important to verify the running PostgREST version. Server: postgrest/11.0.1 -- Query ``application_name`` on `pg_stat_activity `_. - -.. code-block:: postgres - - select distinct application_name - from pg_stat_activity - where application_name ilike '%postgrest%'; - - application_name - ------------------------------ - PostgREST 11.1.0 - -- The ``stderr`` logs also contain the version, as noted on :ref:`pgrst_logging`. - .. _trace_header: Trace Header @@ -348,6 +319,7 @@ For example, to only allow requests from an IP address to get the execution plan const redirects = { '#health_check': 'health_check.html', + '#server-version': '#server-version-header', }; let willRedirectTo = redirects[hash]; From b006016d078e3fe28e7fb4fe2f8875403e1c7b18 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 7 May 2024 08:09:00 +0200 Subject: [PATCH 17/24] ci: Make "release / tag" job detect existing tags The release / tag job has logic to decide whether to push a new tag on stable branches, which depends on the all the tags being fetched. The checkout action doesn't do that by default, so enable that. --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 66932806c5..e689d46eab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -120,6 +120,7 @@ jobs: steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: + fetch-tags: true ssh-key: ${{ secrets.POSTGREST_SSH_KEY }} - name: Tag latest commit run: | From cb6151eb1b283501f59bcbd071dffe3fe2025f43 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 7 May 2024 08:29:31 +0200 Subject: [PATCH 18/24] ci: Make artifact-from-cirrus action succeed when cirrus job doesn't start up in PR --- .github/actions/artifact-from-cirrus/action.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/actions/artifact-from-cirrus/action.yaml b/.github/actions/artifact-from-cirrus/action.yaml index ff8933e8ee..8d4d5265dc 100644 --- a/.github/actions/artifact-from-cirrus/action.yaml +++ b/.github/actions/artifact-from-cirrus/action.yaml @@ -38,16 +38,14 @@ runs: echo "Cirrus CI task has not started, yet. Waiting..." sleep 10 else - echo "check_suite_found=1" >> "$GITHUB_OUTPUT" echo "check_runs_url=$check_runs_url" >> "$GITHUB_OUTPUT" exit 0 fi done >&2 echo "Cirrus CI check suite not found. Is Cirrus CI enabled for this repo?" - echo "check_suite_found=0" >> "$GITHUB_OUTPUT" - name: Find task by name id: find-task - if: steps.check-suite.outputs.check_suite_found + if: steps.check-suite.outputs.check_runs_url shell: bash run: | get_number_of_tasks() { From 1374178f274fed9c37af698395c79514ceea7094 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 7 May 2024 08:12:57 +0200 Subject: [PATCH 19/24] ci: Improve performance for nix jobs in CI Defaulting to max-jobs = auto should improve build times by using more cores. Setting always-allow-substitutes to true should cause all nix derivations to be cached on cachix, which should improve performance of the MacOS job dramatically, when no rebuilds need to happen. --- .github/actions/setup-nix/action.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/setup-nix/action.yaml b/.github/actions/setup-nix/action.yaml index d5674352f9..6116b90c8b 100644 --- a/.github/actions/setup-nix/action.yaml +++ b/.github/actions/setup-nix/action.yaml @@ -12,6 +12,10 @@ runs: using: composite steps: - uses: nixbuild/nix-quick-install-action@60e9c39264d4714139af3cdf15f691b19eec3530 # v28 + with: + nix_conf: |- + always-allow-substitutes = true + max-jobs = auto - uses: cachix/cachix-action@18cf96c7c98e048e10a83abd92116114cd8504be # v14 with: name: postgrest From 1b584f7e9ceda69b956e0b50146d8ba05f2f337b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 7 May 2024 11:19:39 -0500 Subject: [PATCH 20/24] refactor: is ready Admin logic to AppState --- src/PostgREST/Admin.hs | 20 +++---- src/PostgREST/App.hs | 2 +- src/PostgREST/AppState.hs | 110 +++++++++++++++++++++----------------- 3 files changed, 69 insertions(+), 63 deletions(-) diff --git a/src/PostgREST/Admin.hs b/src/PostgREST/Admin.hs index 36eace86a1..40b87243be 100644 --- a/src/PostgREST/Admin.hs +++ b/src/PostgREST/Admin.hs @@ -5,7 +5,6 @@ module PostgREST.Admin ) where import qualified Data.Aeson as JSON -import qualified Hasql.Session as SQL import qualified Network.HTTP.Types.Status as HTTP import qualified Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp @@ -28,28 +27,25 @@ import qualified PostgREST.Config as Config import Protolude -runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO () -runAdmin conf@AppConfig{configAdminServerPort} appState settings = +runAdmin :: AppState -> Warp.Settings -> IO () +runAdmin appState settings = do + AppConfig{configAdminServerPort} <- AppState.getConfig appState whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do observer $ AdminStartObs configAdminServerPort void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp where - adminApp = admin appState conf + adminApp = admin appState observer = AppState.getObserver appState -- | PostgREST admin application -admin :: AppState.AppState -> AppConfig -> Wai.Application -admin appState appConfig req respond = do +admin :: AppState.AppState -> Wai.Application +admin appState req respond = do isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState) - isSchemaCacheLoaded <- AppState.getSchemaCacheLoaded appState - isConnectionUp <- - if configDbChannelEnabled appConfig - then AppState.getIsListenerOn appState - else isRight <$> AppState.usePool appState (SQL.sql "SELECT 1") + isLoaded <- AppState.isLoaded appState case Wai.pathInfo req of ["ready"] -> - respond $ Wai.responseLBS (if isMainAppReachable && isConnectionUp && isSchemaCacheLoaded then HTTP.status200 else HTTP.status503) [] mempty + respond $ Wai.responseLBS (if isMainAppReachable && isLoaded then HTTP.status200 else HTTP.status503) [] mempty ["live"] -> respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status503) [] mempty ["config"] -> do diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 06a2055610..9fe3b1b841 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -72,7 +72,7 @@ run appState = do -- reload schema cache + config on NOTIFY AppState.runListener conf appState - Admin.runAdmin conf appState (serverSettings conf) + Admin.runAdmin appState (serverSettings conf) let app = postgrest configLogLevel appState (AppState.connectionWorker appState) diff --git a/src/PostgREST/AppState.hs b/src/PostgREST/AppState.hs index 844a99cb02..3fad53ed62 100644 --- a/src/PostgREST/AppState.hs +++ b/src/PostgREST/AppState.hs @@ -9,7 +9,6 @@ module PostgREST.AppState , destroy , getConfig , getSchemaCache - , getIsListenerOn , getMainThreadId , getPgVersion , getRetryNextIn @@ -17,7 +16,6 @@ module PostgREST.AppState , getJwtCache , getSocketREST , getSocketAdmin - , getSchemaCacheLoaded , init , initSockets , initWithPool @@ -28,6 +26,7 @@ module PostgREST.AppState , connectionWorker , runListener , getObserver + , isLoaded ) where import qualified Data.Aeson as JSON @@ -90,14 +89,14 @@ data AppState = AppState , statePgVersion :: IORef PgVersion -- | No schema cache at the start. Will be filled in by the connectionWorker , stateSchemaCache :: IORef (Maybe SchemaCache) - -- | If schema cache is loaded - , stateSchemaCacheLoaded :: IORef Bool + -- | The schema cache status + , stateSCacheStatus :: IORef SchemaCacheStatus + -- | The connection status + , stateConnStatus :: IORef ConnectionStatus -- | starts the connection worker with a debounce , debouncedConnectionWorker :: IO () -- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker. , stateListener :: MVar () - -- | State of the LISTEN channel, used for the admin server checks - , stateIsListenerOn :: IORef Bool -- | Config that can change at runtime , stateConf :: IORef AppConfig -- | Time used for verifying JWT expiration @@ -118,6 +117,20 @@ data AppState = AppState , stateMetrics :: Metrics.MetricsState } +-- | Schema cache status +data SchemaCacheStatus + = SCLoaded + | SCPending + | SCFatalFail + deriving Eq + +-- | Current database connection status +data ConnectionStatus + = ConnEstablished + | ConnPending + | ConnFatalFail Text + deriving Eq + type AppSockets = (NS.Socket, Maybe NS.Socket) @@ -138,10 +151,10 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do appState <- AppState pool <$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step <*> newIORef Nothing - <*> newIORef False + <*> newIORef SCPending + <*> newIORef ConnPending <*> pure (pure ()) <*> newEmptyMVar - <*> newIORef False <*> newIORef conf <*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime } <*> myThreadId @@ -286,29 +299,33 @@ waitListener = takeMVar . stateListener signalListener :: AppState -> IO () signalListener appState = void $ tryPutMVar (stateListener appState) () -getIsListenerOn :: AppState -> IO Bool -getIsListenerOn = readIORef . stateIsListenerOn +isConnEstablished :: AppState -> IO Bool +isConnEstablished x = do + conf <- getConfig x + if configDbChannelEnabled conf + then do -- if the listener is enabled, we can be sure the connection status is always up to date + st <- readIORef $ stateConnStatus x + return $ st == ConnEstablished + else -- otherwise the only way to check the connection is to make a query + isRight <$> usePool x (SQL.sql "SELECT 1") -putIsListenerOn :: AppState -> Bool -> IO () -putIsListenerOn = atomicWriteIORef . stateIsListenerOn +isLoaded :: AppState -> IO Bool +isLoaded x = do + scacheStatus <- readIORef $ stateSCacheStatus x + connEstablished <- isConnEstablished x + return $ scacheStatus == SCLoaded && connEstablished -getSchemaCacheLoaded :: AppState -> IO Bool -getSchemaCacheLoaded = readIORef . stateSchemaCacheLoaded +putSCacheStatus :: AppState -> SchemaCacheStatus -> IO () +putSCacheStatus = atomicWriteIORef . stateSCacheStatus -putSchemaCacheLoaded :: AppState -> Bool -> IO () -putSchemaCacheLoaded = atomicWriteIORef . stateSchemaCacheLoaded +putConnStatus :: AppState -> ConnectionStatus -> IO () +putConnStatus = atomicWriteIORef . stateConnStatus getObserver :: AppState -> ObservationHandler getObserver = stateObserver --- | Schema cache status -data SCacheStatus - = SCLoaded - | SCOnRetry - | SCFatalFail - -- | Load the SchemaCache by using a connection from the pool. -loadSchemaCache :: AppState -> IO SCacheStatus +loadSchemaCache :: AppState -> IO SchemaCacheStatus loadSchemaCache appState@AppState{stateObserver=observer} = do conf@AppConfig{..} <- getConfig appState (resultTime, result) <- @@ -323,24 +340,17 @@ loadSchemaCache appState@AppState{stateObserver=observer} = do Nothing -> do putSchemaCache appState Nothing observer $ SchemaCacheNormalErrorObs e - putSchemaCacheLoaded appState False - return SCOnRetry + putSCacheStatus appState SCPending + return SCPending Right sCache -> do putSchemaCache appState $ Just sCache observer $ SchemaCacheQueriedObs resultTime (t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache observer $ SchemaCacheLoadedObs t - putSchemaCacheLoaded appState True + putSCacheStatus appState SCLoaded return SCLoaded --- | Current database connection status data ConnectionStatus -data ConnectionStatus - = NotConnected - | Connected PgVersion - | FatalConnectionError Text - deriving (Eq) - -- | The purpose of this worker is to obtain a healthy connection to pg and an -- up-to-date schema cache(SchemaCache). This method is meant to be called -- multiple times by the same thread, but does nothing if the previous @@ -358,20 +368,19 @@ internalConnectionWorker appState@AppState{stateObserver=observer} = work work = do AppConfig{..} <- getConfig appState observer DBConnectAttemptObs - connected <- establishConnection appState - case connected of - FatalConnectionError reason -> + connStatus <- establishConnection appState + case connStatus of + ConnFatalFail reason -> -- Fatal error when connecting observer (ExitFatalObs reason) >> killThread (getMainThreadId appState) - NotConnected -> - -- Unreachable because establishConnection will keep trying to connect, unless disable-recovery is turned on + ConnPending -> unless configDbPoolAutomaticRecovery $ observer ExitDBNoRecoveryObs >> killThread (getMainThreadId appState) - Connected actualPgVersion -> do + ConnEstablished -> do -- Procede with initialization - putPgVersion appState actualPgVersion when configDbChannelEnabled $ signalListener appState + actualPgVersion <- getPgVersion appState observer (DBConnectedObs $ pgvFullName actualPgVersion) -- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again -- We cannot retry after it fails immediately, because db-pre-config could have user errors. We just log the error and continue. @@ -381,7 +390,7 @@ internalConnectionWorker appState@AppState{stateObserver=observer} = work SCLoaded -> -- do nothing and proceed if the load was successful return () - SCOnRetry -> + SCPending -> -- retry reloading the schema cache work SCFatalFail -> @@ -415,23 +424,26 @@ establishConnection appState@AppState{stateObserver=observer} = observer $ ConnectionPgVersionErrorObs e case checkIsFatal e of Just reason -> - return $ FatalConnectionError reason - Nothing -> - return NotConnected + return $ ConnFatalFail reason + Nothing -> do + putConnStatus appState ConnPending + return ConnPending Right version -> if version < minimumPgVersion then - return . FatalConnectionError $ + return . ConnFatalFail $ "Cannot run in this PostgreSQL version, PostgREST needs at least " <> pgvName minimumPgVersion - else - return . Connected $ version + else do + putConnStatus appState ConnEstablished + putPgVersion appState version + return ConnEstablished shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool shouldRetry rs isConnSucc = do AppConfig{..} <- getConfig appState let delay = fromMaybe 0 (rsPreviousDelay rs) `div` backoffMicroseconds - itShould = NotConnected == isConnSucc && configDbPoolAutomaticRecovery + itShould = ConnPending == isConnSucc && configDbPoolAutomaticRecovery when itShould $ observer $ ConnectionRetryObs delay when itShould $ putRetryNextIn appState delay return itShould @@ -503,7 +515,6 @@ listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do case dbOrError of Right db -> do observer $ DBListenerStart dbChannel - putIsListenerOn appState True SQL.listen db $ SQL.toPgIdentifier dbChannel SQL.waitForNotifications handleNotification db @@ -517,7 +528,6 @@ listener appState@AppState{stateObserver=observer} conf@AppConfig{..} = do handleFinally dbChannel True err = do -- if the thread dies, we try to recover observer $ DBListenerFailRecoverObs True dbChannel err - putIsListenerOn appState False -- assume the pool connection was also lost, call the connection worker connectionWorker appState -- retry the listener From f9e974099926b8c1eeab9cfc9fc935fd142bddbf Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 8 May 2024 12:03:21 -0500 Subject: [PATCH 21/24] nix: add postgrest-ctags command Generates ctags for Haskell and Python code. --- nix/tools/devTools.nix | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/nix/tools/devTools.nix b/nix/tools/devTools.nix index 6185865843..229d6b1f1b 100644 --- a/nix/tools/devTools.nix +++ b/nix/tools/devTools.nix @@ -13,6 +13,8 @@ , style , tests , withTools +, haskellPackages +, ctags }: let watch = @@ -292,7 +294,7 @@ let parallelCurl = checkedShellScript { - name = "parallel-curl"; + name = "postgrest-parallel-curl"; docs = "wrapper for using parallel curl requests on the same "; args = [ "ARG_POSITIONAL_SINGLE([num], [number of parallel requests])" @@ -314,6 +316,17 @@ let eval "$curl_command" ''; + genCtags = + checkedShellScript + { + name = "postgrest-ctags"; + docs = "Generate ctags for Haskell and Python code"; + workingDir = "/"; + } + '' + ${ctags}/bin/ctags -a -R --fields=+l --languages=python --python-kinds=-iv -f ./tags test/io/ + ${haskellPackages.hasktags}/bin/hasktags -a -R -c -f ./tags . + ''; in buildToolbox { @@ -327,6 +340,7 @@ buildToolbox hsieGraphSymbols hsieMinimalImports parallelCurl + genCtags pushCachix watch; }; From 0060abeb01395c1a2e45168f76b68009dde9617a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 7 May 2024 13:30:27 -0500 Subject: [PATCH 22/24] feat: /live and /ready respond with 500 on failure 503 is still used by /ready to indicate a transient state that can be recovered from. --- CHANGELOG.md | 2 ++ docs/references/admin_server.rst | 8 +++++--- docs/references/configuration.rst | 2 +- src/PostgREST/Admin.hs | 13 ++++++++++--- src/PostgREST/AppState.hs | 12 +++++++++++- test/io/postgrest.py | 15 +++++++++++---- test/io/test_big_schema.py | 9 +++++---- test/io/test_io.py | 4 ++-- 8 files changed, 47 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f437ac62ae..3cbd8343da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #3404, Clarify the `PGRST121` (could not parse RAISE 'PGRST') error message - @laurenceisla - #3267, Fix wrong `503 Service Unavailable` on pg error `53400` - @taimoorzaeem - #2985, Fix not adding `application_name` on all connection strings - @steve-chavez + - #3424, Admin `/live` and `/ready` now differentiates a failure as 500 status - @steve-chavez + + 503 status is still given when postgREST is in a recovering state ### Deprecated diff --git a/docs/references/admin_server.rst b/docs/references/admin_server.rst index df9161f460..e06b7272f7 100644 --- a/docs/references/admin_server.rst +++ b/docs/references/admin_server.rst @@ -3,7 +3,7 @@ Admin Server ############ -PostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port` to the port number of your preference. +PostgREST provides an admin server that can be enabled by setting :ref:`admin-server-port`. .. _health_check: @@ -23,7 +23,7 @@ Two endpoints ``live`` and ``ready`` will then be available. Live ---- -The ``live`` endpoint verifies if PostgREST is running on its configured port. A request will return ``200 OK`` if PostgREST is alive or ``503`` otherwise. +The ``live`` endpoint verifies if PostgREST is running on its configured port. A request will return ``200 OK`` if PostgREST is alive or ``500`` otherwise. For instance, to verify if PostgREST is running while the ``admin-server-port`` is set to ``3001``: @@ -38,7 +38,7 @@ For instance, to verify if PostgREST is running while the ``admin-server-port`` Ready ----- -In addition, the ``ready`` endpoint checks the state of the :ref:`connection_pool` and the :ref:`schema_cache`. A request will return ``200 OK`` if both are good or ``503`` if not. +Additionally to the ``live`` check, the ``ready`` endpoint checks the state of the :ref:`connection_pool` and the :ref:`schema_cache`. A request will return ``200 OK`` if both are good or ``503`` if not. .. code-block:: bash @@ -48,6 +48,8 @@ In addition, the ``ready`` endpoint checks the state of the :ref:`connection_poo HTTP/1.1 200 OK +PostgREST will try to recover from the ``503`` state with :ref:`automatic_recovery`. + Metrics ======= diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 8c347dadf7..95514e1cd0 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -161,7 +161,7 @@ admin-server-port **In-Database** `n/a` =============== ======================= - Specifies the port for the :ref:`health_check` endpoints. + Specifies the port for the :ref:`admin_server`. .. _app.settings.*: diff --git a/src/PostgREST/Admin.hs b/src/PostgREST/Admin.hs index 40b87243be..325e95c065 100644 --- a/src/PostgREST/Admin.hs +++ b/src/PostgREST/Admin.hs @@ -42,12 +42,19 @@ admin :: AppState.AppState -> Wai.Application admin appState req respond = do isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState) isLoaded <- AppState.isLoaded appState + isPending <- AppState.isPending appState case Wai.pathInfo req of - ["ready"] -> - respond $ Wai.responseLBS (if isMainAppReachable && isLoaded then HTTP.status200 else HTTP.status503) [] mempty ["live"] -> - respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status503) [] mempty + respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status500) [] mempty + ["ready"] -> + let + status | not isMainAppReachable = HTTP.status500 + | isPending = HTTP.status503 + | isLoaded = HTTP.status200 + | otherwise = HTTP.status500 + in + respond $ Wai.responseLBS status [] mempty ["config"] -> do config <- AppState.getConfig appState respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config) diff --git a/src/PostgREST/AppState.hs b/src/PostgREST/AppState.hs index 3fad53ed62..d5abd95836 100644 --- a/src/PostgREST/AppState.hs +++ b/src/PostgREST/AppState.hs @@ -27,6 +27,7 @@ module PostgREST.AppState , runListener , getObserver , isLoaded + , isPending ) where import qualified Data.Aeson as JSON @@ -315,6 +316,12 @@ isLoaded x = do connEstablished <- isConnEstablished x return $ scacheStatus == SCLoaded && connEstablished +isPending :: AppState -> IO Bool +isPending x = do + scacheStatus <- readIORef $ stateSCacheStatus x + connStatus <- readIORef $ stateConnStatus x + return $ scacheStatus == SCPending || connStatus == ConnPending + putSCacheStatus :: AppState -> SchemaCacheStatus -> IO () putSCacheStatus = atomicWriteIORef . stateSCacheStatus @@ -338,12 +345,15 @@ loadSchemaCache appState@AppState{stateObserver=observer} = do observer $ SchemaCacheFatalErrorObs e hint return SCFatalFail Nothing -> do + putSCacheStatus appState SCPending putSchemaCache appState Nothing observer $ SchemaCacheNormalErrorObs e - putSCacheStatus appState SCPending return SCPending Right sCache -> do + -- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an + -- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state. + putSCacheStatus appState SCPending putSchemaCache appState $ Just sCache observer $ SchemaCacheQueriedObs resultTime (t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache diff --git a/test/io/postgrest.py b/test/io/postgrest.py index 43fe25ad43..a91c7775eb 100644 --- a/test/io/postgrest.py +++ b/test/io/postgrest.py @@ -70,6 +70,13 @@ def read_stdout(self, nlines=1): time.sleep(0.1) return output + def wait_until_scache_starts_loading(self, max_seconds=1): + "Wait for the admin /ready return a status of 503" + + wait_until_status_code( + self.admin.baseurl + "/ready", max_seconds=max_seconds, status_code=503 + ) + @contextlib.contextmanager def run( @@ -120,7 +127,7 @@ def run( process.stdin.close() if wait_for_readiness: - wait_until_ready(adminurl + "/ready", wait_max_seconds) + wait_until_status_code(adminurl + "/ready", wait_max_seconds, 200) if no_startup_stdout: process.stdout.read() @@ -178,14 +185,14 @@ def wait_until_exit(postgrest): raise PostgrestTimedOut() -def wait_until_ready(url, max_seconds): - "Wait for the given HTTP endpoint to return a status of 200." +def wait_until_status_code(url, max_seconds, status_code): + "Wait for the given HTTP endpoint to return a status code" session = requests_unixsocket.Session() for _ in range(max_seconds * 10): try: response = session.get(url, timeout=1) - if response.status_code == 200: + if response.status_code == status_code: return except (requests.ConnectionError, requests.ReadTimeout): pass diff --git a/test/io/test_big_schema.py b/test/io/test_big_schema.py index 3baa4a63c9..1e6ecf6c8a 100644 --- a/test/io/test_big_schema.py +++ b/test/io/test_big_schema.py @@ -7,7 +7,7 @@ from postgrest import * -def test_requests__wait_for_schema_cache_reload(defaultenv): +def test_requests_wait_for_schema_cache_reload(defaultenv): "requests that use the schema cache (e.g. resource embedding) wait for the schema cache to reload" env = { @@ -23,13 +23,14 @@ def test_requests__wait_for_schema_cache_reload(defaultenv): response = postgrest.session.get("/rpc/notify_pgrst") assert response.status_code == 204 - time.sleep(1.5) # wait for schema cache to start reloading + postgrest.wait_until_scache_starts_loading() response = postgrest.session.get("/tpopmassn?select=*,tpop(*)") assert response.status_code == 200 - server_timings = parse_server_timings_header(response.headers["Server-Timing"]) - plan_dur = server_timings["plan"] + plan_dur = parse_server_timings_header(response.headers["Server-Timing"])[ + "plan" + ] assert plan_dur > 10000.0 diff --git a/test/io/test_io.py b/test/io/test_io.py index d9cad71d9e..e36bb99631 100644 --- a/test/io/test_io.py +++ b/test/io/test_io.py @@ -739,7 +739,7 @@ def test_admin_ready_dependent_on_main_app(defaultenv): # delete the unix socket to make the main app fail os.remove(defaultenv["PGRST_SERVER_UNIX_SOCKET"]) response = postgrest.admin.get("/ready") - assert response.status_code == 503 + assert response.status_code == 500 def test_admin_live_good(defaultenv): @@ -757,7 +757,7 @@ def test_admin_live_dependent_on_main_app(defaultenv): # delete the unix socket to make the main app fail os.remove(defaultenv["PGRST_SERVER_UNIX_SOCKET"]) response = postgrest.admin.get("/live") - assert response.status_code == 503 + assert response.status_code == 500 @pytest.mark.parametrize("specialhostvalue", FIXTURES["specialhostvalues"]) From 50000a5087c7d7d7214a4803c47babe75d71a1cc Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 9 May 2024 09:17:43 +0200 Subject: [PATCH 23/24] test: Reset statement timeout after each test The statement timeout needs to be cleaned up after each test that modifies it instead of before the test. Otherwise the changed timeout leaks into other tests. --- test/io/test_io.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/io/test_io.py b/test/io/test_io.py index e36bb99631..289be9fa90 100644 --- a/test/io/test_io.py +++ b/test/io/test_io.py @@ -506,12 +506,13 @@ def test_statement_timeout(defaultenv, metapostgrest): data = response.json() assert data["message"] == "canceling statement due to statement timeout" + reset_statement_timeout(metapostgrest, role) + def test_change_statement_timeout(defaultenv, metapostgrest): "Statement timeout changes take effect immediately" role = "timeout_authenticator" - reset_statement_timeout(metapostgrest, role) env = { **defaultenv, @@ -544,6 +545,8 @@ def test_change_statement_timeout(defaultenv, metapostgrest): response = postgrest.session.get("/rpc/sleep?seconds=1") assert response.status_code == 204 + reset_statement_timeout(metapostgrest, role) + def test_pool_size(defaultenv, metapostgrest): "Verify that PGRST_DB_POOL setting allows the correct number of parallel requests" @@ -606,7 +609,6 @@ def test_change_statement_timeout_held_connection(defaultenv, metapostgrest): "Statement timeout changes take effect immediately, even with a request outliving the reconfiguration" role = "timeout_authenticator" - reset_statement_timeout(metapostgrest, role) env = { **defaultenv, @@ -651,6 +653,8 @@ def sleep(i=i): for t in threads: t.join() + reset_statement_timeout(metapostgrest, role) + def test_admin_config(defaultenv): "Should get a success response from the admin server containing current configuration" @@ -701,7 +705,6 @@ def test_admin_ready_includes_schema_cache_state(defaultenv, metapostgrest): "Should get a failed response from the admin server ready endpoint when the schema cache is not loaded" role = "timeout_authenticator" - reset_statement_timeout(metapostgrest, role) env = { **defaultenv, @@ -723,6 +726,8 @@ def test_admin_ready_includes_schema_cache_state(defaultenv, metapostgrest): response = postgrest.session.get("/projects") assert response.status_code == 503 + reset_statement_timeout(metapostgrest, role) + def test_admin_not_found(defaultenv): "Should get a not found from a undefined endpoint on the admin server" @@ -1388,6 +1393,8 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest): assert " 500 " in output[0] assert "canceling statement due to statement timeout" in output[1] + reset_statement_timeout(metapostgrest, role) + def test_function_setting_statement_timeout_fails(defaultenv): "statement that takes three seconds to execute should fail with one second timeout" From c869433b2e4e424ce8c73d7ca0fef40dcf7c2e38 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 9 May 2024 09:19:31 +0200 Subject: [PATCH 24/24] test: Prevent test_admin_ready_includes_schema_cache_state from timing out By increasing the delays in this test by factor 400x, postgrest will not swamp pg with connection retries after the failed schema cache anymore. This would happen because there is no backoff included after fatal errors. Once it does, the io tests hang indefinitely in CI. --- src/PostgREST/SchemaCache.hs | 6 +++--- test/io/test_io.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index 055d02a7cd..0c30e76231 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -145,6 +145,9 @@ type SqlQuery = ByteString querySchemaCache :: AppConfig -> SQL.Transaction SchemaCache querySchemaCache AppConfig{..} = do + _ <- + let sleepCall = SQL.Statement "select pg_sleep($1 / 1000.0)" (param HE.int4) HD.noResult prepared in + whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object pgVer <- SQL.statement mempty $ pgVersionStatement prepared tabs <- SQL.statement schemas $ allTables pgVer prepared @@ -155,9 +158,6 @@ querySchemaCache AppConfig{..} = do reps <- SQL.statement schemas $ dataRepresentations prepared mHdlers <- SQL.statement schemas $ mediaHandlers pgVer prepared tzones <- SQL.statement mempty $ timezones prepared - _ <- - let sleepCall = SQL.Statement "select pg_sleep($1)" (param HE.int4) HD.noResult prepared in - whenJust configInternalSCSleep (`SQL.statement` sleepCall) -- only used for testing let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps rels = addInverseRels $ addM2MRels tabsWViewsPks $ addViewM2OAndO2ORels keyDeps m2oRels diff --git a/test/io/test_io.py b/test/io/test_io.py index 289be9fa90..e825841aaf 100644 --- a/test/io/test_io.py +++ b/test/io/test_io.py @@ -710,20 +710,23 @@ def test_admin_ready_includes_schema_cache_state(defaultenv, metapostgrest): **defaultenv, "PGUSER": role, "PGRST_DB_ANON_ROLE": role, + "PGRST_INTERNAL_SCHEMA_CACHE_SLEEP": "500", } with run(env=env) as postgrest: - # make it impossible to load the schema cache, by setting statement timeout to 1ms - set_statement_timeout(metapostgrest, role, 1) + # The schema cache query takes at least 500ms, due do PGRST_INTERNAL_SCHEMA_CACHE_SLEEP above. + # Make it impossible to load the schema cache, by setting statement timeout to 400ms. + set_statement_timeout(metapostgrest, role, 400) # force a reconnection so the new role setting is picked up postgrest.process.send_signal(signal.SIGUSR1) - sleep_until_postgrest_scache_reload() + # wait 600ms to finish schema cache reload attempt + time.sleep(0.6) - response = postgrest.admin.get("/ready") + response = postgrest.admin.get("/ready", timeout=1) assert response.status_code == 503 - response = postgrest.session.get("/projects") + response = postgrest.session.get("/projects", timeout=1) assert response.status_code == 503 reset_statement_timeout(metapostgrest, role)