Compare commits
@@ -2,4 +2,6 @@
|
|||||||
# and made its way to us through nixpkgs.
|
# and made its way to us through nixpkgs.
|
||||||
self-hosted-runner:
|
self-hosted-runner:
|
||||||
labels:
|
labels:
|
||||||
|
- macos-15-intel
|
||||||
|
- macos-26
|
||||||
- ubuntu-24.04-arm
|
- ubuntu-24.04-arm
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ runs:
|
|||||||
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
||||||
- name: Save artifact to GitHub Actions
|
- name: Save artifact to GitHub Actions
|
||||||
if: steps.find-task.outputs.task_found
|
if: steps.find-task.outputs.task_found
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ inputs.upload }}
|
name: ${{ inputs.upload }}
|
||||||
path: ${{ steps.download.outputs.artifacts }}
|
path: ${{ steps.download.outputs.artifacts }}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ inputs.prefix }}-
|
${{ runner.os }}-${{ inputs.prefix }}-
|
||||||
- uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ runs:
|
|||||||
nix_conf: |-
|
nix_conf: |-
|
||||||
always-allow-substitutes = true
|
always-allow-substitutes = true
|
||||||
max-jobs = auto
|
max-jobs = auto
|
||||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||||
with:
|
with:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
authToken: ${{ inputs.authToken }}
|
authToken: ${{ inputs.authToken }}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
# This actions creates the github token using the postgrest app secrets
|
# This actions creates the github token using the postgrest app secrets
|
||||||
- name: Create Github App Token
|
- name: Create Github App Token
|
||||||
id: app-token
|
id: app-token
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||||
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
# Backport action that creates the PR with given settings
|
# Backport action that creates the PR with given settings
|
||||||
- name: Create backport PR
|
- name: Create backport PR
|
||||||
uses: korthout/backport-action@3c06f323a58619da1e8522229ebc8d5de2633e46 # v4.3.0
|
uses: korthout/backport-action@66065406958f46e82238fd59546f5a99e69e22aa # v4.5
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.app-token.outputs.token }}
|
github_token: ${{ steps.app-token.outputs.token }}
|
||||||
pull_description: 'Backport for #${pull_number}.'
|
pull_description: 'Backport for #${pull_number}.'
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Build static executable
|
- name: Build static executable
|
||||||
run: nix-build -A postgrestStatic -A postgrestStatic.tests
|
run: nix-build -A postgrestStatic -A postgrestStatic.tests
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-linux-static-x86-64
|
name: postgrest-linux-static-x86-64
|
||||||
path: result/bin/postgrest
|
path: result/bin/postgrest
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||||
- name: Save built Docker image as artifact
|
- name: Save built Docker image as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-x86-64
|
name: postgrest-docker-x86-64
|
||||||
path: postgrest-docker.tar.gz
|
path: postgrest-docker.tar.gz
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: Nix - MacOS
|
name: Nix - MacOS
|
||||||
runs-on: macos-15
|
runs-on: macos-26
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
@@ -105,6 +105,15 @@ jobs:
|
|||||||
artifact: postgrest-macos-aarch64
|
artifact: postgrest-macos-aarch64
|
||||||
deps: brew link --force libpq
|
deps: brew link --force libpq
|
||||||
|
|
||||||
|
- name: MacOS x86-64
|
||||||
|
runs-on: macos-15-intel
|
||||||
|
cache: |
|
||||||
|
~/.stack/pantry
|
||||||
|
~/.stack/snapshots
|
||||||
|
~/.stack/stack.sqlite3
|
||||||
|
artifact: postgrest-macos-x86-64
|
||||||
|
deps: brew link --force libpq
|
||||||
|
|
||||||
- name: Windows
|
- name: Windows
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
cache: |
|
cache: |
|
||||||
@@ -118,7 +127,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: haskell-actions/setup@de26526e12bc780fb9d384c1fb61c0bf02e3a40d # v2.10.4
|
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||||
with:
|
with:
|
||||||
# This must match the version in stack.yaml's resolver
|
# This must match the version in stack.yaml's resolver
|
||||||
ghc-version: 9.6.7
|
ghc-version: 9.6.7
|
||||||
@@ -146,7 +155,7 @@ jobs:
|
|||||||
- name: Strip Executable
|
- name: Strip Executable
|
||||||
run: strip result/postgrest*
|
run: strip result/postgrest*
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.artifact }}
|
name: ${{ matrix.artifact }}
|
||||||
path: |
|
path: |
|
||||||
@@ -177,7 +186,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: haskell-actions/setup@de26526e12bc780fb9d384c1fb61c0bf02e3a40d # v2.10.4
|
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||||
with:
|
with:
|
||||||
ghc-version: ${{ matrix.ghc }}
|
ghc-version: ${{ matrix.ghc }}
|
||||||
- name: Cache .cabal
|
- name: Cache .cabal
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ jobs:
|
|||||||
concurrency:
|
concurrency:
|
||||||
# Never tag outdated commits on the main branch by skipping superseded commits
|
# Never tag outdated commits on the main branch by skipping superseded commits
|
||||||
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
|
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
|
||||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
cancel-in-progress: true
|
||||||
cancel-in-progress: false
|
|
||||||
if: vars.RELEASE_ENABLED
|
if: vars.RELEASE_ENABLED
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linkcheck:
|
linkcheck:
|
||||||
|
name: Linkcheck
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -15,4 +16,28 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
tools: docs.linkcheck.bin
|
tools: docs.linkcheck.bin
|
||||||
- run: postgrest-docs-linkcheck
|
|
||||||
|
- name: Run Linkcheck
|
||||||
|
id: linkcheck
|
||||||
|
run: postgrest-docs-linkcheck
|
||||||
|
|
||||||
|
# This actions creates the github token using the postgrest app secrets
|
||||||
|
- name: Create Github App Token (Runs only on linkcheck failure)
|
||||||
|
id: app-token
|
||||||
|
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }} # only create the token on linkcheck failure
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||||
|
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||||
|
permission-issues: write # required for commenting on issues
|
||||||
|
|
||||||
|
- name: Notify on linkcheck failure by commenting on GH Issue 4106
|
||||||
|
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }}
|
||||||
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
issue-number: 4106
|
||||||
|
body: |
|
||||||
|
**Linkcheck Job Failed!**
|
||||||
|
|
||||||
|
A broken link was detected in the docs. Please check the [failed run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ on:
|
|||||||
concurrency:
|
concurrency:
|
||||||
# Terminate all previous runs of the same workflow for the same tag.
|
# Terminate all previous runs of the same workflow for the same tag.
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
cancel-in-progress: true
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -49,7 +48,7 @@ jobs:
|
|||||||
echo "Relevant extract from CHANGELOG.md:"
|
echo "Relevant extract from CHANGELOG.md:"
|
||||||
cat CHANGES.md
|
cat CHANGES.md
|
||||||
- name: Save CHANGES.md as artifact
|
- name: Save CHANGES.md as artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-changes
|
name: release-changes
|
||||||
path: CHANGES.md
|
path: CHANGES.md
|
||||||
@@ -81,6 +80,9 @@ jobs:
|
|||||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
|
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
|
||||||
-C artifacts/postgrest-macos-aarch64 postgrest
|
-C artifacts/postgrest-macos-aarch64 postgrest
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x86-64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-macos-x86-64 postgrest
|
||||||
|
|
||||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
||||||
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@ jobs:
|
|||||||
artifacts/postgrest-windows-x86-64/postgrest.exe
|
artifacts/postgrest-windows-x86-64/postgrest.exe
|
||||||
|
|
||||||
- name: Save release bundle
|
- name: Save release bundle
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: release-bundle
|
path: release-bundle
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||||
run: postgrest-coverage
|
run: postgrest-coverage
|
||||||
- name: Upload coverage to codecov
|
- name: Upload coverage to codecov
|
||||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage/codecov.json
|
files: ./coverage/codecov.json
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -156,6 +156,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
runs-on:
|
runs-on:
|
||||||
|
- macos-15-intel # x86_64-darwin
|
||||||
- macos-14 # aarch64-darwin
|
- macos-14 # aarch64-darwin
|
||||||
- ubuntu-24.04 # x86_64-linux
|
- ubuntu-24.04 # x86_64-linux
|
||||||
- ubuntu-24.04-arm # aarch64-linux
|
- ubuntu-24.04-arm # aarch64-linux
|
||||||
|
|||||||
@@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [14.12] - 2026-05-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix race condition in pool_available metric causing negative values during network instability by @mkleczek in #4622
|
||||||
|
|
||||||
|
## [14.11] - 2026-05-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
|
||||||
|
- Restore Listener query shape so it can be found in `pg_stat_activity` by @mkleczek in #4857 #4859
|
||||||
|
- The LISTEN channel now automatically recovers when it stops working due to a PostgreSQL bug @laurenceisla in #3147
|
||||||
|
- Fix misleading "Functions" name on schema cache summary in startup logs by @taimoorzaeem in #4821
|
||||||
|
|
||||||
|
## [14.10] - 2026-04-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
|
||||||
|
|
||||||
## [14.9] - 2026-04-10
|
## [14.9] - 2026-04-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||||
# nix/tools/docker/README.md
|
# nix/tools/docker/README.md
|
||||||
|
|
||||||
FROM ubuntu:noble@sha256:84e77dee7d1bc93fb029a45e3c6cb9d8aa4831ccfcc7103d36e876938d28895b AS postgrest
|
FROM ubuntu:resolute@sha256:f3d28607ddd78734bb7f71f117f3c6706c666b8b76cbff7c9ff6e5718d46ff64 AS postgrest
|
||||||
|
|
||||||
RUN apt-get update -y \
|
RUN apt-get update -y \
|
||||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ Community Tutorials
|
|||||||
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
||||||
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
||||||
|
|
||||||
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||||
|
|
||||||
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
||||||
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
||||||
|
|||||||
@@ -318,144 +318,6 @@ You can insert a new product using a JSON object for the ``extra_info`` column:
|
|||||||
|
|
||||||
To query and filter the data see :ref:`json_columns` for a complete reference.
|
To query and filter the data see :ref:`json_columns` for a complete reference.
|
||||||
|
|
||||||
.. _ww_postgis:
|
|
||||||
|
|
||||||
PostGIS
|
|
||||||
-------
|
|
||||||
|
|
||||||
You can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
-- Activate the postgis module in the current database
|
|
||||||
create extension if not exists postgis;
|
|
||||||
|
|
||||||
create table coverage (
|
|
||||||
id int primary key,
|
|
||||||
name text unique,
|
|
||||||
area geometry
|
|
||||||
);
|
|
||||||
|
|
||||||
To add areas in polygon format, you can use string representation:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "http://localhost:3000/coverage" \
|
|
||||||
-X POST -H "Content-Type: application/json" \
|
|
||||||
-d @- << EOF
|
|
||||||
[
|
|
||||||
{ "id": 1, "name": "small", "area": "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" },
|
|
||||||
{ "id": 2, "name": "big", "area": "SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))" }
|
|
||||||
]
|
|
||||||
EOF
|
|
||||||
|
|
||||||
Now, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "http://localhost:3000/coverage" \
|
|
||||||
-H "Accept: application/geo+json"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "small"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "big"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
alter table coverage
|
|
||||||
add square_units double precision generated always as ( st_area(area) ) stored;
|
|
||||||
|
|
||||||
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
create or replace function coverage_geo_collection() returns json as $$
|
|
||||||
select
|
|
||||||
json_build_object(
|
|
||||||
'type', 'FeatureCollection',
|
|
||||||
'features', json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'type', 'Feature',
|
|
||||||
'geometry', st_AsGeoJSON(c.area)::json,
|
|
||||||
'properties', json_build_object('id', c.id, 'name', c.name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
from coverage c;
|
|
||||||
$$ language sql;
|
|
||||||
|
|
||||||
Now this query will return the same results:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "small"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "big"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ranges
|
Ranges
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -609,3 +471,20 @@ You can use other comparative filters and also all the `PostgreSQL special date/
|
|||||||
"due_date": "2022-02-27T06:00:00-05:00"
|
"due_date": "2022-02-27T06:00:00-05:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
let hash = window.location.hash;
|
||||||
|
|
||||||
|
const redirects = {
|
||||||
|
// PostGIS
|
||||||
|
'#postgis': '../integrations/postgis.html#postgis',
|
||||||
|
};
|
||||||
|
|
||||||
|
let willRedirectTo = redirects[hash];
|
||||||
|
|
||||||
|
if (willRedirectTo) {
|
||||||
|
window.location.href = willRedirectTo;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
.. _ww_postgis:
|
||||||
|
|
||||||
|
PostGIS
|
||||||
|
=======
|
||||||
|
|
||||||
|
You can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).
|
||||||
|
|
||||||
|
.. code-block:: postgres
|
||||||
|
|
||||||
|
-- Activate the postgis module in the current database
|
||||||
|
create extension if not exists postgis;
|
||||||
|
|
||||||
|
create table coverage (
|
||||||
|
id int primary key,
|
||||||
|
name text unique,
|
||||||
|
area geometry
|
||||||
|
);
|
||||||
|
|
||||||
|
To add areas in polygon format, you can use string representation:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/coverage" \
|
||||||
|
-X POST -H "Content-Type: application/json" \
|
||||||
|
-d @- << EOF
|
||||||
|
[
|
||||||
|
{ "id": 1, "name": "small", "area": "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" },
|
||||||
|
{ "id": 2, "name": "big", "area": "SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))" }
|
||||||
|
]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
Now, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/coverage" \
|
||||||
|
-H "Accept: application/geo+json"
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "small"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 2,
|
||||||
|
"name": "big"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
||||||
|
|
||||||
|
.. code-block:: postgres
|
||||||
|
|
||||||
|
alter table coverage
|
||||||
|
add square_units double precision generated always as ( st_area(area) ) stored;
|
||||||
|
|
||||||
|
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
||||||
|
|
||||||
|
.. code-block:: postgres
|
||||||
|
|
||||||
|
create or replace function coverage_geo_collection() returns json as $$
|
||||||
|
select
|
||||||
|
json_build_object(
|
||||||
|
'type', 'FeatureCollection',
|
||||||
|
'features', json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'type', 'Feature',
|
||||||
|
'geometry', st_AsGeoJSON(c.area)::json,
|
||||||
|
'properties', json_build_object('id', c.id, 'name', c.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from coverage c;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
Now this query will return the same results:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "small"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 2,
|
||||||
|
"name": "big"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -46,7 +46,9 @@ This will cause the :ref:`connection_pool` to connect to the read replica host a
|
|||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
|
- Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
|
||||||
|
So if you specify ``target_session_attrs=read-only`` as mentioned above, PostgREST will override it for the ``LISTEN``.
|
||||||
|
- ``read-only`` is only available on libpq >= 14, if you use a lower version you will get an error like ``invalid target_session_attrs value: \"read-only\"``.
|
||||||
|
|
||||||
.. _listener_automatic_recovery:
|
.. _listener_automatic_recovery:
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
|
|||||||
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
|
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
|
||||||
06/May/2024:08:16:11 -0500: Config reloaded
|
06/May/2024:08:16:11 -0500: Config reloaded
|
||||||
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
||||||
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
|
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 RPCs, 0 Domain Representations, 4 Media Type Handlers
|
||||||
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
||||||
06/May/2024:14:11:27 -0500: Config reloaded
|
06/May/2024:14:11:27 -0500: Config reloaded
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ let
|
|||||||
git add CHANGELOG.md > /dev/null
|
git add CHANGELOG.md > /dev/null
|
||||||
|
|
||||||
echo "Committing ..."
|
echo "Committing ..."
|
||||||
git commit -m "bump version to $new_version" > /dev/null
|
git commit -m "chore: bump version to $new_version" > /dev/null
|
||||||
|
|
||||||
if [[ "$current_branch" == "main" ]]; then
|
if [[ "$current_branch" == "main" ]]; then
|
||||||
bump devel
|
bump devel
|
||||||
@@ -74,7 +74,7 @@ let
|
|||||||
git branch "v$A"
|
git branch "v$A"
|
||||||
|
|
||||||
echo "Committing (devel bump)..."
|
echo "Committing (devel bump)..."
|
||||||
git commit -m "bump version to $new_version" > /dev/null
|
git commit -m "chore: bump version to $new_version" > /dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
trap "echo Remote not found. Please push manually ..." ERR
|
trap "echo Remote not found. Please push manually ..." ERR
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
, hostPlatform
|
, hostPlatform
|
||||||
, jq
|
, jq
|
||||||
, lib
|
, lib
|
||||||
|
, nginx
|
||||||
, postgrest
|
, postgrest
|
||||||
, python3
|
, python3
|
||||||
, runtimeShell
|
, runtimeShell
|
||||||
@@ -94,6 +95,7 @@ let
|
|||||||
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
|
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
|
||||||
workingDir = "/";
|
workingDir = "/";
|
||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
|
withPath = [ nginx ];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||||
@@ -156,6 +158,7 @@ let
|
|||||||
redirectTixFiles = false;
|
redirectTixFiles = false;
|
||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
withTmpDir = true;
|
withTmpDir = true;
|
||||||
|
withPath = [ nginx ];
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ let
|
|||||||
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])"
|
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from])"
|
||||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
"ARG_LEFTOVERS([command arguments])"
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
"ARG_USE_ENV([PGUSER], [Postgrest_Test_Authenticator], [Authenticator PG role])" # user is written in mixed case to implicitly test that it is being properly quoted in schema cache queries
|
||||||
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
||||||
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
||||||
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
|
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 14.9
|
version: 14.12
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -157,6 +157,7 @@ library
|
|||||||
, stm-hamt >= 1.2 && < 2
|
, stm-hamt >= 1.2 && < 2
|
||||||
, focus >= 1.0 && < 2
|
, focus >= 1.0 && < 2
|
||||||
, some >= 1.0.4.1 && < 2
|
, some >= 1.0.4.1 && < 2
|
||||||
|
, uuid >= 1.3 && < 2
|
||||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||||
-- -optP-Wno-nonportable-include-path
|
-- -optP-Wno-nonportable-include-path
|
||||||
@@ -304,6 +305,7 @@ test-suite observability
|
|||||||
other-modules: ObsHelper
|
other-modules: ObsHelper
|
||||||
Observation.JwtCache
|
Observation.JwtCache
|
||||||
Observation.MetricsSpec
|
Observation.MetricsSpec
|
||||||
|
Observation.SchemaCacheSpec
|
||||||
build-depends: base >= 4.9 && < 4.20
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
|
|||||||
@@ -219,10 +219,14 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
|
|||||||
|
|
||||||
-- | Flush the connection pool so that any future use of the pool will
|
-- | Flush the connection pool so that any future use of the pool will
|
||||||
-- use connections freshly established after this call.
|
-- use connections freshly established after this call.
|
||||||
|
-- | Emits PoolFlushed observation
|
||||||
flushPool :: AppState -> IO ()
|
flushPool :: AppState -> IO ()
|
||||||
flushPool AppState{..} = SQL.release statePool
|
flushPool AppState{..} = do
|
||||||
|
SQL.release statePool
|
||||||
|
stateObserver PoolFlushed
|
||||||
|
|
||||||
-- | Destroy the pool on shutdown.
|
-- | Destroy the pool on shutdown.
|
||||||
|
-- | Differs from flushPool in not emiting PoolFlushed observation.
|
||||||
destroyPool :: AppState -> IO ()
|
destroyPool :: AppState -> IO ()
|
||||||
destroyPool AppState{..} = SQL.release statePool
|
destroyPool AppState{..} = SQL.release statePool
|
||||||
|
|
||||||
@@ -309,8 +313,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
observer $ ConnectionRetryObs delay
|
observer $ ConnectionRetryObs delay
|
||||||
putNextListenerDelay appState delay
|
putNextListenerDelay appState delay
|
||||||
|
|
||||||
flushPool appState
|
|
||||||
|
|
||||||
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@@ -359,6 +361,10 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
-- 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.
|
-- 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
|
putSCacheStatus appState SCPending
|
||||||
putSchemaCache appState $ Just sCache
|
putSchemaCache appState $ Just sCache
|
||||||
|
-- Flush the pool after loading the schema cache to reset any stale session cache entries
|
||||||
|
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
|
||||||
|
-- and after marking sCacheStatus as pending,
|
||||||
|
flushPool appState
|
||||||
observer $ SchemaCacheQueriedObs resultTime
|
observer $ SchemaCacheQueriedObs resultTime
|
||||||
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
||||||
observer $ SchemaCacheLoadedObs t
|
observer $ SchemaCacheLoadedObs t
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ queryDbSettings preConfFunc prepared =
|
|||||||
SELECT setdatabase as database,
|
SELECT setdatabase as database,
|
||||||
unnest(setconfig) as setting
|
unnest(setconfig) as setting
|
||||||
FROM pg_catalog.pg_db_role_setting
|
FROM pg_catalog.pg_db_role_setting
|
||||||
WHERE setrole = CURRENT_USER::regrole::oid
|
WHERE setrole = quote_ident(CURRENT_USER)::regrole::oid
|
||||||
AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))
|
AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))
|
||||||
),
|
),
|
||||||
kv_settings AS (
|
kv_settings AS (
|
||||||
@@ -142,7 +142,7 @@ queryRoleSettings pgVer prepared =
|
|||||||
select r.rolname, unnest(r.rolconfig) as setting
|
select r.rolname, unnest(r.rolconfig) as setting
|
||||||
from pg_auth_members m
|
from pg_auth_members m
|
||||||
join pg_roles r on r.oid = m.roleid
|
join pg_roles r on r.oid = m.roleid
|
||||||
where member = current_user::regrole::oid
|
where member = quote_ident(current_user)::regrole::oid
|
||||||
),
|
),
|
||||||
kv_settings AS (
|
kv_settings AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -167,7 +167,7 @@ queryRoleSettings pgVer prepared =
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
hasParameterPrivilege
|
hasParameterPrivilege
|
||||||
| pgVer >= pgVersion150 = "or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')"
|
| pgVer >= pgVersion150 = "or has_parameter_privilege(quote_ident(current_user)::regrole::oid, ps.name, 'set')"
|
||||||
| otherwise = ""
|
| otherwise = ""
|
||||||
|
|
||||||
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
|
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import qualified Hasql.Connection as SQL
|
|||||||
import qualified Hasql.Notifications as SQL
|
import qualified Hasql.Notifications as SQL
|
||||||
import PostgREST.AppState (AppState, getConfig)
|
import PostgREST.AppState (AppState, getConfig)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.Observation (Observation (..),
|
import PostgREST.Observation (Observation (..))
|
||||||
isDbListenerBug)
|
|
||||||
import PostgREST.Version (prettyVersion)
|
import PostgREST.Version (prettyVersion)
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
@@ -20,6 +19,7 @@ import qualified PostgREST.Config as Config
|
|||||||
import Control.Arrow ((&&&))
|
import Control.Arrow ((&&&))
|
||||||
import Data.Bitraversable (bisequence)
|
import Data.Bitraversable (bisequence)
|
||||||
import Data.Either.Combinators (whenRight)
|
import Data.Either.Combinators (whenRight)
|
||||||
|
import qualified Data.Text as T
|
||||||
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||||
import qualified Hasql.Session as SQL
|
import qualified Hasql.Session as SQL
|
||||||
import PostgREST.Config.Database (queryPgVersion)
|
import PostgREST.Config.Database (queryPgVersion)
|
||||||
@@ -31,12 +31,12 @@ runListener :: AppState -> IO ()
|
|||||||
runListener appState = do
|
runListener appState = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
when configDbChannelEnabled $
|
when configDbChannelEnabled $
|
||||||
void . forkIO . void $ retryingListen appState
|
void . forkIO . void $ retryingListen appState False
|
||||||
|
|
||||||
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
||||||
-- | This function never returns (but can throw) and return type enforces that.
|
-- | This function never returns (but can throw) and return type enforces that.
|
||||||
retryingListen :: AppState -> IO Void
|
retryingListen :: AppState -> Bool -> IO Void
|
||||||
retryingListen appState = do
|
retryingListen appState hasDbListenerBug = do
|
||||||
AppConfig{..} <- AppState.getConfig appState
|
AppConfig{..} <- AppState.getConfig appState
|
||||||
let
|
let
|
||||||
dbChannel = toS configDbChannel
|
dbChannel = toS configDbChannel
|
||||||
@@ -44,7 +44,7 @@ retryingListen appState = do
|
|||||||
AppState.putIsListenerOn appState False
|
AppState.putIsListenerOn appState False
|
||||||
observer $ DBListenFail dbChannel (Right err)
|
observer $ DBListenFail dbChannel (Right err)
|
||||||
when (isDbListenerBug err) $
|
when (isDbListenerBug err) $
|
||||||
observer DBListenBugHint
|
observer DBListenBugCallQueryFix
|
||||||
unless configDbPoolAutomaticRecovery $
|
unless configDbPoolAutomaticRecovery $
|
||||||
killThread mainThreadId
|
killThread mainThreadId
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ retryingListen appState = do
|
|||||||
unless (delay == maxDelay) $
|
unless (delay == maxDelay) $
|
||||||
AppState.putNextListenerDelay appState (delay * 2)
|
AppState.putNextListenerDelay appState (delay * 2)
|
||||||
-- loop running the listener
|
-- loop running the listener
|
||||||
retryingListen appState
|
retryingListen appState (isDbListenerBug err)
|
||||||
|
|
||||||
-- Execute the listener with with error handling
|
-- Execute the listener with with error handling
|
||||||
handle onError $ do
|
handle onError $ do
|
||||||
@@ -68,9 +68,10 @@ retryingListen appState = do
|
|||||||
-- use connection
|
-- use connection
|
||||||
\case
|
\case
|
||||||
Right db -> do
|
Right db -> do
|
||||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
|
||||||
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
||||||
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
|
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
|
||||||
|
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
|
||||||
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
|
|
||||||
AppState.putIsListenerOn appState True
|
AppState.putIsListenerOn appState True
|
||||||
|
|
||||||
@@ -106,3 +107,10 @@ retryingListen appState = do
|
|||||||
AppState.schemaCacheLoader appState
|
AppState.schemaCacheLoader appState
|
||||||
|
|
||||||
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
||||||
|
|
||||||
|
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
|
||||||
|
|
||||||
|
-- Used to fix a Postgres bug in the listener, see: https://github.com/PostgREST/postgrest/issues/3147#issuecomment-3494591361
|
||||||
|
-- This query advances the async notification query tail, which solves this issue.
|
||||||
|
callNotifQueryUsage :: SQL.Session ()
|
||||||
|
callNotifQueryUsage = SQL.sql "SELECT pg_notification_queue_usage();"
|
||||||
|
|||||||
+126
-1
@@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Logger
|
Module : PostgREST.Logger
|
||||||
@@ -35,7 +36,14 @@ import PostgREST.Config (LogLevel (..))
|
|||||||
import PostgREST.Observation
|
import PostgREST.Observation
|
||||||
import PostgREST.Query (MainQuery (..))
|
import PostgREST.Query (MainQuery (..))
|
||||||
|
|
||||||
import Protolude
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Hasql.Connection as SQL
|
||||||
|
import qualified Hasql.Pool.Observation as SQL
|
||||||
|
import Numeric (showFFloat)
|
||||||
|
import PostgREST.Config.PgVersion (pgvName)
|
||||||
|
import qualified PostgREST.Error as Error
|
||||||
|
import Protolude
|
||||||
|
|
||||||
data LoggerState = LoggerState
|
data LoggerState = LoggerState
|
||||||
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
|
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
|
||||||
@@ -110,6 +118,9 @@ observationLogger loggerState logLevel obs = case obs of
|
|||||||
o@PoolRequestFullfilled ->
|
o@PoolRequestFullfilled ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
|
o@PoolFlushed ->
|
||||||
|
when (logLevel >= LogDebug) $ do
|
||||||
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@JwtCacheEviction ->
|
o@JwtCacheEviction ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
@@ -143,3 +154,117 @@ renderSnippet snippet =
|
|||||||
prepared = False -- unused
|
prepared = False -- unused
|
||||||
in
|
in
|
||||||
sql
|
sql
|
||||||
|
|
||||||
|
|
||||||
|
observationMessage :: Observation -> Text
|
||||||
|
observationMessage = \case
|
||||||
|
AdminStartObs address ->
|
||||||
|
"Admin server listening on " <> address
|
||||||
|
AppStartObs ver ->
|
||||||
|
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||||
|
AppServerAddressObs address ->
|
||||||
|
"API server listening on " <> address
|
||||||
|
DBConnectedObs ver ->
|
||||||
|
"Successfully connected to " <> ver
|
||||||
|
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||||
|
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
||||||
|
ExitDBNoRecoveryObs ->
|
||||||
|
"Automatic recovery disabled, exiting."
|
||||||
|
ExitDBFatalError ServerAuthError usageErr ->
|
||||||
|
"Failed to establish a connection. " <> jsonMessage usageErr
|
||||||
|
ExitDBFatalError ServerPgrstBug usageErr ->
|
||||||
|
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||||
|
ExitDBFatalError ServerError42P05 usageErr ->
|
||||||
|
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||||
|
ExitDBFatalError ServerError08P01 usageErr ->
|
||||||
|
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||||
|
SchemaCacheEmptyObs ->
|
||||||
|
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
|
||||||
|
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||||
|
"Failed to load the schema cache using "
|
||||||
|
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
||||||
|
<> " and "
|
||||||
|
<> "db-extra-search-path=" <> T.intercalate "," extraPaths <> ". " <> jsonMessage usageErr
|
||||||
|
SchemaCacheQueriedObs resultTime ->
|
||||||
|
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
||||||
|
SchemaCacheSummaryObs summary ->
|
||||||
|
"Schema cache loaded " <> summary
|
||||||
|
SchemaCacheLoadedObs resultTime ->
|
||||||
|
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||||
|
ConnectionRetryObs delay ->
|
||||||
|
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||||
|
QueryPgVersionError usageErr ->
|
||||||
|
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||||
|
DBListenStart host port fullName channel -> do
|
||||||
|
"Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
||||||
|
DBListenFail channel listenErr ->
|
||||||
|
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||||
|
either showListenerConnError showListenerException listenErr
|
||||||
|
DBListenRetry delay ->
|
||||||
|
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||||
|
DBListenBugCallQueryFix ->
|
||||||
|
"This is likely a PostgreSQL bug in the notification queue, executing the following to try to solve it: SELECT pg_notification_queue_usage();"
|
||||||
|
DBListenerGotSCacheMsg channel ->
|
||||||
|
"Received a schema cache reload message on the " <> show channel <> " channel"
|
||||||
|
DBListenerGotConfigMsg channel ->
|
||||||
|
"Received a config reload message on the " <> show channel <> " channel"
|
||||||
|
DBListenerConnectionCleanupFail ex ->
|
||||||
|
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
||||||
|
QueryObs{} ->
|
||||||
|
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
||||||
|
ConfigReadErrorObs usageErr ->
|
||||||
|
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||||
|
QueryRoleSettingsErrorObs usageErr ->
|
||||||
|
"Failed to query the role settings. " <> jsonMessage usageErr
|
||||||
|
QueryErrorCodeHighObs usageErr ->
|
||||||
|
jsonMessage usageErr
|
||||||
|
ConfigInvalidObs err ->
|
||||||
|
"Failed reloading config: " <> err
|
||||||
|
ConfigSucceededObs ->
|
||||||
|
"Config reloaded"
|
||||||
|
PoolInit poolSize ->
|
||||||
|
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
||||||
|
PoolAcqTimeoutObs usageErr ->
|
||||||
|
jsonMessage usageErr
|
||||||
|
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
||||||
|
"Connection " <> show uuid <> (
|
||||||
|
case status of
|
||||||
|
SQL.ConnectingConnectionStatus -> " is being established"
|
||||||
|
SQL.ReadyForUseConnectionStatus -> " is available"
|
||||||
|
SQL.InUseConnectionStatus -> " is used"
|
||||||
|
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
||||||
|
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
||||||
|
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
||||||
|
SQL.ReleaseConnectionTerminationReason -> "release"
|
||||||
|
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
||||||
|
)
|
||||||
|
PoolRequest ->
|
||||||
|
"Trying to borrow a connection from pool"
|
||||||
|
PoolRequestFullfilled ->
|
||||||
|
"Borrowed a connection from the pool"
|
||||||
|
PoolFlushed ->
|
||||||
|
"Database connection pool flushed"
|
||||||
|
JwtCacheLookup _ ->
|
||||||
|
"Looked up a JWT in JWT cache"
|
||||||
|
JwtCacheEviction ->
|
||||||
|
"Evicted entry from JWT cache"
|
||||||
|
TerminationUnixSignalObs signal ->
|
||||||
|
"Received termination unix signal " <> signal
|
||||||
|
WarpServerObs txt ->
|
||||||
|
"Warp server: " <> txt
|
||||||
|
where
|
||||||
|
showMillis :: Double -> Text
|
||||||
|
showMillis x = toS $ showFFloat (Just 1) x ""
|
||||||
|
|
||||||
|
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||||
|
|
||||||
|
|
||||||
|
showListenerConnError :: SQL.ConnectionError -> Text
|
||||||
|
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
|
||||||
|
|
||||||
|
showListenerException :: SomeException -> Text
|
||||||
|
showListenerException = showOnSingleLine '\t' . show
|
||||||
|
|
||||||
|
|
||||||
|
showOnSingleLine :: Char -> Text -> Text
|
||||||
|
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||||
|
|||||||
+53
-11
@@ -5,7 +5,10 @@ Description : Metrics based on the Observation module. See Observation.hs.
|
|||||||
-}
|
-}
|
||||||
module PostgREST.Metrics
|
module PostgREST.Metrics
|
||||||
( init
|
( init
|
||||||
|
, ConnTrack
|
||||||
|
, ConnStats (..)
|
||||||
, MetricsState (..)
|
, MetricsState (..)
|
||||||
|
, connectionCounts
|
||||||
, observationMetrics
|
, observationMetrics
|
||||||
, metricsToText
|
, metricsToText
|
||||||
) where
|
) where
|
||||||
@@ -17,12 +20,18 @@ import Prometheus
|
|||||||
|
|
||||||
import PostgREST.Observation
|
import PostgREST.Observation
|
||||||
|
|
||||||
import Protolude
|
import Control.Arrow ((&&&))
|
||||||
|
import Data.Bitraversable (bisequenceA)
|
||||||
|
import Data.Tuple.Extra (both)
|
||||||
|
import Data.UUID (UUID)
|
||||||
|
import qualified Focus
|
||||||
|
import Protolude
|
||||||
|
import qualified StmHamt.SizedHamt as SH
|
||||||
|
|
||||||
data MetricsState =
|
data MetricsState =
|
||||||
MetricsState {
|
MetricsState {
|
||||||
poolTimeouts :: Counter,
|
poolTimeouts :: Counter,
|
||||||
poolAvailable :: Gauge,
|
connTrack :: ConnTrack,
|
||||||
poolWaiting :: Gauge,
|
poolWaiting :: Gauge,
|
||||||
poolMaxSize :: Gauge,
|
poolMaxSize :: Gauge,
|
||||||
schemaCacheLoads :: Vector Label1 Counter,
|
schemaCacheLoads :: Vector Label1 Counter,
|
||||||
@@ -36,7 +45,7 @@ init :: Int -> IO MetricsState
|
|||||||
init configDbPoolSize = do
|
init configDbPoolSize = do
|
||||||
metricState <- MetricsState <$>
|
metricState <- MetricsState <$>
|
||||||
register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
|
register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
|
||||||
register (gauge (Info "pgrst_db_pool_available" "Available connections in the pool")) <*>
|
register (Metric ((identity &&& dbPoolAvailable) <$> connectionTracker)) <*>
|
||||||
register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
|
register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
|
||||||
register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
|
register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
|
||||||
register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
|
register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
|
||||||
@@ -46,20 +55,28 @@ init configDbPoolSize = do
|
|||||||
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
|
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
|
||||||
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
|
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
|
||||||
pure metricState
|
pure metricState
|
||||||
|
where
|
||||||
|
dbPoolAvailable = (pure . noLabelsGroup (Info "pgrst_db_pool_available" "Available connections in the pool") GaugeType . calcAvailable <$>) . connectionCounts
|
||||||
|
where
|
||||||
|
calcAvailable = liftA2 (-) connected inUse
|
||||||
|
toSample name labels = Sample name labels . encodeUtf8 . show
|
||||||
|
noLabelsGroup info sampleType = SampleGroup info sampleType . pure . toSample (metricName info) mempty
|
||||||
|
|
||||||
-- Only some observations are used as metrics
|
-- Only some observations are used as metrics
|
||||||
observationMetrics :: MetricsState -> ObservationHandler
|
observationMetrics :: MetricsState -> ObservationHandler
|
||||||
observationMetrics MetricsState{..} obs = case obs of
|
observationMetrics MetricsState{..} obs = case obs of
|
||||||
(PoolAcqTimeoutObs _) -> do
|
(PoolAcqTimeoutObs _) -> do
|
||||||
incCounter poolTimeouts
|
incCounter poolTimeouts
|
||||||
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
|
-- Handle pool observations with connection tracking
|
||||||
SQL.ReadyForUseConnectionStatus -> do
|
-- this is necessary because it is not possible
|
||||||
incGauge poolAvailable
|
-- to accurately maintain open/in use conneciton counts
|
||||||
SQL.InUseConnectionStatus -> do
|
-- statelessly based only on pool observation events.
|
||||||
decGauge poolAvailable
|
-- The reason is that hasql-pool emits TerminatedConnectionStatus
|
||||||
SQL.TerminatedConnectionStatus _ -> do
|
-- both for connections successfully established and failed when connecting.
|
||||||
decGauge poolAvailable
|
-- When receiving TerminatedConnectionStatus we have to find out
|
||||||
SQL.ConnectingConnectionStatus -> pure ()
|
-- if we can decrement established connection count. To do that we have to track
|
||||||
|
-- established connections.
|
||||||
|
(HasqlPoolObs sqlObs) -> trackConnections connTrack sqlObs
|
||||||
PoolRequest ->
|
PoolRequest ->
|
||||||
incGauge poolWaiting
|
incGauge poolWaiting
|
||||||
PoolRequestFullfilled ->
|
PoolRequestFullfilled ->
|
||||||
@@ -77,3 +94,28 @@ observationMetrics MetricsState{..} obs = case obs of
|
|||||||
|
|
||||||
metricsToText :: IO LBS.ByteString
|
metricsToText :: IO LBS.ByteString
|
||||||
metricsToText = exportMetricsAsText
|
metricsToText = exportMetricsAsText
|
||||||
|
|
||||||
|
data ConnStats = ConnStats {
|
||||||
|
connected :: Int,
|
||||||
|
inUse :: Int
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
data ConnTrack = ConnTrack { connTrackConnected :: SH.SizedHamt UUID, connTrackInUse :: SH.SizedHamt UUID }
|
||||||
|
|
||||||
|
connectionTracker :: IO ConnTrack
|
||||||
|
connectionTracker = ConnTrack <$> SH.newIO <*> SH.newIO
|
||||||
|
|
||||||
|
trackConnections :: ConnTrack -> SQL.Observation -> IO ()
|
||||||
|
trackConnections ConnTrack{..} (SQL.ConnectionObservation uuid status) = case status of
|
||||||
|
SQL.ReadyForUseConnectionStatus -> atomically $
|
||||||
|
SH.insert identity uuid connTrackConnected *>
|
||||||
|
SH.focus Focus.delete identity uuid connTrackInUse
|
||||||
|
SQL.TerminatedConnectionStatus _ -> atomically $
|
||||||
|
SH.focus Focus.delete identity uuid connTrackConnected *>
|
||||||
|
SH.focus Focus.delete identity uuid connTrackInUse
|
||||||
|
SQL.InUseConnectionStatus -> atomically $
|
||||||
|
SH.insert identity uuid connTrackInUse
|
||||||
|
_ -> mempty
|
||||||
|
|
||||||
|
connectionCounts :: ConnTrack -> IO ConnStats
|
||||||
|
connectionCounts = atomically . fmap (uncurry ConnStats) . bisequenceA . both SH.size . (connTrackConnected &&& connTrackInUse)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Observation
|
Module : PostgREST.Observation
|
||||||
Description : This module holds an Observation type which is the core of Observability for PostgREST.
|
Description : This module holds an Observation type which is the core of Observability for PostgREST.
|
||||||
@@ -10,23 +9,14 @@ Description : This module holds an Observation type which is the core of Observa
|
|||||||
module PostgREST.Observation
|
module PostgREST.Observation
|
||||||
( Observation(..)
|
( Observation(..)
|
||||||
, ObsFatalError(..)
|
, ObsFatalError(..)
|
||||||
, observationMessage
|
|
||||||
, ObservationHandler
|
, ObservationHandler
|
||||||
, showOnSingleLine
|
|
||||||
, isDbListenerBug
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
|
||||||
import Data.List.NonEmpty (toList)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import qualified Data.Text.Encoding as T
|
|
||||||
import qualified Hasql.Connection as SQL
|
import qualified Hasql.Connection as SQL
|
||||||
import qualified Hasql.Pool as SQL
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Pool.Observation as SQL
|
import qualified Hasql.Pool.Observation as SQL
|
||||||
import Network.HTTP.Types.Status (Status)
|
import Network.HTTP.Types.Status (Status)
|
||||||
import Numeric (showFFloat)
|
|
||||||
import PostgREST.Config.PgVersion
|
import PostgREST.Config.PgVersion
|
||||||
import qualified PostgREST.Error as Error
|
|
||||||
import PostgREST.Query (MainQuery)
|
import PostgREST.Query (MainQuery)
|
||||||
|
|
||||||
import Protolude hiding (toList)
|
import Protolude hiding (toList)
|
||||||
@@ -48,7 +38,7 @@ data Observation
|
|||||||
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
||||||
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
||||||
| DBListenRetry Int
|
| DBListenRetry Int
|
||||||
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
|
| DBListenBugCallQueryFix
|
||||||
| DBListenerGotSCacheMsg ByteString
|
| DBListenerGotSCacheMsg ByteString
|
||||||
| DBListenerGotConfigMsg ByteString
|
| DBListenerGotConfigMsg ByteString
|
||||||
| DBListenerConnectionCleanupFail SomeException
|
| DBListenerConnectionCleanupFail SomeException
|
||||||
@@ -64,6 +54,7 @@ data Observation
|
|||||||
| HasqlPoolObs SQL.Observation
|
| HasqlPoolObs SQL.Observation
|
||||||
| PoolRequest
|
| PoolRequest
|
||||||
| PoolRequestFullfilled
|
| PoolRequestFullfilled
|
||||||
|
| PoolFlushed
|
||||||
| JwtCacheLookup Bool
|
| JwtCacheLookup Bool
|
||||||
| JwtCacheEviction
|
| JwtCacheEviction
|
||||||
| TerminationUnixSignalObs Text
|
| TerminationUnixSignalObs Text
|
||||||
@@ -73,118 +64,3 @@ data Observation
|
|||||||
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
||||||
|
|
||||||
type ObservationHandler = Observation -> IO ()
|
type ObservationHandler = Observation -> IO ()
|
||||||
|
|
||||||
observationMessage :: Observation -> Text
|
|
||||||
observationMessage = \case
|
|
||||||
AdminStartObs address ->
|
|
||||||
"Admin server listening on " <> address
|
|
||||||
AppStartObs ver ->
|
|
||||||
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
|
||||||
AppServerAddressObs address ->
|
|
||||||
"API server listening on " <> address
|
|
||||||
DBConnectedObs ver ->
|
|
||||||
"Successfully connected to " <> ver
|
|
||||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
|
||||||
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
|
||||||
ExitDBNoRecoveryObs ->
|
|
||||||
"Automatic recovery disabled, exiting."
|
|
||||||
ExitDBFatalError ServerAuthError usageErr ->
|
|
||||||
"Failed to establish a connection. " <> jsonMessage usageErr
|
|
||||||
ExitDBFatalError ServerPgrstBug usageErr ->
|
|
||||||
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
|
||||||
ExitDBFatalError ServerError42P05 usageErr ->
|
|
||||||
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
|
||||||
ExitDBFatalError ServerError08P01 usageErr ->
|
|
||||||
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
|
||||||
SchemaCacheEmptyObs ->
|
|
||||||
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
|
|
||||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
|
||||||
"Failed to load the schema cache using "
|
|
||||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
|
||||||
<> " and "
|
|
||||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
|
||||||
<> ". " <> jsonMessage usageErr
|
|
||||||
SchemaCacheQueriedObs resultTime ->
|
|
||||||
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
|
||||||
SchemaCacheSummaryObs summary ->
|
|
||||||
"Schema cache loaded " <> summary
|
|
||||||
SchemaCacheLoadedObs resultTime ->
|
|
||||||
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
|
||||||
ConnectionRetryObs delay ->
|
|
||||||
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
|
||||||
QueryPgVersionError usageErr ->
|
|
||||||
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
|
||||||
DBListenStart host port fullName channel -> do
|
|
||||||
"Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
|
||||||
DBListenFail channel listenErr ->
|
|
||||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
|
||||||
either showListenerConnError showListenerException listenErr
|
|
||||||
DBListenRetry delay ->
|
|
||||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
|
||||||
DBListenBugHint ->
|
|
||||||
"HINT: This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();"
|
|
||||||
DBListenerGotSCacheMsg channel ->
|
|
||||||
"Received a schema cache reload message on the " <> show channel <> " channel"
|
|
||||||
DBListenerGotConfigMsg channel ->
|
|
||||||
"Received a config reload message on the " <> show channel <> " channel"
|
|
||||||
DBListenerConnectionCleanupFail ex ->
|
|
||||||
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
|
||||||
QueryObs{} ->
|
|
||||||
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
|
||||||
ConfigReadErrorObs usageErr ->
|
|
||||||
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
|
||||||
QueryRoleSettingsErrorObs usageErr ->
|
|
||||||
"Failed to query the role settings. " <> jsonMessage usageErr
|
|
||||||
QueryErrorCodeHighObs usageErr ->
|
|
||||||
jsonMessage usageErr
|
|
||||||
ConfigInvalidObs err ->
|
|
||||||
"Failed reloading config: " <> err
|
|
||||||
ConfigSucceededObs ->
|
|
||||||
"Config reloaded"
|
|
||||||
PoolInit poolSize ->
|
|
||||||
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
|
||||||
PoolAcqTimeoutObs usageErr ->
|
|
||||||
jsonMessage usageErr
|
|
||||||
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
|
||||||
"Connection " <> show uuid <> (
|
|
||||||
case status of
|
|
||||||
SQL.ConnectingConnectionStatus -> " is being established"
|
|
||||||
SQL.ReadyForUseConnectionStatus -> " is available"
|
|
||||||
SQL.InUseConnectionStatus -> " is used"
|
|
||||||
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
|
||||||
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
|
||||||
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
|
||||||
SQL.ReleaseConnectionTerminationReason -> "release"
|
|
||||||
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
|
||||||
)
|
|
||||||
PoolRequest ->
|
|
||||||
"Trying to borrow a connection from pool"
|
|
||||||
PoolRequestFullfilled ->
|
|
||||||
"Borrowed a connection from the pool"
|
|
||||||
JwtCacheLookup _ ->
|
|
||||||
"Looked up a JWT in JWT cache"
|
|
||||||
JwtCacheEviction ->
|
|
||||||
"Evicted entry from JWT cache"
|
|
||||||
TerminationUnixSignalObs signal ->
|
|
||||||
"Received termination unix signal " <> signal
|
|
||||||
WarpServerObs txt ->
|
|
||||||
"Warp server: " <> txt
|
|
||||||
where
|
|
||||||
showMillis :: Double -> Text
|
|
||||||
showMillis x = toS $ showFFloat (Just 1) x ""
|
|
||||||
|
|
||||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
|
||||||
|
|
||||||
|
|
||||||
showListenerConnError :: SQL.ConnectionError -> Text
|
|
||||||
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
|
|
||||||
|
|
||||||
showListenerException :: SomeException -> Text
|
|
||||||
showListenerException = showOnSingleLine '\t' . show
|
|
||||||
|
|
||||||
|
|
||||||
showOnSingleLine :: Char -> Text -> Text
|
|
||||||
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
|
||||||
|
|
||||||
isDbListenerBug :: SomeException -> Bool
|
|
||||||
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) =
|
|||||||
T.intercalate ", "
|
T.intercalate ", "
|
||||||
[ show (HM.size tbls) <> " Relations"
|
[ show (HM.size tbls) <> " Relations"
|
||||||
, show (HM.size rels) <> " Relationships"
|
, show (HM.size rels) <> " Relationships"
|
||||||
, show (HM.size routs) <> " Functions"
|
, show (HM.size routs) <> " RPCs"
|
||||||
, show (HM.size reps) <> " Domain Representations"
|
, show (HM.size reps) <> " Domain Representations"
|
||||||
, show (HM.size mediaHdlrs) <> " Media Type Handlers"
|
, show (HM.size mediaHdlrs) <> " Media Type Handlers"
|
||||||
, show (S.size tzs) <> " Timezones"
|
, show (S.size tzs) <> " Timezones"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ CONFIGSDIR = BASEDIR / "configs"
|
|||||||
FIXTURES = yaml.load(
|
FIXTURES = yaml.load(
|
||||||
(BASEDIR / "fixtures/fixtures.yaml").read_text(), Loader=yaml.Loader
|
(BASEDIR / "fixtures/fixtures.yaml").read_text(), Loader=yaml.Loader
|
||||||
)
|
)
|
||||||
|
NGINX_BIN = shutil.which("nginx")
|
||||||
POSTGREST_BIN = shutil.which("postgrest")
|
POSTGREST_BIN = shutil.which("postgrest")
|
||||||
SECRET = "reallyreallyreallyreallyverysafe"
|
SECRET = "reallyreallyreallyreallyverysafe"
|
||||||
|
|
||||||
|
|||||||
@@ -11399,7 +11399,7 @@ $$;
|
|||||||
DROP ROLE IF EXISTS postgrest_test_anonymous;
|
DROP ROLE IF EXISTS postgrest_test_anonymous;
|
||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
|
|
||||||
GRANT postgrest_test_anonymous TO :PGUSER;
|
GRANT postgrest_test_anonymous TO :"PGUSER";
|
||||||
|
|
||||||
GRANT USAGE ON SCHEMA apflora TO postgrest_test_anonymous;
|
GRANT USAGE ON SCHEMA apflora TO postgrest_test_anonymous;
|
||||||
GRANT USAGE ON SCHEMA fuzzysearch TO postgrest_test_anonymous;
|
GRANT USAGE ON SCHEMA fuzzysearch TO postgrest_test_anonymous;
|
||||||
|
|||||||
+11
-11
@@ -4,7 +4,7 @@ set check_function_bodies = false; -- to allow conditionals based on the pg vers
|
|||||||
set search_path to public;
|
set search_path to public;
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
ALTER ROLE :PGUSER SET pgrst.db_anon_role = 'postgrest_test_anonymous';
|
ALTER ROLE :"PGUSER" SET pgrst.db_anon_role = 'postgrest_test_anonymous';
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_author;
|
CREATE ROLE postgrest_test_author;
|
||||||
|
|
||||||
@@ -21,14 +21,14 @@ alter role postgrest_test_w_superuser_settings set log_min_messages = 'fatal';
|
|||||||
DO $do$BEGIN
|
DO $do$BEGIN
|
||||||
IF (SELECT current_setting('server_version_num')::INT >= 150000) THEN
|
IF (SELECT current_setting('server_version_num')::INT >= 150000) THEN
|
||||||
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_sample = 12345;
|
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_sample = 12345;
|
||||||
GRANT SET ON PARAMETER log_min_duration_sample to postgrest_test_authenticator;
|
GRANT SET ON PARAMETER log_min_duration_sample to "Postgrest_Test_Authenticator";
|
||||||
END IF;
|
END IF;
|
||||||
END$do$;
|
END$do$;
|
||||||
|
|
||||||
GRANT
|
GRANT
|
||||||
postgrest_test_anonymous, postgrest_test_author,
|
postgrest_test_anonymous, postgrest_test_author,
|
||||||
postgrest_test_serializable, postgrest_test_repeatable_read,
|
postgrest_test_serializable, postgrest_test_repeatable_read,
|
||||||
postgrest_test_w_superuser_settings TO :PGUSER;
|
postgrest_test_w_superuser_settings TO :"PGUSER";
|
||||||
|
|
||||||
CREATE SCHEMA v1;
|
CREATE SCHEMA v1;
|
||||||
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
||||||
@@ -57,7 +57,7 @@ $$ language sql;
|
|||||||
create function change_max_rows_config(val int, notify bool default false) returns void as $_$
|
create function change_max_rows_config(val int, notify bool default false) returns void as $_$
|
||||||
begin
|
begin
|
||||||
execute format($$
|
execute format($$
|
||||||
alter role postgrest_test_authenticator set pgrst.db_max_rows = %L;
|
alter role "Postgrest_Test_Authenticator" set pgrst.db_max_rows = %L;
|
||||||
$$, val);
|
$$, val);
|
||||||
if notify then
|
if notify then
|
||||||
perform pg_notify('pgrst', 'reload config');
|
perform pg_notify('pgrst', 'reload config');
|
||||||
@@ -66,13 +66,13 @@ end $_$ volatile security definer language plpgsql ;
|
|||||||
|
|
||||||
create function reset_max_rows_config() returns void as $_$
|
create function reset_max_rows_config() returns void as $_$
|
||||||
begin
|
begin
|
||||||
alter role postgrest_test_authenticator reset pgrst.db_max_rows;
|
alter role "Postgrest_Test_Authenticator" reset pgrst.db_max_rows;
|
||||||
end $_$ volatile security definer language plpgsql ;
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
create function change_db_schema_and_full_reload(schemas text) returns void as $_$
|
create function change_db_schema_and_full_reload(schemas text) returns void as $_$
|
||||||
begin
|
begin
|
||||||
execute format($$
|
execute format($$
|
||||||
alter role postgrest_test_authenticator set pgrst.db_schemas = %L;
|
alter role "Postgrest_Test_Authenticator" set pgrst.db_schemas = %L;
|
||||||
$$, schemas);
|
$$, schemas);
|
||||||
perform pg_notify('pgrst', 'reload config');
|
perform pg_notify('pgrst', 'reload config');
|
||||||
perform pg_notify('pgrst', 'reload schema');
|
perform pg_notify('pgrst', 'reload schema');
|
||||||
@@ -80,14 +80,14 @@ end $_$ volatile security definer language plpgsql ;
|
|||||||
|
|
||||||
create function v1.reset_db_schema_config() returns void as $_$
|
create function v1.reset_db_schema_config() returns void as $_$
|
||||||
begin
|
begin
|
||||||
alter role postgrest_test_authenticator reset pgrst.db_schemas;
|
alter role "Postgrest_Test_Authenticator" reset pgrst.db_schemas;
|
||||||
perform pg_notify('pgrst', 'reload config');
|
perform pg_notify('pgrst', 'reload config');
|
||||||
perform pg_notify('pgrst', 'reload schema');
|
perform pg_notify('pgrst', 'reload schema');
|
||||||
end $_$ volatile security definer language plpgsql ;
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
create function invalid_role_claim_key_reload() returns void as $_$
|
create function invalid_role_claim_key_reload() returns void as $_$
|
||||||
begin
|
begin
|
||||||
alter role postgrest_test_authenticator set pgrst.jwt_role_claim_key = 'test';
|
alter role "Postgrest_Test_Authenticator" set pgrst.jwt_role_claim_key = 'test';
|
||||||
perform pg_notify('pgrst', 'reload config');
|
perform pg_notify('pgrst', 'reload config');
|
||||||
end $_$ volatile security definer language plpgsql ;
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ $_$ language sql;
|
|||||||
|
|
||||||
create function reset_invalid_role_claim_key() returns void as $_$
|
create function reset_invalid_role_claim_key() returns void as $_$
|
||||||
begin
|
begin
|
||||||
alter role postgrest_test_authenticator reset pgrst.jwt_role_claim_key;
|
alter role "Postgrest_Test_Authenticator" reset pgrst.jwt_role_claim_key;
|
||||||
perform pg_notify('pgrst', 'reload config');
|
perform pg_notify('pgrst', 'reload config');
|
||||||
end $_$ volatile security definer language plpgsql ;
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
@@ -235,12 +235,12 @@ $$ language sql;
|
|||||||
|
|
||||||
create function change_db_schemas_config() returns void as $_$
|
create function change_db_schemas_config() returns void as $_$
|
||||||
begin
|
begin
|
||||||
alter role postgrest_test_authenticator set pgrst.db_schemas = 'test';
|
alter role "Postgrest_Test_Authenticator" set pgrst.db_schemas = 'test';
|
||||||
end $_$ volatile security definer language plpgsql;
|
end $_$ volatile security definer language plpgsql;
|
||||||
|
|
||||||
create function reset_db_schemas_config() returns void as $_$
|
create function reset_db_schemas_config() returns void as $_$
|
||||||
begin
|
begin
|
||||||
alter role postgrest_test_authenticator reset pgrst.db_schemas;
|
alter role "Postgrest_Test_Authenticator" reset pgrst.db_schemas;
|
||||||
end $_$ volatile security definer language plpgsql ;
|
end $_$ volatile security definer language plpgsql ;
|
||||||
|
|
||||||
create function test.get_current_schema() returns text as $$
|
create function test.get_current_schema() returns text as $$
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ create table replica.items as select x as id from generate_series(1, 10) x;
|
|||||||
DROP ROLE IF EXISTS postgrest_test_anonymous;
|
DROP ROLE IF EXISTS postgrest_test_anonymous;
|
||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
|
|
||||||
GRANT postgrest_test_anonymous TO :PGUSER;
|
GRANT postgrest_test_anonymous TO :"PGUSER";
|
||||||
|
|
||||||
GRANT USAGE ON SCHEMA replica TO postgrest_test_anonymous;
|
GRANT USAGE ON SCHEMA replica TO postgrest_test_anonymous;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# the PG* variables are replaced by preprocessing, not done by nginx itself
|
||||||
|
daemon off;
|
||||||
|
pid ./nginx.pid;
|
||||||
|
|
||||||
|
events {}
|
||||||
|
|
||||||
|
stream {
|
||||||
|
server {
|
||||||
|
listen unix:$PGPROXYHOST/.s.PGSQL.5432;
|
||||||
|
proxy_timeout $PGPROXY_TIMEOUT;
|
||||||
|
proxy_pass unix:$PGHOST/.s.PGSQL.5432;
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
-3
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import enum
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import string
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import requests_unixsocket
|
import requests_unixsocket
|
||||||
|
|
||||||
from config import POSTGREST_BIN, hpctixfile
|
from config import POSTGREST_BIN, NGINX_BIN, hpctixfile
|
||||||
|
|
||||||
|
|
||||||
def sleep_until_postgrest_scache_reload():
|
def sleep_until_postgrest_scache_reload():
|
||||||
@@ -35,6 +37,13 @@ class PostgrestTimedOut(Exception):
|
|||||||
"Connecting to PostgREST endpoint timed out."
|
"Connecting to PostgREST endpoint timed out."
|
||||||
|
|
||||||
|
|
||||||
|
class Admin(str, enum.Enum):
|
||||||
|
"Admin endpoint to wait for before yielding a PostgREST process."
|
||||||
|
|
||||||
|
live = "live"
|
||||||
|
ready = "ready"
|
||||||
|
|
||||||
|
|
||||||
class PostgrestSession(requests_unixsocket.Session):
|
class PostgrestSession(requests_unixsocket.Session):
|
||||||
"HTTP client session directed at a PostgREST endpoint."
|
"HTTP client session directed at a PostgREST endpoint."
|
||||||
|
|
||||||
@@ -86,7 +95,7 @@ def run(
|
|||||||
env=None,
|
env=None,
|
||||||
port=None,
|
port=None,
|
||||||
host=None,
|
host=None,
|
||||||
wait_for_readiness=True,
|
wait_for=Admin.ready,
|
||||||
wait_max_seconds=1,
|
wait_max_seconds=1,
|
||||||
no_pool_connection_available=False,
|
no_pool_connection_available=False,
|
||||||
no_startup_stdout=True,
|
no_startup_stdout=True,
|
||||||
@@ -138,8 +147,10 @@ def run(
|
|||||||
process.stdin.write(stdin or b"")
|
process.stdin.write(stdin or b"")
|
||||||
process.stdin.close()
|
process.stdin.close()
|
||||||
|
|
||||||
if wait_for_readiness:
|
if wait_for == Admin.ready:
|
||||||
wait_until_status_code(adminurl + "/ready", wait_max_seconds, 200)
|
wait_until_status_code(adminurl + "/ready", wait_max_seconds, 200)
|
||||||
|
elif wait_for == Admin.live:
|
||||||
|
wait_until_status_code(adminurl + "/live", wait_max_seconds, 200)
|
||||||
|
|
||||||
if no_startup_stdout:
|
if no_startup_stdout:
|
||||||
process.stdout.read()
|
process.stdout.read()
|
||||||
@@ -165,6 +176,52 @@ def run(
|
|||||||
process.wait()
|
process.wait()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def run_pgproxy(env=None, proxy_timeout="1s"):
|
||||||
|
"Run nginx as a unix socket proxy for PostgreSQL and expose PGPROXYHOST."
|
||||||
|
env = dict(os.environ if env is None else env)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# build a <tmpdir>/conf/ so `nginx -p` picks the config automatically
|
||||||
|
tmpdir = pathlib.Path(tmpdir)
|
||||||
|
conf_dir = tmpdir / "conf"
|
||||||
|
conf_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
nginx_env = dict(env)
|
||||||
|
nginx_env["PGPROXYHOST"] = str(tmpdir)
|
||||||
|
nginx_env["PGPROXY_TIMEOUT"] = proxy_timeout
|
||||||
|
|
||||||
|
source_conf = pathlib.Path("test/io/nginx/nginx.conf")
|
||||||
|
out_conf = conf_dir / "nginx.conf"
|
||||||
|
out_conf.write_text(
|
||||||
|
string.Template(source_conf.read_text()).substitute(nginx_env)
|
||||||
|
)
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[NGINX_BIN, "-p", str(tmpdir), "-e", "stderr"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
env=nginx_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
if process.poll() is not None:
|
||||||
|
(_, stderr_output) = process.communicate(timeout=1)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{NGINX_BIN} exited with {process.returncode}: {stderr_output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield str(tmpdir)
|
||||||
|
finally:
|
||||||
|
process.terminate()
|
||||||
|
try:
|
||||||
|
process.wait(timeout=1)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
|
||||||
def freeport(used_ports=None):
|
def freeport(used_ports=None):
|
||||||
"Find an unused free port on localhost."
|
"Find an unused free port on localhost."
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ def test_fail_with_invalid_password(defaultenv):
|
|||||||
"Connecting with an invalid password should fail without retries."
|
"Connecting with an invalid password should fail without retries."
|
||||||
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
|
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
|
||||||
env = {**defaultenv, "PGRST_DB_URI": uri}
|
env = {**defaultenv, "PGRST_DB_URI": uri}
|
||||||
with run(env=env, wait_for_readiness=False) as postgrest:
|
with run(env=env, wait_for=None) as postgrest:
|
||||||
exitCode = wait_until_exit(postgrest)
|
exitCode = wait_until_exit(postgrest)
|
||||||
assert exitCode == 1
|
assert exitCode == 1
|
||||||
|
|
||||||
|
|||||||
+108
-55
@@ -3,16 +3,24 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from config import CONFIGSDIR, FIXTURES, SECRET
|
from config import CONFIGSDIR, FIXTURES, SECRET
|
||||||
from util import Thread, jwtauthheader, parse_server_timings_header
|
from util import (
|
||||||
|
Thread,
|
||||||
|
jwtauthheader,
|
||||||
|
parse_server_timings_header,
|
||||||
|
match_log,
|
||||||
|
)
|
||||||
from postgrest import (
|
from postgrest import (
|
||||||
|
Admin,
|
||||||
freeport,
|
freeport,
|
||||||
is_ipv6,
|
is_ipv6,
|
||||||
reset_statement_timeout,
|
reset_statement_timeout,
|
||||||
run,
|
run,
|
||||||
|
run_pgproxy,
|
||||||
set_statement_timeout,
|
set_statement_timeout,
|
||||||
sleep_until_postgrest_config_reload,
|
sleep_until_postgrest_config_reload,
|
||||||
sleep_until_postgrest_full_reload,
|
sleep_until_postgrest_full_reload,
|
||||||
@@ -529,6 +537,39 @@ def test_admin_ready_w_channel(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_listener_query_is_visible_in_pg_stat_activity(defaultenv):
|
||||||
|
"The listener connection should show the LISTEN pgrst statement in pg_stat_activity"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
|
"PGAPPNAME": "listener-query-test",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env):
|
||||||
|
query = """
|
||||||
|
select query
|
||||||
|
from pg_stat_activity
|
||||||
|
where application_name = 'listener-query-test'
|
||||||
|
and query = 'LISTEN "pgrst"'
|
||||||
|
limit 1;
|
||||||
|
"""
|
||||||
|
output = subprocess.check_output(
|
||||||
|
[
|
||||||
|
"psql",
|
||||||
|
"--set",
|
||||||
|
"ON_ERROR_STOP=1",
|
||||||
|
"--tuples-only",
|
||||||
|
"--no-align",
|
||||||
|
"-c",
|
||||||
|
query,
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
assert output == 'LISTEN "pgrst"'
|
||||||
|
|
||||||
|
|
||||||
def test_admin_ready_wo_channel(defaultenv):
|
def test_admin_ready_wo_channel(defaultenv):
|
||||||
"Should get a success response from the admin server ready endpoint when the LISTEN channel is disabled"
|
"Should get a success response from the admin server ready endpoint when the LISTEN channel is disabled"
|
||||||
|
|
||||||
@@ -679,55 +720,45 @@ def test_log_level(level, defaultenv):
|
|||||||
response = postgrest.session.get("/")
|
response = postgrest.session.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
output = sorted(postgrest.read_stdout(nlines=7))
|
output = postgrest.read_stdout(nlines=9)
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
elif level == "error":
|
elif level == "error":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"'],
|
||||||
)
|
)
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
elif level == "warn":
|
elif level == "warn":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[
|
||||||
)
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
assert re.match(
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
],
|
||||||
output[1],
|
|
||||||
)
|
)
|
||||||
assert len(output) == 2
|
assert len(output) == 2
|
||||||
elif level == "info":
|
elif level == "info":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[
|
||||||
)
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
assert re.match(
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
output[1],
|
],
|
||||||
)
|
|
||||||
assert re.match(
|
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
|
||||||
output[2],
|
|
||||||
)
|
)
|
||||||
assert len(output) == 3
|
assert len(output) == 3
|
||||||
elif level == "debug":
|
elif level == "debug":
|
||||||
assert re.match(
|
match_log(
|
||||||
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
output,
|
||||||
output[0],
|
[
|
||||||
|
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
|
||||||
|
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
||||||
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
assert re.match(
|
assert len(output) == 9
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
|
||||||
output[1],
|
|
||||||
)
|
|
||||||
assert re.match(
|
|
||||||
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
|
|
||||||
output[2],
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(output) == 7
|
|
||||||
assert any("Connection" and "is available" in line for line in output)
|
assert any("Connection" and "is available" in line for line in output)
|
||||||
assert any("Connection" and "is used" in line for line in output)
|
assert any("Connection" and "is used" in line for line in output)
|
||||||
|
|
||||||
@@ -1049,7 +1080,7 @@ def test_schema_cache_concurrent_notifications(slow_schema_cache_env):
|
|||||||
int(slow_schema_cache_env["PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP"]) / 1000
|
int(slow_schema_cache_env["PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP"]) / 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
with run(env=slow_schema_cache_env, wait_for_readiness=False) as postgrest:
|
with run(env=slow_schema_cache_env, wait_for=None) as postgrest:
|
||||||
time.sleep(2 * internal_sleep + 0.1) # wait for readiness manually
|
time.sleep(2 * internal_sleep + 0.1) # wait for readiness manually
|
||||||
|
|
||||||
# first request, create a function and set a schema cache reload in progress
|
# first request, create a function and set a schema cache reload in progress
|
||||||
@@ -1240,7 +1271,7 @@ def test_fail_with_invalid_dbname_and_automatic_recovery_disabled(defaultenv):
|
|||||||
"PGRST_DB_POOL_AUTOMATIC_RECOVERY": "false",
|
"PGRST_DB_POOL_AUTOMATIC_RECOVERY": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, wait_for_readiness=False) as postgrest:
|
with run(env=env, wait_for=None) as postgrest:
|
||||||
exitCode = wait_until_exit(postgrest)
|
exitCode = wait_until_exit(postgrest)
|
||||||
assert exitCode == 1
|
assert exitCode == 1
|
||||||
|
|
||||||
@@ -1364,16 +1395,21 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest):
|
|||||||
assert response.status_code == 500
|
assert response.status_code == 500
|
||||||
|
|
||||||
# ensure the message appears on the logs
|
# ensure the message appears on the logs
|
||||||
output = sorted(postgrest.read_stdout(nlines=6))
|
output = postgrest.read_stdout(nlines=8)
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
elif level == "debug":
|
elif level == "debug":
|
||||||
assert " 500 " in output[0]
|
match_log(
|
||||||
assert "canceling statement due to statement timeout" in output[5]
|
output,
|
||||||
|
[
|
||||||
|
r".*canceling statement due to statement timeout.*",
|
||||||
|
r".*500.*",
|
||||||
|
],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
assert " 500 " in output[0]
|
assert " 500 " in output[1]
|
||||||
assert "canceling statement due to statement timeout" in output[1]
|
assert "canceling statement due to statement timeout" in output[0]
|
||||||
|
|
||||||
reset_statement_timeout(metapostgrest, role)
|
reset_statement_timeout(metapostgrest, role)
|
||||||
|
|
||||||
@@ -1524,7 +1560,7 @@ def test_log_error_when_empty_schema_cache_on_startup_to_stderr(defaultenv):
|
|||||||
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
|
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, wait_for_readiness=False) as postgrest:
|
with run(env=env, wait_for=None) as postgrest:
|
||||||
postgrest.wait_until_scache_starts_loading()
|
postgrest.wait_until_scache_starts_loading()
|
||||||
|
|
||||||
response = postgrest.session.get("/projects")
|
response = postgrest.session.get("/projects")
|
||||||
@@ -1545,7 +1581,7 @@ def test_no_double_schema_cache_reload_on_empty_schema(defaultenv):
|
|||||||
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
|
"PGRST_INTERNAL_SCHEMA_CACHE_QUERY_SLEEP": "300",
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, port=freeport(), wait_for_readiness=False) as postgrest:
|
with run(env=env, port=freeport(), wait_for=None) as postgrest:
|
||||||
postgrest.wait_until_scache_starts_loading()
|
postgrest.wait_until_scache_starts_loading()
|
||||||
|
|
||||||
response = postgrest.session.get("/projects")
|
response = postgrest.session.get("/projects")
|
||||||
@@ -1567,18 +1603,17 @@ def test_log_pool_req_observation(level, defaultenv):
|
|||||||
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
|
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
|
||||||
|
|
||||||
pool_req = "Trying to borrow a connection from pool"
|
pool_req = r".*Trying to borrow a connection from pool.*"
|
||||||
pool_req_fullfill = "Borrowed a connection from the pool"
|
pool_req_fullfill = r".*Borrowed a connection from the pool.*"
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
|
|
||||||
postgrest.session.get("/authors_only", headers=headers)
|
postgrest.session.get("/authors_only", headers=headers)
|
||||||
|
|
||||||
if level == "debug":
|
if level == "debug":
|
||||||
output = postgrest.read_stdout(nlines=5)
|
output = postgrest.read_stdout(nlines=7)
|
||||||
assert pool_req in output[1]
|
assert len(output) == 7
|
||||||
assert pool_req_fullfill in output[4]
|
match_log(output, [pool_req, pool_req_fullfill])
|
||||||
assert len(output) == 5
|
|
||||||
elif level == "info":
|
elif level == "info":
|
||||||
output = postgrest.read_stdout(nlines=4)
|
output = postgrest.read_stdout(nlines=4)
|
||||||
assert len(output) == 1
|
assert len(output) == 1
|
||||||
@@ -1628,7 +1663,7 @@ def test_schema_cache_error_observation(defaultenv):
|
|||||||
"PGRST_DB_EXTRA_SEARCH_PATH": "x",
|
"PGRST_DB_EXTRA_SEARCH_PATH": "x",
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
|
||||||
# TODO: postgrest should exit here, instead it keeps retrying
|
# TODO: postgrest should exit here, instead it keeps retrying
|
||||||
# exitCode = wait_until_exit(postgrest)
|
# exitCode = wait_until_exit(postgrest)
|
||||||
# assert exitCode == 1
|
# assert exitCode == 1
|
||||||
@@ -1649,7 +1684,7 @@ def test_log_listener_connection_errors(defaultenv):
|
|||||||
"PGRST_DB_CHANNEL_ENABLED": "true",
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
|
||||||
output = postgrest.read_stdout(nlines=5)
|
output = postgrest.read_stdout(nlines=5)
|
||||||
assert any(
|
assert any(
|
||||||
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
|
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
|
||||||
@@ -1666,7 +1701,7 @@ def test_log_listener_connection_start(defaultenv):
|
|||||||
"PGRST_DB_CHANNEL_ENABLED": "true",
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, no_startup_stdout=False, wait_for_readiness=True) as postgrest:
|
with run(env=env, no_startup_stdout=False, wait_for=Admin.ready) as postgrest:
|
||||||
output = postgrest.read_stdout(nlines=10)
|
output = postgrest.read_stdout(nlines=10)
|
||||||
# Check for the listener start message containing host and port
|
# Check for the listener start message containing host and port
|
||||||
# Do not check if pg version is displayed properly as it is tricky to test it
|
# Do not check if pg version is displayed properly as it is tricky to test it
|
||||||
@@ -1694,7 +1729,7 @@ def test_db_pre_config_with_pg_reserved_words(defaultenv):
|
|||||||
"PGRST_DB_PRE_CONFIG": "select", # no "select" function in our fixtures, fail gracefully at startup
|
"PGRST_DB_PRE_CONFIG": "select", # no "select" function in our fixtures, fail gracefully at startup
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
with run(env=env, no_startup_stdout=False, wait_for=None) as postgrest:
|
||||||
output = postgrest.read_stdout(nlines=8)
|
output = postgrest.read_stdout(nlines=8)
|
||||||
assert any(
|
assert any(
|
||||||
'Failed to query database settings for the config parameters.{"code":"42883","details":null,"hint":"No function matches the given name and argument types. You might need to add explicit type casts.","message":"function select() does not exist"}'
|
'Failed to query database settings for the config parameters.{"code":"42883","details":null,"hint":"No function matches the given name and argument types. You might need to add explicit type casts.","message":"function select() does not exist"}'
|
||||||
@@ -1775,3 +1810,21 @@ def test_server_timing_transaction_duration(defaultenv, metapostgrest):
|
|||||||
]
|
]
|
||||||
|
|
||||||
assert 2000 <= response_dur < 3000
|
assert 2000 <= response_dur < 3000
|
||||||
|
|
||||||
|
|
||||||
|
def test_positive_pool_metric(defaultenv):
|
||||||
|
"When a network failure is caused on the pg connection, pgrst_db_pool_available stays positive"
|
||||||
|
|
||||||
|
with run_pgproxy(defaultenv, proxy_timeout="1ms") as pgproxyhost:
|
||||||
|
env = {**defaultenv, "PGHOST": pgproxyhost}
|
||||||
|
|
||||||
|
with run(env=env, wait_for=Admin.live) as postgrest:
|
||||||
|
response = postgrest.admin.get("/metrics", timeout=1)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
metrics = float(
|
||||||
|
re.search(
|
||||||
|
r"pgrst_db_pool_available (-?\d+(?:\.\d+)?)", response.text
|
||||||
|
).group(1)
|
||||||
|
)
|
||||||
|
assert metrics >= 0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
@@ -21,6 +22,19 @@ class Thread(threading.Thread):
|
|||||||
raise self._exception
|
raise self._exception
|
||||||
|
|
||||||
|
|
||||||
|
def match_log(output, matchers):
|
||||||
|
ito = iter(output)
|
||||||
|
itm = iter(matchers)
|
||||||
|
nextMatcher = next(itm, None)
|
||||||
|
while nextMatcher is not None and (line := next(ito, None)) is not None:
|
||||||
|
if re.match(nextMatcher, line) is not None:
|
||||||
|
nextMatcher = next(itm, None)
|
||||||
|
if nextMatcher is not None:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Expected log line matching {nextMatcher} not found in output"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def authheader(token):
|
def authheader(token):
|
||||||
"Bearer token HTTP authorization header."
|
"Bearer token HTTP authorization header."
|
||||||
return {"Authorization": f"Bearer {token}"}
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
CREATE ROLE postgrest_test_author;
|
CREATE ROLE postgrest_test_author;
|
||||||
GRANT postgrest_test_anonymous TO :PGUSER;
|
GRANT postgrest_test_anonymous TO :"PGUSER";
|
||||||
GRANT postgrest_test_author TO :PGUSER;
|
GRANT postgrest_test_author TO :"PGUSER";
|
||||||
CREATE SCHEMA test;
|
CREATE SCHEMA test;
|
||||||
|
|
||||||
-- PUT+PATCH target needs one record and column to modify
|
-- PUT+PATCH target needs one record and column to modify
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import PostgREST.SchemaCache (querySchemaCache)
|
|||||||
import qualified Observation.JwtCache
|
import qualified Observation.JwtCache
|
||||||
import qualified Observation.MetricsSpec
|
import qualified Observation.MetricsSpec
|
||||||
|
|
||||||
import ObsHelper
|
import qualified Observation.SchemaCacheSpec
|
||||||
import PostgREST.Observation (Observation (HasqlPoolObs))
|
import ObsHelper
|
||||||
import Protolude hiding (toList, toS)
|
import PostgREST.Observation (Observation (HasqlPoolObs))
|
||||||
import Test.Hspec
|
import Protolude hiding (toList, toS)
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
@@ -64,6 +65,8 @@ main = do
|
|||||||
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
|
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
|
||||||
before (initApp baseSchemaCache testCfg) $
|
before (initApp baseSchemaCache testCfg) $
|
||||||
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
|
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
|
||||||
|
before (initApp baseSchemaCache testCfg) $
|
||||||
|
describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
loadSCache pool conf =
|
loadSCache pool conf =
|
||||||
|
|||||||
@@ -6,17 +6,21 @@
|
|||||||
|
|
||||||
module Observation.MetricsSpec where
|
module Observation.MetricsSpec where
|
||||||
|
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Network.Wai (Application)
|
import qualified Hasql.Pool.Observation as SQL
|
||||||
|
import Network.Wai (Application)
|
||||||
import ObsHelper
|
import ObsHelper
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
import PostgREST.Config (AppConfig (configDbSchemas))
|
import PostgREST.Config (AppConfig (configDbSchemas))
|
||||||
import qualified PostgREST.Metrics as Metrics
|
import PostgREST.Metrics (ConnStats (..),
|
||||||
|
MetricsState (..),
|
||||||
|
connectionCounts)
|
||||||
import PostgREST.Observation
|
import PostgREST.Observation
|
||||||
import Prometheus (getCounter, getVectorWith)
|
import Prometheus (getCounter, getVectorWith)
|
||||||
import Protolude
|
import Test.Hspec (SpecWith, describe, it)
|
||||||
import Test.Hspec (SpecWith, describe, it)
|
import Test.Hspec.Wai (getState)
|
||||||
import Test.Hspec.Wai (getState)
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
spec :: SpecWith (SpecState, Application)
|
spec :: SpecWith (SpecState, Application)
|
||||||
spec = describe "Server started with metrics enabled" $ do
|
spec = describe "Server started with metrics enabled" $ do
|
||||||
@@ -71,9 +75,40 @@ spec = describe "Server started with metrics enabled" $ do
|
|||||||
-- (there should be none but we need to verify that)
|
-- (there should be none but we need to verify that)
|
||||||
threadDelay $ 1 * sec
|
threadDelay $ 1 * sec
|
||||||
|
|
||||||
|
-- The test verifies we properly count in use connections
|
||||||
|
-- The idea is to fork a worker thread that
|
||||||
|
-- borrows connection from the pool and waits for a signal to release it
|
||||||
|
-- Main thread checks that
|
||||||
|
-- in use connections counter is incremented by worker
|
||||||
|
-- then it signals the worker to release the connection
|
||||||
|
-- and finally verifies that in use connection counter is back to original value
|
||||||
|
it "Should track in use connections" $ do
|
||||||
|
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ checkState' metrics [
|
||||||
|
-- we expect in use connections to be the same once finished
|
||||||
|
inUseConnections (+ 0)
|
||||||
|
] $ do
|
||||||
|
signal <- newEmptyMVar
|
||||||
|
-- make sure waiting thread is signaled
|
||||||
|
(`finally` tryPutMVar signal ()) $
|
||||||
|
-- expecting one more connection in use
|
||||||
|
checkState' metrics [
|
||||||
|
inUseConnections (+ 1)
|
||||||
|
] $ do
|
||||||
|
-- start a thread hanging on a single connection until signaled
|
||||||
|
void $ forkIO $ void $ AppState.usePool appState $ liftIO (readMVar signal)
|
||||||
|
-- main thread waits for ConnectionObservation with InUseConnectionStatus
|
||||||
|
-- after which used connections count should be incremented
|
||||||
|
waitFor (1 * sec) "InUseConnectionStatus" $ \x -> [ o | o@(HasqlPoolObs (SQL.ConnectionObservation _ SQL.InUseConnectionStatus)) <- pure x]
|
||||||
|
|
||||||
|
-- hanging thread was signaled and should return the connection
|
||||||
|
waitFor (1 * sec) "ReadyForUseConnectionStatus" $ \x -> [ o | o@(HasqlPoolObs (SQL.ConnectionObservation _ SQL.ReadyForUseConnectionStatus)) <- pure x]
|
||||||
|
|
||||||
where
|
where
|
||||||
-- prometheus-client api to handle vectors is convoluted
|
-- prometheus-client api to handle vectors is convoluted
|
||||||
schemaCacheLoads label = expectField @"schemaCacheLoads" $
|
schemaCacheLoads label = expectField @"schemaCacheLoads" $
|
||||||
fmap (maybe (0::Int) round . lookup label) . (`getVectorWith` getCounter)
|
fmap (maybe (0::Int) round . lookup label) . (`getVectorWith` getCounter)
|
||||||
|
inUseConnections = expectField @"connTrack" ((inUse <$>) . connectionCounts)
|
||||||
sec = 1000000
|
sec = 1000000
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE MonadComprehensions #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module Observation.SchemaCacheSpec where
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
import ObsHelper
|
||||||
|
import qualified PostgREST.AppState as AppState
|
||||||
|
import PostgREST.Config (configDbSchemas)
|
||||||
|
import PostgREST.Observation
|
||||||
|
import Protolude
|
||||||
|
import Test.Hspec (SpecWith, describe, it)
|
||||||
|
import Test.Hspec.Wai (getState)
|
||||||
|
|
||||||
|
spec :: SpecWith (SpecState, Application)
|
||||||
|
spec = describe "Server started with metrics enabled" $ do
|
||||||
|
|
||||||
|
it "Should emit PoolFlushed, SchemaCacheQueriedObs and SchemaCacheLoadedObs when schema cache is reloaded" $ do
|
||||||
|
SpecState{specAppState = appState, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
AppState.schemaCacheLoader appState
|
||||||
|
|
||||||
|
waitFor (1 * sec) "PoolFlushed" $ \x -> [ o | o@PoolFlushed <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheQueriedObs" $ \x -> [ o | o@SchemaCacheQueriedObs{} <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@SchemaCacheLoadedObs{} <- pure x ]
|
||||||
|
|
||||||
|
|
||||||
|
it "Should flush pool once when schema reloading retries" $ do
|
||||||
|
SpecState{specAppState = appState, specObsChan} <- getState
|
||||||
|
let waitFor = waitForObs specObsChan
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
AppState.getConfig appState >>= \cfg -> do
|
||||||
|
AppState.putConfig appState $ cfg { configDbSchemas = pure "bad_schema" }
|
||||||
|
AppState.schemaCacheLoader appState
|
||||||
|
|
||||||
|
waitFor (1 * sec) "SchemaCacheErrorObs" $ \x -> [ o | o@SchemaCacheErrorObs{} <- pure x ]
|
||||||
|
|
||||||
|
-- Restore configuration
|
||||||
|
AppState.putConfig appState cfg
|
||||||
|
|
||||||
|
-- Wait for 2 seconds so that retry can happen
|
||||||
|
waitFor (2 * sec) "PoolFlushed" $ \x -> [ o | o@PoolFlushed <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheQueriedObs" $ \x -> [ o | o@SchemaCacheQueriedObs{} <- pure x ]
|
||||||
|
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@SchemaCacheLoadedObs{} <- pure x ]
|
||||||
|
where
|
||||||
|
sec = 1000000
|
||||||
@@ -2,4 +2,4 @@ DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_author;
|
|||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
CREATE ROLE postgrest_test_author;
|
CREATE ROLE postgrest_test_author;
|
||||||
|
|
||||||
GRANT postgrest_test_anonymous, postgrest_test_author TO :PGUSER;
|
GRANT postgrest_test_anonymous, postgrest_test_author TO :"PGUSER";
|
||||||
|
|||||||
Vendored
+1
-1
@@ -4,4 +4,4 @@ CREATE ROLE postgrest_test_default_role;
|
|||||||
CREATE ROLE postgrest_test_author;
|
CREATE ROLE postgrest_test_author;
|
||||||
CREATE ROLE postgrest_test_superuser WITH SUPERUSER;
|
CREATE ROLE postgrest_test_superuser WITH SUPERUSER;
|
||||||
|
|
||||||
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author, postgrest_test_superuser TO :PGUSER;
|
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author, postgrest_test_superuser TO :"PGUSER";
|
||||||
|
|||||||
Reference in New Issue
Block a user