mirror of
https://github.com/neondatabase/neon.git
synced 2026-03-06 01:40:37 +00:00
Compare commits
2 Commits
proxy-kick
...
layer_map_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5edb0ccfa7 | ||
|
|
2bc1324bed |
@@ -15,10 +15,8 @@
|
|||||||
!proxy/
|
!proxy/
|
||||||
!safekeeper/
|
!safekeeper/
|
||||||
!storage_broker/
|
!storage_broker/
|
||||||
!trace/
|
|
||||||
!vendor/postgres-v14/
|
!vendor/postgres-v14/
|
||||||
!vendor/postgres-v15/
|
!vendor/postgres-v15/
|
||||||
!workspace_hack/
|
!workspace_hack/
|
||||||
!neon_local/
|
!neon_local/
|
||||||
!scripts/ninstall.sh
|
!scripts/ninstall.sh
|
||||||
!vm-cgconfig.conf
|
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ runs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "${{ inputs.run_in_parallel }}" == "true" ]]; then
|
if [[ "${{ inputs.run_in_parallel }}" == "true" ]]; then
|
||||||
# -n16 uses sixteen processes to run tests via pytest-xdist
|
# -n4 uses four processes to run tests via pytest-xdist
|
||||||
EXTRA_PARAMS="-n16 $EXTRA_PARAMS"
|
EXTRA_PARAMS="-n4 $EXTRA_PARAMS"
|
||||||
|
|
||||||
# --dist=loadgroup points tests marked with @pytest.mark.xdist_group
|
# --dist=loadgroup points tests marked with @pytest.mark.xdist_group
|
||||||
# to the same worker to make @pytest.mark.order work with xdist
|
# to the same worker to make @pytest.mark.order work with xdist
|
||||||
|
|||||||
4
.github/ansible/deploy.yaml
vendored
4
.github/ansible/deploy.yaml
vendored
@@ -118,7 +118,7 @@
|
|||||||
cmd: |
|
cmd: |
|
||||||
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
|
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
|
||||||
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version
|
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version
|
||||||
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/pageservers
|
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/pageservers
|
||||||
tags:
|
tags:
|
||||||
- pageserver
|
- pageserver
|
||||||
|
|
||||||
@@ -188,6 +188,6 @@
|
|||||||
cmd: |
|
cmd: |
|
||||||
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
|
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
|
||||||
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version
|
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/$INSTANCE_ID | jq '.version = {{ current_version }}' > /tmp/new_version
|
||||||
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/safekeepers
|
curl -sfS -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -X POST -d@/tmp/new_version {{ console_mgmt_base_url }}/management/api/v2/safekeepers
|
||||||
tags:
|
tags:
|
||||||
- safekeeper
|
- safekeeper
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ storage:
|
|||||||
vars:
|
vars:
|
||||||
bucket_name: neon-prod-storage-ap-southeast-1
|
bucket_name: neon-prod-storage-ap-southeast-1
|
||||||
bucket_region: ap-southeast-1
|
bucket_region: ap-southeast-1
|
||||||
console_mgmt_base_url: http://neon-internal-api.aws.neon.tech
|
console_mgmt_base_url: http://console-release.local
|
||||||
broker_endpoint: http://storage-broker-lb.epsilon.ap-southeast-1.internal.aws.neon.tech:50051
|
broker_endpoint: http://storage-broker-lb.epsilon.ap-southeast-1.internal.aws.neon.tech:50051
|
||||||
pageserver_config_stub:
|
pageserver_config_stub:
|
||||||
pg_distrib_dir: /usr/local
|
pg_distrib_dir: /usr/local
|
||||||
metric_collection_endpoint: http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events
|
metric_collection_endpoint: http://console-release.local/billing/api/v1/usage_events
|
||||||
metric_collection_interval: 10min
|
metric_collection_interval: 10min
|
||||||
remote_storage:
|
remote_storage:
|
||||||
bucket_name: "{{ bucket_name }}"
|
bucket_name: "{{ bucket_name }}"
|
||||||
@@ -32,7 +32,7 @@ storage:
|
|||||||
hosts:
|
hosts:
|
||||||
safekeeper-0.ap-southeast-1.aws.neon.tech:
|
safekeeper-0.ap-southeast-1.aws.neon.tech:
|
||||||
ansible_host: i-0d6f1dc5161eef894
|
ansible_host: i-0d6f1dc5161eef894
|
||||||
|
safekeeper-1.ap-southeast-1.aws.neon.tech:
|
||||||
|
ansible_host: i-0e338adda8eb2d19f
|
||||||
safekeeper-2.ap-southeast-1.aws.neon.tech:
|
safekeeper-2.ap-southeast-1.aws.neon.tech:
|
||||||
ansible_host: i-04fb63634e4679eb9
|
ansible_host: i-04fb63634e4679eb9
|
||||||
safekeeper-3.ap-southeast-1.aws.neon.tech:
|
|
||||||
ansible_host: i-05481f3bc88cfc2d4
|
|
||||||
|
|||||||
4
.github/ansible/prod.eu-central-1.hosts.yaml
vendored
4
.github/ansible/prod.eu-central-1.hosts.yaml
vendored
@@ -2,11 +2,11 @@ storage:
|
|||||||
vars:
|
vars:
|
||||||
bucket_name: neon-prod-storage-eu-central-1
|
bucket_name: neon-prod-storage-eu-central-1
|
||||||
bucket_region: eu-central-1
|
bucket_region: eu-central-1
|
||||||
console_mgmt_base_url: http://neon-internal-api.aws.neon.tech
|
console_mgmt_base_url: http://console-release.local
|
||||||
broker_endpoint: http://storage-broker-lb.gamma.eu-central-1.internal.aws.neon.tech:50051
|
broker_endpoint: http://storage-broker-lb.gamma.eu-central-1.internal.aws.neon.tech:50051
|
||||||
pageserver_config_stub:
|
pageserver_config_stub:
|
||||||
pg_distrib_dir: /usr/local
|
pg_distrib_dir: /usr/local
|
||||||
metric_collection_endpoint: http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events
|
metric_collection_endpoint: http://console-release.local/billing/api/v1/usage_events
|
||||||
metric_collection_interval: 10min
|
metric_collection_interval: 10min
|
||||||
remote_storage:
|
remote_storage:
|
||||||
bucket_name: "{{ bucket_name }}"
|
bucket_name: "{{ bucket_name }}"
|
||||||
|
|||||||
6
.github/ansible/prod.us-east-2.hosts.yaml
vendored
6
.github/ansible/prod.us-east-2.hosts.yaml
vendored
@@ -2,11 +2,11 @@ storage:
|
|||||||
vars:
|
vars:
|
||||||
bucket_name: neon-prod-storage-us-east-2
|
bucket_name: neon-prod-storage-us-east-2
|
||||||
bucket_region: us-east-2
|
bucket_region: us-east-2
|
||||||
console_mgmt_base_url: http://neon-internal-api.aws.neon.tech
|
console_mgmt_base_url: http://console-release.local
|
||||||
broker_endpoint: http://storage-broker-lb.delta.us-east-2.internal.aws.neon.tech:50051
|
broker_endpoint: http://storage-broker-lb.delta.us-east-2.internal.aws.neon.tech:50051
|
||||||
pageserver_config_stub:
|
pageserver_config_stub:
|
||||||
pg_distrib_dir: /usr/local
|
pg_distrib_dir: /usr/local
|
||||||
metric_collection_endpoint: http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events
|
metric_collection_endpoint: http://console-release.local/billing/api/v1/usage_events
|
||||||
metric_collection_interval: 10min
|
metric_collection_interval: 10min
|
||||||
remote_storage:
|
remote_storage:
|
||||||
bucket_name: "{{ bucket_name }}"
|
bucket_name: "{{ bucket_name }}"
|
||||||
@@ -27,8 +27,6 @@ storage:
|
|||||||
ansible_host: i-062227ba7f119eb8c
|
ansible_host: i-062227ba7f119eb8c
|
||||||
pageserver-1.us-east-2.aws.neon.tech:
|
pageserver-1.us-east-2.aws.neon.tech:
|
||||||
ansible_host: i-0b3ec0afab5968938
|
ansible_host: i-0b3ec0afab5968938
|
||||||
pageserver-2.us-east-2.aws.neon.tech:
|
|
||||||
ansible_host: i-0d7a1c4325e71421d
|
|
||||||
|
|
||||||
safekeepers:
|
safekeepers:
|
||||||
hosts:
|
hosts:
|
||||||
|
|||||||
6
.github/ansible/prod.us-west-2.hosts.yaml
vendored
6
.github/ansible/prod.us-west-2.hosts.yaml
vendored
@@ -2,11 +2,11 @@ storage:
|
|||||||
vars:
|
vars:
|
||||||
bucket_name: neon-prod-storage-us-west-2
|
bucket_name: neon-prod-storage-us-west-2
|
||||||
bucket_region: us-west-2
|
bucket_region: us-west-2
|
||||||
console_mgmt_base_url: http://neon-internal-api.aws.neon.tech
|
console_mgmt_base_url: http://console-release.local
|
||||||
broker_endpoint: http://storage-broker-lb.eta.us-west-2.internal.aws.neon.tech:50051
|
broker_endpoint: http://storage-broker-lb.eta.us-west-2.internal.aws.neon.tech:50051
|
||||||
pageserver_config_stub:
|
pageserver_config_stub:
|
||||||
pg_distrib_dir: /usr/local
|
pg_distrib_dir: /usr/local
|
||||||
metric_collection_endpoint: http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events
|
metric_collection_endpoint: http://console-release.local/billing/api/v1/usage_events
|
||||||
metric_collection_interval: 10min
|
metric_collection_interval: 10min
|
||||||
remote_storage:
|
remote_storage:
|
||||||
bucket_name: "{{ bucket_name }}"
|
bucket_name: "{{ bucket_name }}"
|
||||||
@@ -29,8 +29,6 @@ storage:
|
|||||||
ansible_host: i-0c834be1dddba8b3f
|
ansible_host: i-0c834be1dddba8b3f
|
||||||
pageserver-2.us-west-2.aws.neon.tech:
|
pageserver-2.us-west-2.aws.neon.tech:
|
||||||
ansible_host: i-051642d372c0a4f32
|
ansible_host: i-051642d372c0a4f32
|
||||||
pageserver-3.us-west-2.aws.neon.tech:
|
|
||||||
ansible_host: i-00c3844beb9ad1c6b
|
|
||||||
|
|
||||||
safekeepers:
|
safekeepers:
|
||||||
hosts:
|
hosts:
|
||||||
|
|||||||
40
.github/ansible/production.hosts.yaml
vendored
Normal file
40
.github/ansible/production.hosts.yaml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
storage:
|
||||||
|
vars:
|
||||||
|
console_mgmt_base_url: http://console-release.local
|
||||||
|
bucket_name: zenith-storage-oregon
|
||||||
|
bucket_region: us-west-2
|
||||||
|
broker_endpoint: http://storage-broker.prod.local:50051
|
||||||
|
pageserver_config_stub:
|
||||||
|
pg_distrib_dir: /usr/local
|
||||||
|
metric_collection_endpoint: http://console-release.local/billing/api/v1/usage_events
|
||||||
|
metric_collection_interval: 10min
|
||||||
|
remote_storage:
|
||||||
|
bucket_name: "{{ bucket_name }}"
|
||||||
|
bucket_region: "{{ bucket_region }}"
|
||||||
|
prefix_in_bucket: "{{ inventory_hostname }}"
|
||||||
|
safekeeper_s3_prefix: prod-1/wal
|
||||||
|
hostname_suffix: ".local"
|
||||||
|
remote_user: admin
|
||||||
|
sentry_environment: production
|
||||||
|
|
||||||
|
children:
|
||||||
|
pageservers:
|
||||||
|
hosts:
|
||||||
|
zenith-1-ps-2:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
|
zenith-1-ps-3:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
|
zenith-1-ps-4:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
|
zenith-1-ps-5:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
|
|
||||||
|
safekeepers:
|
||||||
|
hosts:
|
||||||
|
zenith-1-sk-1:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
|
zenith-1-sk-2:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
|
zenith-1-sk-4:
|
||||||
|
console_region_id: aws-us-west-2
|
||||||
2
.github/ansible/scripts/init_pageserver.sh
vendored
2
.github/ansible/scripts/init_pageserver.sh
vendored
@@ -26,7 +26,7 @@ EOF
|
|||||||
if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/${INSTANCE_ID} -o /dev/null; then
|
if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers/${INSTANCE_ID} -o /dev/null; then
|
||||||
|
|
||||||
# not registered, so register it now
|
# not registered, so register it now
|
||||||
ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" {{ console_mgmt_base_url }}/management/api/v2/pageservers -d@/tmp/payload | jq -r '.id')
|
ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/pageservers -d@/tmp/payload | jq -r '.id')
|
||||||
|
|
||||||
# init pageserver
|
# init pageserver
|
||||||
sudo -u pageserver /usr/local/bin/pageserver -c "id=${ID}" -c "pg_distrib_dir='/usr/local'" --init -D /storage/pageserver/data
|
sudo -u pageserver /usr/local/bin/pageserver -c "id=${ID}" -c "pg_distrib_dir='/usr/local'" --init -D /storage/pageserver/data
|
||||||
|
|||||||
2
.github/ansible/scripts/init_safekeeper.sh
vendored
2
.github/ansible/scripts/init_safekeeper.sh
vendored
@@ -25,7 +25,7 @@ EOF
|
|||||||
if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/${INSTANCE_ID} -o /dev/null; then
|
if ! curl -sf -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers/${INSTANCE_ID} -o /dev/null; then
|
||||||
|
|
||||||
# not registered, so register it now
|
# not registered, so register it now
|
||||||
ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" -H "Content-Type: application/json" {{ console_mgmt_base_url }}/management/api/v2/safekeepers -d@/tmp/payload | jq -r '.id')
|
ID=$(curl -sf -X POST -H "Authorization: Bearer {{ CONSOLE_API_TOKEN }}" {{ console_mgmt_base_url }}/management/api/v2/safekeepers -d@/tmp/payload | jq -r '.id')
|
||||||
# init safekeeper
|
# init safekeeper
|
||||||
sudo -u safekeeper /usr/local/bin/safekeeper --id ${ID} --init -D /storage/safekeeper/data
|
sudo -u safekeeper /usr/local/bin/safekeeper --id ${ID} --init -D /storage/safekeeper/data
|
||||||
fi
|
fi
|
||||||
|
|||||||
9
.github/ansible/staging.eu-west-1.hosts.yaml
vendored
9
.github/ansible/staging.eu-west-1.hosts.yaml
vendored
@@ -2,17 +2,12 @@ storage:
|
|||||||
vars:
|
vars:
|
||||||
bucket_name: neon-dev-storage-eu-west-1
|
bucket_name: neon-dev-storage-eu-west-1
|
||||||
bucket_region: eu-west-1
|
bucket_region: eu-west-1
|
||||||
console_mgmt_base_url: http://neon-internal-api.aws.neon.build
|
console_mgmt_base_url: http://console-staging.local
|
||||||
broker_endpoint: http://storage-broker-lb.zeta.eu-west-1.internal.aws.neon.build:50051
|
broker_endpoint: http://storage-broker-lb.zeta.eu-west-1.internal.aws.neon.build:50051
|
||||||
pageserver_config_stub:
|
pageserver_config_stub:
|
||||||
pg_distrib_dir: /usr/local
|
pg_distrib_dir: /usr/local
|
||||||
metric_collection_endpoint: http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events
|
metric_collection_endpoint: http://console-staging.local/billing/api/v1/usage_events
|
||||||
metric_collection_interval: 10min
|
metric_collection_interval: 10min
|
||||||
tenant_config:
|
|
||||||
eviction_policy:
|
|
||||||
kind: "LayerAccessThreshold"
|
|
||||||
period: "20m"
|
|
||||||
threshold: "20m"
|
|
||||||
remote_storage:
|
remote_storage:
|
||||||
bucket_name: "{{ bucket_name }}"
|
bucket_name: "{{ bucket_name }}"
|
||||||
bucket_region: "{{ bucket_region }}"
|
bucket_region: "{{ bucket_region }}"
|
||||||
|
|||||||
13
.github/ansible/staging.us-east-2.hosts.yaml
vendored
13
.github/ansible/staging.us-east-2.hosts.yaml
vendored
@@ -2,17 +2,12 @@ storage:
|
|||||||
vars:
|
vars:
|
||||||
bucket_name: neon-staging-storage-us-east-2
|
bucket_name: neon-staging-storage-us-east-2
|
||||||
bucket_region: us-east-2
|
bucket_region: us-east-2
|
||||||
console_mgmt_base_url: http://neon-internal-api.aws.neon.build
|
console_mgmt_base_url: http://console-staging.local
|
||||||
broker_endpoint: http://storage-broker-lb.beta.us-east-2.internal.aws.neon.build:50051
|
broker_endpoint: http://storage-broker-lb.beta.us-east-2.internal.aws.neon.build:50051
|
||||||
pageserver_config_stub:
|
pageserver_config_stub:
|
||||||
pg_distrib_dir: /usr/local
|
pg_distrib_dir: /usr/local
|
||||||
metric_collection_endpoint: http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events
|
metric_collection_endpoint: http://console-staging.local/billing/api/v1/usage_events
|
||||||
metric_collection_interval: 10min
|
metric_collection_interval: 10min
|
||||||
tenant_config:
|
|
||||||
eviction_policy:
|
|
||||||
kind: "LayerAccessThreshold"
|
|
||||||
period: "20m"
|
|
||||||
threshold: "20m"
|
|
||||||
remote_storage:
|
remote_storage:
|
||||||
bucket_name: "{{ bucket_name }}"
|
bucket_name: "{{ bucket_name }}"
|
||||||
bucket_region: "{{ bucket_region }}"
|
bucket_region: "{{ bucket_region }}"
|
||||||
@@ -36,8 +31,6 @@ storage:
|
|||||||
ansible_host: i-01e31cdf7e970586a
|
ansible_host: i-01e31cdf7e970586a
|
||||||
pageserver-3.us-east-2.aws.neon.build:
|
pageserver-3.us-east-2.aws.neon.build:
|
||||||
ansible_host: i-0602a0291365ef7cc
|
ansible_host: i-0602a0291365ef7cc
|
||||||
pageserver-99.us-east-2.aws.neon.build:
|
|
||||||
ansible_host: i-0c39491109bb88824
|
|
||||||
|
|
||||||
safekeepers:
|
safekeepers:
|
||||||
hosts:
|
hosts:
|
||||||
@@ -47,5 +40,3 @@ storage:
|
|||||||
ansible_host: i-0171efc3604a7b907
|
ansible_host: i-0171efc3604a7b907
|
||||||
safekeeper-2.us-east-2.aws.neon.build:
|
safekeeper-2.us-east-2.aws.neon.build:
|
||||||
ansible_host: i-0de0b03a51676a6ce
|
ansible_host: i-0de0b03a51676a6ce
|
||||||
safekeeper-99.us-east-2.aws.neon.build:
|
|
||||||
ansible_host: i-0d61b6a2ea32028d5
|
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
# Helm chart values for neon-proxy-scram.
|
||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: neondatabase/neon
|
repository: neondatabase/neon
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2"
|
authEndpoint: "http://console-staging.local/management/api/v2"
|
||||||
domain: "*.eu-west-1.aws.neon.build"
|
domain: "*.eu-west-1.aws.neon.build"
|
||||||
sentryEnvironment: "staging"
|
sentryEnvironment: "staging"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-staging.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "1min"
|
metricCollectionInterval: "1min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ settings:
|
|||||||
authBackend: "link"
|
authBackend: "link"
|
||||||
authEndpoint: "https://console.stage.neon.tech/authenticate_proxy_request/"
|
authEndpoint: "https://console.stage.neon.tech/authenticate_proxy_request/"
|
||||||
uri: "https://console.stage.neon.tech/psql_session/"
|
uri: "https://console.stage.neon.tech/psql_session/"
|
||||||
domain: "pg.neon.build"
|
|
||||||
sentryEnvironment: "staging"
|
sentryEnvironment: "staging"
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-staging.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "1min"
|
metricCollectionInterval: "1min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy-link pods
|
# -- Additional labels for neon-proxy-link pods
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ image:
|
|||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2"
|
authEndpoint: "http://console-staging.local/management/api/v2"
|
||||||
domain: "*.cloud.stage.neon.tech"
|
domain: "*.cloud.stage.neon.tech"
|
||||||
sentryEnvironment: "staging"
|
sentryEnvironment: "staging"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-staging.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "1min"
|
metricCollectionInterval: "1min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
# Helm chart values for neon-proxy-scram.
|
||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: neondatabase/neon
|
repository: neondatabase/neon
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2"
|
authEndpoint: "http://console-staging.local/management/api/v2"
|
||||||
domain: "*.us-east-2.aws.neon.build"
|
domain: "*.us-east-2.aws.neon.build"
|
||||||
sentryEnvironment: "staging"
|
sentryEnvironment: "staging"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-staging.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "1min"
|
metricCollectionInterval: "1min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
@@ -1,32 +1,16 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
# Helm chart values for neon-proxy-scram.
|
||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: neondatabase/neon
|
repository: neondatabase/neon
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.tech/management/api/v2"
|
authEndpoint: "http://console-release.local/management/api/v2"
|
||||||
domain: "*.ap-southeast-1.aws.neon.tech"
|
domain: "*.ap-southeast-1.aws.neon.tech"
|
||||||
sentryEnvironment: "production"
|
sentryEnvironment: "production"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-release.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "10min"
|
metricCollectionInterval: "10min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
@@ -1,32 +1,16 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
# Helm chart values for neon-proxy-scram.
|
||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: neondatabase/neon
|
repository: neondatabase/neon
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.tech/management/api/v2"
|
authEndpoint: "http://console-release.local/management/api/v2"
|
||||||
domain: "*.eu-central-1.aws.neon.tech"
|
domain: "*.eu-central-1.aws.neon.tech"
|
||||||
sentryEnvironment: "production"
|
sentryEnvironment: "production"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-release.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "10min"
|
metricCollectionInterval: "10min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
@@ -1,32 +1,16 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
# Helm chart values for neon-proxy-scram.
|
||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: neondatabase/neon
|
repository: neondatabase/neon
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.tech/management/api/v2"
|
authEndpoint: "http://console-release.local/management/api/v2"
|
||||||
domain: "*.us-east-2.aws.neon.tech"
|
domain: "*.us-east-2.aws.neon.tech"
|
||||||
sentryEnvironment: "production"
|
sentryEnvironment: "production"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-release.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "10min"
|
metricCollectionInterval: "10min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
@@ -1,32 +1,16 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
# Helm chart values for neon-proxy-scram.
|
||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: neondatabase/neon
|
repository: neondatabase/neon
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.tech/management/api/v2"
|
authEndpoint: "http://console-release.local/management/api/v2"
|
||||||
domain: "*.us-west-2.aws.neon.tech"
|
domain: "*.us-west-2.aws.neon.tech"
|
||||||
sentryEnvironment: "production"
|
sentryEnvironment: "production"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-release.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "10min"
|
metricCollectionInterval: "10min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
# -- Additional labels for neon-proxy pods
|
||||||
|
|||||||
56
.github/helm-values/production.neon-storage-broker.yaml
vendored
Normal file
56
.github/helm-values/production.neon-storage-broker.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Helm chart values for neon-storage-broker
|
||||||
|
podLabels:
|
||||||
|
neon_env: production
|
||||||
|
neon_service: storage-broker
|
||||||
|
|
||||||
|
# Use L4 LB
|
||||||
|
service:
|
||||||
|
# service.annotations -- Annotations to add to the service
|
||||||
|
annotations:
|
||||||
|
service.beta.kubernetes.io/aws-load-balancer-type: external # use newer AWS Load Balancer Controller
|
||||||
|
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
||||||
|
service.beta.kubernetes.io/aws-load-balancer-scheme: internal # deploy LB to private subnet
|
||||||
|
# assign service to this name at external-dns
|
||||||
|
external-dns.alpha.kubernetes.io/hostname: storage-broker.prod.local
|
||||||
|
# service.type -- Service type
|
||||||
|
type: LoadBalancer
|
||||||
|
# service.port -- broker listen port
|
||||||
|
port: 50051
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
selector:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
|
||||||
|
extraManifests:
|
||||||
|
- apiVersion: operator.victoriametrics.com/v1beta1
|
||||||
|
kind: VMServiceScrape
|
||||||
|
metadata:
|
||||||
|
name: "{{ include \"neon-storage-broker.fullname\" . }}"
|
||||||
|
labels:
|
||||||
|
helm.sh/chart: neon-storage-broker-{{ .Chart.Version }}
|
||||||
|
app.kubernetes.io/name: neon-storage-broker
|
||||||
|
app.kubernetes.io/instance: neon-storage-broker
|
||||||
|
app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
|
||||||
|
app.kubernetes.io/managed-by: Helm
|
||||||
|
namespace: "{{ .Release.Namespace }}"
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: "neon-storage-broker"
|
||||||
|
endpoints:
|
||||||
|
- port: broker
|
||||||
|
path: /metrics
|
||||||
|
interval: 10s
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- "{{ .Release.Namespace }}"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
sentryEnvironment: "production"
|
||||||
@@ -1,55 +1,32 @@
|
|||||||
# Helm chart values for neon-proxy-scram.
|
|
||||||
# This is a YAML-formatted file.
|
|
||||||
|
|
||||||
deploymentStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 100%
|
|
||||||
maxUnavailable: 50%
|
|
||||||
|
|
||||||
# Delay the kill signal by 7 days (7 * 24 * 60 * 60)
|
|
||||||
# The pod(s) will stay in Terminating, keeps the existing connections
|
|
||||||
# but doesn't receive new ones
|
|
||||||
containerLifecycle:
|
|
||||||
preStop:
|
|
||||||
exec:
|
|
||||||
command: ["/bin/sh", "-c", "sleep 604800"]
|
|
||||||
terminationGracePeriodSeconds: 604800
|
|
||||||
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: neondatabase/neon
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "console"
|
authBackend: "console"
|
||||||
authEndpoint: "http://neon-internal-api.aws.neon.tech/management/api/v2"
|
authEndpoint: "http://console-release.local/management/api/v2"
|
||||||
domain: "*.cloud.neon.tech"
|
domain: "*.cloud.neon.tech"
|
||||||
sentryEnvironment: "production"
|
sentryEnvironment: "production"
|
||||||
wssPort: 8443
|
wssPort: 8443
|
||||||
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events"
|
metricCollectionEndpoint: "http://console-release.local/billing/api/v1/usage_events"
|
||||||
metricCollectionInterval: "10min"
|
metricCollectionInterval: "10min"
|
||||||
|
|
||||||
# -- Additional labels for neon-proxy pods
|
|
||||||
podLabels:
|
podLabels:
|
||||||
zenith_service: proxy-scram
|
zenith_service: proxy-scram
|
||||||
zenith_env: prod
|
zenith_env: production
|
||||||
zenith_region: us-west-2
|
zenith_region: us-west-2
|
||||||
zenith_region_slug: us-west-2
|
zenith_region_slug: oregon
|
||||||
|
|
||||||
exposedService:
|
exposedService:
|
||||||
annotations:
|
annotations:
|
||||||
service.beta.kubernetes.io/aws-load-balancer-type: external
|
service.beta.kubernetes.io/aws-load-balancer-type: external
|
||||||
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
||||||
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
|
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
|
||||||
external-dns.alpha.kubernetes.io/hostname: neon-proxy-scram-legacy.eta.us-west-2.aws.neon.tech
|
external-dns.alpha.kubernetes.io/hostname: '*.cloud.neon.tech'
|
||||||
httpsPort: 443
|
httpsPort: 443
|
||||||
|
|
||||||
#metrics:
|
metrics:
|
||||||
# enabled: true
|
enabled: true
|
||||||
# serviceMonitor:
|
serviceMonitor:
|
||||||
# enabled: true
|
enabled: true
|
||||||
# selector:
|
selector:
|
||||||
# release: kube-prometheus-stack
|
release: kube-prometheus-stack
|
||||||
|
|
||||||
extraManifests:
|
extraManifests:
|
||||||
- apiVersion: operator.victoriametrics.com/v1beta1
|
- apiVersion: operator.victoriametrics.com/v1beta1
|
||||||
@@ -1,37 +1,37 @@
|
|||||||
# Helm chart values for neon-proxy-link.
|
|
||||||
# This is a YAML-formatted file.
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: neondatabase/neon
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
authBackend: "link"
|
authBackend: "link"
|
||||||
authEndpoint: "https://console.neon.tech/authenticate_proxy_request/"
|
authEndpoint: "https://console.neon.tech/authenticate_proxy_request/"
|
||||||
uri: "https://console.neon.tech/psql_session/"
|
uri: "https://console.neon.tech/psql_session/"
|
||||||
domain: "pg.neon.tech"
|
|
||||||
sentryEnvironment: "production"
|
sentryEnvironment: "production"
|
||||||
|
|
||||||
# -- Additional labels for zenith-proxy pods
|
# -- Additional labels for zenith-proxy pods
|
||||||
podLabels:
|
podLabels:
|
||||||
zenith_service: proxy
|
zenith_service: proxy
|
||||||
zenith_env: production
|
zenith_env: production
|
||||||
zenith_region: us-east-2
|
zenith_region: us-west-2
|
||||||
zenith_region_slug: us-east-2
|
zenith_region_slug: oregon
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: LoadBalancer
|
|
||||||
annotations:
|
annotations:
|
||||||
service.beta.kubernetes.io/aws-load-balancer-type: external
|
service.beta.kubernetes.io/aws-load-balancer-type: external
|
||||||
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
||||||
service.beta.kubernetes.io/aws-load-balancer-scheme: internal
|
service.beta.kubernetes.io/aws-load-balancer-scheme: internal
|
||||||
external-dns.alpha.kubernetes.io/hostname: neon-proxy-link-mgmt.delta.us-east-2.aws.neon.tech
|
external-dns.alpha.kubernetes.io/hostname: proxy-release.local
|
||||||
|
type: LoadBalancer
|
||||||
|
|
||||||
exposedService:
|
exposedService:
|
||||||
annotations:
|
annotations:
|
||||||
service.beta.kubernetes.io/aws-load-balancer-type: external
|
service.beta.kubernetes.io/aws-load-balancer-type: external
|
||||||
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
|
||||||
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
|
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
|
||||||
external-dns.alpha.kubernetes.io/hostname: neon-proxy-link.delta.us-east-2.aws.neon.tech
|
external-dns.alpha.kubernetes.io/hostname: connect.neon.tech,pg.neon.tech
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
serviceMonitor:
|
||||||
|
enabled: true
|
||||||
|
selector:
|
||||||
|
release: kube-prometheus-stack
|
||||||
|
|
||||||
extraManifests:
|
extraManifests:
|
||||||
- apiVersion: operator.victoriametrics.com/v1beta1
|
- apiVersion: operator.victoriametrics.com/v1beta1
|
||||||
648
.github/workflows/build_and_test.yml
vendored
648
.github/workflows/build_and_test.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Test
|
name: Test and Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -19,12 +19,10 @@ concurrency:
|
|||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
COPT: '-Werror'
|
COPT: '-Werror'
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag:
|
tag:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
||||||
outputs:
|
outputs:
|
||||||
build-tag: ${{steps.build-tag.outputs.tag}}
|
build-tag: ${{steps.build-tag.outputs.tag}}
|
||||||
@@ -52,9 +50,9 @@ jobs:
|
|||||||
id: build-tag
|
id: build-tag
|
||||||
|
|
||||||
check-codestyle-python:
|
check-codestyle-python:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cloud:pinned
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -74,17 +72,20 @@ jobs:
|
|||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
run: ./scripts/pysync
|
run: ./scripts/pysync
|
||||||
|
|
||||||
- name: Run ruff to ensure code format
|
- name: Run isort to ensure code format
|
||||||
run: poetry run ruff .
|
run: poetry run isort --diff --check .
|
||||||
|
|
||||||
- name: Run black to ensure code format
|
- name: Run black to ensure code format
|
||||||
run: poetry run black --diff --check .
|
run: poetry run black --diff --check .
|
||||||
|
|
||||||
|
- name: Run flake8 to ensure code format
|
||||||
|
run: poetry run flake8 .
|
||||||
|
|
||||||
- name: Run mypy to check types
|
- name: Run mypy to check types
|
||||||
run: poetry run mypy .
|
run: poetry run mypy .
|
||||||
|
|
||||||
check-codestyle-rust:
|
check-codestyle-rust:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -96,16 +97,16 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Disabled for now
|
- name: Restore cargo deps cache
|
||||||
# - name: Restore cargo deps cache
|
id: cache_cargo
|
||||||
# id: cache_cargo
|
uses: actions/cache@v3
|
||||||
# uses: actions/cache@v3
|
with:
|
||||||
# with:
|
path: |
|
||||||
# path: |
|
~/.cargo/registry/
|
||||||
# !~/.cargo/registry/src
|
!~/.cargo/registry/src
|
||||||
# ~/.cargo/git/
|
~/.cargo/git/
|
||||||
# target/
|
target/
|
||||||
# key: v1-${{ runner.os }}-cargo-clippy-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
key: v1-${{ runner.os }}-cargo-clippy-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
# Some of our rust modules use FFI and need those to be checked
|
# Some of our rust modules use FFI and need those to be checked
|
||||||
- name: Get postgres headers
|
- name: Get postgres headers
|
||||||
@@ -132,7 +133,7 @@ jobs:
|
|||||||
run: cargo deny check
|
run: cargo deny check
|
||||||
|
|
||||||
build-neon:
|
build-neon:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -140,6 +141,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
build_type: [ debug, release ]
|
build_type: [ debug, release ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TYPE: ${{ matrix.build_type }}
|
BUILD_TYPE: ${{ matrix.build_type }}
|
||||||
GIT_VERSION: ${{ github.sha }}
|
GIT_VERSION: ${{ github.sha }}
|
||||||
@@ -192,26 +194,24 @@ jobs:
|
|||||||
echo "cov_prefix=${cov_prefix}" >> $GITHUB_ENV
|
echo "cov_prefix=${cov_prefix}" >> $GITHUB_ENV
|
||||||
echo "CARGO_FEATURES=${CARGO_FEATURES}" >> $GITHUB_ENV
|
echo "CARGO_FEATURES=${CARGO_FEATURES}" >> $GITHUB_ENV
|
||||||
echo "CARGO_FLAGS=${CARGO_FLAGS}" >> $GITHUB_ENV
|
echo "CARGO_FLAGS=${CARGO_FLAGS}" >> $GITHUB_ENV
|
||||||
echo "CARGO_HOME=${GITHUB_WORKSPACE}/.cargo" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Disabled for now
|
|
||||||
# Don't include the ~/.cargo/registry/src directory. It contains just
|
# Don't include the ~/.cargo/registry/src directory. It contains just
|
||||||
# uncompressed versions of the crates in ~/.cargo/registry/cache
|
# uncompressed versions of the crates in ~/.cargo/registry/cache
|
||||||
# directory, and it's faster to let 'cargo' to rebuild it from the
|
# directory, and it's faster to let 'cargo' to rebuild it from the
|
||||||
# compressed crates.
|
# compressed crates.
|
||||||
# - name: Cache cargo deps
|
- name: Cache cargo deps
|
||||||
# id: cache_cargo
|
id: cache_cargo
|
||||||
# uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
# with:
|
with:
|
||||||
# path: |
|
path: |
|
||||||
# ~/.cargo/registry/
|
~/.cargo/registry/
|
||||||
# !~/.cargo/registry/src
|
!~/.cargo/registry/src
|
||||||
# ~/.cargo/git/
|
~/.cargo/git/
|
||||||
# target/
|
target/
|
||||||
# # Fall back to older versions of the key, if no cache for current Cargo.lock was found
|
# Fall back to older versions of the key, if no cache for current Cargo.lock was found
|
||||||
# key: |
|
key: |
|
||||||
# v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
# v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
|
v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
|
||||||
|
|
||||||
- name: Cache postgres v14 build
|
- name: Cache postgres v14 build
|
||||||
id: cache_pg_14
|
id: cache_pg_14
|
||||||
@@ -301,7 +301,7 @@ jobs:
|
|||||||
uses: ./.github/actions/save-coverage-data
|
uses: ./.github/actions/save-coverage-data
|
||||||
|
|
||||||
regress-tests:
|
regress-tests:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -334,7 +334,7 @@ jobs:
|
|||||||
uses: ./.github/actions/save-coverage-data
|
uses: ./.github/actions/save-coverage-data
|
||||||
|
|
||||||
benchmarks:
|
benchmarks:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -365,7 +365,7 @@ jobs:
|
|||||||
# while coverage is currently collected for the debug ones
|
# while coverage is currently collected for the debug ones
|
||||||
|
|
||||||
merge-allure-report:
|
merge-allure-report:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -402,7 +402,7 @@ jobs:
|
|||||||
DATABASE_URL="$TEST_RESULT_CONNSTR" poetry run python3 scripts/ingest_regress_test_result.py --revision ${SHA} --reference ${GITHUB_REF} --build-type ${BUILD_TYPE} --ingest suites.json
|
DATABASE_URL="$TEST_RESULT_CONNSTR" poetry run python3 scripts/ingest_regress_test_result.py --revision ${SHA} --reference ${GITHUB_REF} --build-type ${BUILD_TYPE} --ingest suites.json
|
||||||
|
|
||||||
coverage-report:
|
coverage-report:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -418,17 +418,16 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Disabled for now
|
- name: Restore cargo deps cache
|
||||||
# - name: Restore cargo deps cache
|
id: cache_cargo
|
||||||
# id: cache_cargo
|
uses: actions/cache@v3
|
||||||
# uses: actions/cache@v3
|
with:
|
||||||
# with:
|
path: |
|
||||||
# path: |
|
~/.cargo/registry/
|
||||||
# ~/.cargo/registry/
|
!~/.cargo/registry/src
|
||||||
# !~/.cargo/registry/src
|
~/.cargo/git/
|
||||||
# ~/.cargo/git/
|
target/
|
||||||
# target/
|
key: v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
# key: v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Get Neon artifact
|
- name: Get Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
@@ -478,7 +477,7 @@ jobs:
|
|||||||
}"
|
}"
|
||||||
|
|
||||||
trigger-e2e-tests:
|
trigger-e2e-tests:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
||||||
options: --init
|
options: --init
|
||||||
@@ -523,10 +522,9 @@ jobs:
|
|||||||
}"
|
}"
|
||||||
|
|
||||||
neon-image:
|
neon-image:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
needs: [ tag ]
|
needs: [ tag ]
|
||||||
# https://github.com/GoogleContainerTools/kaniko/issues/2005
|
container: gcr.io/kaniko-project/executor:v1.9.0-debug
|
||||||
container: gcr.io/kaniko-project/executor:v1.7.0-debug
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: sh -eu {0}
|
shell: sh -eu {0}
|
||||||
@@ -542,58 +540,12 @@ jobs:
|
|||||||
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
|
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
- name: Kaniko build neon
|
- name: Kaniko build neon
|
||||||
run: /kaniko/executor --reproducible --snapshotMode=redo --skip-unused-stages --cache=true --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --context . --build-arg GIT_VERSION=${{ github.sha }} --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}}
|
run: /kaniko/executor --snapshotMode=redo --cache=true --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --snapshotMode=redo --context . --build-arg GIT_VERSION=${{ github.sha }} --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
# Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied
|
|
||||||
- name: Cleanup ECR folder
|
|
||||||
run: rm -rf ~/.ecr
|
|
||||||
|
|
||||||
|
|
||||||
neon-image-depot:
|
|
||||||
# For testing this will run side-by-side for a few merges.
|
|
||||||
# This action is not really optimized yet, but gets the job done
|
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
|
||||||
needs: [ tag ]
|
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.19'
|
|
||||||
|
|
||||||
- name: Set up Depot CLI
|
|
||||||
uses: depot/setup-action@v1
|
|
||||||
|
|
||||||
- name: Install Crane & ECR helper
|
|
||||||
run: go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
|
|
||||||
|
|
||||||
- name: Configure ECR login
|
|
||||||
run: |
|
|
||||||
mkdir /github/home/.docker/
|
|
||||||
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: depot/build-push-action@v1
|
|
||||||
with:
|
|
||||||
# if no depot.json file is at the root of your repo, you must specify the project id
|
|
||||||
project: nrdv0s4kcs
|
|
||||||
push: true
|
|
||||||
tags: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:depot-${{needs.tag.outputs.build-tag}}
|
|
||||||
|
|
||||||
compute-tools-image:
|
compute-tools-image:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
needs: [ tag ]
|
needs: [ tag ]
|
||||||
container: gcr.io/kaniko-project/executor:v1.7.0-debug
|
container: gcr.io/kaniko-project/executor:v1.9.0-debug
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: sh -eu {0}
|
shell: sh -eu {0}
|
||||||
@@ -606,14 +558,11 @@ jobs:
|
|||||||
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
|
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
- name: Kaniko build compute tools
|
- name: Kaniko build compute tools
|
||||||
run: /kaniko/executor --reproducible --snapshotMode=redo --skip-unused-stages --cache=true --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --context . --build-arg GIT_VERSION=${{ github.sha }} --dockerfile Dockerfile.compute-tools --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}}
|
run: /kaniko/executor --snapshotMode=redo --cache=true --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --snapshotMode=redo --context . --build-arg GIT_VERSION=${{ github.sha }} --dockerfile Dockerfile.compute-tools --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Cleanup ECR folder
|
|
||||||
run: rm -rf ~/.ecr
|
|
||||||
|
|
||||||
compute-node-image:
|
compute-node-image:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container: gcr.io/kaniko-project/executor:v1.7.0-debug
|
container: gcr.io/kaniko-project/executor:v1.9.0-debug
|
||||||
needs: [ tag ]
|
needs: [ tag ]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -634,13 +583,10 @@ jobs:
|
|||||||
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
|
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json
|
||||||
|
|
||||||
- name: Kaniko build compute node with extensions
|
- name: Kaniko build compute node with extensions
|
||||||
run: /kaniko/executor --reproducible --snapshotMode=redo --skip-unused-stages --cache=true --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --context . --build-arg GIT_VERSION=${{ github.sha }} --build-arg PG_VERSION=${{ matrix.version }} --dockerfile Dockerfile.compute-node --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
run: /kaniko/executor --skip-unused-stages --snapshotMode=redo --cache=true --cache-repo 369495373322.dkr.ecr.eu-central-1.amazonaws.com/cache --context . --build-arg GIT_VERSION=${{ github.sha }} --dockerfile Dockerfile.compute-node-${{ matrix.version }} --destination 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Cleanup ECR folder
|
|
||||||
run: rm -rf ~/.ecr
|
|
||||||
|
|
||||||
vm-compute-node-image:
|
vm-compute-node-image:
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
needs: [ tag, compute-node-image ]
|
needs: [ tag, compute-node-image ]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -650,31 +596,34 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: sh -eu {0}
|
shell: sh -eu {0}
|
||||||
env:
|
env:
|
||||||
VM_BUILDER_VERSION: v0.4.6
|
VM_INFORMANT_VERSION: 0.1.1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Downloading latest vm-builder
|
||||||
uses: actions/checkout@v1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Downloading vm-builder
|
|
||||||
run: |
|
run: |
|
||||||
curl -L https://github.com/neondatabase/neonvm/releases/download/$VM_BUILDER_VERSION/vm-builder -o vm-builder
|
curl -L https://github.com/neondatabase/neonvm/releases/latest/download/vm-builder -o vm-builder
|
||||||
chmod +x vm-builder
|
chmod +x vm-builder
|
||||||
|
|
||||||
- name: Pulling compute-node image
|
- name: Pulling compute-node image
|
||||||
run: |
|
run: |
|
||||||
docker pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
docker pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Building VM compute-node rootfs
|
- name: Downloading VM informant version ${{ env.VM_INFORMANT_VERSION }}
|
||||||
run: |
|
run: |
|
||||||
docker build -t temp-vm-compute-node --build-arg SRC_IMAGE=369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} -f Dockerfile.vm-compute-node .
|
curl -fL https://github.com/neondatabase/autoscaling/releases/download/${{ env.VM_INFORMANT_VERSION }}/vm-informant -o vm-informant
|
||||||
|
chmod +x vm-informant
|
||||||
|
|
||||||
|
- name: Adding VM informant to compute-node image
|
||||||
|
run: |
|
||||||
|
ID=$(docker create 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}})
|
||||||
|
docker cp vm-informant $ID:/bin/vm-informant
|
||||||
|
docker commit $ID temp-vm-compute-node
|
||||||
|
docker rm -f $ID
|
||||||
|
|
||||||
- name: Build vm image
|
- name: Build vm image
|
||||||
run: |
|
run: |
|
||||||
# note: as of 2023-01-12, vm-builder requires a trailing ":latest" for local images
|
# note: as of 2023-01-12, vm-builder requires a trailing ":latest" for local images
|
||||||
./vm-builder -use-inittab -src=temp-vm-compute-node:latest -dst=369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
./vm-builder -src=temp-vm-compute-node:latest -dst=369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
||||||
|
|
||||||
- name: Pushing vm-compute-node image
|
- name: Pushing vm-compute-node image
|
||||||
run: |
|
run: |
|
||||||
@@ -682,7 +631,7 @@ jobs:
|
|||||||
|
|
||||||
test-images:
|
test-images:
|
||||||
needs: [ tag, neon-image, compute-node-image, compute-tools-image ]
|
needs: [ tag, neon-image, compute-node-image, compute-tools-image ]
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -724,39 +673,20 @@ jobs:
|
|||||||
docker compose -f ./docker-compose/docker-compose.yml down
|
docker compose -f ./docker-compose/docker-compose.yml down
|
||||||
|
|
||||||
promote-images:
|
promote-images:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
needs: [ tag, test-images, vm-compute-node-image ]
|
needs: [ tag, test-images, vm-compute-node-image ]
|
||||||
container: golang:1.19-bullseye
|
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
|
container: amazon/aws-cli
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
name: [ neon, compute-node-v14, vm-compute-node-v14, compute-node-v15, vm-compute-node-v15, compute-tools]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Crane & ECR helper
|
- name: Promote image to latest
|
||||||
if: |
|
|
||||||
(github.ref_name == 'main' || github.ref_name == 'release') &&
|
|
||||||
github.event_name != 'workflow_dispatch'
|
|
||||||
run: |
|
run: |
|
||||||
go install github.com/google/go-containerregistry/cmd/crane@31786c6cbb82d6ec4fb8eb79cd9387905130534e # v0.11.0
|
export MANIFEST=$(aws ecr batch-get-image --repository-name ${{ matrix.name }} --image-ids imageTag=${{needs.tag.outputs.build-tag}} --query 'images[].imageManifest' --output text)
|
||||||
go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
|
aws ecr put-image --repository-name ${{ matrix.name }} --image-tag latest --image-manifest "$MANIFEST"
|
||||||
|
|
||||||
- name: Configure ECR login
|
|
||||||
run: |
|
|
||||||
mkdir /github/home/.docker/
|
|
||||||
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
|
|
||||||
|
|
||||||
- name: Add latest tag to images
|
|
||||||
if: |
|
|
||||||
(github.ref_name == 'main' || github.ref_name == 'release') &&
|
|
||||||
github.event_name != 'workflow_dispatch'
|
|
||||||
run: |
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
|
|
||||||
- name: Cleanup ECR folder
|
|
||||||
run: rm -rf ~/.ecr
|
|
||||||
|
|
||||||
push-docker-hub:
|
push-docker-hub:
|
||||||
runs-on: [ self-hosted, dev, x64 ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
@@ -846,11 +776,114 @@ jobs:
|
|||||||
crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
||||||
|
|
||||||
- name: Cleanup ECR folder
|
calculate-deploy-targets:
|
||||||
run: rm -rf ~/.ecr
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
if: |
|
||||||
|
github.ref_name == 'release' &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
outputs:
|
||||||
|
matrix-include: ${{ steps.set-matrix.outputs.include }}
|
||||||
|
steps:
|
||||||
|
- id: set-matrix
|
||||||
|
run: |
|
||||||
|
if [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
||||||
|
PRODUCTION='{"env_name": "production", "proxy_job": "neon-proxy", "proxy_config": "production.proxy", "storage_broker_ns": "neon-storage-broker", "storage_broker_config": "production.neon-storage-broker", "kubeconfig_secret": "PRODUCTION_KUBECONFIG_DATA", "console_api_key_secret": "NEON_PRODUCTION_API_KEY"}'
|
||||||
|
echo "include=[$PRODUCTION]" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "GITHUB_REF_NAME (value '$GITHUB_REF_NAME') is not set to 'release'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
||||||
|
# We need both storage **and** compute images for deploy, because control plane picks the compute version based on the storage version.
|
||||||
|
# If it notices a fresh storage it may bump the compute version. And if compute image failed to build it may break things badly
|
||||||
|
needs: [ push-docker-hub, calculate-deploy-targets, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
github.ref_name == 'release' &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include: ${{fromJSON(needs.calculate-deploy-targets.outputs.matrix-include)}}
|
||||||
|
environment:
|
||||||
|
name: prod-old
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Redeploy
|
||||||
|
run: |
|
||||||
|
export DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
cd "$(pwd)/.github/ansible"
|
||||||
|
|
||||||
|
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
||||||
|
./get_binaries.sh
|
||||||
|
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
||||||
|
RELEASE=true ./get_binaries.sh
|
||||||
|
else
|
||||||
|
echo "GITHUB_REF_NAME (value '$GITHUB_REF_NAME') is not set to either 'main' or 'release'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
eval $(ssh-agent)
|
||||||
|
echo "${{ secrets.TELEPORT_SSH_KEY }}" | tr -d '\n'| base64 --decode >ssh-key
|
||||||
|
echo "${{ secrets.TELEPORT_SSH_CERT }}" | tr -d '\n'| base64 --decode >ssh-key-cert.pub
|
||||||
|
chmod 0600 ssh-key
|
||||||
|
ssh-add ssh-key
|
||||||
|
rm -f ssh-key ssh-key-cert.pub
|
||||||
|
ANSIBLE_CONFIG=./ansible.cfg ansible-galaxy collection install sivel.toiletwater
|
||||||
|
ANSIBLE_CONFIG=./ansible.cfg ansible-playbook deploy.yaml -i ${{ matrix.env_name }}.hosts.yaml -e CONSOLE_API_TOKEN=${{ secrets[matrix.console_api_key_secret] }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
||||||
|
rm -f neon_install.tar.gz .neon_current_version
|
||||||
|
|
||||||
|
deploy-new:
|
||||||
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
||||||
|
# We need both storage **and** compute images for deploy, because control plane picks the compute version based on the storage version.
|
||||||
|
# If it notices a fresh storage it may bump the compute version. And if compute image failed to build it may break things badly
|
||||||
|
needs: [ push-docker-hub, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
(github.ref_name == 'main') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target_region: [ eu-west-1, us-east-2 ]
|
||||||
|
environment:
|
||||||
|
name: dev-${{ matrix.target_region }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Redeploy
|
||||||
|
run: |
|
||||||
|
export DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
cd "$(pwd)/.github/ansible"
|
||||||
|
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
||||||
|
./get_binaries.sh
|
||||||
|
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
||||||
|
RELEASE=true ./get_binaries.sh
|
||||||
|
else
|
||||||
|
echo "GITHUB_REF_NAME (value '$GITHUB_REF_NAME') is not set to either 'main' or 'release'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ansible-galaxy collection install sivel.toiletwater
|
||||||
|
ansible-playbook deploy.yaml -i staging.${{ matrix.target_region }}.hosts.yaml -e @ssm_config -e CONSOLE_API_TOKEN=${{ secrets.NEON_STAGING_API_KEY }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
||||||
|
rm -f neon_install.tar.gz .neon_current_version
|
||||||
|
|
||||||
deploy-pr-test-new:
|
deploy-pr-test-new:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
||||||
# We need both storage **and** compute images for deploy, because control plane picks the compute version based on the storage version.
|
# We need both storage **and** compute images for deploy, because control plane picks the compute version based on the storage version.
|
||||||
# If it notices a fresh storage it may bump the compute version. And if compute image failed to build it may break things badly
|
# If it notices a fresh storage it may bump the compute version. And if compute image failed to build it may break things badly
|
||||||
@@ -882,40 +915,311 @@ jobs:
|
|||||||
ansible-playbook deploy.yaml -i staging.${{ matrix.target_region }}.hosts.yaml -e @ssm_config -e CONSOLE_API_TOKEN=${{ secrets.NEON_STAGING_API_KEY }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
ansible-playbook deploy.yaml -i staging.${{ matrix.target_region }}.hosts.yaml -e @ssm_config -e CONSOLE_API_TOKEN=${{ secrets.NEON_STAGING_API_KEY }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
||||||
rm -f neon_install.tar.gz .neon_current_version
|
rm -f neon_install.tar.gz .neon_current_version
|
||||||
|
|
||||||
- name: Cleanup ansible folder
|
deploy-prod-new:
|
||||||
run: rm -rf ~/.ansible
|
runs-on: prod
|
||||||
|
container: 093970136003.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
||||||
deploy:
|
# We need both storage **and** compute images for deploy, because control plane picks the compute version based on the storage version.
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
# If it notices a fresh storage it may bump the compute version. And if compute image failed to build it may break things badly
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
|
||||||
needs: [ push-docker-hub, tag, regress-tests ]
|
needs: [ push-docker-hub, tag, regress-tests ]
|
||||||
if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch'
|
if: |
|
||||||
|
(github.ref_name == 'release') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target_region: [ us-east-2, us-west-2, eu-central-1, ap-southeast-1 ]
|
||||||
|
environment:
|
||||||
|
name: prod-${{ matrix.target_region }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: false
|
submodules: true
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Trigger deploy workflow
|
- name: Redeploy
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
run: |
|
||||||
|
export DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
cd "$(pwd)/.github/ansible"
|
||||||
|
|
||||||
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
||||||
gh workflow run deploy-dev.yml --ref main -f branch=${{ github.sha }} -f dockerTag=${{needs.tag.outputs.build-tag}}
|
./get_binaries.sh
|
||||||
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
||||||
gh workflow run deploy-prod.yml --ref release -f branch=${{ github.sha }} -f dockerTag=${{needs.tag.outputs.build-tag}} -f disclamerAcknowledged=true
|
RELEASE=true ./get_binaries.sh
|
||||||
else
|
else
|
||||||
echo "GITHUB_REF_NAME (value '$GITHUB_REF_NAME') is not set to either 'main' or 'release'"
|
echo "GITHUB_REF_NAME (value '$GITHUB_REF_NAME') is not set to either 'main' or 'release'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
ansible-galaxy collection install sivel.toiletwater
|
||||||
|
ansible-playbook deploy.yaml -i prod.${{ matrix.target_region }}.hosts.yaml -e @ssm_config -e CONSOLE_API_TOKEN=${{ secrets.NEON_PRODUCTION_API_KEY }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
||||||
|
rm -f neon_install.tar.gz .neon_current_version
|
||||||
|
|
||||||
|
deploy-proxy:
|
||||||
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
||||||
|
# Compute image isn't strictly required for proxy deploy, but let's still wait for it to run all deploy jobs consistently.
|
||||||
|
needs: [ push-docker-hub, calculate-deploy-targets, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
github.ref_name == 'release' &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include: ${{fromJSON(needs.calculate-deploy-targets.outputs.matrix-include)}}
|
||||||
|
environment:
|
||||||
|
name: prod-old
|
||||||
|
env:
|
||||||
|
KUBECONFIG: .kubeconfig
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Add curl
|
||||||
|
run: apt update && apt install curl -y
|
||||||
|
|
||||||
|
- name: Store kubeconfig file
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets[matrix.kubeconfig_secret] }}" | base64 --decode > ${KUBECONFIG}
|
||||||
|
chmod 0600 ${KUBECONFIG}
|
||||||
|
|
||||||
|
- name: Setup helm v3
|
||||||
|
run: |
|
||||||
|
curl -s https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||||
|
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
||||||
|
|
||||||
|
- name: Re-deploy proxy
|
||||||
|
run: |
|
||||||
|
DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
helm upgrade ${{ matrix.proxy_job }} neondatabase/neon-proxy --namespace neon-proxy --install --atomic -f .github/helm-values/${{ matrix.proxy_config }}.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
||||||
|
helm upgrade ${{ matrix.proxy_job }}-scram neondatabase/neon-proxy --namespace neon-proxy --install --atomic -f .github/helm-values/${{ matrix.proxy_config }}-scram.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
||||||
|
|
||||||
|
deploy-storage-broker:
|
||||||
|
name: deploy storage broker on old staging and old prod
|
||||||
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
||||||
|
# Compute image isn't strictly required for proxy deploy, but let's still wait for it to run all deploy jobs consistently.
|
||||||
|
needs: [ push-docker-hub, calculate-deploy-targets, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
github.ref_name == 'release' &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include: ${{fromJSON(needs.calculate-deploy-targets.outputs.matrix-include)}}
|
||||||
|
environment:
|
||||||
|
name: prod-old
|
||||||
|
env:
|
||||||
|
KUBECONFIG: .kubeconfig
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Add curl
|
||||||
|
run: apt update && apt install curl -y
|
||||||
|
|
||||||
|
- name: Store kubeconfig file
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets[matrix.kubeconfig_secret] }}" | base64 --decode > ${KUBECONFIG}
|
||||||
|
chmod 0600 ${KUBECONFIG}
|
||||||
|
|
||||||
|
- name: Setup helm v3
|
||||||
|
run: |
|
||||||
|
curl -s https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||||
|
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
||||||
|
|
||||||
|
- name: Deploy storage-broker
|
||||||
|
run:
|
||||||
|
helm upgrade neon-storage-broker neondatabase/neon-storage-broker --namespace ${{ matrix.storage_broker_ns }} --create-namespace --install --atomic -f .github/helm-values/${{ matrix.storage_broker_config }}.yaml --set image.tag=${{ needs.tag.outputs.build-tag }} --set settings.sentryUrl=${{ secrets.SENTRY_URL_BROKER }} --wait --timeout 5m0s
|
||||||
|
|
||||||
|
deploy-proxy-new:
|
||||||
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
||||||
|
# Compute image isn't strictly required for proxy deploy, but let's still wait for it to run all deploy jobs consistently.
|
||||||
|
needs: [ push-docker-hub, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
(github.ref_name == 'main') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target_region: us-east-2
|
||||||
|
target_cluster: dev-us-east-2-beta
|
||||||
|
deploy_link_proxy: true
|
||||||
|
deploy_legacy_scram_proxy: true
|
||||||
|
- target_region: eu-west-1
|
||||||
|
target_cluster: dev-eu-west-1-zeta
|
||||||
|
deploy_link_proxy: false
|
||||||
|
deploy_legacy_scram_proxy: false
|
||||||
|
environment:
|
||||||
|
name: dev-${{ matrix.target_region }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure environment
|
||||||
|
run: |
|
||||||
|
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
||||||
|
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
||||||
|
|
||||||
|
- name: Re-deploy scram proxy
|
||||||
|
run: |
|
||||||
|
DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
helm upgrade neon-proxy-scram neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
||||||
|
|
||||||
|
- name: Re-deploy link proxy
|
||||||
|
if: matrix.deploy_link_proxy
|
||||||
|
run: |
|
||||||
|
DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
helm upgrade neon-proxy-link neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-link.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
||||||
|
|
||||||
|
- name: Re-deploy legacy scram proxy
|
||||||
|
if: matrix.deploy_legacy_scram_proxy
|
||||||
|
run: |
|
||||||
|
DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
helm upgrade neon-proxy-scram-legacy neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram-legacy.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
||||||
|
|
||||||
|
deploy-storage-broker-dev-new:
|
||||||
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
|
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
||||||
|
# Compute image isn't strictly required for proxy deploy, but let's still wait for it to run all deploy jobs consistently.
|
||||||
|
needs: [ push-docker-hub, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
(github.ref_name == 'main') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target_region: us-east-2
|
||||||
|
target_cluster: dev-us-east-2-beta
|
||||||
|
- target_region: eu-west-1
|
||||||
|
target_cluster: dev-eu-west-1-zeta
|
||||||
|
environment:
|
||||||
|
name: dev-${{ matrix.target_region }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure environment
|
||||||
|
run: |
|
||||||
|
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
||||||
|
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
||||||
|
|
||||||
|
- name: Deploy storage-broker
|
||||||
|
run:
|
||||||
|
helm upgrade neon-storage-broker-lb neondatabase/neon-storage-broker --namespace neon-storage-broker-lb --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-storage-broker.yaml --set image.tag=${{ needs.tag.outputs.build-tag }} --set settings.sentryUrl=${{ secrets.SENTRY_URL_BROKER }} --wait --timeout 5m0s
|
||||||
|
|
||||||
|
deploy-proxy-prod-new:
|
||||||
|
runs-on: prod
|
||||||
|
container: 093970136003.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
||||||
|
# Compute image isn't strictly required for proxy deploy, but let's still wait for it to run all deploy jobs consistently.
|
||||||
|
needs: [ push-docker-hub, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
(github.ref_name == 'release') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target_region: us-east-2
|
||||||
|
target_cluster: prod-us-east-2-delta
|
||||||
|
- target_region: us-west-2
|
||||||
|
target_cluster: prod-us-west-2-eta
|
||||||
|
- target_region: eu-central-1
|
||||||
|
target_cluster: prod-eu-central-1-gamma
|
||||||
|
- target_region: ap-southeast-1
|
||||||
|
target_cluster: prod-ap-southeast-1-epsilon
|
||||||
|
environment:
|
||||||
|
name: prod-${{ matrix.target_region }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure environment
|
||||||
|
run: |
|
||||||
|
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
||||||
|
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
||||||
|
|
||||||
|
- name: Re-deploy proxy
|
||||||
|
run: |
|
||||||
|
DOCKER_TAG=${{needs.tag.outputs.build-tag}}
|
||||||
|
helm upgrade neon-proxy-scram neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
||||||
|
|
||||||
|
deploy-storage-broker-prod-new:
|
||||||
|
runs-on: prod
|
||||||
|
container: 093970136003.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
||||||
|
# Compute image isn't strictly required for proxy deploy, but let's still wait for it to run all deploy jobs consistently.
|
||||||
|
needs: [ push-docker-hub, tag, regress-tests ]
|
||||||
|
if: |
|
||||||
|
(github.ref_name == 'release') &&
|
||||||
|
github.event_name != 'workflow_dispatch'
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target_region: us-east-2
|
||||||
|
target_cluster: prod-us-east-2-delta
|
||||||
|
- target_region: us-west-2
|
||||||
|
target_cluster: prod-us-west-2-eta
|
||||||
|
- target_region: eu-central-1
|
||||||
|
target_cluster: prod-eu-central-1-gamma
|
||||||
|
- target_region: ap-southeast-1
|
||||||
|
target_cluster: prod-ap-southeast-1-epsilon
|
||||||
|
environment:
|
||||||
|
name: prod-${{ matrix.target_region }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure environment
|
||||||
|
run: |
|
||||||
|
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
||||||
|
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
||||||
|
|
||||||
|
- name: Deploy storage-broker
|
||||||
|
run:
|
||||||
|
helm upgrade neon-storage-broker-lb neondatabase/neon-storage-broker --namespace neon-storage-broker-lb --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-storage-broker.yaml --set image.tag=${{ needs.tag.outputs.build-tag }} --set settings.sentryUrl=${{ secrets.SENTRY_URL_BROKER }} --wait --timeout 5m0s
|
||||||
|
|
||||||
promote-compatibility-data:
|
promote-compatibility-data:
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, dev, x64 ]
|
||||||
container:
|
container:
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
||||||
options: --init
|
options: --init
|
||||||
needs: [ push-docker-hub, tag, regress-tests ]
|
needs: [ deploy, deploy-proxy ]
|
||||||
if: github.ref_name == 'release' && github.event_name != 'workflow_dispatch'
|
if: github.ref_name == 'release' && github.event_name != 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- name: Promote compatibility snapshot for the release
|
- name: Promote compatibility snapshot for the release
|
||||||
|
|||||||
179
.github/workflows/deploy-dev.yml
vendored
179
.github/workflows/deploy-dev.yml
vendored
@@ -1,179 +0,0 @@
|
|||||||
name: Neon Deploy dev
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
dockerTag:
|
|
||||||
description: 'Docker tag to deploy'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
branch:
|
|
||||||
description: 'Branch or commit used for deploy scripts and configs'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: 'main'
|
|
||||||
deployStorage:
|
|
||||||
description: 'Deploy storage'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
deployProxy:
|
|
||||||
description: 'Deploy proxy'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
deployStorageBroker:
|
|
||||||
description: 'Deploy storage-broker'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-dev
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-storage-new:
|
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
|
||||||
container:
|
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
|
||||||
options: --user root --privileged
|
|
||||||
if: inputs.deployStorage
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
target_region: [ eu-west-1, us-east-2 ]
|
|
||||||
environment:
|
|
||||||
name: dev-${{ matrix.target_region }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Redeploy
|
|
||||||
run: |
|
|
||||||
export DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
cd "$(pwd)/.github/ansible"
|
|
||||||
|
|
||||||
./get_binaries.sh
|
|
||||||
|
|
||||||
ansible-galaxy collection install sivel.toiletwater
|
|
||||||
ansible-playbook -v deploy.yaml -i staging.${{ matrix.target_region }}.hosts.yaml -e @ssm_config -e CONSOLE_API_TOKEN=${{ secrets.NEON_STAGING_API_KEY }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
|
||||||
rm -f neon_install.tar.gz .neon_current_version
|
|
||||||
|
|
||||||
- name: Cleanup ansible folder
|
|
||||||
run: rm -rf ~/.ansible
|
|
||||||
|
|
||||||
deploy-proxy-new:
|
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
|
||||||
if: inputs.deployProxy
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target_region: us-east-2
|
|
||||||
target_cluster: dev-us-east-2-beta
|
|
||||||
deploy_link_proxy: true
|
|
||||||
deploy_legacy_scram_proxy: true
|
|
||||||
- target_region: eu-west-1
|
|
||||||
target_cluster: dev-eu-west-1-zeta
|
|
||||||
deploy_link_proxy: false
|
|
||||||
deploy_legacy_scram_proxy: false
|
|
||||||
environment:
|
|
||||||
name: dev-${{ matrix.target_region }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1-node16
|
|
||||||
with:
|
|
||||||
role-to-assume: arn:aws:iam::369495373322:role/github-runner
|
|
||||||
aws-region: eu-central-1
|
|
||||||
role-skip-session-tagging: true
|
|
||||||
role-duration-seconds: 1800
|
|
||||||
|
|
||||||
- name: Configure environment
|
|
||||||
run: |
|
|
||||||
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
|
||||||
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
|
||||||
|
|
||||||
- name: Re-deploy scram proxy
|
|
||||||
run: |
|
|
||||||
DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
helm upgrade neon-proxy-scram neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
|
||||||
|
|
||||||
- name: Re-deploy link proxy
|
|
||||||
if: matrix.deploy_link_proxy
|
|
||||||
run: |
|
|
||||||
DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
helm upgrade neon-proxy-link neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-link.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
|
||||||
|
|
||||||
- name: Re-deploy legacy scram proxy
|
|
||||||
if: matrix.deploy_legacy_scram_proxy
|
|
||||||
run: |
|
|
||||||
DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
helm upgrade neon-proxy-scram-legacy neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram-legacy.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
|
||||||
|
|
||||||
- name: Cleanup helm folder
|
|
||||||
run: rm -rf ~/.cache
|
|
||||||
|
|
||||||
deploy-storage-broker-new:
|
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
|
||||||
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:pinned
|
|
||||||
if: inputs.deployStorageBroker
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target_region: us-east-2
|
|
||||||
target_cluster: dev-us-east-2-beta
|
|
||||||
- target_region: eu-west-1
|
|
||||||
target_cluster: dev-eu-west-1-zeta
|
|
||||||
environment:
|
|
||||||
name: dev-${{ matrix.target_region }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1-node16
|
|
||||||
with:
|
|
||||||
role-to-assume: arn:aws:iam::369495373322:role/github-runner
|
|
||||||
aws-region: eu-central-1
|
|
||||||
role-skip-session-tagging: true
|
|
||||||
role-duration-seconds: 1800
|
|
||||||
|
|
||||||
- name: Configure environment
|
|
||||||
run: |
|
|
||||||
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
|
||||||
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
|
||||||
|
|
||||||
- name: Deploy storage-broker
|
|
||||||
run:
|
|
||||||
helm upgrade neon-storage-broker-lb neondatabase/neon-storage-broker --namespace neon-storage-broker-lb --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-storage-broker.yaml --set image.tag=${{ inputs.dockerTag }} --set settings.sentryUrl=${{ secrets.SENTRY_URL_BROKER }} --wait --timeout 5m0s
|
|
||||||
|
|
||||||
- name: Cleanup helm folder
|
|
||||||
run: rm -rf ~/.cache
|
|
||||||
167
.github/workflows/deploy-prod.yml
vendored
167
.github/workflows/deploy-prod.yml
vendored
@@ -1,167 +0,0 @@
|
|||||||
name: Neon Deploy prod
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
dockerTag:
|
|
||||||
description: 'Docker tag to deploy'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
branch:
|
|
||||||
description: 'Branch or commit used for deploy scripts and configs'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: 'release'
|
|
||||||
deployStorage:
|
|
||||||
description: 'Deploy storage'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
deployProxy:
|
|
||||||
description: 'Deploy proxy'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
deployStorageBroker:
|
|
||||||
description: 'Deploy storage-broker'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
disclamerAcknowledged:
|
|
||||||
description: 'I confirm that there is an emergency and I can not use regular release workflow'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-prod
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-prod-new:
|
|
||||||
runs-on: prod
|
|
||||||
container:
|
|
||||||
image: 093970136003.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
|
||||||
options: --user root --privileged
|
|
||||||
if: inputs.deployStorage && inputs.disclamerAcknowledged
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
target_region: [ us-east-2, us-west-2, eu-central-1, ap-southeast-1 ]
|
|
||||||
environment:
|
|
||||||
name: prod-${{ matrix.target_region }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Redeploy
|
|
||||||
run: |
|
|
||||||
export DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
cd "$(pwd)/.github/ansible"
|
|
||||||
|
|
||||||
./get_binaries.sh
|
|
||||||
|
|
||||||
ansible-galaxy collection install sivel.toiletwater
|
|
||||||
ansible-playbook -v deploy.yaml -i prod.${{ matrix.target_region }}.hosts.yaml -e @ssm_config -e CONSOLE_API_TOKEN=${{ secrets.NEON_PRODUCTION_API_KEY }} -e SENTRY_URL_PAGESERVER=${{ secrets.SENTRY_URL_PAGESERVER }} -e SENTRY_URL_SAFEKEEPER=${{ secrets.SENTRY_URL_SAFEKEEPER }}
|
|
||||||
rm -f neon_install.tar.gz .neon_current_version
|
|
||||||
|
|
||||||
deploy-proxy-prod-new:
|
|
||||||
runs-on: prod
|
|
||||||
container: 093970136003.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
|
||||||
if: inputs.deployProxy && inputs.disclamerAcknowledged
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target_region: us-east-2
|
|
||||||
target_cluster: prod-us-east-2-delta
|
|
||||||
deploy_link_proxy: true
|
|
||||||
deploy_legacy_scram_proxy: false
|
|
||||||
- target_region: us-west-2
|
|
||||||
target_cluster: prod-us-west-2-eta
|
|
||||||
deploy_link_proxy: false
|
|
||||||
deploy_legacy_scram_proxy: true
|
|
||||||
- target_region: eu-central-1
|
|
||||||
target_cluster: prod-eu-central-1-gamma
|
|
||||||
deploy_link_proxy: false
|
|
||||||
deploy_legacy_scram_proxy: false
|
|
||||||
- target_region: ap-southeast-1
|
|
||||||
target_cluster: prod-ap-southeast-1-epsilon
|
|
||||||
deploy_link_proxy: false
|
|
||||||
deploy_legacy_scram_proxy: false
|
|
||||||
environment:
|
|
||||||
name: prod-${{ matrix.target_region }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Configure environment
|
|
||||||
run: |
|
|
||||||
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
|
||||||
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
|
||||||
|
|
||||||
- name: Re-deploy scram proxy
|
|
||||||
run: |
|
|
||||||
DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
helm upgrade neon-proxy-scram neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
|
||||||
|
|
||||||
- name: Re-deploy link proxy
|
|
||||||
if: matrix.deploy_link_proxy
|
|
||||||
run: |
|
|
||||||
DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
helm upgrade neon-proxy-link neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-link.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
|
||||||
|
|
||||||
- name: Re-deploy legacy scram proxy
|
|
||||||
if: matrix.deploy_legacy_scram_proxy
|
|
||||||
run: |
|
|
||||||
DOCKER_TAG=${{ inputs.dockerTag }}
|
|
||||||
helm upgrade neon-proxy-scram-legacy neondatabase/neon-proxy --namespace neon-proxy --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-proxy-scram-legacy.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
|
|
||||||
|
|
||||||
deploy-storage-broker-prod-new:
|
|
||||||
runs-on: prod
|
|
||||||
container: 093970136003.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
|
|
||||||
if: inputs.deployStorageBroker && inputs.disclamerAcknowledged
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target_region: us-east-2
|
|
||||||
target_cluster: prod-us-east-2-delta
|
|
||||||
- target_region: us-west-2
|
|
||||||
target_cluster: prod-us-west-2-eta
|
|
||||||
- target_region: eu-central-1
|
|
||||||
target_cluster: prod-eu-central-1-gamma
|
|
||||||
- target_region: ap-southeast-1
|
|
||||||
target_cluster: prod-ap-southeast-1-epsilon
|
|
||||||
environment:
|
|
||||||
name: prod-${{ matrix.target_region }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.branch }}
|
|
||||||
|
|
||||||
- name: Configure environment
|
|
||||||
run: |
|
|
||||||
helm repo add neondatabase https://neondatabase.github.io/helm-charts
|
|
||||||
aws --region ${{ matrix.target_region }} eks update-kubeconfig --name ${{ matrix.target_cluster }}
|
|
||||||
|
|
||||||
- name: Deploy storage-broker
|
|
||||||
run:
|
|
||||||
helm upgrade neon-storage-broker-lb neondatabase/neon-storage-broker --namespace neon-storage-broker-lb --create-namespace --install --atomic -f .github/helm-values/${{ matrix.target_cluster }}.neon-storage-broker.yaml --set image.tag=${{ inputs.dockerTag }} --set settings.sentryUrl=${{ secrets.SENTRY_URL_BROKER }} --wait --timeout 5m0s
|
|
||||||
46
.github/workflows/neon_extra_builds.yml
vendored
46
.github/workflows/neon_extra_builds.yml
vendored
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -21,7 +20,6 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-macos-build:
|
check-macos-build:
|
||||||
if: github.ref_name == 'main' || contains(github.event.pull_request.labels.*.name, 'run-extra-build-macos')
|
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
||||||
@@ -95,16 +93,11 @@ jobs:
|
|||||||
run: ./run_clippy.sh
|
run: ./run_clippy.sh
|
||||||
|
|
||||||
gather-rust-build-stats:
|
gather-rust-build-stats:
|
||||||
if: github.ref_name == 'main' || contains(github.event.pull_request.labels.*.name, 'run-extra-build-stats')
|
timeout-minutes: 90
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: ubuntu-latest
|
||||||
container:
|
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned
|
|
||||||
options: --init
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TYPE: release
|
BUILD_TYPE: release
|
||||||
# remove the cachepot wrapper and build without crate caches
|
|
||||||
RUSTC_WRAPPER: ""
|
|
||||||
# build with incremental compilation produce partial results
|
# build with incremental compilation produce partial results
|
||||||
# so do not attempt to cache this build, also disable the incremental compilation
|
# so do not attempt to cache this build, also disable the incremental compilation
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
@@ -116,6 +109,11 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install Ubuntu postgres dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install build-essential libreadline-dev zlib1g-dev flex bison libseccomp-dev libssl-dev protobuf-compiler
|
||||||
|
|
||||||
# Some of our rust modules use FFI and need those to be checked
|
# Some of our rust modules use FFI and need those to be checked
|
||||||
- name: Get postgres headers
|
- name: Get postgres headers
|
||||||
run: make postgres-headers -j$(nproc)
|
run: make postgres-headers -j$(nproc)
|
||||||
@@ -124,31 +122,7 @@ jobs:
|
|||||||
run: cargo build --all --release --timings
|
run: cargo build --all --release --timings
|
||||||
|
|
||||||
- name: Upload the build stats
|
- name: Upload the build stats
|
||||||
id: upload-stats
|
uses: actions/upload-artifact@v3
|
||||||
env:
|
|
||||||
BUCKET: neon-github-public-dev
|
|
||||||
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
run: |
|
|
||||||
REPORT_URL=https://${BUCKET}.s3.amazonaws.com/build-stats/${SHA}/${GITHUB_RUN_ID}/cargo-timing.html
|
|
||||||
aws s3 cp --only-show-errors ./target/cargo-timings/cargo-timing.html "s3://${BUCKET}/build-stats/${SHA}/${GITHUB_RUN_ID}/"
|
|
||||||
echo "report-url=${REPORT_URL}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Publish build stats report
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
env:
|
|
||||||
REPORT_URL: ${{ steps.upload-stats.outputs.report-url }}
|
|
||||||
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
with:
|
with:
|
||||||
script: |
|
name: neon-${{ runner.os }}-release-build-stats
|
||||||
const { REPORT_URL, SHA } = process.env
|
path: ./target/cargo-timings/
|
||||||
|
|
||||||
await github.rest.repos.createCommitStatus({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
sha: `${SHA}`,
|
|
||||||
state: 'success',
|
|
||||||
target_url: `${REPORT_URL}`,
|
|
||||||
context: `Build stats (release)`,
|
|
||||||
})
|
|
||||||
|
|||||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Create Release Branch
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 10 * * 2'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create_release_branch:
|
|
||||||
runs-on: [ubuntu-latest]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Get current date
|
|
||||||
id: date
|
|
||||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create release branch
|
|
||||||
run: git checkout -b releases/${{ steps.date.outputs.date }}
|
|
||||||
|
|
||||||
- name: Push new branch
|
|
||||||
run: git push origin releases/${{ steps.date.outputs.date }}
|
|
||||||
|
|
||||||
- name: Create pull request into release
|
|
||||||
uses: thomaseizinger/create-pull-request@e3972219c86a56550fb70708d96800d8e24ba862 # 1.3.0
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
head: releases/${{ steps.date.outputs.date }}
|
|
||||||
base: release
|
|
||||||
title: Release ${{ steps.date.outputs.date }}
|
|
||||||
team_reviewers: release
|
|
||||||
764
Cargo.lock
generated
764
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -7,7 +7,6 @@ members = [
|
|||||||
"safekeeper",
|
"safekeeper",
|
||||||
"storage_broker",
|
"storage_broker",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
"trace",
|
|
||||||
"libs/*",
|
"libs/*",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -32,15 +31,12 @@ bstr = "1.0"
|
|||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
clap = { version = "4.0", features = ["derive"] }
|
clap = "4.0"
|
||||||
close_fds = "0.3.2"
|
close_fds = "0.3.2"
|
||||||
comfy-table = "6.1"
|
comfy-table = "6.1"
|
||||||
const_format = "0.2"
|
const_format = "0.2"
|
||||||
crc32c = "0.6"
|
crc32c = "0.6"
|
||||||
crossbeam-utils = "0.8.5"
|
crossbeam-utils = "0.8.5"
|
||||||
either = "1.8"
|
|
||||||
enum-map = "2.4.2"
|
|
||||||
enumset = "1.0.12"
|
|
||||||
fail = "0.5.0"
|
fail = "0.5.0"
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
@@ -48,7 +44,6 @@ futures-core = "0.3"
|
|||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
git-version = "0.3"
|
git-version = "0.3"
|
||||||
hashbrown = "0.13"
|
hashbrown = "0.13"
|
||||||
hashlink = "0.8.1"
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hex-literal = "0.3"
|
hex-literal = "0.3"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
@@ -64,12 +59,8 @@ md5 = "0.7.0"
|
|||||||
memoffset = "0.8"
|
memoffset = "0.8"
|
||||||
nix = "0.26"
|
nix = "0.26"
|
||||||
notify = "5.0.0"
|
notify = "5.0.0"
|
||||||
num_cpus = "1.15"
|
|
||||||
num-traits = "0.2.15"
|
num-traits = "0.2.15"
|
||||||
once_cell = "1.13"
|
once_cell = "1.13"
|
||||||
opentelemetry = "0.18.0"
|
|
||||||
opentelemetry-otlp = { version = "0.11.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
|
||||||
opentelemetry-semantic-conventions = "0.10.0"
|
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
prometheus = {version = "0.13", default_features=false, features = ["process"]} # removes protobuf dependency
|
prometheus = {version = "0.13", default_features=false, features = ["process"]} # removes protobuf dependency
|
||||||
@@ -77,8 +68,6 @@ prost = "0.11"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.4"
|
regex = "1.4"
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
|
||||||
reqwest-tracing = { version = "0.4.0", features = ["opentelemetry_0_18"] }
|
|
||||||
reqwest-middleware = "0.2.0"
|
|
||||||
routerify = "3"
|
routerify = "3"
|
||||||
rpds = "0.12.0"
|
rpds = "0.12.0"
|
||||||
rustls = "0.20"
|
rustls = "0.20"
|
||||||
@@ -95,7 +84,6 @@ socket2 = "0.4.4"
|
|||||||
strum = "0.24"
|
strum = "0.24"
|
||||||
strum_macros = "0.24"
|
strum_macros = "0.24"
|
||||||
svg_fmt = "0.4.1"
|
svg_fmt = "0.4.1"
|
||||||
sync_wrapper = "0.1.2"
|
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tls-listener = { version = "0.6", features = ["rustls", "hyper-h1"] }
|
tls-listener = { version = "0.6", features = ["rustls", "hyper-h1"] }
|
||||||
@@ -108,7 +96,6 @@ toml = "0.5"
|
|||||||
toml_edit = { version = "0.17", features = ["easy"] }
|
toml_edit = { version = "0.17", features = ["easy"] }
|
||||||
tonic = {version = "0.8", features = ["tls", "tls-roots"]}
|
tonic = {version = "0.8", features = ["tls", "tls-roots"]}
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-opentelemetry = "0.18.0"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
uuid = { version = "1.2", features = ["v4", "serde"] }
|
uuid = { version = "1.2", features = ["v4", "serde"] }
|
||||||
@@ -127,14 +114,10 @@ postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", re
|
|||||||
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" }
|
||||||
tokio-tar = { git = "https://github.com/neondatabase/tokio-tar.git", rev="404df61437de0feef49ba2ccdbdd94eb8ad6e142" }
|
tokio-tar = { git = "https://github.com/neondatabase/tokio-tar.git", rev="404df61437de0feef49ba2ccdbdd94eb8ad6e142" }
|
||||||
|
|
||||||
## Other git libraries
|
|
||||||
heapless = { default-features=false, features=[], git = "https://github.com/japaric/heapless.git", rev = "644653bf3b831c6bb4963be2de24804acf5e5001" } # upstream release pending
|
|
||||||
|
|
||||||
## Local libraries
|
## Local libraries
|
||||||
consumption_metrics = { version = "0.1", path = "./libs/consumption_metrics/" }
|
consumption_metrics = { version = "0.1", path = "./libs/consumption_metrics/" }
|
||||||
metrics = { version = "0.1", path = "./libs/metrics/" }
|
metrics = { version = "0.1", path = "./libs/metrics/" }
|
||||||
pageserver_api = { version = "0.1", path = "./libs/pageserver_api/" }
|
pageserver_api = { version = "0.1", path = "./libs/pageserver_api/" }
|
||||||
postgres_backend = { version = "0.1", path = "./libs/postgres_backend/" }
|
|
||||||
postgres_connection = { version = "0.1", path = "./libs/postgres_connection/" }
|
postgres_connection = { version = "0.1", path = "./libs/postgres_connection/" }
|
||||||
postgres_ffi = { version = "0.1", path = "./libs/postgres_ffi/" }
|
postgres_ffi = { version = "0.1", path = "./libs/postgres_ffi/" }
|
||||||
pq_proto = { version = "0.1", path = "./libs/pq_proto/" }
|
pq_proto = { version = "0.1", path = "./libs/pq_proto/" }
|
||||||
@@ -142,7 +125,6 @@ remote_storage = { version = "0.1", path = "./libs/remote_storage/" }
|
|||||||
safekeeper_api = { version = "0.1", path = "./libs/safekeeper_api" }
|
safekeeper_api = { version = "0.1", path = "./libs/safekeeper_api" }
|
||||||
storage_broker = { version = "0.1", path = "./storage_broker/" } # Note: main broker code is inside the binary crate, so linking with the library shouldn't be heavy.
|
storage_broker = { version = "0.1", path = "./storage_broker/" } # Note: main broker code is inside the binary crate, so linking with the library shouldn't be heavy.
|
||||||
tenant_size_model = { version = "0.1", path = "./libs/tenant_size_model/" }
|
tenant_size_model = { version = "0.1", path = "./libs/tenant_size_model/" }
|
||||||
tracing-utils = { version = "0.1", path = "./libs/tracing-utils/" }
|
|
||||||
utils = { version = "0.1", path = "./libs/utils/" }
|
utils = { version = "0.1", path = "./libs/utils/" }
|
||||||
|
|
||||||
## Common library dependency
|
## Common library dependency
|
||||||
@@ -152,7 +134,7 @@ workspace_hack = { version = "0.1", path = "./workspace_hack/" }
|
|||||||
criterion = "0.4"
|
criterion = "0.4"
|
||||||
rcgen = "0.10"
|
rcgen = "0.10"
|
||||||
rstest = "0.16"
|
rstest = "0.16"
|
||||||
tempfile = "3.4"
|
tempfile = "3.2"
|
||||||
tonic-build = "0.8"
|
tonic-build = "0.8"
|
||||||
|
|
||||||
# This is only needed for proxy's tests.
|
# This is only needed for proxy's tests.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ ARG CACHEPOT_BUCKET=neon-github-dev
|
|||||||
|
|
||||||
COPY --from=pg-build /home/nonroot/pg_install/v14/include/postgresql/server pg_install/v14/include/postgresql/server
|
COPY --from=pg-build /home/nonroot/pg_install/v14/include/postgresql/server pg_install/v14/include/postgresql/server
|
||||||
COPY --from=pg-build /home/nonroot/pg_install/v15/include/postgresql/server pg_install/v15/include/postgresql/server
|
COPY --from=pg-build /home/nonroot/pg_install/v15/include/postgresql/server pg_install/v15/include/postgresql/server
|
||||||
COPY --chown=nonroot . .
|
COPY . .
|
||||||
|
|
||||||
# Show build caching stats to check if it was used in the end.
|
# Show build caching stats to check if it was used in the end.
|
||||||
# Has to be the part of the same RUN since cachepot daemon is killed in the end of this RUN, losing the compilation stats.
|
# Has to be the part of the same RUN since cachepot daemon is killed in the end of this RUN, losing the compilation stats.
|
||||||
|
|||||||
@@ -1,493 +0,0 @@
|
|||||||
ARG PG_VERSION
|
|
||||||
ARG REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
|
||||||
ARG IMAGE=rust
|
|
||||||
ARG TAG=pinned
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "build-deps"
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM debian:bullseye-slim AS build-deps
|
|
||||||
RUN apt update && \
|
|
||||||
apt install -y git autoconf automake libtool build-essential bison flex libreadline-dev \
|
|
||||||
zlib1g-dev libxml2-dev libcurl4-openssl-dev libossp-uuid-dev wget pkg-config libssl-dev \
|
|
||||||
libicu-dev libxslt1-dev
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pg-build"
|
|
||||||
# Build Postgres from the neon postgres repository.
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS pg-build
|
|
||||||
ARG PG_VERSION
|
|
||||||
COPY vendor/postgres-${PG_VERSION} postgres
|
|
||||||
RUN cd postgres && \
|
|
||||||
./configure CFLAGS='-O2 -g3' --enable-debug --with-openssl --with-uuid=ossp --with-icu \
|
|
||||||
--with-libxml --with-libxslt && \
|
|
||||||
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s install && \
|
|
||||||
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C contrib/ install && \
|
|
||||||
# Install headers
|
|
||||||
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C src/include install && \
|
|
||||||
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C src/interfaces/libpq install && \
|
|
||||||
# Enable some of contrib extensions
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/autoinc.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/bloom.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/earthdistance.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/insert_username.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/intagg.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/moddatetime.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgrowlocks.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgstattuple.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/refint.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/xml2.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "postgis-build"
|
|
||||||
# Build PostGIS from the upstream PostGIS mirror.
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS postgis-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
RUN apt update && \
|
|
||||||
apt install -y cmake gdal-bin libboost-dev libboost-thread-dev libboost-filesystem-dev \
|
|
||||||
libboost-system-dev libboost-iostreams-dev libboost-program-options-dev libboost-timer-dev \
|
|
||||||
libcgal-dev libgdal-dev libgmp-dev libmpfr-dev libopenscenegraph-dev libprotobuf-c-dev \
|
|
||||||
protobuf-c-compiler xsltproc
|
|
||||||
|
|
||||||
# SFCGAL > 1.3 requires CGAL > 5.2, Bullseye's libcgal-dev is 5.2
|
|
||||||
RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \
|
|
||||||
mkdir sfcgal-src && cd sfcgal-src && tar xvzf ../SFCGAL.tar.gz --strip-components=1 -C . && \
|
|
||||||
cmake . && make -j $(getconf _NPROCESSORS_ONLN) && \
|
|
||||||
DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \
|
|
||||||
make clean && cp -R /sfcgal/* /
|
|
||||||
|
|
||||||
ENV PATH "/usr/local/pgsql/bin:$PATH"
|
|
||||||
|
|
||||||
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.2.tar.gz -O postgis.tar.gz && \
|
|
||||||
mkdir postgis-src && cd postgis-src && tar xvzf ../postgis.tar.gz --strip-components=1 -C . && \
|
|
||||||
./autogen.sh && \
|
|
||||||
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
|
||||||
cd extensions/postgis && \
|
|
||||||
make clean && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_raster.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_sfcgal.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_tiger_geocoder.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_topology.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer_data_us.control
|
|
||||||
|
|
||||||
RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \
|
|
||||||
mkdir pgrouting-src && cd pgrouting-src && tar xvzf ../pgrouting.tar.gz --strip-components=1 -C . && \
|
|
||||||
mkdir build && \
|
|
||||||
cd build && \
|
|
||||||
cmake .. && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgrouting.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "plv8-build"
|
|
||||||
# Build plv8
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS plv8-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
RUN apt update && \
|
|
||||||
apt install -y ninja-build python3-dev libncurses5 binutils clang
|
|
||||||
|
|
||||||
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.5.tar.gz -O plv8.tar.gz && \
|
|
||||||
mkdir plv8-src && cd plv8-src && tar xvzf ../plv8.tar.gz --strip-components=1 -C . && \
|
|
||||||
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
|
||||||
make DOCKER=1 -j $(getconf _NPROCESSORS_ONLN) install && \
|
|
||||||
rm -rf /plv8-* && \
|
|
||||||
find /usr/local/pgsql/ -name "plv8-*.so" | xargs strip && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plv8.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plcoffee.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plls.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "h3-pg-build"
|
|
||||||
# Build h3_pg
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS h3-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
# packaged cmake is too old
|
|
||||||
RUN wget https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh \
|
|
||||||
-q -O /tmp/cmake-install.sh \
|
|
||||||
&& chmod u+x /tmp/cmake-install.sh \
|
|
||||||
&& /tmp/cmake-install.sh --skip-license --prefix=/usr/local/ \
|
|
||||||
&& rm /tmp/cmake-install.sh
|
|
||||||
|
|
||||||
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz && \
|
|
||||||
mkdir h3-src && cd h3-src && tar xvzf ../h3.tar.gz --strip-components=1 -C . && \
|
|
||||||
mkdir build && cd build && \
|
|
||||||
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
|
||||||
DESTDIR=/h3 make install && \
|
|
||||||
cp -R /h3/usr / && \
|
|
||||||
rm -rf build
|
|
||||||
|
|
||||||
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.1.2.tar.gz -O h3-pg.tar.gz && \
|
|
||||||
mkdir h3-pg-src && cd h3-pg-src && tar xvzf ../h3-pg.tar.gz --strip-components=1 -C . && \
|
|
||||||
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/h3.control && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/h3_postgis.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "unit-pg-build"
|
|
||||||
# compile unit extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS unit-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -O postgresql-unit.tar.gz && \
|
|
||||||
mkdir postgresql-unit-src && cd postgresql-unit-src && tar xvzf ../postgresql-unit.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
# unit extension's "create extension" script relies on absolute install path to fill some reference tables.
|
|
||||||
# We move the extension from '/usr/local/pgsql/' to '/usr/local/' after it is build. So we need to adjust the path.
|
|
||||||
# This one-liner removes pgsql/ part of the path.
|
|
||||||
# NOTE: Other extensions that rely on MODULEDIR variable after building phase will need the same fix.
|
|
||||||
find /usr/local/pgsql/share/extension/ -name "unit*.sql" -print0 | xargs -0 sed -i "s|pgsql/||g" && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/unit.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "vector-pg-build"
|
|
||||||
# compile pgvector extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS vector-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.4.0.tar.gz -O pgvector.tar.gz && \
|
|
||||||
mkdir pgvector-src && cd pgvector-src && tar xvzf ../pgvector.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/vector.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pgjwt-pg-build"
|
|
||||||
# compile pgjwt extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS pgjwt-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
# 9742dab1b2f297ad3811120db7b21451bca2d3c9 made on 13/11/2021
|
|
||||||
RUN wget https://github.com/michelp/pgjwt/archive/9742dab1b2f297ad3811120db7b21451bca2d3c9.tar.gz -O pgjwt.tar.gz && \
|
|
||||||
mkdir pgjwt-src && cd pgjwt-src && tar xvzf ../pgjwt.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgjwt.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "hypopg-pg-build"
|
|
||||||
# compile hypopg extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS hypopg-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/HypoPG/hypopg/archive/refs/tags/1.3.1.tar.gz -O hypopg.tar.gz && \
|
|
||||||
mkdir hypopg-src && cd hypopg-src && tar xvzf ../hypopg.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hypopg.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pg-hashids-pg-build"
|
|
||||||
# compile pg_hashids extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS pg-hashids-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/iCyberon/pg_hashids/archive/refs/tags/v1.2.1.tar.gz -O pg_hashids.tar.gz && \
|
|
||||||
mkdir pg_hashids-src && cd pg_hashids-src && tar xvzf ../pg_hashids.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_hashids.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "rum-pg-build"
|
|
||||||
# compile rum extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS rum-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/postgrespro/rum/archive/refs/tags/1.3.13.tar.gz -O rum.tar.gz && \
|
|
||||||
mkdir rum-src && cd rum-src && tar xvzf ../rum.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rum.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pgtap-pg-build"
|
|
||||||
# compile pgTAP extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS pgtap-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/theory/pgtap/archive/refs/tags/v1.2.0.tar.gz -O pgtap.tar.gz && \
|
|
||||||
mkdir pgtap-src && cd pgtap-src && tar xvzf ../pgtap.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgtap.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "prefix-pg-build"
|
|
||||||
# compile Prefix extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS prefix-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/dimitri/prefix/archive/refs/tags/v1.2.9.tar.gz -O prefix.tar.gz && \
|
|
||||||
mkdir prefix-src && cd prefix-src && tar xvzf ../prefix.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/prefix.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "hll-pg-build"
|
|
||||||
# compile hll extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS hll-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/citusdata/postgresql-hll/archive/refs/tags/v2.17.tar.gz -O hll.tar.gz && \
|
|
||||||
mkdir hll-src && cd hll-src && tar xvzf ../hll.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hll.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "plpgsql-check-pg-build"
|
|
||||||
# compile plpgsql_check extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS plpgsql-check-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.3.2.tar.gz -O plpgsql_check.tar.gz && \
|
|
||||||
mkdir plpgsql_check-src && cd plpgsql_check-src && tar xvzf ../plpgsql_check.tar.gz --strip-components=1 -C . && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plpgsql_check.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "rust extensions"
|
|
||||||
# This layer is used to build `pgx` deps
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS rust-extensions-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y curl libclang-dev cmake && \
|
|
||||||
useradd -ms /bin/bash nonroot -b /home
|
|
||||||
|
|
||||||
ENV HOME=/home/nonroot
|
|
||||||
ENV PATH="/home/nonroot/.cargo/bin:/usr/local/pgsql/bin/:$PATH"
|
|
||||||
USER nonroot
|
|
||||||
WORKDIR /home/nonroot
|
|
||||||
ARG PG_VERSION
|
|
||||||
|
|
||||||
RUN curl -sSO https://static.rust-lang.org/rustup/dist/$(uname -m)-unknown-linux-gnu/rustup-init && \
|
|
||||||
chmod +x rustup-init && \
|
|
||||||
./rustup-init -y --no-modify-path --profile minimal --default-toolchain stable && \
|
|
||||||
rm rustup-init && \
|
|
||||||
cargo install --locked --version 0.7.3 cargo-pgx && \
|
|
||||||
/bin/bash -c 'cargo pgx init --pg${PG_VERSION:1}=/usr/local/pgsql/bin/pg_config'
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pg-jsonschema-pg-build"
|
|
||||||
# Compile "pg_jsonschema" extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
|
|
||||||
FROM rust-extensions-build AS pg-jsonschema-pg-build
|
|
||||||
|
|
||||||
# there is no release tag yet, but we need it due to the superuser fix in the control file
|
|
||||||
RUN wget https://github.com/supabase/pg_jsonschema/archive/caeab60d70b2fd3ae421ec66466a3abbb37b7ee6.tar.gz -O pg_jsonschema.tar.gz && \
|
|
||||||
mkdir pg_jsonschema-src && cd pg_jsonschema-src && tar xvzf ../pg_jsonschema.tar.gz --strip-components=1 -C . && \
|
|
||||||
sed -i 's/pgx = "0.7.1"/pgx = { version = "0.7.3", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
|
||||||
cargo pgx install --release && \
|
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pg-graphql-pg-build"
|
|
||||||
# Compile "pg_graphql" extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
|
|
||||||
FROM rust-extensions-build AS pg-graphql-pg-build
|
|
||||||
|
|
||||||
# Currently pgx version bump to >= 0.7.2 causes "call to unsafe function" compliation errors in
|
|
||||||
# pgx-contrib-spiext. There is a branch that removes that dependency, so use it. It is on the
|
|
||||||
# same 1.1 version we've used before.
|
|
||||||
RUN git clone -b remove-pgx-contrib-spiext --single-branch https://github.com/yrashk/pg_graphql && \
|
|
||||||
cd pg_graphql && \
|
|
||||||
sed -i 's/pgx = "~0.7.1"/pgx = { version = "0.7.3", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
|
||||||
sed -i 's/pgx-tests = "~0.7.1"/pgx-tests = "0.7.3"/g' Cargo.toml && \
|
|
||||||
cargo pgx install --release && \
|
|
||||||
# it's needed to enable extension because it uses untrusted C language
|
|
||||||
sed -i 's/superuser = false/superuser = true/g' /usr/local/pgsql/share/extension/pg_graphql.control && \
|
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_graphql.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "pg-tiktoken-build"
|
|
||||||
# Compile "pg_tiktoken" extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
|
|
||||||
FROM rust-extensions-build AS pg-tiktoken-pg-build
|
|
||||||
|
|
||||||
RUN git clone --depth=1 --single-branch https://github.com/kelvich/pg_tiktoken && \
|
|
||||||
cd pg_tiktoken && \
|
|
||||||
cargo pgx install --release && \
|
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_tiktoken.control
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "neon-pg-ext-build"
|
|
||||||
# compile neon extensions
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS neon-pg-ext-build
|
|
||||||
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=postgis-build /sfcgal/* /
|
|
||||||
COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=h3-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=h3-pg-build /h3/usr /
|
|
||||||
COPY --from=unit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=vector-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pgjwt-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pg-jsonschema-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pg-graphql-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pg-tiktoken-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=hypopg-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pg-hashids-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=rum-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pgtap-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=prefix-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=hll-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=plpgsql-check-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY pgxn/ pgxn/
|
|
||||||
|
|
||||||
RUN make -j $(getconf _NPROCESSORS_ONLN) \
|
|
||||||
PG_CONFIG=/usr/local/pgsql/bin/pg_config \
|
|
||||||
-C pgxn/neon \
|
|
||||||
-s install && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) \
|
|
||||||
PG_CONFIG=/usr/local/pgsql/bin/pg_config \
|
|
||||||
-C pgxn/neon_utils \
|
|
||||||
-s install
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Compile and run the Neon-specific `compute_ctl` binary
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM $REPOSITORY/$IMAGE:$TAG AS compute-tools
|
|
||||||
USER nonroot
|
|
||||||
# Copy entire project to get Cargo.* files with proper dependencies for the whole project
|
|
||||||
COPY --chown=nonroot . .
|
|
||||||
RUN cd compute_tools && cargo build --locked --profile release-line-debug-size-lto
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Clean up postgres folder before inclusion
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM neon-pg-ext-build AS postgres-cleanup-layer
|
|
||||||
COPY --from=neon-pg-ext-build /usr/local/pgsql /usr/local/pgsql
|
|
||||||
|
|
||||||
# Remove binaries from /bin/ that we won't use (or would manually copy & install otherwise)
|
|
||||||
RUN cd /usr/local/pgsql/bin && rm ecpg raster2pgsql shp2pgsql pgtopo_export pgtopo_import pgsql2shp
|
|
||||||
|
|
||||||
# Remove headers that we won't need anymore - we've completed installation of all extensions
|
|
||||||
RUN rm -r /usr/local/pgsql/include
|
|
||||||
|
|
||||||
# Remove static postgresql libraries - all compilation is finished, so we
|
|
||||||
# can now remove these files - they must be included in other binaries by now
|
|
||||||
# if they were to be used by other libraries.
|
|
||||||
RUN rm /usr/local/pgsql/lib/lib*.a
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Final layer
|
|
||||||
# Put it all together into the final image
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM debian:bullseye-slim
|
|
||||||
# Add user postgres
|
|
||||||
RUN mkdir /var/db && useradd -m -d /var/db/postgres postgres && \
|
|
||||||
echo "postgres:test_console_pass" | chpasswd && \
|
|
||||||
mkdir /var/db/postgres/compute && mkdir /var/db/postgres/specs && \
|
|
||||||
chown -R postgres:postgres /var/db/postgres && \
|
|
||||||
chmod 0750 /var/db/postgres/compute && \
|
|
||||||
echo '/usr/local/lib' >> /etc/ld.so.conf && /sbin/ldconfig && \
|
|
||||||
# create folder for file cache
|
|
||||||
mkdir -p -m 777 /neon/cache
|
|
||||||
|
|
||||||
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
|
||||||
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
|
||||||
|
|
||||||
# Install:
|
|
||||||
# libreadline8 for psql
|
|
||||||
# libicu67, locales for collations (including ICU and plpgsql_check)
|
|
||||||
# libossp-uuid16 for extension ossp-uuid
|
|
||||||
# libgeos, libgdal, libsfcgal1, libproj and libprotobuf-c1 for PostGIS
|
|
||||||
# libxml2, libxslt1.1 for xml2
|
|
||||||
RUN apt update && \
|
|
||||||
apt install --no-install-recommends -y \
|
|
||||||
locales \
|
|
||||||
libicu67 \
|
|
||||||
libreadline8 \
|
|
||||||
libossp-uuid16 \
|
|
||||||
libgeos-c1v5 \
|
|
||||||
libgdal28 \
|
|
||||||
libproj19 \
|
|
||||||
libprotobuf-c1 \
|
|
||||||
libsfcgal1 \
|
|
||||||
libxml2 \
|
|
||||||
libxslt1.1 \
|
|
||||||
gdb && \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
|
|
||||||
localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
|
|
||||||
|
|
||||||
ENV LANG en_US.utf8
|
|
||||||
USER postgres
|
|
||||||
ENTRYPOINT ["/usr/local/bin/compute_ctl"]
|
|
||||||
220
Dockerfile.compute-node-v14
Normal file
220
Dockerfile.compute-node-v14
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
#
|
||||||
|
# This file is identical to the Dockerfile.compute-node-v15 file
|
||||||
|
# except for the version of Postgres that is built.
|
||||||
|
#
|
||||||
|
|
||||||
|
ARG TAG=pinned
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "build-deps"
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM debian:bullseye-slim AS build-deps
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y git autoconf automake libtool build-essential bison flex libreadline-dev \
|
||||||
|
zlib1g-dev libxml2-dev libcurl4-openssl-dev libossp-uuid-dev wget pkg-config libssl-dev
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "pg-build"
|
||||||
|
# Build Postgres from the neon postgres repository.
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS pg-build
|
||||||
|
COPY vendor/postgres-v14 postgres
|
||||||
|
RUN cd postgres && \
|
||||||
|
./configure CFLAGS='-O2 -g3' --enable-debug --with-openssl --with-uuid=ossp && \
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s install && \
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C contrib/ install && \
|
||||||
|
# Install headers
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C src/include install && \
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C src/interfaces/libpq install && \
|
||||||
|
# Enable some of contrib extensions
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/bloom.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgrowlocks.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/intagg.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgstattuple.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/earthdistance.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "postgis-build"
|
||||||
|
# Build PostGIS from the upstream PostGIS mirror.
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS postgis-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y gdal-bin libgdal-dev libprotobuf-c-dev protobuf-c-compiler xsltproc
|
||||||
|
|
||||||
|
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.1.tar.gz && \
|
||||||
|
tar xvzf postgis-3.3.1.tar.gz && \
|
||||||
|
cd postgis-3.3.1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
|
./configure && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
cd extensions/postgis && \
|
||||||
|
make clean && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_raster.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_tiger_geocoder.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_topology.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer_data_us.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "plv8-build"
|
||||||
|
# Build plv8
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS plv8-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y ninja-build python3-dev libc++-dev libc++abi-dev libncurses5 binutils
|
||||||
|
|
||||||
|
# https://github.com/plv8/plv8/issues/475:
|
||||||
|
# v8 uses gold for linking and sets `--thread-count=4` which breaks
|
||||||
|
# gold version <= 1.35 (https://sourceware.org/bugzilla/show_bug.cgi?id=23607)
|
||||||
|
# Install newer gold version manually as debian-testing binutils version updates
|
||||||
|
# libc version, which in turn breaks other extension built against non-testing libc.
|
||||||
|
RUN wget https://ftp.gnu.org/gnu/binutils/binutils-2.38.tar.gz && \
|
||||||
|
tar xvzf binutils-2.38.tar.gz && \
|
||||||
|
cd binutils-2.38 && \
|
||||||
|
cd libiberty && ./configure && make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
cd ../bfd && ./configure && make bfdver.h && \
|
||||||
|
cd ../gold && ./configure && make -j $(getconf _NPROCESSORS_ONLN) && make install && \
|
||||||
|
cp /usr/local/bin/ld.gold /usr/bin/gold
|
||||||
|
|
||||||
|
# Sed is used to patch for https://github.com/plv8/plv8/issues/503
|
||||||
|
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.4.tar.gz && \
|
||||||
|
tar xvzf v3.1.4.tar.gz && \
|
||||||
|
cd plv8-3.1.4 && \
|
||||||
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
|
sed -i 's/MemoryContextAlloc(/MemoryContextAllocZero(/' plv8.cc && \
|
||||||
|
make DOCKER=1 -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
rm -rf /plv8-* && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plv8.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "h3-pg-build"
|
||||||
|
# Build h3_pg
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS h3-pg-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
|
# packaged cmake is too old
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh \
|
||||||
|
-q -O /tmp/cmake-install.sh \
|
||||||
|
&& chmod u+x /tmp/cmake-install.sh \
|
||||||
|
&& /tmp/cmake-install.sh --skip-license --prefix=/usr/local/ \
|
||||||
|
&& rm /tmp/cmake-install.sh
|
||||||
|
|
||||||
|
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.0.1.tar.gz -O h3.tgz && \
|
||||||
|
tar xvzf h3.tgz && \
|
||||||
|
cd h3-4.0.1 && \
|
||||||
|
mkdir build && \
|
||||||
|
cd build && \
|
||||||
|
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
DESTDIR=/h3 make install && \
|
||||||
|
cp -R /h3/usr / && \
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.0.1.tar.gz -O h3-pg.tgz && \
|
||||||
|
tar xvzf h3-pg.tgz && \
|
||||||
|
cd h3-pg-4.0.1 && \
|
||||||
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/h3.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/h3_postgis.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "neon-pg-ext-build"
|
||||||
|
# compile neon extensions
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS neon-pg-ext-build
|
||||||
|
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=h3-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=h3-pg-build /h3/usr /
|
||||||
|
COPY pgxn/ pgxn/
|
||||||
|
|
||||||
|
RUN make -j $(getconf _NPROCESSORS_ONLN) \
|
||||||
|
PG_CONFIG=/usr/local/pgsql/bin/pg_config \
|
||||||
|
-C pgxn/neon \
|
||||||
|
-s install
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Compile and run the Neon-specific `compute_ctl` binary
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:$TAG AS compute-tools
|
||||||
|
USER nonroot
|
||||||
|
# Copy entire project to get Cargo.* files with proper dependencies for the whole project
|
||||||
|
COPY --chown=nonroot . .
|
||||||
|
RUN cd compute_tools && cargo build --locked --profile release-line-debug-size-lto
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Clean up postgres folder before inclusion
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM neon-pg-ext-build AS postgres-cleanup-layer
|
||||||
|
COPY --from=neon-pg-ext-build /usr/local/pgsql /usr/local/pgsql
|
||||||
|
|
||||||
|
# Remove binaries from /bin/ that we won't use (or would manually copy & install otherwise)
|
||||||
|
RUN cd /usr/local/pgsql/bin && rm ecpg raster2pgsql shp2pgsql pgtopo_export pgtopo_import pgsql2shp
|
||||||
|
|
||||||
|
# Remove headers that we won't need anymore - we've completed installation of all extensions
|
||||||
|
RUN rm -r /usr/local/pgsql/include
|
||||||
|
|
||||||
|
# Remove static postgresql libraries - all compilation is finished, so we
|
||||||
|
# can now remove these files - they must be included in other binaries by now
|
||||||
|
# if they were to be used by other libraries.
|
||||||
|
RUN rm /usr/local/pgsql/lib/lib*.a
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Final layer
|
||||||
|
# Put it all together into the final image
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
# Add user postgres
|
||||||
|
RUN mkdir /var/db && useradd -m -d /var/db/postgres postgres && \
|
||||||
|
echo "postgres:test_console_pass" | chpasswd && \
|
||||||
|
mkdir /var/db/postgres/compute && mkdir /var/db/postgres/specs && \
|
||||||
|
chown -R postgres:postgres /var/db/postgres && \
|
||||||
|
chmod 0750 /var/db/postgres/compute && \
|
||||||
|
echo '/usr/local/lib' >> /etc/ld.so.conf && /sbin/ldconfig
|
||||||
|
|
||||||
|
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
||||||
|
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
||||||
|
|
||||||
|
# Install:
|
||||||
|
# libreadline8 for psql
|
||||||
|
# libossp-uuid16 for extension ossp-uuid
|
||||||
|
# libgeos, libgdal, libproj and libprotobuf-c1 for PostGIS
|
||||||
|
RUN apt update && \
|
||||||
|
apt install --no-install-recommends -y \
|
||||||
|
libreadline8 \
|
||||||
|
libossp-uuid16 \
|
||||||
|
libgeos-c1v5 \
|
||||||
|
libgdal28 \
|
||||||
|
libproj19 \
|
||||||
|
libprotobuf-c1 \
|
||||||
|
gdb && \
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
USER postgres
|
||||||
|
ENTRYPOINT ["/usr/local/bin/compute_ctl"]
|
||||||
220
Dockerfile.compute-node-v15
Normal file
220
Dockerfile.compute-node-v15
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
#
|
||||||
|
# This file is identical to the Dockerfile.compute-node-v14 file
|
||||||
|
# except for the version of Postgres that is built.
|
||||||
|
#
|
||||||
|
|
||||||
|
ARG TAG=pinned
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "build-deps"
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM debian:bullseye-slim AS build-deps
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y git autoconf automake libtool build-essential bison flex libreadline-dev \
|
||||||
|
zlib1g-dev libxml2-dev libcurl4-openssl-dev libossp-uuid-dev wget pkg-config libssl-dev
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "pg-build"
|
||||||
|
# Build Postgres from the neon postgres repository.
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS pg-build
|
||||||
|
COPY vendor/postgres-v15 postgres
|
||||||
|
RUN cd postgres && \
|
||||||
|
./configure CFLAGS='-O2 -g3' --enable-debug --with-openssl --with-uuid=ossp && \
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s install && \
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C contrib/ install && \
|
||||||
|
# Install headers
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C src/include install && \
|
||||||
|
make MAKELEVEL=0 -j $(getconf _NPROCESSORS_ONLN) -s -C src/interfaces/libpq install && \
|
||||||
|
# Enable some of contrib extensions
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/bloom.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgrowlocks.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/intagg.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgstattuple.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/earthdistance.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "postgis-build"
|
||||||
|
# Build PostGIS from the upstream PostGIS mirror.
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS postgis-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y gdal-bin libgdal-dev libprotobuf-c-dev protobuf-c-compiler xsltproc
|
||||||
|
|
||||||
|
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.1.tar.gz && \
|
||||||
|
tar xvzf postgis-3.3.1.tar.gz && \
|
||||||
|
cd postgis-3.3.1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
|
./configure && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
cd extensions/postgis && \
|
||||||
|
make clean && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_raster.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_tiger_geocoder.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/postgis_topology.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/address_standardizer_data_us.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "plv8-build"
|
||||||
|
# Build plv8
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS plv8-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
RUN apt update && \
|
||||||
|
apt install -y ninja-build python3-dev libc++-dev libc++abi-dev libncurses5 binutils
|
||||||
|
|
||||||
|
# https://github.com/plv8/plv8/issues/475:
|
||||||
|
# v8 uses gold for linking and sets `--thread-count=4` which breaks
|
||||||
|
# gold version <= 1.35 (https://sourceware.org/bugzilla/show_bug.cgi?id=23607)
|
||||||
|
# Install newer gold version manually as debian-testing binutils version updates
|
||||||
|
# libc version, which in turn breaks other extension built against non-testing libc.
|
||||||
|
RUN wget https://ftp.gnu.org/gnu/binutils/binutils-2.38.tar.gz && \
|
||||||
|
tar xvzf binutils-2.38.tar.gz && \
|
||||||
|
cd binutils-2.38 && \
|
||||||
|
cd libiberty && ./configure && make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
cd ../bfd && ./configure && make bfdver.h && \
|
||||||
|
cd ../gold && ./configure && make -j $(getconf _NPROCESSORS_ONLN) && make install && \
|
||||||
|
cp /usr/local/bin/ld.gold /usr/bin/gold
|
||||||
|
|
||||||
|
# Sed is used to patch for https://github.com/plv8/plv8/issues/503
|
||||||
|
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.4.tar.gz && \
|
||||||
|
tar xvzf v3.1.4.tar.gz && \
|
||||||
|
cd plv8-3.1.4 && \
|
||||||
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
|
sed -i 's/MemoryContextAlloc(/MemoryContextAllocZero(/' plv8.cc && \
|
||||||
|
make DOCKER=1 -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
rm -rf /plv8-* && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plv8.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "h3-pg-build"
|
||||||
|
# Build h3_pg
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS h3-pg-build
|
||||||
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
|
# packaged cmake is too old
|
||||||
|
RUN wget https://github.com/Kitware/CMake/releases/download/v3.24.2/cmake-3.24.2-linux-x86_64.sh \
|
||||||
|
-q -O /tmp/cmake-install.sh \
|
||||||
|
&& chmod u+x /tmp/cmake-install.sh \
|
||||||
|
&& /tmp/cmake-install.sh --skip-license --prefix=/usr/local/ \
|
||||||
|
&& rm /tmp/cmake-install.sh
|
||||||
|
|
||||||
|
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.0.1.tar.gz -O h3.tgz && \
|
||||||
|
tar xvzf h3.tgz && \
|
||||||
|
cd h3-4.0.1 && \
|
||||||
|
mkdir build && \
|
||||||
|
cd build && \
|
||||||
|
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
DESTDIR=/h3 make install && \
|
||||||
|
cp -R /h3/usr / && \
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.0.1.tar.gz -O h3-pg.tgz && \
|
||||||
|
tar xvzf h3-pg.tgz && \
|
||||||
|
cd h3-pg-4.0.1 && \
|
||||||
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/h3.control && \
|
||||||
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/h3_postgis.control
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer "neon-pg-ext-build"
|
||||||
|
# compile neon extensions
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM build-deps AS neon-pg-ext-build
|
||||||
|
COPY --from=postgis-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=plv8-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=h3-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
COPY --from=h3-pg-build /h3/usr /
|
||||||
|
COPY pgxn/ pgxn/
|
||||||
|
|
||||||
|
RUN make -j $(getconf _NPROCESSORS_ONLN) \
|
||||||
|
PG_CONFIG=/usr/local/pgsql/bin/pg_config \
|
||||||
|
-C pgxn/neon \
|
||||||
|
-s install
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Compile and run the Neon-specific `compute_ctl` binary
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:$TAG AS compute-tools
|
||||||
|
USER nonroot
|
||||||
|
# Copy entire project to get Cargo.* files with proper dependencies for the whole project
|
||||||
|
COPY --chown=nonroot . .
|
||||||
|
RUN cd compute_tools && cargo build --locked --profile release-line-debug-size-lto
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Clean up postgres folder before inclusion
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM neon-pg-ext-build AS postgres-cleanup-layer
|
||||||
|
COPY --from=neon-pg-ext-build /usr/local/pgsql /usr/local/pgsql
|
||||||
|
|
||||||
|
# Remove binaries from /bin/ that we won't use (or would manually copy & install otherwise)
|
||||||
|
RUN cd /usr/local/pgsql/bin && rm ecpg raster2pgsql shp2pgsql pgtopo_export pgtopo_import pgsql2shp
|
||||||
|
|
||||||
|
# Remove headers that we won't need anymore - we've completed installation of all extensions
|
||||||
|
RUN rm -r /usr/local/pgsql/include
|
||||||
|
|
||||||
|
# Remove static postgresql libraries - all compilation is finished, so we
|
||||||
|
# can now remove these files - they must be included in other binaries by now
|
||||||
|
# if they were to be used by other libraries.
|
||||||
|
RUN rm /usr/local/pgsql/lib/lib*.a
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Final layer
|
||||||
|
# Put it all together into the final image
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
# Add user postgres
|
||||||
|
RUN mkdir /var/db && useradd -m -d /var/db/postgres postgres && \
|
||||||
|
echo "postgres:test_console_pass" | chpasswd && \
|
||||||
|
mkdir /var/db/postgres/compute && mkdir /var/db/postgres/specs && \
|
||||||
|
chown -R postgres:postgres /var/db/postgres && \
|
||||||
|
chmod 0750 /var/db/postgres/compute && \
|
||||||
|
echo '/usr/local/lib' >> /etc/ld.so.conf && /sbin/ldconfig
|
||||||
|
|
||||||
|
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
||||||
|
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
||||||
|
|
||||||
|
# Install:
|
||||||
|
# libreadline8 for psql
|
||||||
|
# libossp-uuid16 for extension ossp-uuid
|
||||||
|
# libgeos, libgdal, libproj and libprotobuf-c1 for PostGIS
|
||||||
|
RUN apt update && \
|
||||||
|
apt install --no-install-recommends -y \
|
||||||
|
libreadline8 \
|
||||||
|
libossp-uuid16 \
|
||||||
|
libgeos-c1v5 \
|
||||||
|
libgdal28 \
|
||||||
|
libproj19 \
|
||||||
|
libprotobuf-c1 \
|
||||||
|
gdb && \
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
USER postgres
|
||||||
|
ENTRYPOINT ["/usr/local/bin/compute_ctl"]
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Note: this file *mostly* just builds on Dockerfile.compute-node
|
|
||||||
|
|
||||||
ARG SRC_IMAGE
|
|
||||||
ARG VM_INFORMANT_VERSION=v0.1.14
|
|
||||||
|
|
||||||
# Pull VM informant and set up inittab
|
|
||||||
FROM neondatabase/vm-informant:$VM_INFORMANT_VERSION as informant
|
|
||||||
|
|
||||||
RUN set -e \
|
|
||||||
&& rm -f /etc/inittab \
|
|
||||||
&& touch /etc/inittab
|
|
||||||
|
|
||||||
RUN set -e \
|
|
||||||
&& CONNSTR="dbname=neondb user=cloud_admin sslmode=disable" \
|
|
||||||
&& ARGS="--auto-restart --pgconnstr=\"$CONNSTR\"" \
|
|
||||||
&& echo "::respawn:su vm-informant -c '/usr/local/bin/vm-informant $ARGS'" >> /etc/inittab
|
|
||||||
|
|
||||||
# Combine, starting from non-VM compute node image.
|
|
||||||
FROM $SRC_IMAGE as base
|
|
||||||
|
|
||||||
# Temporarily set user back to root so we can run adduser
|
|
||||||
USER root
|
|
||||||
RUN adduser vm-informant --disabled-password --no-create-home
|
|
||||||
USER postgres
|
|
||||||
|
|
||||||
COPY --from=informant /etc/inittab /etc/inittab
|
|
||||||
COPY --from=informant /usr/bin/vm-informant /usr/local/bin/vm-informant
|
|
||||||
20
Makefile
20
Makefile
@@ -133,26 +133,12 @@ neon-pg-ext-%: postgres-%
|
|||||||
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config CFLAGS='$(PG_CFLAGS) $(COPT)' \
|
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config CFLAGS='$(PG_CFLAGS) $(COPT)' \
|
||||||
-C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \
|
-C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \
|
||||||
-f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile install
|
-f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile install
|
||||||
+@echo "Compiling neon_utils $*"
|
|
||||||
mkdir -p $(POSTGRES_INSTALL_DIR)/build/neon-utils-$*
|
|
||||||
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config CFLAGS='$(PG_CFLAGS) $(COPT)' \
|
|
||||||
-C $(POSTGRES_INSTALL_DIR)/build/neon-utils-$* \
|
|
||||||
-f $(ROOT_PROJECT_DIR)/pgxn/neon_utils/Makefile install
|
|
||||||
|
|
||||||
.PHONY: neon-pg-ext-clean-%
|
.PHONY: neon-pg-ext-clean-%
|
||||||
neon-pg-ext-clean-%:
|
neon-pg-ext-clean-%:
|
||||||
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/pgxn/neon-$* -f $(ROOT_PROJECT_DIR)/pgxn/neon/Makefile clean
|
||||||
-C $(POSTGRES_INSTALL_DIR)/build/neon-$* \
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/pgxn/neon_walredo-$* -f $(ROOT_PROJECT_DIR)/pgxn/neon_walredo/Makefile clean
|
||||||
-f $(ROOT_PROJECT_DIR)/pgxn/neon/Makefile clean
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/pgxn/neon_test_utils-$* -f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile clean
|
||||||
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \
|
|
||||||
-C $(POSTGRES_INSTALL_DIR)/build/neon-walredo-$* \
|
|
||||||
-f $(ROOT_PROJECT_DIR)/pgxn/neon_walredo/Makefile clean
|
|
||||||
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \
|
|
||||||
-C $(POSTGRES_INSTALL_DIR)/build/neon-test-utils-$* \
|
|
||||||
-f $(ROOT_PROJECT_DIR)/pgxn/neon_test_utils/Makefile clean
|
|
||||||
$(MAKE) PG_CONFIG=$(POSTGRES_INSTALL_DIR)/$*/bin/pg_config \
|
|
||||||
-C $(POSTGRES_INSTALL_DIR)/build/neon-utils-$* \
|
|
||||||
-f $(ROOT_PROJECT_DIR)/pgxn/neon_utils/Makefile clean
|
|
||||||
|
|
||||||
.PHONY: neon-pg-ext
|
.PHONY: neon-pg-ext
|
||||||
neon-pg-ext: \
|
neon-pg-ext: \
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -34,11 +34,6 @@ dnf install flex bison readline-devel zlib-devel openssl-devel \
|
|||||||
libseccomp-devel perl clang cmake postgresql postgresql-contrib protobuf-compiler \
|
libseccomp-devel perl clang cmake postgresql postgresql-contrib protobuf-compiler \
|
||||||
protobuf-devel
|
protobuf-devel
|
||||||
```
|
```
|
||||||
* On Arch based systems, these packages are needed:
|
|
||||||
```bash
|
|
||||||
pacman -S base-devel readline zlib libseccomp openssl clang \
|
|
||||||
postgresql-libs cmake postgresql protobuf
|
|
||||||
```
|
|
||||||
|
|
||||||
2. [Install Rust](https://www.rust-lang.org/tools/install)
|
2. [Install Rust](https://www.rust-lang.org/tools/install)
|
||||||
```
|
```
|
||||||
@@ -88,10 +83,9 @@ cd neon
|
|||||||
|
|
||||||
# The preferred and default is to make a debug build. This will create a
|
# The preferred and default is to make a debug build. This will create a
|
||||||
# demonstrably slower build than a release build. For a release build,
|
# demonstrably slower build than a release build. For a release build,
|
||||||
# use "BUILD_TYPE=release make -j`nproc` -s"
|
# use "BUILD_TYPE=release make -j`nproc`"
|
||||||
# Remove -s for the verbose build log
|
|
||||||
|
|
||||||
make -j`nproc` -s
|
make -j`nproc`
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Building on OSX
|
#### Building on OSX
|
||||||
@@ -105,17 +99,16 @@ cd neon
|
|||||||
|
|
||||||
# The preferred and default is to make a debug build. This will create a
|
# The preferred and default is to make a debug build. This will create a
|
||||||
# demonstrably slower build than a release build. For a release build,
|
# demonstrably slower build than a release build. For a release build,
|
||||||
# use "BUILD_TYPE=release make -j`sysctl -n hw.logicalcpu` -s"
|
# use "BUILD_TYPE=release make -j`sysctl -n hw.logicalcpu`"
|
||||||
# Remove -s for the verbose build log
|
|
||||||
|
|
||||||
make -j`sysctl -n hw.logicalcpu` -s
|
make -j`sysctl -n hw.logicalcpu`
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Dependency installation notes
|
#### Dependency installation notes
|
||||||
To run the `psql` client, install the `postgresql-client` package or modify `PATH` and `LD_LIBRARY_PATH` to include `pg_install/bin` and `pg_install/lib`, respectively.
|
To run the `psql` client, install the `postgresql-client` package or modify `PATH` and `LD_LIBRARY_PATH` to include `pg_install/bin` and `pg_install/lib`, respectively.
|
||||||
|
|
||||||
To run the integration tests or Python scripts (not required to use the code), install
|
To run the integration tests or Python scripts (not required to use the code), install
|
||||||
Python (3.9 or higher), and install python3 packages using `./scripts/pysync` (requires [poetry>=1.3](https://python-poetry.org/)) in the project directory.
|
Python (3.9 or higher), and install python3 packages using `./scripts/pysync` (requires [poetry](https://python-poetry.org/)) in the project directory.
|
||||||
|
|
||||||
|
|
||||||
#### Running neon database
|
#### Running neon database
|
||||||
|
|||||||
@@ -11,20 +11,15 @@ clap.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hyper = { workspace = true, features = ["full"] }
|
hyper = { workspace = true, features = ["full"] }
|
||||||
notify.workspace = true
|
notify.workspace = true
|
||||||
num_cpus.workspace = true
|
|
||||||
opentelemetry.workspace = true
|
|
||||||
postgres.workspace = true
|
postgres.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tar.workspace = true
|
tar.workspace = true
|
||||||
reqwest = { workspace = true, features = ["json"] }
|
|
||||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||||
tokio-postgres.workspace = true
|
tokio-postgres.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-opentelemetry.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing-utils.workspace = true
|
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ use tracing::{error, info};
|
|||||||
|
|
||||||
use compute_tools::compute::{ComputeMetrics, ComputeNode, ComputeState, ComputeStatus};
|
use compute_tools::compute::{ComputeMetrics, ComputeNode, ComputeState, ComputeStatus};
|
||||||
use compute_tools::http::api::launch_http_server;
|
use compute_tools::http::api::launch_http_server;
|
||||||
|
use compute_tools::informant::spawn_vm_informant_if_present;
|
||||||
use compute_tools::logger::*;
|
use compute_tools::logger::*;
|
||||||
use compute_tools::monitor::launch_monitor;
|
use compute_tools::monitor::launch_monitor;
|
||||||
use compute_tools::params::*;
|
use compute_tools::params::*;
|
||||||
@@ -52,7 +53,7 @@ use compute_tools::spec::*;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
init_logger(DEFAULT_LOG_LEVEL)?;
|
||||||
|
|
||||||
let matches = cli().get_matches();
|
let matches = cli().get_matches();
|
||||||
|
|
||||||
@@ -65,9 +66,6 @@ fn main() -> Result<()> {
|
|||||||
let spec = matches.get_one::<String>("spec");
|
let spec = matches.get_one::<String>("spec");
|
||||||
let spec_path = matches.get_one::<String>("spec-path");
|
let spec_path = matches.get_one::<String>("spec-path");
|
||||||
|
|
||||||
let compute_id = matches.get_one::<String>("compute-id");
|
|
||||||
let control_plane_uri = matches.get_one::<String>("control-plane-uri");
|
|
||||||
|
|
||||||
// Try to use just 'postgres' if no path is provided
|
// Try to use just 'postgres' if no path is provided
|
||||||
let pgbin = matches.get_one::<String>("pgbin").unwrap();
|
let pgbin = matches.get_one::<String>("pgbin").unwrap();
|
||||||
|
|
||||||
@@ -80,54 +78,12 @@ fn main() -> Result<()> {
|
|||||||
let path = Path::new(sp);
|
let path = Path::new(sp);
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
serde_json::from_reader(file)?
|
serde_json::from_reader(file)?
|
||||||
} else if let Some(id) = compute_id {
|
|
||||||
if let Some(cp_base) = control_plane_uri {
|
|
||||||
let cp_uri = format!("{cp_base}/management/api/v1/{id}/spec");
|
|
||||||
let jwt: String = match std::env::var("NEON_CONSOLE_JWT") {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => "".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
reqwest::blocking::Client::new()
|
|
||||||
.get(cp_uri)
|
|
||||||
.header("Authorization", jwt)
|
|
||||||
.send()?
|
|
||||||
.json()?
|
|
||||||
} else {
|
|
||||||
panic!(
|
|
||||||
"must specify --control-plane-uri \"{:#?}\" and --compute-id \"{:#?}\"",
|
|
||||||
control_plane_uri, compute_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
panic!("compute spec should be provided via --spec or --spec-path argument");
|
panic!("cluster spec should be provided via --spec or --spec-path argument");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract OpenTelemetry context for the startup actions from the spec, and
|
|
||||||
// attach it to the current tracing context.
|
|
||||||
//
|
|
||||||
// This is used to propagate the context for the 'start_compute' operation
|
|
||||||
// from the neon control plane. This allows linking together the wider
|
|
||||||
// 'start_compute' operation that creates the compute container, with the
|
|
||||||
// startup actions here within the container.
|
|
||||||
//
|
|
||||||
// Switch to the startup context here, and exit it once the startup has
|
|
||||||
// completed and Postgres is up and running.
|
|
||||||
//
|
|
||||||
// NOTE: This is supposed to only cover the *startup* actions. Once
|
|
||||||
// postgres is configured and up-and-running, we exit this span. Any other
|
|
||||||
// actions that are performed on incoming HTTP requests, for example, are
|
|
||||||
// performed in separate spans.
|
|
||||||
let startup_context_guard = if let Some(ref carrier) = spec.startup_tracing_context {
|
|
||||||
use opentelemetry::propagation::TextMapPropagator;
|
|
||||||
use opentelemetry::sdk::propagation::TraceContextPropagator;
|
|
||||||
Some(TraceContextPropagator::new().extract(carrier).attach())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let pageserver_connstr = spec
|
let pageserver_connstr = spec
|
||||||
.cluster
|
.cluster
|
||||||
.settings
|
.settings
|
||||||
@@ -162,6 +118,8 @@ fn main() -> Result<()> {
|
|||||||
// requests, while configuration is still in progress.
|
// requests, while configuration is still in progress.
|
||||||
let _http_handle = launch_http_server(&compute).expect("cannot launch http endpoint thread");
|
let _http_handle = launch_http_server(&compute).expect("cannot launch http endpoint thread");
|
||||||
let _monitor_handle = launch_monitor(&compute).expect("cannot launch compute monitor thread");
|
let _monitor_handle = launch_monitor(&compute).expect("cannot launch compute monitor thread");
|
||||||
|
// Also spawn the thread responsible for handling the VM informant -- if it's present
|
||||||
|
let _vm_informant_handle = spawn_vm_informant_if_present().expect("cannot launch VM informant");
|
||||||
|
|
||||||
// Start Postgres
|
// Start Postgres
|
||||||
let mut delay_exit = false;
|
let mut delay_exit = false;
|
||||||
@@ -182,9 +140,6 @@ fn main() -> Result<()> {
|
|||||||
// Wait for the child Postgres process forever. In this state Ctrl+C will
|
// Wait for the child Postgres process forever. In this state Ctrl+C will
|
||||||
// propagate to Postgres and it will be shut down as well.
|
// propagate to Postgres and it will be shut down as well.
|
||||||
if let Some(mut pg) = pg {
|
if let Some(mut pg) = pg {
|
||||||
// Startup is finished, exit the startup tracing span
|
|
||||||
drop(startup_context_guard);
|
|
||||||
|
|
||||||
let ecode = pg
|
let ecode = pg
|
||||||
.wait()
|
.wait()
|
||||||
.expect("failed to start waiting on Postgres process");
|
.expect("failed to start waiting on Postgres process");
|
||||||
@@ -204,10 +159,6 @@ fn main() -> Result<()> {
|
|||||||
info!("shutting down");
|
info!("shutting down");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown trace pipeline gracefully, so that it has a chance to send any
|
|
||||||
// pending traces before we exit.
|
|
||||||
tracing_utils::shutdown_tracing();
|
|
||||||
|
|
||||||
exit(exit_code.unwrap_or(1))
|
exit(exit_code.unwrap_or(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,18 +200,6 @@ fn cli() -> clap::Command {
|
|||||||
.long("spec-path")
|
.long("spec-path")
|
||||||
.value_name("SPEC_PATH"),
|
.value_name("SPEC_PATH"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new("compute-id")
|
|
||||||
.short('i')
|
|
||||||
.long("compute-id")
|
|
||||||
.value_name("COMPUTE_ID"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new("control-plane-uri")
|
|
||||||
.short('p')
|
|
||||||
.long("control-plane-uri")
|
|
||||||
.value_name("CONTROL_PLANE"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use anyhow::{Context, Result};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use postgres::{Client, NoTls};
|
use postgres::{Client, NoTls};
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use tokio_postgres;
|
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
|
|
||||||
use crate::checker::create_writability_check_data;
|
use crate::checker::create_writability_check_data;
|
||||||
@@ -285,7 +284,6 @@ impl ComputeNode {
|
|||||||
handle_role_deletions(self, &mut client)?;
|
handle_role_deletions(self, &mut client)?;
|
||||||
handle_grants(self, &mut client)?;
|
handle_grants(self, &mut client)?;
|
||||||
create_writability_check_data(&mut client)?;
|
create_writability_check_data(&mut client)?;
|
||||||
handle_extensions(&self.spec, &mut client)?;
|
|
||||||
|
|
||||||
// 'Close' connection
|
// 'Close' connection
|
||||||
drop(client);
|
drop(client);
|
||||||
@@ -402,43 +400,4 @@ impl ComputeNode {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select `pg_stat_statements` data and return it as a stringified JSON
|
|
||||||
pub async fn collect_insights(&self) -> String {
|
|
||||||
let mut result_rows: Vec<String> = Vec::new();
|
|
||||||
let connect_result = tokio_postgres::connect(self.connstr.as_str(), NoTls).await;
|
|
||||||
let (client, connection) = connect_result.unwrap();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = connection.await {
|
|
||||||
eprintln!("connection error: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let result = client
|
|
||||||
.simple_query(
|
|
||||||
"SELECT
|
|
||||||
row_to_json(pg_stat_statements)
|
|
||||||
FROM
|
|
||||||
pg_stat_statements
|
|
||||||
WHERE
|
|
||||||
userid != 'cloud_admin'::regrole::oid
|
|
||||||
ORDER BY
|
|
||||||
(mean_exec_time + mean_plan_time) DESC
|
|
||||||
LIMIT 100",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(raw_rows) = result {
|
|
||||||
for message in raw_rows.iter() {
|
|
||||||
if let postgres::SimpleQueryMessage::Row(row) = message {
|
|
||||||
if let Some(json) = row.get(0) {
|
|
||||||
result_rows.push(json.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("{{\"pg_stat_statements\": [{}]}}", result_rows.join(","))
|
|
||||||
} else {
|
|
||||||
"{{\"pg_stat_statements\": []}}".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,16 @@ use std::net::SocketAddr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::compute::ComputeNode;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
||||||
use num_cpus;
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use tracing_utils::http::OtelName;
|
|
||||||
|
use crate::compute::ComputeNode;
|
||||||
|
|
||||||
// Service function to handle all available routes.
|
// Service function to handle all available routes.
|
||||||
async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body> {
|
async fn routes(req: Request<Body>, compute: Arc<ComputeNode>) -> Response<Body> {
|
||||||
//
|
|
||||||
// NOTE: The URI path is currently included in traces. That's OK because
|
|
||||||
// it doesn't contain any variable parts or sensitive information. But
|
|
||||||
// please keep that in mind if you change the routing here.
|
|
||||||
//
|
|
||||||
match (req.method(), req.uri().path()) {
|
match (req.method(), req.uri().path()) {
|
||||||
// Serialized compute state.
|
// Serialized compute state.
|
||||||
(&Method::GET, "/status") => {
|
(&Method::GET, "/status") => {
|
||||||
@@ -34,33 +28,15 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
|
|||||||
Response::new(Body::from(serde_json::to_string(&compute.metrics).unwrap()))
|
Response::new(Body::from(serde_json::to_string(&compute.metrics).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect Postgres current usage insights
|
|
||||||
(&Method::GET, "/insights") => {
|
|
||||||
info!("serving /insights GET request");
|
|
||||||
let insights = compute.collect_insights().await;
|
|
||||||
Response::new(Body::from(insights))
|
|
||||||
}
|
|
||||||
|
|
||||||
(&Method::POST, "/check_writability") => {
|
(&Method::POST, "/check_writability") => {
|
||||||
info!("serving /check_writability POST request");
|
info!("serving /check_writability POST request");
|
||||||
let res = crate::checker::check_writability(compute).await;
|
let res = crate::checker::check_writability(&compute).await;
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => Response::new(Body::from("true")),
|
Ok(_) => Response::new(Body::from("true")),
|
||||||
Err(e) => Response::new(Body::from(e.to_string())),
|
Err(e) => Response::new(Body::from(e.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(&Method::GET, "/info") => {
|
|
||||||
let num_cpus = num_cpus::get_physical();
|
|
||||||
info!("serving /info GET request. num_cpus: {}", num_cpus);
|
|
||||||
Response::new(Body::from(
|
|
||||||
serde_json::json!({
|
|
||||||
"num_cpus": num_cpus,
|
|
||||||
})
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the `404 Not Found` for any other routes.
|
// Return the `404 Not Found` for any other routes.
|
||||||
_ => {
|
_ => {
|
||||||
let mut not_found = Response::new(Body::from("404 Not Found"));
|
let mut not_found = Response::new(Body::from("404 Not Found"));
|
||||||
@@ -80,19 +56,7 @@ async fn serve(state: Arc<ComputeNode>) {
|
|||||||
async move {
|
async move {
|
||||||
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
|
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
async move {
|
async move { Ok::<_, Infallible>(routes(req, state).await) }
|
||||||
Ok::<_, Infallible>(
|
|
||||||
// NOTE: We include the URI path in the string. It
|
|
||||||
// doesn't contain any variable parts or sensitive
|
|
||||||
// information in this API.
|
|
||||||
tracing_utils::http::tracing_handler(
|
|
||||||
req,
|
|
||||||
|req| routes(req, &state),
|
|
||||||
OtelName::UriPath,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ paths:
|
|||||||
/status:
|
/status:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Info
|
- "info"
|
||||||
summary: Get compute node internal status
|
summary: Get compute node internal status
|
||||||
description: ""
|
description: ""
|
||||||
operationId: getComputeStatus
|
operationId: getComputeStatus
|
||||||
responses:
|
responses:
|
||||||
200:
|
"200":
|
||||||
description: ComputeState
|
description: ComputeState
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -25,58 +25,27 @@ paths:
|
|||||||
/metrics.json:
|
/metrics.json:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Info
|
- "info"
|
||||||
summary: Get compute node startup metrics in JSON format
|
summary: Get compute node startup metrics in JSON format
|
||||||
description: ""
|
description: ""
|
||||||
operationId: getComputeMetricsJSON
|
operationId: getComputeMetricsJSON
|
||||||
responses:
|
responses:
|
||||||
200:
|
"200":
|
||||||
description: ComputeMetrics
|
description: ComputeMetrics
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ComputeMetrics"
|
$ref: "#/components/schemas/ComputeMetrics"
|
||||||
|
|
||||||
/insights:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- Info
|
|
||||||
summary: Get current compute insights in JSON format
|
|
||||||
description: |
|
|
||||||
Note, that this doesn't include any historical data
|
|
||||||
operationId: getComputeInsights
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Compute insights
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/ComputeInsights"
|
|
||||||
|
|
||||||
/info:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- "info"
|
|
||||||
summary: Get info about the compute Pod/VM
|
|
||||||
description: ""
|
|
||||||
operationId: getInfo
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Info
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: "#/components/schemas/Info"
|
|
||||||
|
|
||||||
/check_writability:
|
/check_writability:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Check
|
- "check"
|
||||||
summary: Check that we can write new data on this compute
|
summary: Check that we can write new data on this compute
|
||||||
description: ""
|
description: ""
|
||||||
operationId: checkComputeWritability
|
operationId: checkComputeWritability
|
||||||
responses:
|
responses:
|
||||||
200:
|
"200":
|
||||||
description: Check result
|
description: Check result
|
||||||
content:
|
content:
|
||||||
text/plain:
|
text/plain:
|
||||||
@@ -111,15 +80,6 @@ components:
|
|||||||
total_startup_ms:
|
total_startup_ms:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
Info:
|
|
||||||
type: object
|
|
||||||
description: Information about VM/Pod
|
|
||||||
required:
|
|
||||||
- num_cpus
|
|
||||||
properties:
|
|
||||||
num_cpus:
|
|
||||||
type: integer
|
|
||||||
|
|
||||||
ComputeState:
|
ComputeState:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -136,15 +96,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Text of the error during compute startup, if any
|
description: Text of the error during compute startup, if any
|
||||||
|
|
||||||
ComputeInsights:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
pg_stat_statements:
|
|
||||||
description: Contains raw output from pg_stat_statements in JSON format
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
|
|
||||||
ComputeStatus:
|
ComputeStatus:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
|||||||
50
compute_tools/src/informant.rs
Normal file
50
compute_tools/src/informant.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::process;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
const VM_INFORMANT_PATH: &str = "/bin/vm-informant";
|
||||||
|
const RESTART_INFORMANT_AFTER_MILLIS: u64 = 5000;
|
||||||
|
|
||||||
|
/// Launch a thread to start the VM informant if it's present (and restart, on failure)
|
||||||
|
pub fn spawn_vm_informant_if_present() -> Result<Option<thread::JoinHandle<()>>> {
|
||||||
|
let exists = Path::new(VM_INFORMANT_PATH)
|
||||||
|
.try_exists()
|
||||||
|
.context("could not check if path exists")?;
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("run-vm-informant".into())
|
||||||
|
.spawn(move || run_informant())?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_informant() -> ! {
|
||||||
|
let restart_wait = Duration::from_millis(RESTART_INFORMANT_AFTER_MILLIS);
|
||||||
|
|
||||||
|
info!("starting VM informant");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut cmd = process::Command::new(VM_INFORMANT_PATH);
|
||||||
|
// Block on subprocess:
|
||||||
|
let result = cmd.status();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Err(e) => warn!("failed to run VM informant at {VM_INFORMANT_PATH:?}: {e}"),
|
||||||
|
Ok(status) if !status.success() => {
|
||||||
|
warn!("{VM_INFORMANT_PATH} exited with code {status:?}, retrying")
|
||||||
|
}
|
||||||
|
Ok(_) => info!("{VM_INFORMANT_PATH} ended gracefully (unexpectedly). Retrying"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
thread::sleep(restart_wait);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub mod http;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod compute;
|
pub mod compute;
|
||||||
|
pub mod informant;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
pub mod pg_helpers;
|
pub mod pg_helpers;
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
use tracing_opentelemetry::OpenTelemetryLayer;
|
use anyhow::Result;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
/// Initialize logging to stderr, and OpenTelemetry tracing and exporter.
|
/// Initialize `env_logger` using either `default_level` or
|
||||||
///
|
|
||||||
/// Logging is configured using either `default_log_level` or
|
|
||||||
/// `RUST_LOG` environment variable as default log level.
|
/// `RUST_LOG` environment variable as default log level.
|
||||||
///
|
pub fn init_logger(default_level: &str) -> Result<()> {
|
||||||
/// OpenTelemetry is configured with OTLP/HTTP exporter. It picks up
|
|
||||||
/// configuration from environment variables. For example, to change the destination,
|
|
||||||
/// set `OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318`. See
|
|
||||||
/// `tracing-utils` package description.
|
|
||||||
///
|
|
||||||
pub fn init_tracing_and_logging(default_log_level: &str) -> anyhow::Result<()> {
|
|
||||||
// Initialize Logging
|
|
||||||
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_log_level));
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_level));
|
||||||
|
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_writer(std::io::stderr);
|
.with_writer(std::io::stderr);
|
||||||
|
|
||||||
// Initialize OpenTelemetry
|
|
||||||
let otlp_layer =
|
|
||||||
tracing_utils::init_tracing_without_runtime("compute_ctl").map(OpenTelemetryLayer::new);
|
|
||||||
|
|
||||||
// Put it all together
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(env_filter)
|
.with(env_filter)
|
||||||
.with(otlp_layer)
|
|
||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
.init();
|
.init();
|
||||||
tracing::info!("logging and tracing started");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,23 +47,12 @@ pub struct GenericOption {
|
|||||||
/// declare a `trait` on it.
|
/// declare a `trait` on it.
|
||||||
pub type GenericOptions = Option<Vec<GenericOption>>;
|
pub type GenericOptions = Option<Vec<GenericOption>>;
|
||||||
|
|
||||||
/// Escape a string for including it in a SQL literal
|
|
||||||
fn escape_literal(s: &str) -> String {
|
|
||||||
s.replace('\'', "''").replace('\\', "\\\\")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Escape a string so that it can be used in postgresql.conf.
|
|
||||||
/// Same as escape_literal, currently.
|
|
||||||
fn escape_conf_value(s: &str) -> String {
|
|
||||||
s.replace('\'', "''").replace('\\', "\\\\")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GenericOption {
|
impl GenericOption {
|
||||||
/// Represent `GenericOption` as SQL statement parameter.
|
/// Represent `GenericOption` as SQL statement parameter.
|
||||||
pub fn to_pg_option(&self) -> String {
|
pub fn to_pg_option(&self) -> String {
|
||||||
if let Some(val) = &self.value {
|
if let Some(val) = &self.value {
|
||||||
match self.vartype.as_ref() {
|
match self.vartype.as_ref() {
|
||||||
"string" => format!("{} '{}'", self.name, escape_literal(val)),
|
"string" => format!("{} '{}'", self.name, val),
|
||||||
_ => format!("{} {}", self.name, val),
|
_ => format!("{} {}", self.name, val),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -74,8 +63,6 @@ impl GenericOption {
|
|||||||
/// Represent `GenericOption` as configuration option.
|
/// Represent `GenericOption` as configuration option.
|
||||||
pub fn to_pg_setting(&self) -> String {
|
pub fn to_pg_setting(&self) -> String {
|
||||||
if let Some(val) = &self.value {
|
if let Some(val) = &self.value {
|
||||||
// TODO: check in the console DB that we don't have these settings
|
|
||||||
// set for any non-deleted project and drop this override.
|
|
||||||
let name = match self.name.as_str() {
|
let name = match self.name.as_str() {
|
||||||
"safekeepers" => "neon.safekeepers",
|
"safekeepers" => "neon.safekeepers",
|
||||||
"wal_acceptor_reconnect" => "neon.safekeeper_reconnect_timeout",
|
"wal_acceptor_reconnect" => "neon.safekeeper_reconnect_timeout",
|
||||||
@@ -84,7 +71,7 @@ impl GenericOption {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match self.vartype.as_ref() {
|
match self.vartype.as_ref() {
|
||||||
"string" => format!("{} = '{}'", name, escape_conf_value(val)),
|
"string" => format!("{} = '{}'", name, val),
|
||||||
_ => format!("{} = {}", name, val),
|
_ => format!("{} = {}", name, val),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -120,7 +107,6 @@ impl PgOptionsSerialize for GenericOptions {
|
|||||||
.map(|op| op.to_pg_setting())
|
.map(|op| op.to_pg_setting())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
+ "\n" // newline after last setting
|
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@@ -23,8 +22,6 @@ pub struct ComputeSpec {
|
|||||||
/// Expected cluster state at the end of transition process.
|
/// Expected cluster state at the end of transition process.
|
||||||
pub cluster: Cluster,
|
pub cluster: Cluster,
|
||||||
pub delta_operations: Option<Vec<DeltaOp>>,
|
pub delta_operations: Option<Vec<DeltaOp>>,
|
||||||
|
|
||||||
pub startup_tracing_context: Option<HashMap<String, String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cluster state seen from the perspective of the external tools
|
/// Cluster state seen from the perspective of the external tools
|
||||||
@@ -387,13 +384,13 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
name.pg_quote(),
|
name.pg_quote(),
|
||||||
db.owner.pg_quote()
|
db.owner.pg_quote()
|
||||||
);
|
);
|
||||||
let _guard = info_span!("executing", query).entered();
|
let _ = info_span!("executing", query).entered();
|
||||||
client.execute(query.as_str(), &[])?;
|
client.execute(query.as_str(), &[])?;
|
||||||
}
|
}
|
||||||
DatabaseAction::Create => {
|
DatabaseAction::Create => {
|
||||||
let mut query: String = format!("CREATE DATABASE {} ", name.pg_quote());
|
let mut query: String = format!("CREATE DATABASE {} ", name.pg_quote());
|
||||||
query.push_str(&db.to_pg_options());
|
query.push_str(&db.to_pg_options());
|
||||||
let _guard = info_span!("executing", query).entered();
|
let _ = info_span!("executing", query).entered();
|
||||||
client.execute(query.as_str(), &[])?;
|
client.execute(query.as_str(), &[])?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -515,18 +512,3 @@ pub fn handle_grants(node: &ComputeNode, client: &mut Client) -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create required system extensions
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub fn handle_extensions(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|
||||||
if let Some(libs) = spec.cluster.settings.find("shared_preload_libraries") {
|
|
||||||
if libs.contains("pg_stat_statements") {
|
|
||||||
// Create extension only if this compute really needs it
|
|
||||||
let query = "CREATE EXTENSION IF NOT EXISTS pg_stat_statements";
|
|
||||||
info!("creating system extensions with query: {}", query);
|
|
||||||
client.simple_query(query)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -178,11 +178,6 @@
|
|||||||
"name": "neon.pageserver_connstring",
|
"name": "neon.pageserver_connstring",
|
||||||
"value": "host=127.0.0.1 port=6400",
|
"value": "host=127.0.0.1 port=6400",
|
||||||
"vartype": "string"
|
"vartype": "string"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test.escaping",
|
|
||||||
"value": "here's a backslash \\ and a quote ' and a double-quote \" hooray",
|
|
||||||
"vartype": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,30 +28,7 @@ mod pg_helpers_tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
spec.cluster.settings.as_pg_settings(),
|
spec.cluster.settings.as_pg_settings(),
|
||||||
r#"fsync = off
|
"fsync = off\nwal_level = replica\nhot_standby = on\nneon.safekeepers = '127.0.0.1:6502,127.0.0.1:6503,127.0.0.1:6501'\nwal_log_hints = on\nlog_connections = on\nshared_buffers = 32768\nport = 55432\nmax_connections = 100\nmax_wal_senders = 10\nlisten_addresses = '0.0.0.0'\nwal_sender_timeout = 0\npassword_encryption = md5\nmaintenance_work_mem = 65536\nmax_parallel_workers = 8\nmax_worker_processes = 8\nneon.tenant_id = 'b0554b632bd4d547a63b86c3630317e8'\nmax_replication_slots = 10\nneon.timeline_id = '2414a61ffc94e428f14b5758fe308e13'\nshared_preload_libraries = 'neon'\nsynchronous_standby_names = 'walproposer'\nneon.pageserver_connstring = 'host=127.0.0.1 port=6400'"
|
||||||
wal_level = replica
|
|
||||||
hot_standby = on
|
|
||||||
neon.safekeepers = '127.0.0.1:6502,127.0.0.1:6503,127.0.0.1:6501'
|
|
||||||
wal_log_hints = on
|
|
||||||
log_connections = on
|
|
||||||
shared_buffers = 32768
|
|
||||||
port = 55432
|
|
||||||
max_connections = 100
|
|
||||||
max_wal_senders = 10
|
|
||||||
listen_addresses = '0.0.0.0'
|
|
||||||
wal_sender_timeout = 0
|
|
||||||
password_encryption = md5
|
|
||||||
maintenance_work_mem = 65536
|
|
||||||
max_parallel_workers = 8
|
|
||||||
max_worker_processes = 8
|
|
||||||
neon.tenant_id = 'b0554b632bd4d547a63b86c3630317e8'
|
|
||||||
max_replication_slots = 10
|
|
||||||
neon.timeline_id = '2414a61ffc94e428f14b5758fe308e13'
|
|
||||||
shared_preload_libraries = 'neon'
|
|
||||||
synchronous_standby_names = 'walproposer'
|
|
||||||
neon.pageserver_connstring = 'host=127.0.0.1 port=6400'
|
|
||||||
test.escaping = 'here''s a backslash \\ and a quote '' and a double-quote " hooray'
|
|
||||||
"#
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ postgres.workspace = true
|
|||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest = { workspace = true, features = ["blocking", "json"] }
|
reqwest = { workspace = true, features = ["blocking", "json"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
tar.workspace = true
|
tar.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
@@ -24,7 +23,6 @@ url.workspace = true
|
|||||||
# Note: Do not directly depend on pageserver or safekeeper; use pageserver_api or safekeeper_api
|
# Note: Do not directly depend on pageserver or safekeeper; use pageserver_api or safekeeper_api
|
||||||
# instead, so that recompile times are better.
|
# instead, so that recompile times are better.
|
||||||
pageserver_api.workspace = true
|
pageserver_api.workspace = true
|
||||||
postgres_backend.workspace = true
|
|
||||||
safekeeper_api.workspace = true
|
safekeeper_api.workspace = true
|
||||||
postgres_connection.workspace = true
|
postgres_connection.workspace = true
|
||||||
storage_broker.workspace = true
|
storage_broker.workspace = true
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use pageserver_api::{
|
|||||||
DEFAULT_HTTP_LISTEN_ADDR as DEFAULT_PAGESERVER_HTTP_ADDR,
|
DEFAULT_HTTP_LISTEN_ADDR as DEFAULT_PAGESERVER_HTTP_ADDR,
|
||||||
DEFAULT_PG_LISTEN_ADDR as DEFAULT_PAGESERVER_PG_ADDR,
|
DEFAULT_PG_LISTEN_ADDR as DEFAULT_PAGESERVER_PG_ADDR,
|
||||||
};
|
};
|
||||||
use postgres_backend::AuthType;
|
|
||||||
use safekeeper_api::{
|
use safekeeper_api::{
|
||||||
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_SAFEKEEPER_HTTP_PORT,
|
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_SAFEKEEPER_HTTP_PORT,
|
||||||
DEFAULT_PG_LISTEN_PORT as DEFAULT_SAFEKEEPER_PG_PORT,
|
DEFAULT_PG_LISTEN_PORT as DEFAULT_SAFEKEEPER_PG_PORT,
|
||||||
@@ -31,6 +30,7 @@ use utils::{
|
|||||||
auth::{Claims, Scope},
|
auth::{Claims, Scope},
|
||||||
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
|
postgres_backend::AuthType,
|
||||||
project_git_version,
|
project_git_version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use postgres_backend::AuthType;
|
|
||||||
use utils::{
|
use utils::{
|
||||||
id::{TenantId, TimelineId},
|
id::{TenantId, TimelineId},
|
||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
|
postgres_backend::AuthType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::local_env::{LocalEnv, DEFAULT_PG_VERSION};
|
use crate::local_env::{LocalEnv, DEFAULT_PG_VERSION};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
use anyhow::{bail, ensure, Context};
|
use anyhow::{bail, ensure, Context};
|
||||||
|
|
||||||
use postgres_backend::AuthType;
|
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
@@ -20,6 +19,7 @@ use std::process::{Command, Stdio};
|
|||||||
use utils::{
|
use utils::{
|
||||||
auth::{encode_from_key_file, Claims, Scope},
|
auth::{encode_from_key_file, Claims, Scope},
|
||||||
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
||||||
|
postgres_backend::AuthType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::safekeeper::SafekeeperNode;
|
use crate::safekeeper::SafekeeperNode;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use anyhow::{bail, Context};
|
|||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
TenantConfigRequest, TenantCreateRequest, TenantInfo, TimelineCreateRequest, TimelineInfo,
|
TenantConfigRequest, TenantCreateRequest, TenantInfo, TimelineCreateRequest, TimelineInfo,
|
||||||
};
|
};
|
||||||
use postgres_backend::AuthType;
|
|
||||||
use postgres_connection::{parse_host_port, PgConnectionConfig};
|
use postgres_connection::{parse_host_port, PgConnectionConfig};
|
||||||
use reqwest::blocking::{Client, RequestBuilder, Response};
|
use reqwest::blocking::{Client, RequestBuilder, Response};
|
||||||
use reqwest::{IntoUrl, Method};
|
use reqwest::{IntoUrl, Method};
|
||||||
@@ -21,6 +20,7 @@ use utils::{
|
|||||||
http::error::HttpErrorBody,
|
http::error::HttpErrorBody,
|
||||||
id::{TenantId, TimelineId},
|
id::{TenantId, TimelineId},
|
||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
|
postgres_backend::AuthType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{background_process, local_env::LocalEnv};
|
use crate::{background_process, local_env::LocalEnv};
|
||||||
@@ -419,11 +419,6 @@ impl PageServerNode {
|
|||||||
.map(|x| x.parse::<bool>())
|
.map(|x| x.parse::<bool>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'trace_read_requests' as bool")?,
|
.context("Failed to parse 'trace_read_requests' as bool")?,
|
||||||
eviction_policy: settings
|
|
||||||
.get("eviction_policy")
|
|
||||||
.map(|x| serde_json::from_str(x))
|
|
||||||
.transpose()
|
|
||||||
.context("Failed to parse 'eviction_policy' json")?,
|
|
||||||
})
|
})
|
||||||
.send()?
|
.send()?
|
||||||
.error_from_body()?;
|
.error_from_body()?;
|
||||||
|
|||||||
@@ -29,41 +29,6 @@ These components should not have access to the private key and may only get toke
|
|||||||
The key pair is generated once for an installation of compute/pageserver/safekeeper, e.g. by `neon_local init`.
|
The key pair is generated once for an installation of compute/pageserver/safekeeper, e.g. by `neon_local init`.
|
||||||
There is currently no way to rotate the key without bringing down all components.
|
There is currently no way to rotate the key without bringing down all components.
|
||||||
|
|
||||||
### Token format
|
|
||||||
|
|
||||||
The JWT tokens in Neon use RSA as the algorithm. Example:
|
|
||||||
|
|
||||||
Header:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"alg": "RS512", # RS256, RS384, or RS512
|
|
||||||
"typ": "JWT"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Payload:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"scope": "tenant", # "tenant", "pageserverapi", or "safekeeperdata"
|
|
||||||
"tenant_id": "5204921ff44f09de8094a1390a6a50f6",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Meanings of scope:
|
|
||||||
|
|
||||||
"tenant": Provides access to all data for a specific tenant
|
|
||||||
|
|
||||||
"pageserverapi": Provides blanket access to all tenants on the pageserver plus pageserver-wide APIs.
|
|
||||||
Should only be used e.g. for status check/tenant creation/list.
|
|
||||||
|
|
||||||
"safekeeperdata": Provides blanket access to all data on the safekeeper plus safekeeper-wide APIs.
|
|
||||||
Should only be used e.g. for status check.
|
|
||||||
Currently also used for connection from any pageserver to any safekeeper.
|
|
||||||
|
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
CLI generates a key pair during call to `neon_local init` with the following commands:
|
CLI generates a key pair during call to `neon_local init` with the following commands:
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ listen_http_addr = '127.0.0.1:9898'
|
|||||||
checkpoint_distance = '268435456' # in bytes
|
checkpoint_distance = '268435456' # in bytes
|
||||||
checkpoint_timeout = '10m'
|
checkpoint_timeout = '10m'
|
||||||
|
|
||||||
gc_period = '1 hour'
|
gc_period = '100 s'
|
||||||
gc_horizon = '67108864'
|
gc_horizon = '67108864'
|
||||||
|
|
||||||
max_file_descriptors = '100'
|
max_file_descriptors = '100'
|
||||||
@@ -101,7 +101,7 @@ away.
|
|||||||
|
|
||||||
#### gc_period
|
#### gc_period
|
||||||
|
|
||||||
Interval at which garbage collection is triggered. Default is 1 hour.
|
Interval at which garbage collection is triggered. Default is 100 s.
|
||||||
|
|
||||||
#### image_creation_threshold
|
#### image_creation_threshold
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ L0 delta layer threshold for L1 image layer creation. Default is 3.
|
|||||||
|
|
||||||
#### pitr_interval
|
#### pitr_interval
|
||||||
|
|
||||||
WAL retention duration for PITR branching. Default is 7 days.
|
WAL retention duration for PITR branching. Default is 30 days.
|
||||||
|
|
||||||
#### walreceiver_connect_timeout
|
#### walreceiver_connect_timeout
|
||||||
|
|
||||||
|
|||||||
@@ -129,12 +129,13 @@ Run `poetry shell` to activate the virtual environment.
|
|||||||
Alternatively, use `poetry run` to run a single command in the venv, e.g. `poetry run pytest`.
|
Alternatively, use `poetry run` to run a single command in the venv, e.g. `poetry run pytest`.
|
||||||
|
|
||||||
### Obligatory checks
|
### Obligatory checks
|
||||||
We force code formatting via `black`, `ruff`, and type hints via `mypy`.
|
We force code formatting via `black`, `isort` and type hints via `mypy`.
|
||||||
Run the following commands in the repository's root (next to `pyproject.toml`):
|
Run the following commands in the repository's root (next to `pyproject.toml`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
poetry run isort . # Imports are reformatted
|
||||||
poetry run black . # All code is reformatted
|
poetry run black . # All code is reformatted
|
||||||
poetry run ruff . # Python linter
|
poetry run flake8 . # Python linter
|
||||||
poetry run mypy . # Ensure there are no typing errors
|
poetry run mypy . # Ensure there are no typing errors
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
# Synthetic size
|
|
||||||
|
|
||||||
Neon storage has copy-on-write branching, which makes it difficult to
|
|
||||||
answer the question "how large is my database"? To give one reasonable
|
|
||||||
answer, we calculate _synthetic size_ for a project.
|
|
||||||
|
|
||||||
The calculation is called "synthetic", because it is based purely on
|
|
||||||
the user-visible logical size, which is the size that you would see on
|
|
||||||
a standalone PostgreSQL installation, and the amount of WAL, which is
|
|
||||||
also the same as what you'd see on a standalone PostgreSQL, for the
|
|
||||||
same set of updates.
|
|
||||||
|
|
||||||
The synthetic size does *not* depend on the actual physical size
|
|
||||||
consumed in the storage, or implementation details of the Neon storage
|
|
||||||
like garbage collection, compaction and compression. There is a
|
|
||||||
strong *correlation* between the physical size and the synthetic size,
|
|
||||||
but the synthetic size is designed to be independent of the
|
|
||||||
implementation details, so that any improvements we make in the
|
|
||||||
storage system simply reduce our COGS. And vice versa: any bugs or bad
|
|
||||||
implementation where we keep more data than we would need to, do not
|
|
||||||
change the synthetic size or incur any costs to the user.
|
|
||||||
|
|
||||||
The synthetic size is calculated for the whole project. It is not
|
|
||||||
straighforward to attribute size to individual branches. See "What is
|
|
||||||
the size of an individual branch?" for discussion on those
|
|
||||||
difficulties.
|
|
||||||
|
|
||||||
The synthetic size is designed to:
|
|
||||||
|
|
||||||
- Take into account the copy-on-write nature of the storage. For
|
|
||||||
example, if you create a branch, it doesn't immediately add anything
|
|
||||||
to the synthetic size. It starts to affect the synthetic size only
|
|
||||||
as it diverges from the parent branch.
|
|
||||||
|
|
||||||
- Be independent of any implementation details of the storage, like
|
|
||||||
garbage collection, remote storage, or compression.
|
|
||||||
|
|
||||||
## Terms & assumptions
|
|
||||||
|
|
||||||
- logical size is the size of a branch *at a given point in
|
|
||||||
time*. It's the total size of all tables in all databases, as you
|
|
||||||
see with "\l+" in psql for example, plus the Postgres SLRUs and some
|
|
||||||
small amount of metadata. NOTE that currently, Neon does not include
|
|
||||||
the SLRUs and metadata in the logical size. See comment to `get_current_logical_size_non_incremental()`.
|
|
||||||
|
|
||||||
- a "point in time" is defined as an LSN value. You can convert a
|
|
||||||
timestamp to an LSN, but the storage internally works with LSNs.
|
|
||||||
|
|
||||||
- PITR horizon can be set per-branch.
|
|
||||||
|
|
||||||
- PITR horizon can be set as a time interval, e.g. 5 days or hours, or
|
|
||||||
as amount of WAL, in bytes. If it's given as a time interval, it's
|
|
||||||
converted to an LSN for the calculation.
|
|
||||||
|
|
||||||
- PITR horizon can be set to 0, if you don't want to retain any history.
|
|
||||||
|
|
||||||
## Calculation
|
|
||||||
|
|
||||||
Inputs to the calculation are:
|
|
||||||
- logical size of the database at different points in time,
|
|
||||||
- amount of WAL generated, and
|
|
||||||
- the PITR horizon settings
|
|
||||||
|
|
||||||
The synthetic size is based on an idealistic model of the storage
|
|
||||||
system, where we pretend that the storage consists of two things:
|
|
||||||
- snapshots, containing a full snapshot of the database, at a given
|
|
||||||
point in time, and
|
|
||||||
- WAL.
|
|
||||||
|
|
||||||
In the simple case that the project contains just one branch (main),
|
|
||||||
and a fixed PITR horizon, the synthetic size is the sum of:
|
|
||||||
|
|
||||||
- the logical size of the branch *at the beginning of the PITR
|
|
||||||
horizon*, i.e. at the oldest point that you can still recover to, and
|
|
||||||
- the size of the WAL covering the PITR horizon.
|
|
||||||
|
|
||||||
The snapshot allows you to recover to the beginning of the PITR
|
|
||||||
horizon, and the WAL allows you to recover from that point to any
|
|
||||||
point within the horizon.
|
|
||||||
|
|
||||||
```
|
|
||||||
WAL
|
|
||||||
-----------------------#########>
|
|
||||||
^
|
|
||||||
snapshot
|
|
||||||
|
|
||||||
Legend:
|
|
||||||
##### PITR horizon. This is the region that you can still access
|
|
||||||
with Point-in-time query and you can still create branches
|
|
||||||
from.
|
|
||||||
----- history that has fallen out of the PITR horizon, and can no
|
|
||||||
longer be accessed
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: This is not how the storage system actually works! The actual
|
|
||||||
implementation is also based on snapshots and WAL, but the snapshots
|
|
||||||
are taken for individual database pages and ranges of pages rather
|
|
||||||
than the whole database, and it is much more complicated. This model
|
|
||||||
is a reasonable approximation, however, to make the synthetic size a
|
|
||||||
useful proxy for the actual storage consumption.
|
|
||||||
|
|
||||||
|
|
||||||
## Example: Data is INSERTed
|
|
||||||
|
|
||||||
For example, let's assume that your database contained 10 GB of data
|
|
||||||
at the beginning of the PITR horizon, and you have since then inserted
|
|
||||||
5 GB of additional data into it. The additional insertions of 5 GB of
|
|
||||||
data consume roughly 5 GB of WAL. In that case, the synthetic size is:
|
|
||||||
|
|
||||||
> 10 GB (snapshot) + 5 GB (WAL) = 15 GB
|
|
||||||
|
|
||||||
If you now set the PITR horizon on the project to 0, so that no
|
|
||||||
historical data is retained, then the beginning PITR horizon would be
|
|
||||||
at the end of the branch, so the size of the snapshot would be
|
|
||||||
calculated at the end of the branch, after the insertions. Then the
|
|
||||||
synthetic size is:
|
|
||||||
|
|
||||||
> 15 GB (snapshot) + 0 GB (WAL) = 15 GB.
|
|
||||||
|
|
||||||
In this case, the synthetic size is the same, regardless of the PITR horizon,
|
|
||||||
because all the history consists of inserts. The newly inserted data takes
|
|
||||||
up the same amount of space, whether it's stored as part of the logical
|
|
||||||
snapshot, or as WAL. (*)
|
|
||||||
|
|
||||||
(*) This is a rough approximation. In reality, the WAL contains
|
|
||||||
headers and other overhead, and on the other hand, the logical
|
|
||||||
snapshot includes empty space on pages, so the size of insertions in
|
|
||||||
WAL can be smaller or greater than the size of the final table after
|
|
||||||
the insertions. But in most cases, it's in the same ballpark.
|
|
||||||
|
|
||||||
## Example: Data is DELETEd
|
|
||||||
|
|
||||||
Let's look at another example:
|
|
||||||
|
|
||||||
Let's start again with a database that contains 10 GB of data. Then,
|
|
||||||
you DELETE 5 GB of the data, and run VACUUM to free up the space, so
|
|
||||||
that the logical size of the database is now only 5 GB.
|
|
||||||
|
|
||||||
Let's assume that the WAL for the deletions and the vacuum take up
|
|
||||||
100 MB of space. In that case, the synthetic size of the project is:
|
|
||||||
|
|
||||||
> 10 GB (snapshot) + 100 MB (WAL) = 10.1 GB
|
|
||||||
|
|
||||||
This is much larger than the logical size of the database after the
|
|
||||||
deletions (5 GB). That's because the system still needs to retain the
|
|
||||||
deleted data, because it's still accessible to queries and branching
|
|
||||||
in the PITR window.
|
|
||||||
|
|
||||||
If you now set the PITR horizon to 0 or just wait for time to pass so
|
|
||||||
that the data falls out of the PITR horizon, making the deleted data
|
|
||||||
inaccessible, the synthetic size shrinks:
|
|
||||||
|
|
||||||
> 5 GB (snapshot) + 0 GB (WAL) = 5 GB
|
|
||||||
|
|
||||||
|
|
||||||
# Branching
|
|
||||||
|
|
||||||
Things get more complicated with branching. Branches in Neon are
|
|
||||||
copy-on-write, which is also reflected in the synthetic size.
|
|
||||||
|
|
||||||
When you create a branch, it doesn't immediately change the synthetic
|
|
||||||
size at all. The branch point is within the PITR horizon, and all the
|
|
||||||
data needed to recover to that point in time needs to be retained
|
|
||||||
anyway.
|
|
||||||
|
|
||||||
However, if you make modifications on the branch, the system needs to
|
|
||||||
keep the WAL of those modifications. The WAL is included in the
|
|
||||||
synthetic size.
|
|
||||||
|
|
||||||
## Example: branch and INSERT
|
|
||||||
|
|
||||||
Let's assume that you again start with a 10 GB database.
|
|
||||||
On the main branch, you insert 2 GB of data. Then you create
|
|
||||||
a branch at that point, and insert another 3 GB of data on the
|
|
||||||
main branch, and 1 GB of data on the child branch
|
|
||||||
|
|
||||||
```
|
|
||||||
child +#####>
|
|
||||||
|
|
|
||||||
| WAL
|
|
||||||
main ---------###############>
|
|
||||||
^
|
|
||||||
snapshot
|
|
||||||
```
|
|
||||||
|
|
||||||
In this case, the synthetic size consists of:
|
|
||||||
- the snapshot at the beginning of the PITR horizon (10 GB)
|
|
||||||
- the WAL on the main branch (2 GB + 3 GB = 5 GB)
|
|
||||||
- the WAL on the child branch (1 GB)
|
|
||||||
|
|
||||||
Total: 16 GB
|
|
||||||
|
|
||||||
# Diverging branches
|
|
||||||
|
|
||||||
If there is only a small amount of changes in the database on the
|
|
||||||
different branches, as in the previous example, the synthetic size
|
|
||||||
consists of a snapshot before the branch point, containing all the
|
|
||||||
shared data, and the WAL on both branches. However, if the branches
|
|
||||||
diverge a lot, it is more efficient to store a separate snapshot of
|
|
||||||
branches.
|
|
||||||
|
|
||||||
## Example: diverging branches
|
|
||||||
|
|
||||||
You start with a 10 GB database. You insert 5 GB of data on the main
|
|
||||||
branch. Then you create a branch, and immediately delete all the data
|
|
||||||
on the child branch and insert 5 GB of new data to it. Then you do the
|
|
||||||
same on the main branch. Let's assume
|
|
||||||
that the PITR horizon requires keeping the last 1 GB of WAL on the
|
|
||||||
both branches.
|
|
||||||
|
|
||||||
```
|
|
||||||
snapshot
|
|
||||||
v WAL
|
|
||||||
child +---------##############>
|
|
||||||
|
|
|
||||||
|
|
|
||||||
main -------------+---------##############>
|
|
||||||
^ WAL
|
|
||||||
snapshot
|
|
||||||
```
|
|
||||||
|
|
||||||
In this case, the synthetic size consists of:
|
|
||||||
- snapshot at the beginning of the PITR horizon on the main branch (4 GB)
|
|
||||||
- WAL on the main branch (1 GB)
|
|
||||||
- snapshot at the beginning of the PITR horizon on the child branch (4 GB)
|
|
||||||
- last 1 GB of WAL on the child branch (1 GB)
|
|
||||||
|
|
||||||
Total: 10 GB
|
|
||||||
|
|
||||||
The alternative way to store this would be to take only one snapshot
|
|
||||||
at the beginning of branch point, and keep all the WAL on both
|
|
||||||
branches. However, the size with that method would be larger, as it
|
|
||||||
would require one 10 GB snapshot, and 5 GB + 5 GB of WAL. It depends
|
|
||||||
on the amount of changes (WAL) on both branches, and the logical size
|
|
||||||
at the branch point, which method would result in a smaller synthetic
|
|
||||||
size. On each branch point, the system performs the calculation with
|
|
||||||
both methods, and uses the method that is cheaper, i.e. the one that
|
|
||||||
results in a smaller synthetic size.
|
|
||||||
|
|
||||||
One way to think about this is that when you create a branch, it
|
|
||||||
starts out as a thin branch that only stores the WAL since the branch
|
|
||||||
point. As you modify it, and the amount of WAL grows, at some point
|
|
||||||
it becomes cheaper to store a completely new snapshot of the branch
|
|
||||||
and truncate the WAL.
|
|
||||||
|
|
||||||
|
|
||||||
# What is the size of an individual branch?
|
|
||||||
|
|
||||||
Synthetic size is calculated for the whole project, and includes all
|
|
||||||
branches. There is no such thing as the size of a branch, because it
|
|
||||||
is not straighforward to attribute the parts of size to individual
|
|
||||||
branches.
|
|
||||||
|
|
||||||
## Example: attributing size to branches
|
|
||||||
|
|
||||||
(copied from https://github.com/neondatabase/neon/pull/2884#discussion_r1029365278)
|
|
||||||
|
|
||||||
Imagine that you create two branches, A and B, at the same point from
|
|
||||||
main branch, and do a couple of small updates on both branches. Then
|
|
||||||
six months pass, and during those six months the data on the main
|
|
||||||
branch churns over completely multiple times. The retention period is,
|
|
||||||
say 1 month.
|
|
||||||
|
|
||||||
```
|
|
||||||
+------> A
|
|
||||||
/
|
|
||||||
--------------------*-------------------------------> main
|
|
||||||
\
|
|
||||||
+--------> B
|
|
||||||
```
|
|
||||||
|
|
||||||
In that situation, the synthetic tenant size would be calculated based
|
|
||||||
on a "logical snapshot" at the branch point, that is, the logical size
|
|
||||||
of the database at that point. Plus the WAL on branches A and B. Let's
|
|
||||||
say that the snapshot size is 10 GB, and the WAL is 1 MB on both
|
|
||||||
branches A and B. So the total synthetic storage size is 10002
|
|
||||||
MB. (Let's ignore the main branch for now, that would be just added to
|
|
||||||
the sum)
|
|
||||||
|
|
||||||
How would you break that down per branch? I can think of three
|
|
||||||
different ways to do it, and all of them have their own problems:
|
|
||||||
|
|
||||||
### Subtraction method
|
|
||||||
|
|
||||||
For each branch, calculate how much smaller the total synthetic size
|
|
||||||
would be, if that branch didn't exist. In other words, how much would
|
|
||||||
you save if you dropped the branch. With this method, the size of
|
|
||||||
branches A and B is 1 MB.
|
|
||||||
|
|
||||||
With this method, the 10 GB shared logical snapshot is not included
|
|
||||||
for A nor B. So the size of all branches is not equal to the total
|
|
||||||
synthetic size of the tenant. If you drop branch A, you save 1 MB as
|
|
||||||
you'd expect, but also the size of B suddenly jumps from 1 MB to 10001
|
|
||||||
MB, which might feel surprising.
|
|
||||||
|
|
||||||
### Division method
|
|
||||||
|
|
||||||
Divide the common parts evenly across all branches that need
|
|
||||||
them. With this method, the size of branches A and B would be 5001 MB.
|
|
||||||
|
|
||||||
With this method, the sum of all branches adds up to the total
|
|
||||||
synthetic size. But it's surprising in other ways: if you drop branch
|
|
||||||
A, you might think that you save 5001 MB, but in reality you only save
|
|
||||||
1 MB, and the size of branch B suddenly grows from 5001 to 10001 MB.
|
|
||||||
|
|
||||||
### Addition method
|
|
||||||
|
|
||||||
For each branch, include all the snapshots and WAL that it depends on,
|
|
||||||
even if some of them are shared by other branches. With this method,
|
|
||||||
the size of branches A and B would be 10001 MB.
|
|
||||||
|
|
||||||
The surprise with this method is that the sum of all the branches is
|
|
||||||
larger than the total synthetic size. And if you drop branch A, the
|
|
||||||
total synthetic size doesn't fall by 10001 MB as you might think.
|
|
||||||
|
|
||||||
# Alternatives
|
|
||||||
|
|
||||||
A sort of cop-out method would be to show the whole tree of branches
|
|
||||||
graphically, and for each section of WAL or logical snapshot, display
|
|
||||||
the size of that section. You can then see which branches depend on
|
|
||||||
which sections, which sections are shared etc. That would be good to
|
|
||||||
have in the UI anyway.
|
|
||||||
|
|
||||||
Or perhaps calculate per-branch numbers using the subtraction method,
|
|
||||||
and in addition to that, one more number for "shared size" that
|
|
||||||
includes all the data that is needed by more than one branch.
|
|
||||||
|
|
||||||
## Which is the right method?
|
|
||||||
|
|
||||||
The bottom line is that it's not straightforward to attribute the
|
|
||||||
synthetic size to individual branches. There are things we can do, and
|
|
||||||
all of those methods are pretty straightforward to implement, but they
|
|
||||||
all have their own problems. What makes sense depends a lot on what
|
|
||||||
you want to do with the number, what question you are trying to
|
|
||||||
answer.
|
|
||||||
@@ -8,6 +8,5 @@ license.workspace = true
|
|||||||
prometheus.workspace = true
|
prometheus.workspace = true
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
chrono.workspace = true
|
|
||||||
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
//! A timestamp captured at process startup to identify restarts of the process, e.g., in logs and metrics.
|
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
|
|
||||||
use super::register_uint_gauge;
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
pub struct LaunchTimestamp(chrono::DateTime<Utc>);
|
|
||||||
|
|
||||||
impl LaunchTimestamp {
|
|
||||||
pub fn generate() -> Self {
|
|
||||||
LaunchTimestamp(Utc::now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for LaunchTimestamp {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_launch_timestamp_metric(launch_ts: &'static LaunchTimestamp) {
|
|
||||||
let millis_since_epoch: u64 = launch_ts
|
|
||||||
.0
|
|
||||||
.timestamp_millis()
|
|
||||||
.try_into()
|
|
||||||
.expect("we're after the epoch, this should be positive");
|
|
||||||
let metric = register_uint_gauge!(
|
|
||||||
"libmetrics_launch_timestamp",
|
|
||||||
"Timestamp (millis since epoch) at wich the process launched."
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
metric.set(millis_since_epoch);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ pub use prometheus::opts;
|
|||||||
pub use prometheus::register;
|
pub use prometheus::register;
|
||||||
pub use prometheus::{core, default_registry, proto};
|
pub use prometheus::{core, default_registry, proto};
|
||||||
pub use prometheus::{exponential_buckets, linear_buckets};
|
pub use prometheus::{exponential_buckets, linear_buckets};
|
||||||
pub use prometheus::{register_counter_vec, Counter, CounterVec};
|
|
||||||
pub use prometheus::{register_gauge, Gauge};
|
pub use prometheus::{register_gauge, Gauge};
|
||||||
pub use prometheus::{register_gauge_vec, GaugeVec};
|
pub use prometheus::{register_gauge_vec, GaugeVec};
|
||||||
pub use prometheus::{register_histogram, Histogram};
|
pub use prometheus::{register_histogram, Histogram};
|
||||||
@@ -20,7 +19,6 @@ pub use prometheus::{register_int_gauge_vec, IntGaugeVec};
|
|||||||
pub use prometheus::{Encoder, TextEncoder};
|
pub use prometheus::{Encoder, TextEncoder};
|
||||||
use prometheus::{Registry, Result};
|
use prometheus::{Registry, Result};
|
||||||
|
|
||||||
pub mod launch_timestamp;
|
|
||||||
mod wrappers;
|
mod wrappers;
|
||||||
pub use wrappers::{CountedReader, CountedWriter};
|
pub use wrappers::{CountedReader, CountedWriter};
|
||||||
|
|
||||||
@@ -35,14 +33,6 @@ macro_rules! register_uint_gauge_vec {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! register_uint_gauge {
|
|
||||||
($NAME:expr, $HELP:expr $(,)?) => {{
|
|
||||||
let gauge = $crate::UIntGauge::new($NAME, $HELP).unwrap();
|
|
||||||
$crate::register(Box::new(gauge.clone())).map(|_| gauge)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Special internal registry, to collect metrics independently from the default registry.
|
/// Special internal registry, to collect metrics independently from the default registry.
|
||||||
/// Was introduced to fix deadlock with lazy registration of metrics in the default registry.
|
/// Was introduced to fix deadlock with lazy registration of metrics in the default registry.
|
||||||
static INTERNAL_REGISTRY: Lazy<Registry> = Lazy::new(Registry::new);
|
static INTERNAL_REGISTRY: Lazy<Registry> = Lazy::new(Registry::new);
|
||||||
|
|||||||
@@ -13,7 +13,5 @@ bytes.workspace = true
|
|||||||
byteorder.workspace = true
|
byteorder.workspace = true
|
||||||
utils.workspace = true
|
utils.workspace = true
|
||||||
postgres_ffi.workspace = true
|
postgres_ffi.workspace = true
|
||||||
enum-map.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
use std::{
|
use std::num::{NonZeroU64, NonZeroUsize};
|
||||||
collections::HashMap,
|
|
||||||
num::{NonZeroU64, NonZeroUsize},
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use byteorder::{BigEndian, ReadBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
use utils::{
|
use utils::{
|
||||||
history_buffer::HistoryBufferWithDropCounter,
|
|
||||||
id::{NodeId, TenantId, TimelineId},
|
id::{NodeId, TenantId, TimelineId},
|
||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
};
|
};
|
||||||
@@ -34,14 +29,6 @@ pub enum TenantState {
|
|||||||
Broken,
|
Broken,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod state {
|
|
||||||
pub const LOADING: &str = "loading";
|
|
||||||
pub const ATTACHING: &str = "attaching";
|
|
||||||
pub const ACTIVE: &str = "active";
|
|
||||||
pub const STOPPING: &str = "stopping";
|
|
||||||
pub const BROKEN: &str = "broken";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TenantState {
|
impl TenantState {
|
||||||
pub fn has_in_progress_downloads(&self) -> bool {
|
pub fn has_in_progress_downloads(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
@@ -52,16 +39,6 @@ impl TenantState {
|
|||||||
Self::Broken => false,
|
Self::Broken => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
TenantState::Loading => state::LOADING,
|
|
||||||
TenantState::Attaching => state::ATTACHING,
|
|
||||||
TenantState::Active => state::ACTIVE,
|
|
||||||
TenantState::Stopping => state::STOPPING,
|
|
||||||
TenantState::Broken => state::BROKEN,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A state of a timeline in pageserver's memory.
|
/// A state of a timeline in pageserver's memory.
|
||||||
@@ -142,6 +119,7 @@ pub struct TenantConfigRequest {
|
|||||||
#[serde_as(as = "DisplayFromStr")]
|
#[serde_as(as = "DisplayFromStr")]
|
||||||
pub tenant_id: TenantId,
|
pub tenant_id: TenantId,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
pub checkpoint_distance: Option<u64>,
|
pub checkpoint_distance: Option<u64>,
|
||||||
pub checkpoint_timeout: Option<String>,
|
pub checkpoint_timeout: Option<String>,
|
||||||
pub compaction_target_size: Option<u64>,
|
pub compaction_target_size: Option<u64>,
|
||||||
@@ -155,11 +133,6 @@ pub struct TenantConfigRequest {
|
|||||||
pub lagging_wal_timeout: Option<String>,
|
pub lagging_wal_timeout: Option<String>,
|
||||||
pub max_lsn_wal_lag: Option<NonZeroU64>,
|
pub max_lsn_wal_lag: Option<NonZeroU64>,
|
||||||
pub trace_read_requests: Option<bool>,
|
pub trace_read_requests: Option<bool>,
|
||||||
// We defer the parsing of the eviction_policy field to the request handler.
|
|
||||||
// Otherwise we'd have to move the types for eviction policy into this package.
|
|
||||||
// We might do that once the eviction feature has stabilizied.
|
|
||||||
// For now, this field is not even documented in the openapi_spec.yml.
|
|
||||||
pub eviction_policy: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TenantConfigRequest {
|
impl TenantConfigRequest {
|
||||||
@@ -179,7 +152,6 @@ impl TenantConfigRequest {
|
|||||||
lagging_wal_timeout: None,
|
lagging_wal_timeout: None,
|
||||||
max_lsn_wal_lag: None,
|
max_lsn_wal_lag: None,
|
||||||
trace_read_requests: None,
|
trace_read_requests: None,
|
||||||
eviction_policy: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,130 +209,6 @@ pub struct TimelineInfo {
|
|||||||
pub state: TimelineState,
|
pub state: TimelineState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct LayerMapInfo {
|
|
||||||
pub in_memory_layers: Vec<InMemoryLayerInfo>,
|
|
||||||
pub historic_layers: Vec<HistoricLayerInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, enum_map::Enum)]
|
|
||||||
#[repr(usize)]
|
|
||||||
pub enum LayerAccessKind {
|
|
||||||
GetValueReconstructData,
|
|
||||||
Iter,
|
|
||||||
KeyIter,
|
|
||||||
Dump,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LayerAccessStatFullDetails {
|
|
||||||
pub when_millis_since_epoch: u64,
|
|
||||||
pub task_kind: &'static str,
|
|
||||||
pub access_kind: LayerAccessKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An event that impacts the layer's residence status.
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LayerResidenceEvent {
|
|
||||||
/// The time when the event occurred.
|
|
||||||
/// NB: this timestamp is captured while the residence status changes.
|
|
||||||
/// So, it might be behind/ahead of the actual residence change by a short amount of time.
|
|
||||||
///
|
|
||||||
#[serde(rename = "timestamp_millis_since_epoch")]
|
|
||||||
#[serde_as(as = "serde_with::TimestampMilliSeconds")]
|
|
||||||
pub timestamp: SystemTime,
|
|
||||||
/// The new residence status of the layer.
|
|
||||||
pub status: LayerResidenceStatus,
|
|
||||||
/// The reason why we had to record this event.
|
|
||||||
pub reason: LayerResidenceEventReason,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The reason for recording a given [`ResidenceEvent`].
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum LayerResidenceEventReason {
|
|
||||||
/// The layer map is being populated, e.g. during timeline load or attach.
|
|
||||||
/// This includes [`RemoteLayer`] objects created in [`reconcile_with_remote`].
|
|
||||||
/// We need to record such events because there is no persistent storage for the events.
|
|
||||||
LayerLoad,
|
|
||||||
/// We just created the layer (e.g., freeze_and_flush or compaction).
|
|
||||||
/// Such layers are always [`LayerResidenceStatus::Resident`].
|
|
||||||
LayerCreate,
|
|
||||||
/// We on-demand downloaded or evicted the given layer.
|
|
||||||
ResidenceChange,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The residence status of the layer, after the given [`LayerResidenceEvent`].
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum LayerResidenceStatus {
|
|
||||||
/// Residence status for a layer file that exists locally.
|
|
||||||
/// It may also exist on the remote, we don't care here.
|
|
||||||
Resident,
|
|
||||||
/// Residence status for a layer file that only exists on the remote.
|
|
||||||
Evicted,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayerResidenceEvent {
|
|
||||||
pub fn new(status: LayerResidenceStatus, reason: LayerResidenceEventReason) -> Self {
|
|
||||||
Self {
|
|
||||||
status,
|
|
||||||
reason,
|
|
||||||
timestamp: SystemTime::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct LayerAccessStats {
|
|
||||||
pub access_count_by_access_kind: HashMap<LayerAccessKind, u64>,
|
|
||||||
pub task_kind_access_flag: Vec<&'static str>,
|
|
||||||
pub first: Option<LayerAccessStatFullDetails>,
|
|
||||||
pub accesses_history: HistoryBufferWithDropCounter<LayerAccessStatFullDetails, 16>,
|
|
||||||
pub residence_events_history: HistoryBufferWithDropCounter<LayerResidenceEvent, 16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[serde(tag = "kind")]
|
|
||||||
pub enum InMemoryLayerInfo {
|
|
||||||
Open {
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
lsn_start: Lsn,
|
|
||||||
},
|
|
||||||
Frozen {
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
lsn_start: Lsn,
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
lsn_end: Lsn,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[serde(tag = "kind")]
|
|
||||||
pub enum HistoricLayerInfo {
|
|
||||||
Delta {
|
|
||||||
layer_file_name: String,
|
|
||||||
layer_file_size: Option<u64>,
|
|
||||||
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
lsn_start: Lsn,
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
lsn_end: Lsn,
|
|
||||||
remote: bool,
|
|
||||||
access_stats: LayerAccessStats,
|
|
||||||
},
|
|
||||||
Image {
|
|
||||||
layer_file_name: String,
|
|
||||||
layer_file_size: Option<u64>,
|
|
||||||
|
|
||||||
#[serde_as(as = "DisplayFromStr")]
|
|
||||||
lsn_start: Lsn,
|
|
||||||
remote: bool,
|
|
||||||
access_stats: LayerAccessStats,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct DownloadRemoteLayersTaskSpawnRequest {
|
pub struct DownloadRemoteLayersTaskSpawnRequest {
|
||||||
pub max_concurrent_downloads: NonZeroUsize,
|
pub max_concurrent_downloads: NonZeroUsize,
|
||||||
@@ -401,7 +249,7 @@ pub struct TimelineGcRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrapped in libpq CopyData
|
// Wrapped in libpq CopyData
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum PagestreamFeMessage {
|
pub enum PagestreamFeMessage {
|
||||||
Exists(PagestreamExistsRequest),
|
Exists(PagestreamExistsRequest),
|
||||||
Nblocks(PagestreamNblocksRequest),
|
Nblocks(PagestreamNblocksRequest),
|
||||||
|
|||||||
@@ -98,15 +98,6 @@ impl RelTag {
|
|||||||
|
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_forknum(&self, forknum: u8) -> Self {
|
|
||||||
RelTag {
|
|
||||||
forknum,
|
|
||||||
spcnode: self.spcnode,
|
|
||||||
dbnode: self.dbnode,
|
|
||||||
relnode: self.relnode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "postgres_backend"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-trait.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
bytes.workspace = true
|
|
||||||
futures.workspace = true
|
|
||||||
rustls.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
thiserror.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
tokio-rustls.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
|
|
||||||
pq_proto.workspace = true
|
|
||||||
workspace_hack.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
once_cell.workspace = true
|
|
||||||
rustls-pemfile.workspace = true
|
|
||||||
tokio-postgres.workspace = true
|
|
||||||
tokio-postgres-rustls.workspace = true
|
|
||||||
@@ -1,931 +0,0 @@
|
|||||||
//! Server-side asynchronous Postgres connection, as limited as we need.
|
|
||||||
//! To use, create PostgresBackend and run() it, passing the Handler
|
|
||||||
//! implementation determining how to process the queries. Currently its API
|
|
||||||
//! is rather narrow, but we can extend it once required.
|
|
||||||
use anyhow::Context;
|
|
||||||
use bytes::Bytes;
|
|
||||||
use futures::pin_mut;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::task::{ready, Poll};
|
|
||||||
use std::{fmt, io};
|
|
||||||
use std::{future::Future, str::FromStr};
|
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
|
||||||
use tokio_rustls::TlsAcceptor;
|
|
||||||
use tracing::{debug, error, info, trace};
|
|
||||||
|
|
||||||
use pq_proto::framed::{ConnectionError, Framed, FramedReader, FramedWriter};
|
|
||||||
use pq_proto::{
|
|
||||||
BeMessage, FeMessage, FeStartupPacket, ProtocolError, SQLSTATE_INTERNAL_ERROR,
|
|
||||||
SQLSTATE_SUCCESSFUL_COMPLETION,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// An error, occurred during query processing:
|
|
||||||
/// either during the connection ([`ConnectionError`]) or before/after it.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum QueryError {
|
|
||||||
/// The connection was lost while processing the query.
|
|
||||||
#[error(transparent)]
|
|
||||||
Disconnected(#[from] ConnectionError),
|
|
||||||
/// Some other error
|
|
||||||
#[error(transparent)]
|
|
||||||
Other(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for QueryError {
|
|
||||||
fn from(e: io::Error) -> Self {
|
|
||||||
Self::Disconnected(ConnectionError::Io(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueryError {
|
|
||||||
pub fn pg_error_code(&self) -> &'static [u8; 5] {
|
|
||||||
match self {
|
|
||||||
Self::Disconnected(_) => b"08006", // connection failure
|
|
||||||
Self::Other(_) => SQLSTATE_INTERNAL_ERROR, // internal error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_expected_io_error(e: &io::Error) -> bool {
|
|
||||||
use io::ErrorKind::*;
|
|
||||||
matches!(
|
|
||||||
e.kind(),
|
|
||||||
ConnectionRefused | ConnectionAborted | ConnectionReset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait Handler<IO> {
|
|
||||||
/// Handle single query.
|
|
||||||
/// postgres_backend will issue ReadyForQuery after calling this (this
|
|
||||||
/// might be not what we want after CopyData streaming, but currently we don't
|
|
||||||
/// care). It will also flush out the output buffer.
|
|
||||||
async fn process_query(
|
|
||||||
&mut self,
|
|
||||||
pgb: &mut PostgresBackend<IO>,
|
|
||||||
query_string: &str,
|
|
||||||
) -> Result<(), QueryError>;
|
|
||||||
|
|
||||||
/// Called on startup packet receival, allows to process params.
|
|
||||||
///
|
|
||||||
/// If Ok(false) is returned postgres_backend will skip auth -- that is needed for new users
|
|
||||||
/// creation is the proxy code. That is quite hacky and ad-hoc solution, may be we could allow
|
|
||||||
/// to override whole init logic in implementations.
|
|
||||||
fn startup(
|
|
||||||
&mut self,
|
|
||||||
_pgb: &mut PostgresBackend<IO>,
|
|
||||||
_sm: &FeStartupPacket,
|
|
||||||
) -> Result<(), QueryError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check auth jwt
|
|
||||||
fn check_auth_jwt(
|
|
||||||
&mut self,
|
|
||||||
_pgb: &mut PostgresBackend<IO>,
|
|
||||||
_jwt_response: &[u8],
|
|
||||||
) -> Result<(), QueryError> {
|
|
||||||
Err(QueryError::Other(anyhow::anyhow!("JWT auth failed")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PostgresBackend protocol state.
|
|
||||||
/// XXX: The order of the constructors matters.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd)]
|
|
||||||
pub enum ProtoState {
|
|
||||||
/// Nothing happened yet.
|
|
||||||
Initialization,
|
|
||||||
/// Encryption handshake is done; waiting for encrypted Startup message.
|
|
||||||
Encrypted,
|
|
||||||
/// Waiting for password (auth token).
|
|
||||||
Authentication,
|
|
||||||
/// Performed handshake and auth, ReadyForQuery is issued.
|
|
||||||
Established,
|
|
||||||
Closed,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum ProcessMsgResult {
|
|
||||||
Continue,
|
|
||||||
Break,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Either plain TCP stream or encrypted one, implementing AsyncRead + AsyncWrite.
|
|
||||||
pub enum MaybeTlsStream<IO> {
|
|
||||||
Unencrypted(IO),
|
|
||||||
Tls(Box<tokio_rustls::server::TlsStream<IO>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin> AsyncWrite for MaybeTlsStream<IO> {
|
|
||||||
fn poll_write(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> Poll<io::Result<usize>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
Self::Unencrypted(stream) => Pin::new(stream).poll_write(cx, buf),
|
|
||||||
Self::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
Self::Unencrypted(stream) => Pin::new(stream).poll_flush(cx),
|
|
||||||
Self::Tls(stream) => Pin::new(stream).poll_flush(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn poll_shutdown(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> Poll<io::Result<()>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
Self::Unencrypted(stream) => Pin::new(stream).poll_shutdown(cx),
|
|
||||||
Self::Tls(stream) => Pin::new(stream).poll_shutdown(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin> AsyncRead for MaybeTlsStream<IO> {
|
|
||||||
fn poll_read(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
buf: &mut tokio::io::ReadBuf<'_>,
|
|
||||||
) -> Poll<io::Result<()>> {
|
|
||||||
match self.get_mut() {
|
|
||||||
Self::Unencrypted(stream) => Pin::new(stream).poll_read(cx, buf),
|
|
||||||
Self::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum AuthType {
|
|
||||||
Trust,
|
|
||||||
// This mimics postgres's AuthenticationCleartextPassword but instead of password expects JWT
|
|
||||||
NeonJWT,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for AuthType {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"Trust" => Ok(Self::Trust),
|
|
||||||
"NeonJWT" => Ok(Self::NeonJWT),
|
|
||||||
_ => anyhow::bail!("invalid value \"{s}\" for auth type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for AuthType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.write_str(match self {
|
|
||||||
AuthType::Trust => "Trust",
|
|
||||||
AuthType::NeonJWT => "NeonJWT",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Either full duplex Framed or write only half; the latter is left in
|
|
||||||
/// PostgresBackend after call to `split`. In principle we could always store a
|
|
||||||
/// pair of splitted handles, but that would force to to pay splitting price
|
|
||||||
/// (Arc and kinda mutex inside polling) for all uses (e.g. pageserver).
|
|
||||||
enum MaybeWriteOnly<IO> {
|
|
||||||
Full(Framed<MaybeTlsStream<IO>>),
|
|
||||||
WriteOnly(FramedWriter<MaybeTlsStream<IO>>),
|
|
||||||
Broken, // temporary value palmed off during the split
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin> MaybeWriteOnly<IO> {
|
|
||||||
async fn read_startup_message(&mut self) -> Result<Option<FeStartupPacket>, ConnectionError> {
|
|
||||||
match self {
|
|
||||||
MaybeWriteOnly::Full(framed) => framed.read_startup_message().await,
|
|
||||||
MaybeWriteOnly::WriteOnly(_) => {
|
|
||||||
Err(io::Error::new(ErrorKind::Other, "reading from write only half").into())
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::Broken => panic!("IO on invalid MaybeWriteOnly"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> {
|
|
||||||
match self {
|
|
||||||
MaybeWriteOnly::Full(framed) => framed.read_message().await,
|
|
||||||
MaybeWriteOnly::WriteOnly(_) => {
|
|
||||||
Err(io::Error::new(ErrorKind::Other, "reading from write only half").into())
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::Broken => panic!("IO on invalid MaybeWriteOnly"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_message_noflush(&mut self, msg: &BeMessage<'_>) -> Result<(), ProtocolError> {
|
|
||||||
match self {
|
|
||||||
MaybeWriteOnly::Full(framed) => framed.write_message(msg),
|
|
||||||
MaybeWriteOnly::WriteOnly(framed_writer) => framed_writer.write_message_noflush(msg),
|
|
||||||
MaybeWriteOnly::Broken => panic!("IO on invalid MaybeWriteOnly"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn flush(&mut self) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
MaybeWriteOnly::Full(framed) => framed.flush().await,
|
|
||||||
MaybeWriteOnly::WriteOnly(framed_writer) => framed_writer.flush().await,
|
|
||||||
MaybeWriteOnly::Broken => panic!("IO on invalid MaybeWriteOnly"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shutdown(&mut self) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
MaybeWriteOnly::Full(framed) => framed.shutdown().await,
|
|
||||||
MaybeWriteOnly::WriteOnly(framed_writer) => framed_writer.shutdown().await,
|
|
||||||
MaybeWriteOnly::Broken => panic!("IO on invalid MaybeWriteOnly"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PostgresBackend<IO> {
|
|
||||||
framed: MaybeWriteOnly<IO>,
|
|
||||||
|
|
||||||
pub state: ProtoState,
|
|
||||||
|
|
||||||
auth_type: AuthType,
|
|
||||||
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
pub tls_config: Option<Arc<rustls::ServerConfig>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type PostgresBackendTCP = PostgresBackend<tokio::net::TcpStream>;
|
|
||||||
|
|
||||||
pub fn query_from_cstring(query_string: Bytes) -> Vec<u8> {
|
|
||||||
let mut query_string = query_string.to_vec();
|
|
||||||
if let Some(ch) = query_string.last() {
|
|
||||||
if *ch == 0 {
|
|
||||||
query_string.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query_string
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cast a byte slice to a string slice, dropping null terminator if there's one.
|
|
||||||
fn cstr_to_str(bytes: &[u8]) -> anyhow::Result<&str> {
|
|
||||||
let without_null = bytes.strip_suffix(&[0]).unwrap_or(bytes);
|
|
||||||
std::str::from_utf8(without_null).map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PostgresBackend<tokio::net::TcpStream> {
|
|
||||||
pub fn new(
|
|
||||||
socket: tokio::net::TcpStream,
|
|
||||||
auth_type: AuthType,
|
|
||||||
tls_config: Option<Arc<rustls::ServerConfig>>,
|
|
||||||
) -> io::Result<Self> {
|
|
||||||
let peer_addr = socket.peer_addr()?;
|
|
||||||
let stream = MaybeTlsStream::Unencrypted(socket);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
framed: MaybeWriteOnly::Full(Framed::new(stream)),
|
|
||||||
state: ProtoState::Initialization,
|
|
||||||
auth_type,
|
|
||||||
tls_config,
|
|
||||||
peer_addr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackend<IO> {
|
|
||||||
pub fn new_from_io(
|
|
||||||
socket: IO,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
auth_type: AuthType,
|
|
||||||
tls_config: Option<Arc<rustls::ServerConfig>>,
|
|
||||||
) -> io::Result<Self> {
|
|
||||||
let stream = MaybeTlsStream::Unencrypted(socket);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
framed: MaybeWriteOnly::Full(Framed::new(stream)),
|
|
||||||
state: ProtoState::Initialization,
|
|
||||||
auth_type,
|
|
||||||
tls_config,
|
|
||||||
peer_addr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_peer_addr(&self) -> &SocketAddr {
|
|
||||||
&self.peer_addr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read full message or return None if connection is cleanly closed with no
|
|
||||||
/// unprocessed data.
|
|
||||||
pub async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> {
|
|
||||||
if let ProtoState::Closed = self.state {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
let m = self.framed.read_message().await?;
|
|
||||||
trace!("read msg {:?}", m);
|
|
||||||
Ok(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write message into internal output buffer, doesn't flush it. Technically
|
|
||||||
/// error type can be only ProtocolError here (if, unlikely, serialization
|
|
||||||
/// fails), but callers typically wrap it anyway.
|
|
||||||
pub fn write_message_noflush(
|
|
||||||
&mut self,
|
|
||||||
message: &BeMessage<'_>,
|
|
||||||
) -> Result<&mut Self, ConnectionError> {
|
|
||||||
self.framed.write_message_noflush(message)?;
|
|
||||||
trace!("wrote msg {:?}", message);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flush output buffer into the socket.
|
|
||||||
pub async fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.framed.flush().await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Polling version of `flush()`, saves the caller need to pin.
|
|
||||||
pub fn poll_flush(
|
|
||||||
&mut self,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> Poll<Result<(), std::io::Error>> {
|
|
||||||
let flush_fut = self.flush();
|
|
||||||
pin_mut!(flush_fut);
|
|
||||||
flush_fut.poll(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write message into internal output buffer and flush it to the stream.
|
|
||||||
pub async fn write_message(
|
|
||||||
&mut self,
|
|
||||||
message: &BeMessage<'_>,
|
|
||||||
) -> Result<&mut Self, ConnectionError> {
|
|
||||||
self.write_message_noflush(message)?;
|
|
||||||
self.flush().await?;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an AsyncWrite implementation that wraps all the data written
|
|
||||||
/// to it in CopyData messages, and writes them to the connection
|
|
||||||
///
|
|
||||||
/// The caller is responsible for sending CopyOutResponse and CopyDone messages.
|
|
||||||
pub fn copyout_writer(&mut self) -> CopyDataWriter<IO> {
|
|
||||||
CopyDataWriter { pgb: self }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper for run_message_loop() that shuts down socket when we are done
|
|
||||||
pub async fn run<F, S>(
|
|
||||||
mut self,
|
|
||||||
handler: &mut impl Handler<IO>,
|
|
||||||
shutdown_watcher: F,
|
|
||||||
) -> Result<(), QueryError>
|
|
||||||
where
|
|
||||||
F: Fn() -> S,
|
|
||||||
S: Future,
|
|
||||||
{
|
|
||||||
let ret = self.run_message_loop(handler, shutdown_watcher).await;
|
|
||||||
// socket might be already closed, e.g. if previously received error,
|
|
||||||
// so ignore result.
|
|
||||||
self.framed.shutdown().await.ok();
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_message_loop<F, S>(
|
|
||||||
&mut self,
|
|
||||||
handler: &mut impl Handler<IO>,
|
|
||||||
shutdown_watcher: F,
|
|
||||||
) -> Result<(), QueryError>
|
|
||||||
where
|
|
||||||
F: Fn() -> S,
|
|
||||||
S: Future,
|
|
||||||
{
|
|
||||||
trace!("postgres backend to {:?} started", self.peer_addr);
|
|
||||||
|
|
||||||
tokio::select!(
|
|
||||||
biased;
|
|
||||||
|
|
||||||
_ = shutdown_watcher() => {
|
|
||||||
// We were requested to shut down.
|
|
||||||
tracing::info!("shutdown request received during handshake");
|
|
||||||
return Ok(())
|
|
||||||
},
|
|
||||||
|
|
||||||
result = self.handshake(handler) => {
|
|
||||||
// Handshake complete.
|
|
||||||
result?;
|
|
||||||
if self.state == ProtoState::Closed {
|
|
||||||
return Ok(()); // EOF during handshake
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Authentication completed
|
|
||||||
let mut query_string = Bytes::new();
|
|
||||||
while let Some(msg) = tokio::select!(
|
|
||||||
biased;
|
|
||||||
_ = shutdown_watcher() => {
|
|
||||||
// We were requested to shut down.
|
|
||||||
tracing::info!("shutdown request received in run_message_loop");
|
|
||||||
Ok(None)
|
|
||||||
},
|
|
||||||
msg = self.read_message() => { msg },
|
|
||||||
)? {
|
|
||||||
trace!("got message {:?}", msg);
|
|
||||||
|
|
||||||
let result = self.process_message(handler, msg, &mut query_string).await;
|
|
||||||
self.flush().await?;
|
|
||||||
match result? {
|
|
||||||
ProcessMsgResult::Continue => {
|
|
||||||
self.flush().await?;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ProcessMsgResult::Break => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("postgres backend to {:?} exited", self.peer_addr);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to upgrade MaybeTlsStream into actual TLS one, performing handshake.
|
|
||||||
async fn tls_upgrade(
|
|
||||||
src: MaybeTlsStream<IO>,
|
|
||||||
tls_config: Arc<rustls::ServerConfig>,
|
|
||||||
) -> anyhow::Result<MaybeTlsStream<IO>> {
|
|
||||||
match src {
|
|
||||||
MaybeTlsStream::Unencrypted(s) => {
|
|
||||||
let acceptor = TlsAcceptor::from(tls_config);
|
|
||||||
let tls_stream = acceptor.accept(s).await?;
|
|
||||||
Ok(MaybeTlsStream::Tls(Box::new(tls_stream)))
|
|
||||||
}
|
|
||||||
MaybeTlsStream::Tls(_) => {
|
|
||||||
anyhow::bail!("TLS already started");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_tls(&mut self) -> anyhow::Result<()> {
|
|
||||||
// temporary replace stream with fake to cook TLS one, Indiana Jones style
|
|
||||||
match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) {
|
|
||||||
MaybeWriteOnly::Full(framed) => {
|
|
||||||
let tls_config = self
|
|
||||||
.tls_config
|
|
||||||
.as_ref()
|
|
||||||
.context("start_tls called without conf")?
|
|
||||||
.clone();
|
|
||||||
let tls_framed = framed
|
|
||||||
.map_stream(|s| PostgresBackend::tls_upgrade(s, tls_config))
|
|
||||||
.await?;
|
|
||||||
// push back ready TLS stream
|
|
||||||
self.framed = MaybeWriteOnly::Full(tls_framed);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::WriteOnly(_) => {
|
|
||||||
anyhow::bail!("TLS upgrade attempt in split state")
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::Broken => panic!("TLS upgrade on framed in invalid state"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split off owned read part from which messages can be read in different
|
|
||||||
/// task/thread.
|
|
||||||
pub fn split(&mut self) -> anyhow::Result<PostgresBackendReader<IO>> {
|
|
||||||
// temporary replace stream with fake to cook split one, Indiana Jones style
|
|
||||||
match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) {
|
|
||||||
MaybeWriteOnly::Full(framed) => {
|
|
||||||
let (reader, writer) = framed.split();
|
|
||||||
self.framed = MaybeWriteOnly::WriteOnly(writer);
|
|
||||||
Ok(PostgresBackendReader(reader))
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::WriteOnly(_) => {
|
|
||||||
anyhow::bail!("PostgresBackend is already split")
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::Broken => panic!("split on framed in invalid state"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Join read part back.
|
|
||||||
pub fn unsplit(&mut self, reader: PostgresBackendReader<IO>) -> anyhow::Result<()> {
|
|
||||||
// temporary replace stream with fake to cook joined one, Indiana Jones style
|
|
||||||
match std::mem::replace(&mut self.framed, MaybeWriteOnly::Broken) {
|
|
||||||
MaybeWriteOnly::Full(_) => {
|
|
||||||
anyhow::bail!("PostgresBackend is not split")
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::WriteOnly(writer) => {
|
|
||||||
let joined = Framed::unsplit(reader.0, writer);
|
|
||||||
self.framed = MaybeWriteOnly::Full(joined);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
MaybeWriteOnly::Broken => panic!("unsplit on framed in invalid state"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform handshake with the client, transitioning to Established.
|
|
||||||
/// In case of EOF during handshake logs this, sets state to Closed and returns Ok(()).
|
|
||||||
async fn handshake(&mut self, handler: &mut impl Handler<IO>) -> Result<(), QueryError> {
|
|
||||||
while self.state < ProtoState::Authentication {
|
|
||||||
match self.framed.read_startup_message().await? {
|
|
||||||
Some(msg) => {
|
|
||||||
self.process_startup_message(handler, msg).await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
trace!(
|
|
||||||
"postgres backend to {:?} received EOF during handshake",
|
|
||||||
self.peer_addr
|
|
||||||
);
|
|
||||||
self.state = ProtoState::Closed;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform auth, if needed.
|
|
||||||
if self.state == ProtoState::Authentication {
|
|
||||||
match self.framed.read_message().await? {
|
|
||||||
Some(FeMessage::PasswordMessage(m)) => {
|
|
||||||
assert!(self.auth_type == AuthType::NeonJWT);
|
|
||||||
|
|
||||||
let (_, jwt_response) = m.split_last().context("protocol violation")?;
|
|
||||||
|
|
||||||
if let Err(e) = handler.check_auth_jwt(self, jwt_response) {
|
|
||||||
self.write_message_noflush(&BeMessage::ErrorResponse(
|
|
||||||
&e.to_string(),
|
|
||||||
Some(e.pg_error_code()),
|
|
||||||
))?;
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.write_message_noflush(&BeMessage::AuthenticationOk)?
|
|
||||||
.write_message_noflush(&BeMessage::CLIENT_ENCODING)?
|
|
||||||
.write_message(&BeMessage::ReadyForQuery)
|
|
||||||
.await?;
|
|
||||||
self.state = ProtoState::Established;
|
|
||||||
}
|
|
||||||
Some(m) => {
|
|
||||||
return Err(QueryError::Other(anyhow::anyhow!(
|
|
||||||
"Unexpected message {:?} while waiting for handshake",
|
|
||||||
m
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
trace!(
|
|
||||||
"postgres backend to {:?} received EOF during auth",
|
|
||||||
self.peer_addr
|
|
||||||
);
|
|
||||||
self.state = ProtoState::Closed;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process startup packet:
|
|
||||||
/// - transition to Established if auth type is trust
|
|
||||||
/// - transition to Authentication if auth type is NeonJWT.
|
|
||||||
/// - or perform TLS handshake -- then need to call this again to receive
|
|
||||||
/// actual startup packet.
|
|
||||||
async fn process_startup_message(
|
|
||||||
&mut self,
|
|
||||||
handler: &mut impl Handler<IO>,
|
|
||||||
msg: FeStartupPacket,
|
|
||||||
) -> Result<(), QueryError> {
|
|
||||||
assert!(self.state < ProtoState::Authentication);
|
|
||||||
let have_tls = self.tls_config.is_some();
|
|
||||||
match msg {
|
|
||||||
FeStartupPacket::SslRequest => {
|
|
||||||
debug!("SSL requested");
|
|
||||||
|
|
||||||
self.write_message(&BeMessage::EncryptionResponse(have_tls))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if have_tls {
|
|
||||||
self.start_tls().await?;
|
|
||||||
self.state = ProtoState::Encrypted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FeStartupPacket::GssEncRequest => {
|
|
||||||
debug!("GSS requested");
|
|
||||||
self.write_message(&BeMessage::EncryptionResponse(false))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
FeStartupPacket::StartupMessage { .. } => {
|
|
||||||
if have_tls && !matches!(self.state, ProtoState::Encrypted) {
|
|
||||||
self.write_message(&BeMessage::ErrorResponse("must connect with TLS", None))
|
|
||||||
.await?;
|
|
||||||
return Err(QueryError::Other(anyhow::anyhow!(
|
|
||||||
"client did not connect with TLS"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// NB: startup() may change self.auth_type -- we are using that in proxy code
|
|
||||||
// to bypass auth for new users.
|
|
||||||
handler.startup(self, &msg)?;
|
|
||||||
|
|
||||||
match self.auth_type {
|
|
||||||
AuthType::Trust => {
|
|
||||||
self.write_message_noflush(&BeMessage::AuthenticationOk)?
|
|
||||||
.write_message_noflush(&BeMessage::CLIENT_ENCODING)?
|
|
||||||
.write_message_noflush(&BeMessage::INTEGER_DATETIMES)?
|
|
||||||
// The async python driver requires a valid server_version
|
|
||||||
.write_message_noflush(&BeMessage::server_version("14.1"))?
|
|
||||||
.write_message(&BeMessage::ReadyForQuery)
|
|
||||||
.await?;
|
|
||||||
self.state = ProtoState::Established;
|
|
||||||
}
|
|
||||||
AuthType::NeonJWT => {
|
|
||||||
self.write_message(&BeMessage::AuthenticationCleartextPassword)
|
|
||||||
.await?;
|
|
||||||
self.state = ProtoState::Authentication;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FeStartupPacket::CancelRequest { .. } => {
|
|
||||||
return Err(QueryError::Other(anyhow::anyhow!(
|
|
||||||
"Unexpected CancelRequest message during handshake"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_message(
|
|
||||||
&mut self,
|
|
||||||
handler: &mut impl Handler<IO>,
|
|
||||||
msg: FeMessage,
|
|
||||||
unnamed_query_string: &mut Bytes,
|
|
||||||
) -> Result<ProcessMsgResult, QueryError> {
|
|
||||||
// Allow only startup and password messages during auth. Otherwise client would be able to bypass auth
|
|
||||||
// TODO: change that to proper top-level match of protocol state with separate message handling for each state
|
|
||||||
assert!(self.state == ProtoState::Established);
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
FeMessage::Query(body) => {
|
|
||||||
// remove null terminator
|
|
||||||
let query_string = cstr_to_str(&body)?;
|
|
||||||
|
|
||||||
trace!("got query {query_string:?}");
|
|
||||||
if let Err(e) = handler.process_query(self, query_string).await {
|
|
||||||
log_query_error(query_string, &e);
|
|
||||||
let short_error = short_error(&e);
|
|
||||||
self.write_message_noflush(&BeMessage::ErrorResponse(
|
|
||||||
&short_error,
|
|
||||||
Some(e.pg_error_code()),
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
self.write_message_noflush(&BeMessage::ReadyForQuery)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Parse(m) => {
|
|
||||||
*unnamed_query_string = m.query_string;
|
|
||||||
self.write_message_noflush(&BeMessage::ParseComplete)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Describe(_) => {
|
|
||||||
self.write_message_noflush(&BeMessage::ParameterDescription)?
|
|
||||||
.write_message_noflush(&BeMessage::NoData)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Bind(_) => {
|
|
||||||
self.write_message_noflush(&BeMessage::BindComplete)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Close(_) => {
|
|
||||||
self.write_message_noflush(&BeMessage::CloseComplete)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Execute(_) => {
|
|
||||||
let query_string = cstr_to_str(unnamed_query_string)?;
|
|
||||||
trace!("got execute {query_string:?}");
|
|
||||||
if let Err(e) = handler.process_query(self, query_string).await {
|
|
||||||
log_query_error(query_string, &e);
|
|
||||||
self.write_message_noflush(&BeMessage::ErrorResponse(
|
|
||||||
&e.to_string(),
|
|
||||||
Some(e.pg_error_code()),
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
// NOTE there is no ReadyForQuery message. This handler is used
|
|
||||||
// for basebackup and it uses CopyOut which doesn't require
|
|
||||||
// ReadyForQuery message and backend just switches back to
|
|
||||||
// processing mode after sending CopyDone or ErrorResponse.
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Sync => {
|
|
||||||
self.write_message_noflush(&BeMessage::ReadyForQuery)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
FeMessage::Terminate => {
|
|
||||||
return Ok(ProcessMsgResult::Break);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We prefer explicit pattern matching to wildcards, because
|
|
||||||
// this helps us spot the places where new variants are missing
|
|
||||||
FeMessage::CopyData(_)
|
|
||||||
| FeMessage::CopyDone
|
|
||||||
| FeMessage::CopyFail
|
|
||||||
| FeMessage::PasswordMessage(_) => {
|
|
||||||
return Err(QueryError::Other(anyhow::anyhow!(
|
|
||||||
"unexpected message type: {msg:?}",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ProcessMsgResult::Continue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log as info/error result of handling COPY stream and send back
|
|
||||||
/// ErrorResponse if that makes sense. Shutdown the stream if we got
|
|
||||||
/// Terminate. TODO: transition into waiting for Sync msg if we initiate the
|
|
||||||
/// close.
|
|
||||||
pub async fn handle_copy_stream_end(&mut self, end: CopyStreamHandlerEnd) {
|
|
||||||
use CopyStreamHandlerEnd::*;
|
|
||||||
|
|
||||||
let expected_end = match &end {
|
|
||||||
ServerInitiated(_) | CopyDone | CopyFail | Terminate | EOF => true,
|
|
||||||
CopyStreamHandlerEnd::Disconnected(ConnectionError::Io(io_error))
|
|
||||||
if is_expected_io_error(io_error) =>
|
|
||||||
{
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
if expected_end {
|
|
||||||
info!("terminated: {:#}", end);
|
|
||||||
} else {
|
|
||||||
error!("terminated: {:?}", end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: no current usages ever send this
|
|
||||||
if let CopyDone = &end {
|
|
||||||
if let Err(e) = self.write_message(&BeMessage::CopyDone).await {
|
|
||||||
error!("failed to send CopyDone: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Terminate = &end {
|
|
||||||
self.state = ProtoState::Closed;
|
|
||||||
}
|
|
||||||
|
|
||||||
let err_to_send_and_errcode = match &end {
|
|
||||||
ServerInitiated(_) => Some((end.to_string(), SQLSTATE_SUCCESSFUL_COMPLETION)),
|
|
||||||
Other(_) => Some((end.to_string(), SQLSTATE_INTERNAL_ERROR)),
|
|
||||||
// Note: CopyFail in duplex copy is somewhat unexpected (at least to
|
|
||||||
// PG walsender; evidently and per my docs reading client should
|
|
||||||
// finish it with CopyDone). It is not a problem to recover from it
|
|
||||||
// finishing the stream in both directions like we do, but note that
|
|
||||||
// sync rust-postgres client (which we don't use anymore) hangs if
|
|
||||||
// socket is not closed here.
|
|
||||||
// https://github.com/sfackler/rust-postgres/issues/755
|
|
||||||
// https://github.com/neondatabase/neon/issues/935
|
|
||||||
//
|
|
||||||
// Currently, the version of tokio_postgres replication patch we use
|
|
||||||
// sends this when it closes the stream (e.g. pageserver decided to
|
|
||||||
// switch conn to another safekeeper and client gets dropped).
|
|
||||||
// Moreover, seems like 'connection' task errors with 'unexpected
|
|
||||||
// message from server' when it receives ErrorResponse (anything but
|
|
||||||
// CopyData/CopyDone) back.
|
|
||||||
CopyFail => Some((end.to_string(), SQLSTATE_SUCCESSFUL_COMPLETION)),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some((err, errcode)) = err_to_send_and_errcode {
|
|
||||||
if let Err(ee) = self
|
|
||||||
.write_message(&BeMessage::ErrorResponse(&err, Some(errcode)))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("failed to send ErrorResponse: {}", ee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PostgresBackendReader<IO>(FramedReader<MaybeTlsStream<IO>>);
|
|
||||||
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin> PostgresBackendReader<IO> {
|
|
||||||
/// Read full message or return None if connection is cleanly closed with no
|
|
||||||
/// unprocessed data.
|
|
||||||
pub async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> {
|
|
||||||
let m = self.0.read_message().await?;
|
|
||||||
trace!("read msg {:?}", m);
|
|
||||||
Ok(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get CopyData contents of the next message in COPY stream or error
|
|
||||||
/// closing it. The error type is wider than actual errors which can happen
|
|
||||||
/// here -- it includes 'Other' and 'ServerInitiated', but that's ok for
|
|
||||||
/// current callers.
|
|
||||||
pub async fn read_copy_message(&mut self) -> Result<Bytes, CopyStreamHandlerEnd> {
|
|
||||||
match self.read_message().await? {
|
|
||||||
Some(msg) => match msg {
|
|
||||||
FeMessage::CopyData(m) => Ok(m),
|
|
||||||
FeMessage::CopyDone => Err(CopyStreamHandlerEnd::CopyDone),
|
|
||||||
FeMessage::CopyFail => Err(CopyStreamHandlerEnd::CopyFail),
|
|
||||||
FeMessage::Terminate => Err(CopyStreamHandlerEnd::Terminate),
|
|
||||||
_ => Err(CopyStreamHandlerEnd::from(ConnectionError::Protocol(
|
|
||||||
ProtocolError::Protocol(format!("unexpected message in COPY stream {:?}", msg)),
|
|
||||||
))),
|
|
||||||
},
|
|
||||||
None => Err(CopyStreamHandlerEnd::EOF),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// A futures::AsyncWrite implementation that wraps all data written to it in CopyData
|
|
||||||
/// messages.
|
|
||||||
///
|
|
||||||
|
|
||||||
pub struct CopyDataWriter<'a, IO> {
|
|
||||||
pgb: &'a mut PostgresBackend<IO>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, IO: AsyncRead + AsyncWrite + Unpin> AsyncWrite for CopyDataWriter<'a, IO> {
|
|
||||||
fn poll_write(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> Poll<Result<usize, std::io::Error>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
|
|
||||||
// It's not strictly required to flush between each message, but makes it easier
|
|
||||||
// to view in wireshark, and usually the messages that the callers write are
|
|
||||||
// decently-sized anyway.
|
|
||||||
if let Err(err) = ready!(this.pgb.poll_flush(cx)) {
|
|
||||||
return Poll::Ready(Err(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyData
|
|
||||||
// XXX: if the input is large, we should split it into multiple messages.
|
|
||||||
// Not sure what the threshold should be, but the ultimate hard limit is that
|
|
||||||
// the length cannot exceed u32.
|
|
||||||
this.pgb
|
|
||||||
.write_message_noflush(&BeMessage::CopyData(buf))
|
|
||||||
// write_message only writes to the buffer, so it can fail iff the
|
|
||||||
// message is invaid, but CopyData can't be invalid.
|
|
||||||
.map_err(|_| io::Error::new(ErrorKind::Other, "failed to serialize CopyData"))?;
|
|
||||||
|
|
||||||
Poll::Ready(Ok(buf.len()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> Poll<Result<(), std::io::Error>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
this.pgb.poll_flush(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_shutdown(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> Poll<Result<(), std::io::Error>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
this.pgb.poll_flush(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn short_error(e: &QueryError) -> String {
|
|
||||||
match e {
|
|
||||||
QueryError::Disconnected(connection_error) => connection_error.to_string(),
|
|
||||||
QueryError::Other(e) => format!("{e:#}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log_query_error(query: &str, e: &QueryError) {
|
|
||||||
match e {
|
|
||||||
QueryError::Disconnected(ConnectionError::Io(io_error)) => {
|
|
||||||
if is_expected_io_error(io_error) {
|
|
||||||
info!("query handler for '{query}' failed with expected io error: {io_error}");
|
|
||||||
} else {
|
|
||||||
error!("query handler for '{query}' failed with io error: {io_error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QueryError::Disconnected(other_connection_error) => {
|
|
||||||
error!("query handler for '{query}' failed with connection error: {other_connection_error:?}")
|
|
||||||
}
|
|
||||||
QueryError::Other(e) => {
|
|
||||||
error!("query handler for '{query}' failed: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Something finishing handling of COPY stream, see handle_copy_stream_end.
|
|
||||||
/// This is not always a real error, but it allows to use ? and thiserror impls.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum CopyStreamHandlerEnd {
|
|
||||||
/// Handler initiates the end of streaming.
|
|
||||||
#[error("{0}")]
|
|
||||||
ServerInitiated(String),
|
|
||||||
#[error("received CopyDone")]
|
|
||||||
CopyDone,
|
|
||||||
#[error("received CopyFail")]
|
|
||||||
CopyFail,
|
|
||||||
#[error("received Terminate")]
|
|
||||||
Terminate,
|
|
||||||
#[error("EOF on COPY stream")]
|
|
||||||
EOF,
|
|
||||||
/// The connection was lost
|
|
||||||
#[error(transparent)]
|
|
||||||
Disconnected(#[from] ConnectionError),
|
|
||||||
/// Some other error
|
|
||||||
#[error(transparent)]
|
|
||||||
Other(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
/// Test postgres_backend_async with tokio_postgres
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use postgres_backend::{AuthType, Handler, PostgresBackend, QueryError};
|
|
||||||
use pq_proto::{BeMessage, RowDescriptor};
|
|
||||||
use std::io::Cursor;
|
|
||||||
use std::{future, sync::Arc};
|
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
use tokio_postgres::config::SslMode;
|
|
||||||
use tokio_postgres::tls::MakeTlsConnect;
|
|
||||||
use tokio_postgres::{Config, NoTls, SimpleQueryMessage};
|
|
||||||
use tokio_postgres_rustls::MakeRustlsConnect;
|
|
||||||
|
|
||||||
// generate client, server test streams
|
|
||||||
async fn make_tcp_pair() -> (TcpStream, TcpStream) {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let addr = listener.local_addr().unwrap();
|
|
||||||
let client_stream = TcpStream::connect(addr).await.unwrap();
|
|
||||||
let (server_stream, _) = listener.accept().await.unwrap();
|
|
||||||
(client_stream, server_stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestHandler {}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl<IO: AsyncRead + AsyncWrite + Unpin + Send> Handler<IO> for TestHandler {
|
|
||||||
// return single col 'hey' for any query
|
|
||||||
async fn process_query(
|
|
||||||
&mut self,
|
|
||||||
pgb: &mut PostgresBackend<IO>,
|
|
||||||
_query_string: &str,
|
|
||||||
) -> Result<(), QueryError> {
|
|
||||||
pgb.write_message_noflush(&BeMessage::RowDescription(&[RowDescriptor::text_col(
|
|
||||||
b"hey",
|
|
||||||
)]))?
|
|
||||||
.write_message_noflush(&BeMessage::DataRow(&[Some("hey".as_bytes())]))?
|
|
||||||
.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test that basic select works
|
|
||||||
#[tokio::test]
|
|
||||||
async fn simple_select() {
|
|
||||||
let (client_sock, server_sock) = make_tcp_pair().await;
|
|
||||||
|
|
||||||
// create and run pgbackend
|
|
||||||
let pgbackend =
|
|
||||||
PostgresBackend::new(server_sock, AuthType::Trust, None).expect("pgbackend creation");
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut handler = TestHandler {};
|
|
||||||
pgbackend.run(&mut handler, future::pending::<()>).await
|
|
||||||
});
|
|
||||||
|
|
||||||
let conf = Config::new();
|
|
||||||
let (client, connection) = conf.connect_raw(client_sock, NoTls).await.expect("connect");
|
|
||||||
// The connection object performs the actual communication with the database,
|
|
||||||
// so spawn it off to run on its own.
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = connection.await {
|
|
||||||
eprintln!("connection error: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let first_val = &(client.simple_query("SELECT 42;").await.expect("select"))[0];
|
|
||||||
if let SimpleQueryMessage::Row(row) = first_val {
|
|
||||||
let first_col = row.get(0).expect("first column");
|
|
||||||
assert_eq!(first_col, "hey");
|
|
||||||
} else {
|
|
||||||
panic!("expected SimpleQueryMessage::Row");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static KEY: Lazy<rustls::PrivateKey> = Lazy::new(|| {
|
|
||||||
let mut cursor = Cursor::new(include_bytes!("key.pem"));
|
|
||||||
rustls::PrivateKey(rustls_pemfile::rsa_private_keys(&mut cursor).unwrap()[0].clone())
|
|
||||||
});
|
|
||||||
|
|
||||||
static CERT: Lazy<rustls::Certificate> = Lazy::new(|| {
|
|
||||||
let mut cursor = Cursor::new(include_bytes!("cert.pem"));
|
|
||||||
rustls::Certificate(rustls_pemfile::certs(&mut cursor).unwrap()[0].clone())
|
|
||||||
});
|
|
||||||
|
|
||||||
// test that basic select with ssl works
|
|
||||||
#[tokio::test]
|
|
||||||
async fn simple_select_ssl() {
|
|
||||||
let (client_sock, server_sock) = make_tcp_pair().await;
|
|
||||||
|
|
||||||
let server_cfg = rustls::ServerConfig::builder()
|
|
||||||
.with_safe_defaults()
|
|
||||||
.with_no_client_auth()
|
|
||||||
.with_single_cert(vec![CERT.clone()], KEY.clone())
|
|
||||||
.unwrap();
|
|
||||||
let tls_config = Some(Arc::new(server_cfg));
|
|
||||||
let pgbackend =
|
|
||||||
PostgresBackend::new(server_sock, AuthType::Trust, tls_config).expect("pgbackend creation");
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut handler = TestHandler {};
|
|
||||||
pgbackend.run(&mut handler, future::pending::<()>).await
|
|
||||||
});
|
|
||||||
|
|
||||||
let client_cfg = rustls::ClientConfig::builder()
|
|
||||||
.with_safe_defaults()
|
|
||||||
.with_root_certificates({
|
|
||||||
let mut store = rustls::RootCertStore::empty();
|
|
||||||
store.add(&CERT).unwrap();
|
|
||||||
store
|
|
||||||
})
|
|
||||||
.with_no_client_auth();
|
|
||||||
let mut make_tls_connect = tokio_postgres_rustls::MakeRustlsConnect::new(client_cfg);
|
|
||||||
let tls_connect = <MakeRustlsConnect as MakeTlsConnect<TcpStream>>::make_tls_connect(
|
|
||||||
&mut make_tls_connect,
|
|
||||||
"localhost",
|
|
||||||
)
|
|
||||||
.expect("make_tls_connect");
|
|
||||||
|
|
||||||
let mut conf = Config::new();
|
|
||||||
conf.ssl_mode(SslMode::Require);
|
|
||||||
let (client, connection) = conf
|
|
||||||
.connect_raw(client_sock, tls_connect)
|
|
||||||
.await
|
|
||||||
.expect("connect");
|
|
||||||
// The connection object performs the actual communication with the database,
|
|
||||||
// so spawn it off to run on its own.
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = connection.await {
|
|
||||||
eprintln!("connection error: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let first_val = &(client.simple_query("SELECT 42;").await.expect("select"))[0];
|
|
||||||
if let SimpleQueryMessage::Row(row) = first_val {
|
|
||||||
let first_col = row.get(0).expect("first column");
|
|
||||||
assert_eq!(first_col, "hey");
|
|
||||||
} else {
|
|
||||||
panic!("expected SimpleQueryMessage::Row");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,7 +63,10 @@ fn main() -> anyhow::Result<()> {
|
|||||||
pg_install_dir_versioned = cwd.join("..").join("..").join(pg_install_dir_versioned);
|
pg_install_dir_versioned = cwd.join("..").join("..").join(pg_install_dir_versioned);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pg_config_bin = pg_install_dir_versioned.join("bin").join("pg_config");
|
let pg_config_bin = pg_install_dir_versioned
|
||||||
|
.join(pg_version)
|
||||||
|
.join("bin")
|
||||||
|
.join("pg_config");
|
||||||
let inc_server_path: String = if pg_config_bin.exists() {
|
let inc_server_path: String = if pg_config_bin.exists() {
|
||||||
let output = Command::new(pg_config_bin)
|
let output = Command::new(pg_config_bin)
|
||||||
.arg("--includedir-server")
|
.arg("--includedir-server")
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
byteorder.workspace = true
|
|
||||||
pin-project-lite.workspace = true
|
pin-project-lite.workspace = true
|
||||||
postgres-protocol.workspace = true
|
postgres-protocol.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
//! Provides `Framed` -- writing/flushing and reading Postgres messages to/from
|
|
||||||
//! the async stream based on (and buffered with) BytesMut. All functions are
|
|
||||||
//! cancellation safe.
|
|
||||||
//!
|
|
||||||
//! It is similar to what tokio_util::codec::Framed with appropriate codec
|
|
||||||
//! provides, but `FramedReader` and `FramedWriter` read/write parts can be used
|
|
||||||
//! separately without using split from futures::stream::StreamExt (which
|
|
||||||
//! allocates box[1] in polling internally). tokio::io::split is used for splitting
|
|
||||||
//! instead. Plus we customize error messages more than a single type for all io
|
|
||||||
//! calls.
|
|
||||||
//!
|
|
||||||
//! [1] https://docs.rs/futures-util/0.3.26/src/futures_util/lock/bilock.rs.html#107
|
|
||||||
use bytes::{Buf, BytesMut};
|
|
||||||
use std::{
|
|
||||||
future::Future,
|
|
||||||
io::{self, ErrorKind},
|
|
||||||
};
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf};
|
|
||||||
|
|
||||||
use crate::{BeMessage, FeMessage, FeStartupPacket, ProtocolError};
|
|
||||||
|
|
||||||
const INITIAL_CAPACITY: usize = 8 * 1024;
|
|
||||||
|
|
||||||
/// Error on postgres connection: either IO (physical transport error) or
|
|
||||||
/// protocol violation.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum ConnectionError {
|
|
||||||
#[error(transparent)]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
#[error(transparent)]
|
|
||||||
Protocol(#[from] ProtocolError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectionError {
|
|
||||||
/// Proxy stream.rs uses only io::Error; provide it.
|
|
||||||
pub fn into_io_error(self) -> io::Error {
|
|
||||||
match self {
|
|
||||||
ConnectionError::Io(io) => io,
|
|
||||||
ConnectionError::Protocol(pe) => io::Error::new(io::ErrorKind::Other, pe.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps async io `stream`, providing messages to write/flush + read Postgres
|
|
||||||
/// messages.
|
|
||||||
pub struct Framed<S> {
|
|
||||||
stream: S,
|
|
||||||
read_buf: BytesMut,
|
|
||||||
write_buf: BytesMut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> Framed<S> {
|
|
||||||
pub fn new(stream: S) -> Self {
|
|
||||||
Self {
|
|
||||||
stream,
|
|
||||||
read_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
|
|
||||||
write_buf: BytesMut::with_capacity(INITIAL_CAPACITY),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a shared reference to the underlying stream.
|
|
||||||
pub fn get_ref(&self) -> &S {
|
|
||||||
&self.stream
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract the underlying stream.
|
|
||||||
pub fn into_inner(self) -> S {
|
|
||||||
self.stream
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return new Framed with stream type transformed by async f, for TLS
|
|
||||||
/// upgrade.
|
|
||||||
pub async fn map_stream<S2, E, F, Fut>(self, f: F) -> Result<Framed<S2>, E>
|
|
||||||
where
|
|
||||||
F: FnOnce(S) -> Fut,
|
|
||||||
Fut: Future<Output = Result<S2, E>>,
|
|
||||||
{
|
|
||||||
let stream = f(self.stream).await?;
|
|
||||||
Ok(Framed {
|
|
||||||
stream,
|
|
||||||
read_buf: self.read_buf,
|
|
||||||
write_buf: self.write_buf,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncRead + Unpin> Framed<S> {
|
|
||||||
pub async fn read_startup_message(
|
|
||||||
&mut self,
|
|
||||||
) -> Result<Option<FeStartupPacket>, ConnectionError> {
|
|
||||||
read_message(&mut self.stream, &mut self.read_buf, FeStartupPacket::parse).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> {
|
|
||||||
read_message(&mut self.stream, &mut self.read_buf, FeMessage::parse).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncWrite + Unpin> Framed<S> {
|
|
||||||
/// Write next message to the output buffer; doesn't flush.
|
|
||||||
pub fn write_message(&mut self, msg: &BeMessage<'_>) -> Result<(), ProtocolError> {
|
|
||||||
BeMessage::write(&mut self.write_buf, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flush out the buffer. This function is cancellation safe: it can be
|
|
||||||
/// interrupted and flushing will be continued in the next call.
|
|
||||||
pub async fn flush(&mut self) -> Result<(), io::Error> {
|
|
||||||
flush(&mut self.stream, &mut self.write_buf).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flush out the buffer and shutdown the stream.
|
|
||||||
pub async fn shutdown(&mut self) -> Result<(), io::Error> {
|
|
||||||
shutdown(&mut self.stream, &mut self.write_buf).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncRead + AsyncWrite + Unpin> Framed<S> {
|
|
||||||
/// Split into owned read and write parts. Beware of potential issues with
|
|
||||||
/// using halves in different tasks on TLS stream:
|
|
||||||
/// https://github.com/tokio-rs/tls/issues/40
|
|
||||||
pub fn split(self) -> (FramedReader<S>, FramedWriter<S>) {
|
|
||||||
let (read_half, write_half) = tokio::io::split(self.stream);
|
|
||||||
let reader = FramedReader {
|
|
||||||
stream: read_half,
|
|
||||||
read_buf: self.read_buf,
|
|
||||||
};
|
|
||||||
let writer = FramedWriter {
|
|
||||||
stream: write_half,
|
|
||||||
write_buf: self.write_buf,
|
|
||||||
};
|
|
||||||
(reader, writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Join read and write parts back.
|
|
||||||
pub fn unsplit(reader: FramedReader<S>, writer: FramedWriter<S>) -> Self {
|
|
||||||
Self {
|
|
||||||
stream: reader.stream.unsplit(writer.stream),
|
|
||||||
read_buf: reader.read_buf,
|
|
||||||
write_buf: writer.write_buf,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read-only version of `Framed`.
|
|
||||||
pub struct FramedReader<S> {
|
|
||||||
stream: ReadHalf<S>,
|
|
||||||
read_buf: BytesMut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncRead + Unpin> FramedReader<S> {
|
|
||||||
pub async fn read_message(&mut self) -> Result<Option<FeMessage>, ConnectionError> {
|
|
||||||
read_message(&mut self.stream, &mut self.read_buf, FeMessage::parse).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write-only version of `Framed`.
|
|
||||||
pub struct FramedWriter<S> {
|
|
||||||
stream: WriteHalf<S>,
|
|
||||||
write_buf: BytesMut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncWrite + Unpin> FramedWriter<S> {
|
|
||||||
/// Write next message to the output buffer; doesn't flush.
|
|
||||||
pub fn write_message_noflush(&mut self, msg: &BeMessage<'_>) -> Result<(), ProtocolError> {
|
|
||||||
BeMessage::write(&mut self.write_buf, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flush out the buffer. This function is cancellation safe: it can be
|
|
||||||
/// interrupted and flushing will be continued in the next call.
|
|
||||||
pub async fn flush(&mut self) -> Result<(), io::Error> {
|
|
||||||
flush(&mut self.stream, &mut self.write_buf).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flush out the buffer and shutdown the stream.
|
|
||||||
pub async fn shutdown(&mut self) -> Result<(), io::Error> {
|
|
||||||
shutdown(&mut self.stream, &mut self.write_buf).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read next message from the stream. Returns Ok(None), if EOF happened and we
|
|
||||||
/// don't have remaining data in the buffer. This function is cancellation safe:
|
|
||||||
/// you can drop future which is not yet complete and finalize reading message
|
|
||||||
/// with the next call.
|
|
||||||
///
|
|
||||||
/// Parametrized to allow reading startup or usual message, having different
|
|
||||||
/// format.
|
|
||||||
async fn read_message<S: AsyncRead + Unpin, M, P>(
|
|
||||||
stream: &mut S,
|
|
||||||
read_buf: &mut BytesMut,
|
|
||||||
parse: P,
|
|
||||||
) -> Result<Option<M>, ConnectionError>
|
|
||||||
where
|
|
||||||
P: Fn(&mut BytesMut) -> Result<Option<M>, ProtocolError>,
|
|
||||||
{
|
|
||||||
loop {
|
|
||||||
if let Some(msg) = parse(read_buf)? {
|
|
||||||
return Ok(Some(msg));
|
|
||||||
}
|
|
||||||
// If we can't build a frame yet, try to read more data and try again.
|
|
||||||
// Make sure we've got room for at least one byte to read to ensure
|
|
||||||
// that we don't get a spurious 0 that looks like EOF.
|
|
||||||
read_buf.reserve(1);
|
|
||||||
if stream.read_buf(read_buf).await? == 0 {
|
|
||||||
if read_buf.has_remaining() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
ErrorKind::UnexpectedEof,
|
|
||||||
"EOF with unprocessed data in the buffer",
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
} else {
|
|
||||||
return Ok(None); // clean EOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn flush<S: AsyncWrite + Unpin>(
|
|
||||||
stream: &mut S,
|
|
||||||
write_buf: &mut BytesMut,
|
|
||||||
) -> Result<(), io::Error> {
|
|
||||||
while write_buf.has_remaining() {
|
|
||||||
let bytes_written = stream.write(write_buf.chunk()).await?;
|
|
||||||
if bytes_written == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
ErrorKind::WriteZero,
|
|
||||||
"failed to write message",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// The advanced part will be garbage collected, likely during shifting
|
|
||||||
// data left on next attempt to write to buffer when free space is not
|
|
||||||
// enough.
|
|
||||||
write_buf.advance(bytes_written);
|
|
||||||
}
|
|
||||||
write_buf.clear();
|
|
||||||
stream.flush().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shutdown<S: AsyncWrite + Unpin>(
|
|
||||||
stream: &mut S,
|
|
||||||
write_buf: &mut BytesMut,
|
|
||||||
) -> Result<(), io::Error> {
|
|
||||||
flush(stream, write_buf).await?;
|
|
||||||
stream.shutdown().await
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,24 @@
|
|||||||
//! <https://www.postgresql.org/docs/devel/protocol-message-formats.html>
|
//! <https://www.postgresql.org/docs/devel/protocol-message-formats.html>
|
||||||
//! on message formats.
|
//! on message formats.
|
||||||
|
|
||||||
pub mod framed;
|
// Tools for calling certain async methods in sync contexts.
|
||||||
|
pub mod sync;
|
||||||
|
|
||||||
use byteorder::{BigEndian, ReadBytesExt};
|
use anyhow::{ensure, Context, Result};
|
||||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||||
use postgres_protocol::PG_EPOCH;
|
use postgres_protocol::PG_EPOCH;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt, io, str,
|
fmt,
|
||||||
|
future::Future,
|
||||||
|
io::{self, Cursor},
|
||||||
|
str,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
use sync::{AsyncishRead, SyncFuture};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{trace, warn};
|
use tracing::{trace, warn};
|
||||||
|
|
||||||
pub type Oid = u32;
|
pub type Oid = u32;
|
||||||
@@ -25,6 +31,7 @@ pub const TEXT_OID: Oid = 25;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FeMessage {
|
pub enum FeMessage {
|
||||||
|
StartupPacket(FeStartupPacket),
|
||||||
// Simple query.
|
// Simple query.
|
||||||
Query(Bytes),
|
Query(Bytes),
|
||||||
// Extended query protocol.
|
// Extended query protocol.
|
||||||
@@ -68,36 +75,27 @@ impl StartupMessageParams {
|
|||||||
/// taking into account all escape sequences but leaving them as-is.
|
/// taking into account all escape sequences but leaving them as-is.
|
||||||
/// [`None`] means that there's no `options` in [`Self`].
|
/// [`None`] means that there's no `options` in [`Self`].
|
||||||
pub fn options_raw(&self) -> Option<impl Iterator<Item = &str>> {
|
pub fn options_raw(&self) -> Option<impl Iterator<Item = &str>> {
|
||||||
self.get("options").map(Self::parse_options_raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split command-line options according to PostgreSQL's logic,
|
|
||||||
/// applying all escape sequences (using owned strings as needed).
|
|
||||||
/// [`None`] means that there's no `options` in [`Self`].
|
|
||||||
pub fn options_escaped(&self) -> Option<impl Iterator<Item = Cow<'_, str>>> {
|
|
||||||
self.get("options").map(Self::parse_options_escaped)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split command-line options according to PostgreSQL's logic,
|
|
||||||
/// taking into account all escape sequences but leaving them as-is.
|
|
||||||
pub fn parse_options_raw(input: &str) -> impl Iterator<Item = &str> {
|
|
||||||
// See `postgres: pg_split_opts`.
|
// See `postgres: pg_split_opts`.
|
||||||
let mut last_was_escape = false;
|
let mut last_was_escape = false;
|
||||||
input
|
let iter = self
|
||||||
|
.get("options")?
|
||||||
.split(move |c: char| {
|
.split(move |c: char| {
|
||||||
// We split by non-escaped whitespace symbols.
|
// We split by non-escaped whitespace symbols.
|
||||||
let should_split = c.is_ascii_whitespace() && !last_was_escape;
|
let should_split = c.is_ascii_whitespace() && !last_was_escape;
|
||||||
last_was_escape = c == '\\' && !last_was_escape;
|
last_was_escape = c == '\\' && !last_was_escape;
|
||||||
should_split
|
should_split
|
||||||
})
|
})
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
|
Some(iter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Split command-line options according to PostgreSQL's logic,
|
/// Split command-line options according to PostgreSQL's logic,
|
||||||
/// applying all escape sequences (using owned strings as needed).
|
/// applying all escape sequences (using owned strings as needed).
|
||||||
pub fn parse_options_escaped(input: &str) -> impl Iterator<Item = Cow<'_, str>> {
|
/// [`None`] means that there's no `options` in [`Self`].
|
||||||
|
pub fn options_escaped(&self) -> Option<impl Iterator<Item = Cow<'_, str>>> {
|
||||||
// See `postgres: pg_split_opts`.
|
// See `postgres: pg_split_opts`.
|
||||||
Self::parse_options_raw(input).map(|s| {
|
let iter = self.options_raw()?.map(|s| {
|
||||||
let mut preserve_next_escape = false;
|
let mut preserve_next_escape = false;
|
||||||
let escape = |c| {
|
let escape = |c| {
|
||||||
// We should remove '\\' unless it's preceded by '\\'.
|
// We should remove '\\' unless it's preceded by '\\'.
|
||||||
@@ -110,12 +108,9 @@ impl StartupMessageParams {
|
|||||||
true => Cow::Owned(s.replace(escape, "")),
|
true => Cow::Owned(s.replace(escape, "")),
|
||||||
false => Cow::Borrowed(s),
|
false => Cow::Borrowed(s),
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate through key-value pairs in an arbitrary order.
|
Some(iter)
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
|
||||||
self.params.iter().map(|(k, v)| (k.as_str(), v.as_str()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is mostly useful in tests.
|
// This function is mostly useful in tests.
|
||||||
@@ -184,205 +179,260 @@ pub struct FeExecuteMessage {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FeCloseMessage;
|
pub struct FeCloseMessage;
|
||||||
|
|
||||||
/// An error occured while parsing or serializing raw stream into Postgres
|
/// Retry a read on EINTR
|
||||||
/// messages.
|
///
|
||||||
#[derive(thiserror::Error, Debug)]
|
/// This runs the enclosed expression, and if it returns
|
||||||
pub enum ProtocolError {
|
/// Err(io::ErrorKind::Interrupted), retries it.
|
||||||
/// Invalid packet was received from the client (e.g. unexpected message
|
macro_rules! retry_read {
|
||||||
/// type or broken len).
|
( $x:expr ) => {
|
||||||
#[error("Protocol error: {0}")]
|
loop {
|
||||||
Protocol(String),
|
match $x {
|
||||||
/// Failed to parse or, (unlikely), serialize a protocol message.
|
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||||
#[error("Message parse error: {0}")]
|
res => break res,
|
||||||
BadMessage(String),
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProtocolError {
|
/// An error occured during connection being open.
|
||||||
/// Proxy stream.rs uses only io::Error; provide it.
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ConnectionError {
|
||||||
|
/// IO error during writing to or reading from the connection socket.
|
||||||
|
#[error("Socket IO error: {0}")]
|
||||||
|
Socket(std::io::Error),
|
||||||
|
/// Invalid packet was received from client
|
||||||
|
#[error("Protocol error: {0}")]
|
||||||
|
Protocol(String),
|
||||||
|
/// Failed to parse a protocol mesage
|
||||||
|
#[error("Message parse error: {0}")]
|
||||||
|
MessageParse(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for ConnectionError {
|
||||||
|
fn from(e: anyhow::Error) -> Self {
|
||||||
|
Self::MessageParse(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionError {
|
||||||
pub fn into_io_error(self) -> io::Error {
|
pub fn into_io_error(self) -> io::Error {
|
||||||
io::Error::new(io::ErrorKind::Other, self.to_string())
|
match self {
|
||||||
|
ConnectionError::Socket(io) => io,
|
||||||
|
other => io::Error::new(io::ErrorKind::Other, other.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeMessage {
|
impl FeMessage {
|
||||||
/// Read and parse one message from the `buf` input buffer. If there is at
|
/// Read one message from the stream.
|
||||||
/// least one valid message, returns it, advancing `buf`; redundant copies
|
/// This function returns `Ok(None)` in case of EOF.
|
||||||
/// are avoided, as thanks to `bytes` crate ptrs in parsed message point
|
/// One way to handle this properly:
|
||||||
/// directly into the `buf` (processed data is garbage collected after
|
|
||||||
/// parsed message is dropped).
|
|
||||||
///
|
///
|
||||||
/// Returns None if `buf` doesn't contain enough data for a single message.
|
/// ```
|
||||||
/// For efficiency, tries to reserve large enough space in `buf` for the
|
/// # use std::io;
|
||||||
/// next message in this case to save the repeated calls.
|
/// # use pq_proto::FeMessage;
|
||||||
|
/// #
|
||||||
|
/// # fn process_message(msg: FeMessage) -> anyhow::Result<()> {
|
||||||
|
/// # Ok(())
|
||||||
|
/// # };
|
||||||
|
/// #
|
||||||
|
/// fn do_the_job(stream: &mut (impl io::Read + Unpin)) -> anyhow::Result<()> {
|
||||||
|
/// while let Some(msg) = FeMessage::read(stream)? {
|
||||||
|
/// process_message(msg)?;
|
||||||
|
/// }
|
||||||
///
|
///
|
||||||
/// Returns Error if message is malformed, the only possible ErrorKind is
|
/// Ok(())
|
||||||
/// InvalidInput.
|
/// }
|
||||||
//
|
/// ```
|
||||||
// Inspired by rust-postgres Message::parse.
|
#[inline(never)]
|
||||||
pub fn parse(buf: &mut BytesMut) -> Result<Option<FeMessage>, ProtocolError> {
|
pub fn read(
|
||||||
// Every message contains message type byte and 4 bytes len; can't do
|
stream: &mut (impl io::Read + Unpin),
|
||||||
// much without them.
|
) -> Result<Option<FeMessage>, ConnectionError> {
|
||||||
if buf.len() < 5 {
|
Self::read_fut(&mut AsyncishRead(stream)).wait()
|
||||||
let to_read = 5 - buf.len();
|
}
|
||||||
buf.reserve(to_read);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We shouldn't advance `buf` as probably full message is not there yet,
|
/// Read one message from the stream.
|
||||||
// so can't directly use Bytes::get_u32 etc.
|
/// See documentation for `Self::read`.
|
||||||
let tag = buf[0];
|
pub fn read_fut<Reader>(
|
||||||
let len = (&buf[1..5]).read_u32::<BigEndian>().unwrap();
|
stream: &mut Reader,
|
||||||
if len < 4 {
|
) -> SyncFuture<Reader, impl Future<Output = Result<Option<FeMessage>, ConnectionError>> + '_>
|
||||||
return Err(ProtocolError::Protocol(format!(
|
where
|
||||||
"invalid message length {}",
|
Reader: tokio::io::AsyncRead + Unpin,
|
||||||
len
|
{
|
||||||
)));
|
// We return a Future that's sync (has a `wait` method) if and only if the provided stream is SyncProof.
|
||||||
}
|
// SyncFuture contract: we are only allowed to await on sync-proof futures, the AsyncRead and
|
||||||
|
// AsyncReadExt methods of the stream.
|
||||||
|
SyncFuture::new(async move {
|
||||||
|
// Each libpq message begins with a message type byte, followed by message length
|
||||||
|
// If the client closes the connection, return None. But if the client closes the
|
||||||
|
// connection in the middle of a message, we will return an error.
|
||||||
|
let tag = match retry_read!(stream.read_u8().await) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||||
|
Err(e) => return Err(ConnectionError::Socket(e)),
|
||||||
|
};
|
||||||
|
|
||||||
// length field includes itself, but not message type.
|
// The message length includes itself, so it better be at least 4.
|
||||||
let total_len = len as usize + 1;
|
let len = retry_read!(stream.read_u32().await)
|
||||||
if buf.len() < total_len {
|
.map_err(ConnectionError::Socket)?
|
||||||
// Don't have full message yet.
|
.checked_sub(4)
|
||||||
let to_read = total_len - buf.len();
|
.ok_or_else(|| ConnectionError::Protocol("invalid message length".to_string()))?;
|
||||||
buf.reserve(to_read);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// got the message, advance buffer
|
let body = {
|
||||||
let mut msg = buf.split_to(total_len).freeze();
|
let mut buffer = vec![0u8; len as usize];
|
||||||
msg.advance(5); // consume message type and len
|
stream
|
||||||
|
.read_exact(&mut buffer)
|
||||||
|
.await
|
||||||
|
.map_err(ConnectionError::Socket)?;
|
||||||
|
Bytes::from(buffer)
|
||||||
|
};
|
||||||
|
|
||||||
match tag {
|
match tag {
|
||||||
b'Q' => Ok(Some(FeMessage::Query(msg))),
|
b'Q' => Ok(Some(FeMessage::Query(body))),
|
||||||
b'P' => Ok(Some(FeParseMessage::parse(msg)?)),
|
b'P' => Ok(Some(FeParseMessage::parse(body)?)),
|
||||||
b'D' => Ok(Some(FeDescribeMessage::parse(msg)?)),
|
b'D' => Ok(Some(FeDescribeMessage::parse(body)?)),
|
||||||
b'E' => Ok(Some(FeExecuteMessage::parse(msg)?)),
|
b'E' => Ok(Some(FeExecuteMessage::parse(body)?)),
|
||||||
b'B' => Ok(Some(FeBindMessage::parse(msg)?)),
|
b'B' => Ok(Some(FeBindMessage::parse(body)?)),
|
||||||
b'C' => Ok(Some(FeCloseMessage::parse(msg)?)),
|
b'C' => Ok(Some(FeCloseMessage::parse(body)?)),
|
||||||
b'S' => Ok(Some(FeMessage::Sync)),
|
b'S' => Ok(Some(FeMessage::Sync)),
|
||||||
b'X' => Ok(Some(FeMessage::Terminate)),
|
b'X' => Ok(Some(FeMessage::Terminate)),
|
||||||
b'd' => Ok(Some(FeMessage::CopyData(msg))),
|
b'd' => Ok(Some(FeMessage::CopyData(body))),
|
||||||
b'c' => Ok(Some(FeMessage::CopyDone)),
|
b'c' => Ok(Some(FeMessage::CopyDone)),
|
||||||
b'f' => Ok(Some(FeMessage::CopyFail)),
|
b'f' => Ok(Some(FeMessage::CopyFail)),
|
||||||
b'p' => Ok(Some(FeMessage::PasswordMessage(msg))),
|
b'p' => Ok(Some(FeMessage::PasswordMessage(body))),
|
||||||
tag => Err(ProtocolError::Protocol(format!(
|
tag => {
|
||||||
"unknown message tag: {tag},'{msg:?}'"
|
return Err(ConnectionError::Protocol(format!(
|
||||||
))),
|
"unknown message tag: {tag},'{body:?}'"
|
||||||
}
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeStartupPacket {
|
impl FeStartupPacket {
|
||||||
/// Read and parse startup message from the `buf` input buffer. It is
|
/// Read startup message from the stream.
|
||||||
/// different from [`FeMessage::parse`] because startup messages don't have
|
// XXX: It's tempting yet undesirable to accept `stream` by value,
|
||||||
/// message type byte; otherwise, its comments apply.
|
// since such a change will cause user-supplied &mut references to be consumed
|
||||||
pub fn parse(buf: &mut BytesMut) -> Result<Option<FeStartupPacket>, ProtocolError> {
|
pub fn read(
|
||||||
|
stream: &mut (impl io::Read + Unpin),
|
||||||
|
) -> Result<Option<FeMessage>, ConnectionError> {
|
||||||
|
Self::read_fut(&mut AsyncishRead(stream)).wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read startup message from the stream.
|
||||||
|
// XXX: It's tempting yet undesirable to accept `stream` by value,
|
||||||
|
// since such a change will cause user-supplied &mut references to be consumed
|
||||||
|
pub fn read_fut<Reader>(
|
||||||
|
stream: &mut Reader,
|
||||||
|
) -> SyncFuture<Reader, impl Future<Output = Result<Option<FeMessage>, ConnectionError>> + '_>
|
||||||
|
where
|
||||||
|
Reader: tokio::io::AsyncRead + Unpin,
|
||||||
|
{
|
||||||
const MAX_STARTUP_PACKET_LENGTH: usize = 10000;
|
const MAX_STARTUP_PACKET_LENGTH: usize = 10000;
|
||||||
const RESERVED_INVALID_MAJOR_VERSION: u32 = 1234;
|
const RESERVED_INVALID_MAJOR_VERSION: u32 = 1234;
|
||||||
const CANCEL_REQUEST_CODE: u32 = 5678;
|
const CANCEL_REQUEST_CODE: u32 = 5678;
|
||||||
const NEGOTIATE_SSL_CODE: u32 = 5679;
|
const NEGOTIATE_SSL_CODE: u32 = 5679;
|
||||||
const NEGOTIATE_GSS_CODE: u32 = 5680;
|
const NEGOTIATE_GSS_CODE: u32 = 5680;
|
||||||
|
|
||||||
// need at least 4 bytes with packet len
|
SyncFuture::new(async move {
|
||||||
if buf.len() < 4 {
|
// Read length. If the connection is closed before reading anything (or before
|
||||||
let to_read = 4 - buf.len();
|
// reading 4 bytes, to be precise), return None to indicate that the connection
|
||||||
buf.reserve(to_read);
|
// was closed. This matches the PostgreSQL server's behavior, which avoids noise
|
||||||
return Ok(None);
|
// in the log if the client opens connection but closes it immediately.
|
||||||
}
|
let len = match retry_read!(stream.read_u32().await) {
|
||||||
|
Ok(len) => len as usize,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||||
|
Err(e) => return Err(ConnectionError::Socket(e)),
|
||||||
|
};
|
||||||
|
|
||||||
// We shouldn't advance `buf` as probably full message is not there yet,
|
#[allow(clippy::manual_range_contains)]
|
||||||
// so can't directly use Bytes::get_u32 etc.
|
if len < 4 || len > MAX_STARTUP_PACKET_LENGTH {
|
||||||
let len = (&buf[0..4]).read_u32::<BigEndian>().unwrap() as usize;
|
return Err(ConnectionError::Protocol(format!(
|
||||||
if len < 4 || len > MAX_STARTUP_PACKET_LENGTH {
|
"invalid message length {len}"
|
||||||
return Err(ProtocolError::Protocol(format!(
|
|
||||||
"invalid startup packet message length {}",
|
|
||||||
len
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf.len() < len {
|
|
||||||
// Don't have full message yet.
|
|
||||||
let to_read = len - buf.len();
|
|
||||||
buf.reserve(to_read);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// got the message, advance buffer
|
|
||||||
let mut msg = buf.split_to(len).freeze();
|
|
||||||
msg.advance(4); // consume len
|
|
||||||
|
|
||||||
let request_code = msg.get_u32();
|
|
||||||
let req_hi = request_code >> 16;
|
|
||||||
let req_lo = request_code & ((1 << 16) - 1);
|
|
||||||
// StartupMessage, CancelRequest, SSLRequest etc are differentiated by request code.
|
|
||||||
let message = match (req_hi, req_lo) {
|
|
||||||
(RESERVED_INVALID_MAJOR_VERSION, CANCEL_REQUEST_CODE) => {
|
|
||||||
if msg.remaining() != 8 {
|
|
||||||
return Err(ProtocolError::BadMessage(
|
|
||||||
"CancelRequest message is malformed, backend PID / secret key missing"
|
|
||||||
.to_owned(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
FeStartupPacket::CancelRequest(CancelKeyData {
|
|
||||||
backend_pid: msg.get_i32(),
|
|
||||||
cancel_key: msg.get_i32(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
(RESERVED_INVALID_MAJOR_VERSION, NEGOTIATE_SSL_CODE) => {
|
|
||||||
// Requested upgrade to SSL (aka TLS)
|
|
||||||
FeStartupPacket::SslRequest
|
|
||||||
}
|
|
||||||
(RESERVED_INVALID_MAJOR_VERSION, NEGOTIATE_GSS_CODE) => {
|
|
||||||
// Requested upgrade to GSSAPI
|
|
||||||
FeStartupPacket::GssEncRequest
|
|
||||||
}
|
|
||||||
(RESERVED_INVALID_MAJOR_VERSION, unrecognized_code) => {
|
|
||||||
return Err(ProtocolError::Protocol(format!(
|
|
||||||
"Unrecognized request code {unrecognized_code}"
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// TODO bail if protocol major_version is not 3?
|
|
||||||
(major_version, minor_version) => {
|
|
||||||
// StartupMessage
|
|
||||||
|
|
||||||
// Parse pairs of null-terminated strings (key, value).
|
let request_code =
|
||||||
// See `postgres: ProcessStartupPacket, build_startup_packet`.
|
retry_read!(stream.read_u32().await).map_err(ConnectionError::Socket)?;
|
||||||
let mut tokens = str::from_utf8(&msg)
|
|
||||||
.map_err(|_e| {
|
|
||||||
ProtocolError::BadMessage("StartupMessage params: invalid utf-8".to_owned())
|
|
||||||
})?
|
|
||||||
.strip_suffix('\0') // drop packet's own null
|
|
||||||
.ok_or_else(|| {
|
|
||||||
ProtocolError::Protocol(
|
|
||||||
"StartupMessage params: missing null terminator".to_string(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.split_terminator('\0');
|
|
||||||
|
|
||||||
let mut params = HashMap::new();
|
// the rest of startup packet are params
|
||||||
while let Some(name) = tokens.next() {
|
let params_len = len - 8;
|
||||||
let value = tokens.next().ok_or_else(|| {
|
let mut params_bytes = vec![0u8; params_len];
|
||||||
ProtocolError::Protocol(
|
stream
|
||||||
"StartupMessage params: key without value".to_string(),
|
.read_exact(params_bytes.as_mut())
|
||||||
)
|
.await
|
||||||
})?;
|
.map_err(ConnectionError::Socket)?;
|
||||||
|
|
||||||
params.insert(name.to_owned(), value.to_owned());
|
// Parse params depending on request code
|
||||||
|
let req_hi = request_code >> 16;
|
||||||
|
let req_lo = request_code & ((1 << 16) - 1);
|
||||||
|
let message = match (req_hi, req_lo) {
|
||||||
|
(RESERVED_INVALID_MAJOR_VERSION, CANCEL_REQUEST_CODE) => {
|
||||||
|
if params_len != 8 {
|
||||||
|
return Err(ConnectionError::Protocol(
|
||||||
|
"expected 8 bytes for CancelRequest params".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut cursor = Cursor::new(params_bytes);
|
||||||
|
FeStartupPacket::CancelRequest(CancelKeyData {
|
||||||
|
backend_pid: cursor.read_i32().await.map_err(ConnectionError::Socket)?,
|
||||||
|
cancel_key: cursor.read_i32().await.map_err(ConnectionError::Socket)?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
(RESERVED_INVALID_MAJOR_VERSION, NEGOTIATE_SSL_CODE) => {
|
||||||
FeStartupPacket::StartupMessage {
|
// Requested upgrade to SSL (aka TLS)
|
||||||
major_version,
|
FeStartupPacket::SslRequest
|
||||||
minor_version,
|
|
||||||
params: StartupMessageParams { params },
|
|
||||||
}
|
}
|
||||||
}
|
(RESERVED_INVALID_MAJOR_VERSION, NEGOTIATE_GSS_CODE) => {
|
||||||
};
|
// Requested upgrade to GSSAPI
|
||||||
Ok(Some(message))
|
FeStartupPacket::GssEncRequest
|
||||||
|
}
|
||||||
|
(RESERVED_INVALID_MAJOR_VERSION, unrecognized_code) => {
|
||||||
|
return Err(ConnectionError::Protocol(format!(
|
||||||
|
"Unrecognized request code {unrecognized_code}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
// TODO bail if protocol major_version is not 3?
|
||||||
|
(major_version, minor_version) => {
|
||||||
|
// Parse pairs of null-terminated strings (key, value).
|
||||||
|
// See `postgres: ProcessStartupPacket, build_startup_packet`.
|
||||||
|
let mut tokens = str::from_utf8(¶ms_bytes)
|
||||||
|
.context("StartupMessage params: invalid utf-8")?
|
||||||
|
.strip_suffix('\0') // drop packet's own null
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ConnectionError::Protocol(
|
||||||
|
"StartupMessage params: missing null terminator".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.split_terminator('\0');
|
||||||
|
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
while let Some(name) = tokens.next() {
|
||||||
|
let value = tokens.next().ok_or_else(|| {
|
||||||
|
ConnectionError::Protocol(
|
||||||
|
"StartupMessage params: key without value".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
params.insert(name.to_owned(), value.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
FeStartupPacket::StartupMessage {
|
||||||
|
major_version,
|
||||||
|
minor_version,
|
||||||
|
params: StartupMessageParams { params },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(FeMessage::StartupPacket(message)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeParseMessage {
|
impl FeParseMessage {
|
||||||
fn parse(mut buf: Bytes) -> Result<FeMessage, ProtocolError> {
|
fn parse(mut buf: Bytes) -> anyhow::Result<FeMessage> {
|
||||||
// FIXME: the rust-postgres driver uses a named prepared statement
|
// FIXME: the rust-postgres driver uses a named prepared statement
|
||||||
// for copy_out(). We're not prepared to handle that correctly. For
|
// for copy_out(). We're not prepared to handle that correctly. For
|
||||||
// now, just ignore the statement name, assuming that the client never
|
// now, just ignore the statement name, assuming that the client never
|
||||||
@@ -390,82 +440,55 @@ impl FeParseMessage {
|
|||||||
|
|
||||||
let _pstmt_name = read_cstr(&mut buf)?;
|
let _pstmt_name = read_cstr(&mut buf)?;
|
||||||
let query_string = read_cstr(&mut buf)?;
|
let query_string = read_cstr(&mut buf)?;
|
||||||
if buf.remaining() < 2 {
|
|
||||||
return Err(ProtocolError::BadMessage(
|
|
||||||
"Parse message is malformed, nparams missing".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let nparams = buf.get_i16();
|
let nparams = buf.get_i16();
|
||||||
|
|
||||||
if nparams != 0 {
|
ensure!(nparams == 0, "query params not implemented");
|
||||||
return Err(ProtocolError::BadMessage(
|
|
||||||
"query params not implemented".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FeMessage::Parse(FeParseMessage { query_string }))
|
Ok(FeMessage::Parse(FeParseMessage { query_string }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeDescribeMessage {
|
impl FeDescribeMessage {
|
||||||
fn parse(mut buf: Bytes) -> Result<FeMessage, ProtocolError> {
|
fn parse(mut buf: Bytes) -> anyhow::Result<FeMessage> {
|
||||||
let kind = buf.get_u8();
|
let kind = buf.get_u8();
|
||||||
let _pstmt_name = read_cstr(&mut buf)?;
|
let _pstmt_name = read_cstr(&mut buf)?;
|
||||||
|
|
||||||
// FIXME: see FeParseMessage::parse
|
// FIXME: see FeParseMessage::parse
|
||||||
if kind != b'S' {
|
ensure!(
|
||||||
return Err(ProtocolError::BadMessage(
|
kind == b'S',
|
||||||
"only prepared statemement Describe is implemented".to_string(),
|
"only prepared statemement Describe is implemented"
|
||||||
));
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FeMessage::Describe(FeDescribeMessage { kind }))
|
Ok(FeMessage::Describe(FeDescribeMessage { kind }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeExecuteMessage {
|
impl FeExecuteMessage {
|
||||||
fn parse(mut buf: Bytes) -> Result<FeMessage, ProtocolError> {
|
fn parse(mut buf: Bytes) -> anyhow::Result<FeMessage> {
|
||||||
let portal_name = read_cstr(&mut buf)?;
|
let portal_name = read_cstr(&mut buf)?;
|
||||||
if buf.remaining() < 4 {
|
|
||||||
return Err(ProtocolError::BadMessage(
|
|
||||||
"FeExecuteMessage message is malformed, maxrows missing".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let maxrows = buf.get_i32();
|
let maxrows = buf.get_i32();
|
||||||
|
|
||||||
if !portal_name.is_empty() {
|
ensure!(portal_name.is_empty(), "named portals not implemented");
|
||||||
return Err(ProtocolError::BadMessage(
|
ensure!(maxrows == 0, "row limit in Execute message not implemented");
|
||||||
"named portals not implemented".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if maxrows != 0 {
|
|
||||||
return Err(ProtocolError::BadMessage(
|
|
||||||
"row limit in Execute message not implemented".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FeMessage::Execute(FeExecuteMessage { maxrows }))
|
Ok(FeMessage::Execute(FeExecuteMessage { maxrows }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeBindMessage {
|
impl FeBindMessage {
|
||||||
fn parse(mut buf: Bytes) -> Result<FeMessage, ProtocolError> {
|
fn parse(mut buf: Bytes) -> anyhow::Result<FeMessage> {
|
||||||
let portal_name = read_cstr(&mut buf)?;
|
let portal_name = read_cstr(&mut buf)?;
|
||||||
let _pstmt_name = read_cstr(&mut buf)?;
|
let _pstmt_name = read_cstr(&mut buf)?;
|
||||||
|
|
||||||
// FIXME: see FeParseMessage::parse
|
// FIXME: see FeParseMessage::parse
|
||||||
if !portal_name.is_empty() {
|
ensure!(portal_name.is_empty(), "named portals not implemented");
|
||||||
return Err(ProtocolError::BadMessage(
|
|
||||||
"named portals not implemented".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FeMessage::Bind(FeBindMessage))
|
Ok(FeMessage::Bind(FeBindMessage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeCloseMessage {
|
impl FeCloseMessage {
|
||||||
fn parse(mut buf: Bytes) -> Result<FeMessage, ProtocolError> {
|
fn parse(mut buf: Bytes) -> anyhow::Result<FeMessage> {
|
||||||
let _kind = buf.get_u8();
|
let _kind = buf.get_u8();
|
||||||
let _pstmt_or_portal_name = read_cstr(&mut buf)?;
|
let _pstmt_or_portal_name = read_cstr(&mut buf)?;
|
||||||
|
|
||||||
@@ -494,7 +517,6 @@ pub enum BeMessage<'a> {
|
|||||||
CloseComplete,
|
CloseComplete,
|
||||||
// None means column is NULL
|
// None means column is NULL
|
||||||
DataRow(&'a [Option<&'a [u8]>]),
|
DataRow(&'a [Option<&'a [u8]>]),
|
||||||
// None errcode means internal_error will be sent.
|
|
||||||
ErrorResponse(&'a str, Option<&'a [u8; 5]>),
|
ErrorResponse(&'a str, Option<&'a [u8; 5]>),
|
||||||
/// Single byte - used in response to SSLRequest/GSSENCRequest.
|
/// Single byte - used in response to SSLRequest/GSSENCRequest.
|
||||||
EncryptionResponse(bool),
|
EncryptionResponse(bool),
|
||||||
@@ -525,11 +547,6 @@ impl<'a> BeMessage<'a> {
|
|||||||
value: b"UTF8",
|
value: b"UTF8",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const INTEGER_DATETIMES: Self = Self::ParameterStatus {
|
|
||||||
name: b"integer_datetimes",
|
|
||||||
value: b"on",
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Build a [`BeMessage::ParameterStatus`] holding the server version.
|
/// Build a [`BeMessage::ParameterStatus`] holding the server version.
|
||||||
pub fn server_version(version: &'a str) -> Self {
|
pub fn server_version(version: &'a str) -> Self {
|
||||||
Self::ParameterStatus {
|
Self::ParameterStatus {
|
||||||
@@ -608,7 +625,7 @@ impl RowDescriptor<'_> {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct XLogDataBody<'a> {
|
pub struct XLogDataBody<'a> {
|
||||||
pub wal_start: u64,
|
pub wal_start: u64,
|
||||||
pub wal_end: u64, // current end of WAL on the server
|
pub wal_end: u64,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub data: &'a [u8],
|
pub data: &'a [u8],
|
||||||
}
|
}
|
||||||
@@ -648,11 +665,12 @@ fn write_body<R>(buf: &mut BytesMut, f: impl FnOnce(&mut BytesMut) -> R) -> R {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Safe write of s into buf as cstring (String in the protocol).
|
/// Safe write of s into buf as cstring (String in the protocol).
|
||||||
fn write_cstr(s: impl AsRef<[u8]>, buf: &mut BytesMut) -> Result<(), ProtocolError> {
|
fn write_cstr(s: impl AsRef<[u8]>, buf: &mut BytesMut) -> io::Result<()> {
|
||||||
let bytes = s.as_ref();
|
let bytes = s.as_ref();
|
||||||
if bytes.contains(&0) {
|
if bytes.contains(&0) {
|
||||||
return Err(ProtocolError::BadMessage(
|
return Err(io::Error::new(
|
||||||
"string contains embedded null".to_owned(),
|
io::ErrorKind::InvalidInput,
|
||||||
|
"string contains embedded null",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
buf.put_slice(bytes);
|
buf.put_slice(bytes);
|
||||||
@@ -660,27 +678,22 @@ fn write_cstr(s: impl AsRef<[u8]>, buf: &mut BytesMut) -> Result<(), ProtocolErr
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read cstring from buf, advancing it.
|
fn read_cstr(buf: &mut Bytes) -> anyhow::Result<Bytes> {
|
||||||
fn read_cstr(buf: &mut Bytes) -> Result<Bytes, ProtocolError> {
|
let pos = buf.iter().position(|x| *x == 0);
|
||||||
let pos = buf
|
let result = buf.split_to(pos.context("missing terminator")?);
|
||||||
.iter()
|
|
||||||
.position(|x| *x == 0)
|
|
||||||
.ok_or_else(|| ProtocolError::BadMessage("missing cstring terminator".to_owned()))?;
|
|
||||||
let result = buf.split_to(pos);
|
|
||||||
buf.advance(1); // drop the null terminator
|
buf.advance(1); // drop the null terminator
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const SQLSTATE_INTERNAL_ERROR: &[u8; 5] = b"XX000";
|
pub const SQLSTATE_INTERNAL_ERROR: &[u8; 5] = b"XX000";
|
||||||
pub const SQLSTATE_SUCCESSFUL_COMPLETION: &[u8; 5] = b"00000";
|
|
||||||
|
|
||||||
impl<'a> BeMessage<'a> {
|
impl<'a> BeMessage<'a> {
|
||||||
/// Serialize `message` to the given `buf`.
|
/// Write message to the given buf.
|
||||||
/// Apart from smart memory managemet, BytesMut is good here as msg len
|
// Unlike the reading side, we use BytesMut
|
||||||
/// precedes its body and it is handy to write it down first and then fill
|
// here as msg len precedes its body and it is handy to write it down first
|
||||||
/// the length. With Write we would have to either calc it manually or have
|
// and then fill the length. With Write we would have to either calc it
|
||||||
/// one more buffer.
|
// manually or have one more buffer.
|
||||||
pub fn write(buf: &mut BytesMut, message: &BeMessage) -> Result<(), ProtocolError> {
|
pub fn write(buf: &mut BytesMut, message: &BeMessage) -> io::Result<()> {
|
||||||
match message {
|
match message {
|
||||||
BeMessage::AuthenticationOk => {
|
BeMessage::AuthenticationOk => {
|
||||||
buf.put_u8(b'R');
|
buf.put_u8(b'R');
|
||||||
@@ -725,7 +738,7 @@ impl<'a> BeMessage<'a> {
|
|||||||
buf.put_slice(extra);
|
buf.put_slice(extra);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok::<_, io::Error>(())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +842,7 @@ impl<'a> BeMessage<'a> {
|
|||||||
write_cstr(error_msg, buf)?;
|
write_cstr(error_msg, buf)?;
|
||||||
|
|
||||||
buf.put_u8(0); // terminator
|
buf.put_u8(0); // terminator
|
||||||
Ok(())
|
Ok::<_, io::Error>(())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,7 +865,7 @@ impl<'a> BeMessage<'a> {
|
|||||||
write_cstr(error_msg.as_bytes(), buf)?;
|
write_cstr(error_msg.as_bytes(), buf)?;
|
||||||
|
|
||||||
buf.put_u8(0); // terminator
|
buf.put_u8(0); // terminator
|
||||||
Ok(())
|
Ok::<_, io::Error>(())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -907,7 +920,7 @@ impl<'a> BeMessage<'a> {
|
|||||||
buf.put_i32(-1); /* typmod */
|
buf.put_i32(-1); /* typmod */
|
||||||
buf.put_i16(0); /* format code */
|
buf.put_i16(0); /* format code */
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok::<_, io::Error>(())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,7 +987,7 @@ impl ReplicationFeedback {
|
|||||||
// null-terminated string - key,
|
// null-terminated string - key,
|
||||||
// uint32 - value length in bytes
|
// uint32 - value length in bytes
|
||||||
// value itself
|
// value itself
|
||||||
pub fn serialize(&self, buf: &mut BytesMut) {
|
pub fn serialize(&self, buf: &mut BytesMut) -> Result<()> {
|
||||||
buf.put_u8(REPLICATION_FEEDBACK_FIELDS_NUMBER); // # of keys
|
buf.put_u8(REPLICATION_FEEDBACK_FIELDS_NUMBER); // # of keys
|
||||||
buf.put_slice(b"current_timeline_size\0");
|
buf.put_slice(b"current_timeline_size\0");
|
||||||
buf.put_i32(8);
|
buf.put_i32(8);
|
||||||
@@ -999,6 +1012,7 @@ impl ReplicationFeedback {
|
|||||||
buf.put_slice(b"ps_replytime\0");
|
buf.put_slice(b"ps_replytime\0");
|
||||||
buf.put_i32(8);
|
buf.put_i32(8);
|
||||||
buf.put_i64(timestamp);
|
buf.put_i64(timestamp);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserialize ReplicationFeedback message
|
// Deserialize ReplicationFeedback message
|
||||||
@@ -1066,7 +1080,7 @@ mod tests {
|
|||||||
// because it is rounded up to microseconds during serialization.
|
// because it is rounded up to microseconds during serialization.
|
||||||
rf.ps_replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
|
rf.ps_replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
|
||||||
let mut data = BytesMut::new();
|
let mut data = BytesMut::new();
|
||||||
rf.serialize(&mut data);
|
rf.serialize(&mut data).unwrap();
|
||||||
|
|
||||||
let rf_parsed = ReplicationFeedback::parse(data.freeze());
|
let rf_parsed = ReplicationFeedback::parse(data.freeze());
|
||||||
assert_eq!(rf, rf_parsed);
|
assert_eq!(rf, rf_parsed);
|
||||||
@@ -1081,7 +1095,7 @@ mod tests {
|
|||||||
// because it is rounded up to microseconds during serialization.
|
// because it is rounded up to microseconds during serialization.
|
||||||
rf.ps_replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
|
rf.ps_replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
|
||||||
let mut data = BytesMut::new();
|
let mut data = BytesMut::new();
|
||||||
rf.serialize(&mut data);
|
rf.serialize(&mut data).unwrap();
|
||||||
|
|
||||||
// Add an extra field to the buffer and adjust number of keys
|
// Add an extra field to the buffer and adjust number of keys
|
||||||
if let Some(first) = data.first_mut() {
|
if let Some(first) = data.first_mut() {
|
||||||
@@ -1123,6 +1137,15 @@ mod tests {
|
|||||||
let params = make_params("foo\\ bar \\ \\\\ baz\\ lol");
|
let params = make_params("foo\\ bar \\ \\\\ baz\\ lol");
|
||||||
assert_eq!(split_options(¶ms), ["foo bar", " \\", "baz ", "lol"]);
|
assert_eq!(split_options(¶ms), ["foo bar", " \\", "baz ", "lol"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure that `read` is sync/async callable
|
||||||
|
async fn _assert(stream: &mut (impl tokio::io::AsyncRead + Unpin)) {
|
||||||
|
let _ = FeMessage::read(&mut [].as_ref());
|
||||||
|
let _ = FeMessage::read_fut(stream).await;
|
||||||
|
|
||||||
|
let _ = FeStartupPacket::read(&mut [].as_ref());
|
||||||
|
let _ = FeStartupPacket::read_fut(stream).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terminate_code(code: &[u8; 5]) -> [u8; 6] {
|
fn terminate_code(code: &[u8; 5]) -> [u8; 6] {
|
||||||
|
|||||||
179
libs/pq_proto/src/sync.rs
Normal file
179
libs/pq_proto/src/sync.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
use pin_project_lite::pin_project;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::{io, task};
|
||||||
|
|
||||||
|
pin_project! {
|
||||||
|
/// We use this future to mark certain methods
|
||||||
|
/// as callable in both sync and async modes.
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct SyncFuture<S, T: Future> {
|
||||||
|
#[pin]
|
||||||
|
inner: T,
|
||||||
|
_marker: PhantomData<S>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This wrapper lets us synchronously wait for inner future's completion
|
||||||
|
/// (see [`SyncFuture::wait`]) **provided that `S` implements [`SyncProof`]**.
|
||||||
|
/// For instance, `S` may be substituted with types implementing
|
||||||
|
/// [`tokio::io::AsyncRead`], but it's not the only viable option.
|
||||||
|
impl<S, T: Future> SyncFuture<S, T> {
|
||||||
|
/// NOTE: caller should carefully pick a type for `S`,
|
||||||
|
/// because we don't want to enable [`SyncFuture::wait`] when
|
||||||
|
/// it's in fact impossible to run the future synchronously.
|
||||||
|
/// Violation of this contract will not cause UB, but
|
||||||
|
/// panics and async event loop freezes won't please you.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use pq_proto::sync::SyncFuture;
|
||||||
|
/// # use std::future::Future;
|
||||||
|
/// # use tokio::io::AsyncReadExt;
|
||||||
|
/// #
|
||||||
|
/// // Parse a pair of numbers from a stream
|
||||||
|
/// pub fn parse_pair<Reader>(
|
||||||
|
/// stream: &mut Reader,
|
||||||
|
/// ) -> SyncFuture<Reader, impl Future<Output = anyhow::Result<(u32, u64)>> + '_>
|
||||||
|
/// where
|
||||||
|
/// Reader: tokio::io::AsyncRead + Unpin,
|
||||||
|
/// {
|
||||||
|
/// // If `Reader` is a `SyncProof`, this will give caller
|
||||||
|
/// // an opportunity to use `SyncFuture::wait`, because
|
||||||
|
/// // `.await` will always result in `Poll::Ready`.
|
||||||
|
/// SyncFuture::new(async move {
|
||||||
|
/// let x = stream.read_u32().await?;
|
||||||
|
/// let y = stream.read_u64().await?;
|
||||||
|
/// Ok((x, y))
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, T: Future> Future for SyncFuture<S, T> {
|
||||||
|
type Output = T::Output;
|
||||||
|
|
||||||
|
/// In async code, [`SyncFuture`] behaves like a regular wrapper.
|
||||||
|
#[inline(always)]
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
|
||||||
|
self.project().inner.poll(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Postulates that we can call [`SyncFuture::wait`].
|
||||||
|
/// If implementer is also a [`Future`], it should always
|
||||||
|
/// return [`task::Poll::Ready`] from [`Future::poll`].
|
||||||
|
///
|
||||||
|
/// Each implementation should document which futures
|
||||||
|
/// specifically are being declared sync-proof.
|
||||||
|
pub trait SyncPostulate {}
|
||||||
|
|
||||||
|
impl<T: SyncPostulate> SyncPostulate for &T {}
|
||||||
|
impl<T: SyncPostulate> SyncPostulate for &mut T {}
|
||||||
|
|
||||||
|
impl<P: SyncPostulate, T: Future> SyncFuture<P, T> {
|
||||||
|
/// Synchronously wait for future completion.
|
||||||
|
pub fn wait(mut self) -> T::Output {
|
||||||
|
const RAW_WAKER: task::RawWaker = task::RawWaker::new(
|
||||||
|
std::ptr::null(),
|
||||||
|
&task::RawWakerVTable::new(
|
||||||
|
|_| RAW_WAKER,
|
||||||
|
|_| panic!("SyncFuture: failed to wake"),
|
||||||
|
|_| panic!("SyncFuture: failed to wake by ref"),
|
||||||
|
|_| { /* drop is no-op */ },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// SAFETY: We never move `self` during this call;
|
||||||
|
// furthermore, it will be dropped in the end regardless of panics
|
||||||
|
let this = unsafe { Pin::new_unchecked(&mut self) };
|
||||||
|
|
||||||
|
// SAFETY: This waker doesn't do anything apart from panicking
|
||||||
|
let waker = unsafe { task::Waker::from_raw(RAW_WAKER) };
|
||||||
|
let context = &mut task::Context::from_waker(&waker);
|
||||||
|
|
||||||
|
match this.poll(context) {
|
||||||
|
task::Poll::Ready(res) => res,
|
||||||
|
_ => panic!("SyncFuture: unexpected pending!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This wrapper turns any [`std::io::Read`] into a blocking [`tokio::io::AsyncRead`],
|
||||||
|
/// which lets us abstract over sync & async readers in methods returning [`SyncFuture`].
|
||||||
|
/// NOTE: you **should not** use this in async code.
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct AsyncishRead<T: io::Read + Unpin>(pub T);
|
||||||
|
|
||||||
|
/// This lets us call [`SyncFuture<AsyncishRead<_>, _>::wait`],
|
||||||
|
/// and allows the future to await on any of the [`AsyncRead`]
|
||||||
|
/// and [`AsyncReadExt`] methods on `AsyncishRead`.
|
||||||
|
impl<T: io::Read + Unpin> SyncPostulate for AsyncishRead<T> {}
|
||||||
|
|
||||||
|
impl<T: io::Read + Unpin> tokio::io::AsyncRead for AsyncishRead<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn poll_read(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
_cx: &mut task::Context<'_>,
|
||||||
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
|
) -> task::Poll<io::Result<()>> {
|
||||||
|
task::Poll::Ready(
|
||||||
|
// `Read::read` will block, meaning we don't need a real event loop!
|
||||||
|
self.0
|
||||||
|
.read(buf.initialize_unfilled())
|
||||||
|
.map(|sz| buf.advance(sz)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
// async helper(stream: &mut impl AsyncRead) -> io::Result<u32>
|
||||||
|
fn bytes_add<Reader>(
|
||||||
|
stream: &mut Reader,
|
||||||
|
) -> SyncFuture<Reader, impl Future<Output = io::Result<u32>> + '_>
|
||||||
|
where
|
||||||
|
Reader: tokio::io::AsyncRead + Unpin,
|
||||||
|
{
|
||||||
|
SyncFuture::new(async move {
|
||||||
|
let a = stream.read_u32().await?;
|
||||||
|
let b = stream.read_u32().await?;
|
||||||
|
Ok(a + b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync() {
|
||||||
|
let bytes = [100u32.to_be_bytes(), 200u32.to_be_bytes()].concat();
|
||||||
|
let res = bytes_add(&mut AsyncishRead(&mut &bytes[..]))
|
||||||
|
.wait()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need a single-threaded executor for this test
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn test_async() {
|
||||||
|
let (mut tx, mut rx) = tokio::net::UnixStream::pair().unwrap();
|
||||||
|
|
||||||
|
let write = async move {
|
||||||
|
tx.write_u32(100).await?;
|
||||||
|
tx.write_u32(200).await?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let (res, ()) = tokio::try_join!(bytes_add(&mut rx), write).unwrap();
|
||||||
|
assert_eq!(res, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ toml_edit.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
metrics.workspace = true
|
metrics.workspace = true
|
||||||
utils.workspace = true
|
utils.workspace = true
|
||||||
pin-project-lite.workspace = true
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ pub trait RemoteStorage: Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Download {
|
pub struct Download {
|
||||||
pub download_stream: Pin<Box<dyn io::AsyncRead + Unpin + Send + Sync>>,
|
pub download_stream: Pin<Box<dyn io::AsyncRead + Unpin + Send>>,
|
||||||
/// Extra key-value data, associated with the current remote file.
|
/// Extra key-value data, associated with the current remote file.
|
||||||
pub metadata: Option<StorageMetadata>,
|
pub metadata: Option<StorageMetadata>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ use aws_sdk_s3::{
|
|||||||
};
|
};
|
||||||
use aws_smithy_http::body::SdkBody;
|
use aws_smithy_http::body::SdkBody;
|
||||||
use hyper::Body;
|
use hyper::Body;
|
||||||
use tokio::{
|
use tokio::{io, sync::Semaphore};
|
||||||
io::{self, AsyncRead},
|
|
||||||
sync::Semaphore,
|
|
||||||
};
|
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ pub struct S3Bucket {
|
|||||||
// Every request to S3 can be throttled or cancelled, if a certain number of requests per second is exceeded.
|
// Every request to S3 can be throttled or cancelled, if a certain number of requests per second is exceeded.
|
||||||
// Same goes to IAM, which is queried before every S3 request, if enabled. IAM has even lower RPS threshold.
|
// Same goes to IAM, which is queried before every S3 request, if enabled. IAM has even lower RPS threshold.
|
||||||
// The helps to ensure we don't exceed the thresholds.
|
// The helps to ensure we don't exceed the thresholds.
|
||||||
concurrency_limiter: Arc<Semaphore>,
|
concurrency_limiter: Semaphore,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -165,7 +162,7 @@ impl S3Bucket {
|
|||||||
client,
|
client,
|
||||||
bucket_name: aws_config.bucket_name.clone(),
|
bucket_name: aws_config.bucket_name.clone(),
|
||||||
prefix_in_bucket,
|
prefix_in_bucket,
|
||||||
concurrency_limiter: Arc::new(Semaphore::new(aws_config.concurrency_limit.get())),
|
concurrency_limiter: Semaphore::new(aws_config.concurrency_limit.get()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +194,9 @@ impl S3Bucket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn download_object(&self, request: GetObjectRequest) -> Result<Download, DownloadError> {
|
async fn download_object(&self, request: GetObjectRequest) -> Result<Download, DownloadError> {
|
||||||
let permit = self
|
let _guard = self
|
||||||
.concurrency_limiter
|
.concurrency_limiter
|
||||||
.clone()
|
.acquire()
|
||||||
.acquire_owned()
|
|
||||||
.await
|
.await
|
||||||
.context("Concurrency limiter semaphore got closed during S3 download")
|
.context("Concurrency limiter semaphore got closed during S3 download")
|
||||||
.map_err(DownloadError::Other)?;
|
.map_err(DownloadError::Other)?;
|
||||||
@@ -221,10 +217,9 @@ impl S3Bucket {
|
|||||||
let metadata = object_output.metadata().cloned().map(StorageMetadata);
|
let metadata = object_output.metadata().cloned().map(StorageMetadata);
|
||||||
Ok(Download {
|
Ok(Download {
|
||||||
metadata,
|
metadata,
|
||||||
download_stream: Box::pin(io::BufReader::new(RatelimitedAsyncRead::new(
|
download_stream: Box::pin(io::BufReader::new(
|
||||||
permit,
|
|
||||||
object_output.body.into_async_read(),
|
object_output.body.into_async_read(),
|
||||||
))),
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(SdkError::ServiceError {
|
Err(SdkError::ServiceError {
|
||||||
@@ -245,32 +240,6 @@ impl S3Bucket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pin_project_lite::pin_project! {
|
|
||||||
/// An `AsyncRead` adapter which carries a permit for the lifetime of the value.
|
|
||||||
struct RatelimitedAsyncRead<S> {
|
|
||||||
permit: tokio::sync::OwnedSemaphorePermit,
|
|
||||||
#[pin]
|
|
||||||
inner: S,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncRead> RatelimitedAsyncRead<S> {
|
|
||||||
fn new(permit: tokio::sync::OwnedSemaphorePermit, inner: S) -> Self {
|
|
||||||
RatelimitedAsyncRead { permit, inner }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncRead> AsyncRead for RatelimitedAsyncRead<S> {
|
|
||||||
fn poll_read(
|
|
||||||
self: std::pin::Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
buf: &mut io::ReadBuf<'_>,
|
|
||||||
) -> std::task::Poll<std::io::Result<()>> {
|
|
||||||
let this = self.project();
|
|
||||||
this.inner.poll_read(cx, buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl RemoteStorage for S3Bucket {
|
impl RemoteStorage for S3Bucket {
|
||||||
async fn list(&self) -> anyhow::Result<Vec<RemotePath>> {
|
async fn list(&self) -> anyhow::Result<Vec<RemotePath>> {
|
||||||
|
|||||||
@@ -7,7 +7,5 @@ license.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
use crate::{SegmentMethod, SegmentSizeResult, SizeResult, StorageModel};
|
|
||||||
|
|
||||||
//
|
|
||||||
// *-g--*---D--->
|
|
||||||
// /
|
|
||||||
// /
|
|
||||||
// / *---b----*-B--->
|
|
||||||
// / /
|
|
||||||
// / /
|
|
||||||
// -----*--e---*-----f----* C
|
|
||||||
// E \
|
|
||||||
// \
|
|
||||||
// *--a---*---A-->
|
|
||||||
//
|
|
||||||
// If A and B need to be retained, is it cheaper to store
|
|
||||||
// snapshot at C+a+b, or snapshots at A and B ?
|
|
||||||
//
|
|
||||||
// If D also needs to be retained, which is cheaper:
|
|
||||||
//
|
|
||||||
// 1. E+g+e+f+a+b
|
|
||||||
// 2. D+C+a+b
|
|
||||||
// 3. D+A+B
|
|
||||||
|
|
||||||
/// [`Segment`] which has had it's size calculated.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct SegmentSize {
|
|
||||||
method: SegmentMethod,
|
|
||||||
|
|
||||||
// calculated size of this subtree, using this method
|
|
||||||
accum_size: u64,
|
|
||||||
|
|
||||||
seg_id: usize,
|
|
||||||
children: Vec<SegmentSize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SizeAlternatives {
|
|
||||||
// cheapest alternative if parent is available.
|
|
||||||
incremental: SegmentSize,
|
|
||||||
|
|
||||||
// cheapest alternative if parent node is not available
|
|
||||||
non_incremental: Option<SegmentSize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StorageModel {
|
|
||||||
pub fn calculate(&self) -> SizeResult {
|
|
||||||
// Build adjacency list. 'child_list' is indexed by segment id. Each entry
|
|
||||||
// contains a list of all child segments of the segment.
|
|
||||||
let mut roots: Vec<usize> = Vec::new();
|
|
||||||
let mut child_list: Vec<Vec<usize>> = Vec::new();
|
|
||||||
child_list.resize(self.segments.len(), Vec::new());
|
|
||||||
|
|
||||||
for (seg_id, seg) in self.segments.iter().enumerate() {
|
|
||||||
if let Some(parent_id) = seg.parent {
|
|
||||||
child_list[parent_id].push(seg_id);
|
|
||||||
} else {
|
|
||||||
roots.push(seg_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut segment_results = Vec::new();
|
|
||||||
segment_results.resize(
|
|
||||||
self.segments.len(),
|
|
||||||
SegmentSizeResult {
|
|
||||||
method: SegmentMethod::Skipped,
|
|
||||||
accum_size: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut total_size = 0;
|
|
||||||
for root in roots {
|
|
||||||
if let Some(selected) = self.size_here(root, &child_list).non_incremental {
|
|
||||||
StorageModel::fill_selected_sizes(&selected, &mut segment_results);
|
|
||||||
total_size += selected.accum_size;
|
|
||||||
} else {
|
|
||||||
// Couldn't find any way to get this root. Error?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SizeResult {
|
|
||||||
total_size,
|
|
||||||
segments: segment_results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fill_selected_sizes(selected: &SegmentSize, result: &mut Vec<SegmentSizeResult>) {
|
|
||||||
result[selected.seg_id] = SegmentSizeResult {
|
|
||||||
method: selected.method,
|
|
||||||
accum_size: selected.accum_size,
|
|
||||||
};
|
|
||||||
// recurse to children
|
|
||||||
for child in selected.children.iter() {
|
|
||||||
StorageModel::fill_selected_sizes(child, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// This is the core of the sizing calculation.
|
|
||||||
//
|
|
||||||
// This is a recursive function, that for each Segment calculates the best way
|
|
||||||
// to reach all the Segments that are marked as needed in this subtree, under two
|
|
||||||
// different conditions:
|
|
||||||
// a) when the parent of this segment is available (as a snaphot or through WAL), and
|
|
||||||
// b) when the parent of this segment is not available.
|
|
||||||
//
|
|
||||||
fn size_here(&self, seg_id: usize, child_list: &Vec<Vec<usize>>) -> SizeAlternatives {
|
|
||||||
let seg = &self.segments[seg_id];
|
|
||||||
// First figure out the best way to get each child
|
|
||||||
let mut children = Vec::new();
|
|
||||||
for child_id in &child_list[seg_id] {
|
|
||||||
children.push(self.size_here(*child_id, child_list))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 1. If this node is not needed, we can skip it as long as we
|
|
||||||
// take snapshots later in each sub-tree
|
|
||||||
let snapshot_later = if !seg.needed {
|
|
||||||
let mut snapshot_later = SegmentSize {
|
|
||||||
seg_id,
|
|
||||||
method: SegmentMethod::Skipped,
|
|
||||||
accum_size: 0,
|
|
||||||
children: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut possible = true;
|
|
||||||
for child in children.iter() {
|
|
||||||
if let Some(non_incremental) = &child.non_incremental {
|
|
||||||
snapshot_later.accum_size += non_incremental.accum_size;
|
|
||||||
snapshot_later.children.push(non_incremental.clone())
|
|
||||||
} else {
|
|
||||||
possible = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if possible {
|
|
||||||
Some(snapshot_later)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Method 2. Get a snapshot here. This assumed to be possible, if the 'size' of
|
|
||||||
// this Segment was given.
|
|
||||||
let snapshot_here = if !seg.needed || seg.parent.is_none() {
|
|
||||||
if let Some(snapshot_size) = seg.size {
|
|
||||||
let mut snapshot_here = SegmentSize {
|
|
||||||
seg_id,
|
|
||||||
method: SegmentMethod::SnapshotHere,
|
|
||||||
accum_size: snapshot_size,
|
|
||||||
children: Vec::new(),
|
|
||||||
};
|
|
||||||
for child in children.iter() {
|
|
||||||
snapshot_here.accum_size += child.incremental.accum_size;
|
|
||||||
snapshot_here.children.push(child.incremental.clone())
|
|
||||||
}
|
|
||||||
Some(snapshot_here)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Method 3. Use WAL to get here from parent
|
|
||||||
let wal_here = {
|
|
||||||
let mut wal_here = SegmentSize {
|
|
||||||
seg_id,
|
|
||||||
method: SegmentMethod::Wal,
|
|
||||||
accum_size: if let Some(parent_id) = seg.parent {
|
|
||||||
seg.lsn - self.segments[parent_id].lsn
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
},
|
|
||||||
children: Vec::new(),
|
|
||||||
};
|
|
||||||
for child in children {
|
|
||||||
wal_here.accum_size += child.incremental.accum_size;
|
|
||||||
wal_here.children.push(child.incremental)
|
|
||||||
}
|
|
||||||
wal_here
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the parent is not available, what's the cheapest method involving
|
|
||||||
// a snapshot here or later?
|
|
||||||
let mut cheapest_non_incremental: Option<SegmentSize> = None;
|
|
||||||
if let Some(snapshot_here) = snapshot_here {
|
|
||||||
cheapest_non_incremental = Some(snapshot_here);
|
|
||||||
}
|
|
||||||
if let Some(snapshot_later) = snapshot_later {
|
|
||||||
// Use <=, to prefer skipping if the size is equal
|
|
||||||
if let Some(parent) = &cheapest_non_incremental {
|
|
||||||
if snapshot_later.accum_size <= parent.accum_size {
|
|
||||||
cheapest_non_incremental = Some(snapshot_later);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cheapest_non_incremental = Some(snapshot_later);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And what's the cheapest method, if the parent is available?
|
|
||||||
let cheapest_incremental = if let Some(cheapest_non_incremental) = &cheapest_non_incremental
|
|
||||||
{
|
|
||||||
// Is it cheaper to use a snapshot here or later, anyway?
|
|
||||||
// Use <, to prefer Wal over snapshot if the cost is the same
|
|
||||||
if wal_here.accum_size < cheapest_non_incremental.accum_size {
|
|
||||||
wal_here
|
|
||||||
} else {
|
|
||||||
cheapest_non_incremental.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wal_here
|
|
||||||
};
|
|
||||||
|
|
||||||
SizeAlternatives {
|
|
||||||
incremental: cheapest_incremental,
|
|
||||||
non_incremental: cheapest_non_incremental,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,401 @@
|
|||||||
//! Synthetic size calculation
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
mod calculation;
|
use anyhow::Context;
|
||||||
pub mod svg;
|
|
||||||
|
|
||||||
/// StorageModel is the input to the synthetic size calculation. It represents
|
/// Pricing model or history size builder.
|
||||||
/// a tree of timelines, with just the information that's needed for the
|
|
||||||
/// calculation. This doesn't track timeline names or where each timeline
|
|
||||||
/// begins and ends, for example. Instead, it consists of "points of interest"
|
|
||||||
/// on the timelines. A point of interest could be the timeline start or end point,
|
|
||||||
/// the oldest point on a timeline that needs to be retained because of PITR
|
|
||||||
/// cutoff, or snapshot points named by the user. For each such point, and the
|
|
||||||
/// edge connecting the points (implicit in Segment), we store information about
|
|
||||||
/// whether we need to be able to recover to the point, and if known, the logical
|
|
||||||
/// size at the point.
|
|
||||||
///
|
///
|
||||||
/// The segments must form a well-formed tree, with no loops.
|
/// Maintains knowledge of the branches and their modifications. Generic over the branch name key
|
||||||
#[derive(serde::Serialize)]
|
/// type.
|
||||||
pub struct StorageModel {
|
pub struct Storage<K: 'static> {
|
||||||
pub segments: Vec<Segment>,
|
segments: Vec<Segment>,
|
||||||
|
|
||||||
|
/// Mapping from the branch name to the index of a segment describing it's latest state.
|
||||||
|
branches: HashMap<K, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Segment represents one point in the tree of branches, *and* the edge that leads
|
/// Snapshot of a branch.
|
||||||
/// to it (if any). We don't need separate structs for points and edges, because each
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
/// point can have only one parent.
|
|
||||||
///
|
|
||||||
/// When 'needed' is true, it means that we need to be able to reconstruct
|
|
||||||
/// any version between 'parent.lsn' and 'lsn'. If you want to represent that only
|
|
||||||
/// a single point is needed, create two Segments with the same lsn, and mark only
|
|
||||||
/// the child as needed.
|
|
||||||
///
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct Segment {
|
pub struct Segment {
|
||||||
/// Previous segment index into ['Storage::segments`], if any.
|
/// Previous segment index into ['Storage::segments`], if any.
|
||||||
pub parent: Option<usize>,
|
parent: Option<usize>,
|
||||||
|
|
||||||
/// LSN at this point
|
/// Description of how did we get to this state.
|
||||||
pub lsn: u64,
|
///
|
||||||
|
/// Mainly used in the original scenarios 1..=4 with insert, delete and update. Not used when
|
||||||
|
/// modifying a branch directly.
|
||||||
|
pub op: Cow<'static, str>,
|
||||||
|
|
||||||
/// Logical size at this node, if known.
|
/// LSN before this state
|
||||||
pub size: Option<u64>,
|
start_lsn: u64,
|
||||||
|
|
||||||
/// If true, the segment from parent to this node is needed by `retention_period`
|
/// LSN at this state
|
||||||
|
pub end_lsn: u64,
|
||||||
|
|
||||||
|
/// Logical size before this state
|
||||||
|
start_size: u64,
|
||||||
|
|
||||||
|
/// Logical size at this state. Can be None in the last Segment of a branch.
|
||||||
|
pub end_size: Option<u64>,
|
||||||
|
|
||||||
|
/// Indices to [`Storage::segments`]
|
||||||
|
///
|
||||||
|
/// FIXME: this could be an Option<usize>
|
||||||
|
children_after: Vec<usize>,
|
||||||
|
|
||||||
|
/// Determined by `retention_period` given to [`Storage::calculate`]
|
||||||
pub needed: bool,
|
pub needed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of synthetic size calculation. Returned by StorageModel::calculate()
|
//
|
||||||
pub struct SizeResult {
|
//
|
||||||
pub total_size: u64,
|
//
|
||||||
|
//
|
||||||
|
// *-g--*---D--->
|
||||||
|
// /
|
||||||
|
// /
|
||||||
|
// / *---b----*-B--->
|
||||||
|
// / /
|
||||||
|
// / /
|
||||||
|
// -----*--e---*-----f----* C
|
||||||
|
// E \
|
||||||
|
// \
|
||||||
|
// *--a---*---A-->
|
||||||
|
//
|
||||||
|
// If A and B need to be retained, is it cheaper to store
|
||||||
|
// snapshot at C+a+b, or snapshots at A and B ?
|
||||||
|
//
|
||||||
|
// If D also needs to be retained, which is cheaper:
|
||||||
|
//
|
||||||
|
// 1. E+g+e+f+a+b
|
||||||
|
// 2. D+C+a+b
|
||||||
|
// 3. D+A+B
|
||||||
|
|
||||||
// This has same length as the StorageModel::segments vector in the input.
|
/// [`Segment`] which has had it's size calculated.
|
||||||
// Each entry in this array corresponds to the entry with same index in
|
pub struct SegmentSize {
|
||||||
// StorageModel::segments.
|
pub seg_id: usize,
|
||||||
pub segments: Vec<SegmentSizeResult>,
|
|
||||||
|
pub method: SegmentMethod,
|
||||||
|
|
||||||
|
this_size: u64,
|
||||||
|
|
||||||
|
pub children: Vec<SegmentSize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
impl SegmentSize {
|
||||||
pub struct SegmentSizeResult {
|
fn total(&self) -> u64 {
|
||||||
pub method: SegmentMethod,
|
self.this_size + self.children.iter().fold(0, |acc, x| acc + x.total())
|
||||||
// calculated size of this subtree, using this method
|
}
|
||||||
pub accum_size: u64,
|
|
||||||
|
pub fn total_children(&self) -> u64 {
|
||||||
|
if self.method == SnapshotAfter {
|
||||||
|
self.this_size + self.children.iter().fold(0, |acc, x| acc + x.total())
|
||||||
|
} else {
|
||||||
|
self.children.iter().fold(0, |acc, x| acc + x.total())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Different methods to retain history from a particular state
|
/// Different methods to retain history from a particular state
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum SegmentMethod {
|
pub enum SegmentMethod {
|
||||||
SnapshotHere, // A logical snapshot is needed after this segment
|
SnapshotAfter,
|
||||||
Wal, // Keep WAL leading up to this node
|
Wal,
|
||||||
|
WalNeeded,
|
||||||
Skipped,
|
Skipped,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use SegmentMethod::*;
|
||||||
|
|
||||||
|
impl<K: std::hash::Hash + Eq + 'static> Storage<K> {
|
||||||
|
/// Creates a new storage with the given default branch name.
|
||||||
|
pub fn new(initial_branch: K) -> Storage<K> {
|
||||||
|
let init_segment = Segment {
|
||||||
|
op: "".into(),
|
||||||
|
needed: false,
|
||||||
|
parent: None,
|
||||||
|
start_lsn: 0,
|
||||||
|
end_lsn: 0,
|
||||||
|
start_size: 0,
|
||||||
|
end_size: Some(0),
|
||||||
|
children_after: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Storage {
|
||||||
|
segments: vec![init_segment],
|
||||||
|
branches: HashMap::from([(initial_branch, 0)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances the branch with a new point, at given LSN.
|
||||||
|
pub fn insert_point<Q: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
branch: &Q,
|
||||||
|
op: Cow<'static, str>,
|
||||||
|
lsn: u64,
|
||||||
|
size: Option<u64>,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
K: std::borrow::Borrow<Q>,
|
||||||
|
Q: std::hash::Hash + Eq + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let Some(lastseg_id) = self.branches.get(branch).copied() else { anyhow::bail!("branch not found: {branch:?}") };
|
||||||
|
let newseg_id = self.segments.len();
|
||||||
|
let lastseg = &mut self.segments[lastseg_id];
|
||||||
|
|
||||||
|
assert!(lsn > lastseg.end_lsn);
|
||||||
|
|
||||||
|
let Some(start_size) = lastseg.end_size else { anyhow::bail!("no end_size on latest segment for {branch:?}") };
|
||||||
|
|
||||||
|
let newseg = Segment {
|
||||||
|
op,
|
||||||
|
parent: Some(lastseg_id),
|
||||||
|
start_lsn: lastseg.end_lsn,
|
||||||
|
end_lsn: lsn,
|
||||||
|
start_size,
|
||||||
|
end_size: size,
|
||||||
|
children_after: Vec::new(),
|
||||||
|
needed: false,
|
||||||
|
};
|
||||||
|
lastseg.children_after.push(newseg_id);
|
||||||
|
|
||||||
|
self.segments.push(newseg);
|
||||||
|
*self.branches.get_mut(branch).expect("read already") = newseg_id;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances the branch with the named operation, by the relative LSN and logical size bytes.
|
||||||
|
pub fn modify_branch<Q: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
branch: &Q,
|
||||||
|
op: Cow<'static, str>,
|
||||||
|
lsn_bytes: u64,
|
||||||
|
size_bytes: i64,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
K: std::borrow::Borrow<Q>,
|
||||||
|
Q: std::hash::Hash + Eq + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let Some(lastseg_id) = self.branches.get(branch).copied() else { anyhow::bail!("branch not found: {branch:?}") };
|
||||||
|
let newseg_id = self.segments.len();
|
||||||
|
let lastseg = &mut self.segments[lastseg_id];
|
||||||
|
|
||||||
|
let Some(last_end_size) = lastseg.end_size else { anyhow::bail!("no end_size on latest segment for {branch:?}") };
|
||||||
|
|
||||||
|
let newseg = Segment {
|
||||||
|
op,
|
||||||
|
parent: Some(lastseg_id),
|
||||||
|
start_lsn: lastseg.end_lsn,
|
||||||
|
end_lsn: lastseg.end_lsn + lsn_bytes,
|
||||||
|
start_size: last_end_size,
|
||||||
|
end_size: Some((last_end_size as i64 + size_bytes) as u64),
|
||||||
|
children_after: Vec::new(),
|
||||||
|
needed: false,
|
||||||
|
};
|
||||||
|
lastseg.children_after.push(newseg_id);
|
||||||
|
|
||||||
|
self.segments.push(newseg);
|
||||||
|
*self.branches.get_mut(branch).expect("read already") = newseg_id;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert<Q: ?Sized>(&mut self, branch: &Q, bytes: u64) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
K: std::borrow::Borrow<Q>,
|
||||||
|
Q: std::hash::Hash + Eq + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
self.modify_branch(branch, "insert".into(), bytes, bytes as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update<Q: ?Sized>(&mut self, branch: &Q, bytes: u64) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
K: std::borrow::Borrow<Q>,
|
||||||
|
Q: std::hash::Hash + Eq + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
self.modify_branch(branch, "update".into(), bytes, 0i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete<Q: ?Sized>(&mut self, branch: &Q, bytes: u64) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
K: std::borrow::Borrow<Q>,
|
||||||
|
Q: std::hash::Hash + Eq + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
self.modify_branch(branch, "delete".into(), bytes, -(bytes as i64))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn branch<Q: ?Sized>(&mut self, parent: &Q, name: K) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
K: std::borrow::Borrow<Q> + std::fmt::Debug,
|
||||||
|
Q: std::hash::Hash + Eq + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
// Find the right segment
|
||||||
|
let branchseg_id = *self.branches.get(parent).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"should had found the parent {:?} by key. in branches {:?}",
|
||||||
|
parent, self.branches
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let _branchseg = &mut self.segments[branchseg_id];
|
||||||
|
|
||||||
|
// Create branch name for it
|
||||||
|
self.branches.insert(name, branchseg_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(&mut self, retention_period: u64) -> anyhow::Result<SegmentSize> {
|
||||||
|
// Phase 1: Mark all the segments that need to be retained
|
||||||
|
for (_branch, &last_seg_id) in self.branches.iter() {
|
||||||
|
let last_seg = &self.segments[last_seg_id];
|
||||||
|
let cutoff_lsn = last_seg.start_lsn.saturating_sub(retention_period);
|
||||||
|
let mut seg_id = last_seg_id;
|
||||||
|
loop {
|
||||||
|
let seg = &mut self.segments[seg_id];
|
||||||
|
if seg.end_lsn < cutoff_lsn {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seg.needed = true;
|
||||||
|
if let Some(prev_seg_id) = seg.parent {
|
||||||
|
seg_id = prev_seg_id;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: For each oldest segment in a chain that needs to be retained,
|
||||||
|
// calculate if we should store snapshot or WAL
|
||||||
|
self.size_from_snapshot_later(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_from_wal(&self, seg_id: usize) -> anyhow::Result<SegmentSize> {
|
||||||
|
let seg = &self.segments[seg_id];
|
||||||
|
|
||||||
|
let this_size = seg.end_lsn - seg.start_lsn;
|
||||||
|
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
// try both ways
|
||||||
|
for &child_id in seg.children_after.iter() {
|
||||||
|
// try each child both ways
|
||||||
|
let child = &self.segments[child_id];
|
||||||
|
let p1 = self.size_from_wal(child_id)?;
|
||||||
|
|
||||||
|
let p = if !child.needed {
|
||||||
|
let p2 = self.size_from_snapshot_later(child_id)?;
|
||||||
|
if p1.total() < p2.total() {
|
||||||
|
p1
|
||||||
|
} else {
|
||||||
|
p2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p1
|
||||||
|
};
|
||||||
|
children.push(p);
|
||||||
|
}
|
||||||
|
Ok(SegmentSize {
|
||||||
|
seg_id,
|
||||||
|
method: if seg.needed { WalNeeded } else { Wal },
|
||||||
|
this_size,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_from_snapshot_later(&self, seg_id: usize) -> anyhow::Result<SegmentSize> {
|
||||||
|
// If this is needed, then it's time to do the snapshot and continue
|
||||||
|
// with wal method.
|
||||||
|
let seg = &self.segments[seg_id];
|
||||||
|
//eprintln!("snap: seg{}: {} needed: {}", seg_id, seg.children_after.len(), seg.needed);
|
||||||
|
if seg.needed {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
for &child_id in seg.children_after.iter() {
|
||||||
|
// try each child both ways
|
||||||
|
let child = &self.segments[child_id];
|
||||||
|
let p1 = self.size_from_wal(child_id)?;
|
||||||
|
|
||||||
|
let p = if !child.needed {
|
||||||
|
let p2 = self.size_from_snapshot_later(child_id)?;
|
||||||
|
if p1.total() < p2.total() {
|
||||||
|
p1
|
||||||
|
} else {
|
||||||
|
p2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p1
|
||||||
|
};
|
||||||
|
children.push(p);
|
||||||
|
}
|
||||||
|
Ok(SegmentSize {
|
||||||
|
seg_id,
|
||||||
|
method: WalNeeded,
|
||||||
|
this_size: seg.start_size,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If any of the direct children are "needed", need to be able to reconstruct here
|
||||||
|
let mut children_needed = false;
|
||||||
|
for &child in seg.children_after.iter() {
|
||||||
|
let seg = &self.segments[child];
|
||||||
|
if seg.needed {
|
||||||
|
children_needed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let method1 = if !children_needed {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
for child in seg.children_after.iter() {
|
||||||
|
children.push(self.size_from_snapshot_later(*child)?);
|
||||||
|
}
|
||||||
|
Some(SegmentSize {
|
||||||
|
seg_id,
|
||||||
|
method: Skipped,
|
||||||
|
this_size: 0,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this a junction, consider snapshotting here
|
||||||
|
let method2 = if children_needed || seg.children_after.len() >= 2 {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
for child in seg.children_after.iter() {
|
||||||
|
children.push(self.size_from_wal(*child)?);
|
||||||
|
}
|
||||||
|
let Some(this_size) = seg.end_size else { anyhow::bail!("no end_size at junction {seg_id}") };
|
||||||
|
Some(SegmentSize {
|
||||||
|
seg_id,
|
||||||
|
method: SnapshotAfter,
|
||||||
|
this_size,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match (method1, method2) {
|
||||||
|
(None, None) => anyhow::bail!(
|
||||||
|
"neither method was applicable: children_after={}, children_needed={}",
|
||||||
|
seg.children_after.len(),
|
||||||
|
children_needed
|
||||||
|
),
|
||||||
|
(Some(method), None) => method,
|
||||||
|
(None, Some(method)) => method,
|
||||||
|
(Some(method1), Some(method2)) => {
|
||||||
|
if method1.total() < method2.total() {
|
||||||
|
method1
|
||||||
|
} else {
|
||||||
|
method2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_segments(self) -> Vec<Segment> {
|
||||||
|
self.segments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
269
libs/tenant_size_model/src/main.rs
Normal file
269
libs/tenant_size_model/src/main.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
//! Tenant size model testing ground.
|
||||||
|
//!
|
||||||
|
//! Has a number of scenarios and a `main` for invoking these by number, calculating the history
|
||||||
|
//! size, outputs graphviz graph. Makefile in directory shows how to use graphviz to turn scenarios
|
||||||
|
//! into pngs.
|
||||||
|
|
||||||
|
use tenant_size_model::{Segment, SegmentSize, Storage};
|
||||||
|
|
||||||
|
// Main branch only. Some updates on it.
|
||||||
|
fn scenario_1() -> anyhow::Result<(Vec<Segment>, SegmentSize)> {
|
||||||
|
// Create main branch
|
||||||
|
let mut storage = Storage::new("main");
|
||||||
|
|
||||||
|
// Bulk load 5 GB of data to it
|
||||||
|
storage.insert("main", 5_000)?;
|
||||||
|
|
||||||
|
// Stream of updates
|
||||||
|
for _ in 0..5 {
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = storage.calculate(1000)?;
|
||||||
|
|
||||||
|
Ok((storage.into_segments(), size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main branch only. Some updates on it.
|
||||||
|
fn scenario_2() -> anyhow::Result<(Vec<Segment>, SegmentSize)> {
|
||||||
|
// Create main branch
|
||||||
|
let mut storage = Storage::new("main");
|
||||||
|
|
||||||
|
// Bulk load 5 GB of data to it
|
||||||
|
storage.insert("main", 5_000)?;
|
||||||
|
|
||||||
|
// Stream of updates
|
||||||
|
for _ in 0..5 {
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch
|
||||||
|
storage.branch("main", "child")?;
|
||||||
|
storage.update("child", 1_000)?;
|
||||||
|
|
||||||
|
// More updates on parent
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
|
||||||
|
let size = storage.calculate(1000)?;
|
||||||
|
|
||||||
|
Ok((storage.into_segments(), size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like 2, but more updates on main
|
||||||
|
fn scenario_3() -> anyhow::Result<(Vec<Segment>, SegmentSize)> {
|
||||||
|
// Create main branch
|
||||||
|
let mut storage = Storage::new("main");
|
||||||
|
|
||||||
|
// Bulk load 5 GB of data to it
|
||||||
|
storage.insert("main", 5_000)?;
|
||||||
|
|
||||||
|
// Stream of updates
|
||||||
|
for _ in 0..5 {
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch
|
||||||
|
storage.branch("main", "child")?;
|
||||||
|
storage.update("child", 1_000)?;
|
||||||
|
|
||||||
|
// More updates on parent
|
||||||
|
for _ in 0..5 {
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = storage.calculate(1000)?;
|
||||||
|
|
||||||
|
Ok((storage.into_segments(), size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diverged branches
|
||||||
|
fn scenario_4() -> anyhow::Result<(Vec<Segment>, SegmentSize)> {
|
||||||
|
// Create main branch
|
||||||
|
let mut storage = Storage::new("main");
|
||||||
|
|
||||||
|
// Bulk load 5 GB of data to it
|
||||||
|
storage.insert("main", 5_000)?;
|
||||||
|
|
||||||
|
// Stream of updates
|
||||||
|
for _ in 0..5 {
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch
|
||||||
|
storage.branch("main", "child")?;
|
||||||
|
storage.update("child", 1_000)?;
|
||||||
|
|
||||||
|
// More updates on parent
|
||||||
|
for _ in 0..8 {
|
||||||
|
storage.update("main", 1_000)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = storage.calculate(1000)?;
|
||||||
|
|
||||||
|
Ok((storage.into_segments(), size))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scenario_5() -> anyhow::Result<(Vec<Segment>, SegmentSize)> {
|
||||||
|
let mut storage = Storage::new("a");
|
||||||
|
storage.insert("a", 5000)?;
|
||||||
|
storage.branch("a", "b")?;
|
||||||
|
storage.update("b", 4000)?;
|
||||||
|
storage.update("a", 2000)?;
|
||||||
|
storage.branch("a", "c")?;
|
||||||
|
storage.insert("c", 4000)?;
|
||||||
|
storage.insert("a", 2000)?;
|
||||||
|
|
||||||
|
let size = storage.calculate(5000)?;
|
||||||
|
|
||||||
|
Ok((storage.into_segments(), size))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scenario_6() -> anyhow::Result<(Vec<Segment>, SegmentSize)> {
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
const NO_OP: Cow<'static, str> = Cow::Borrowed("");
|
||||||
|
|
||||||
|
let branches = [
|
||||||
|
Some(0x7ff1edab8182025f15ae33482edb590a_u128),
|
||||||
|
Some(0xb1719e044db05401a05a2ed588a3ad3f),
|
||||||
|
Some(0xb68d6691c895ad0a70809470020929ef),
|
||||||
|
];
|
||||||
|
|
||||||
|
// compared to other scenarios, this one uses bytes instead of kB
|
||||||
|
|
||||||
|
let mut storage = Storage::new(None);
|
||||||
|
|
||||||
|
storage.branch(&None, branches[0])?; // at 0
|
||||||
|
storage.modify_branch(&branches[0], NO_OP, 108951064, 43696128)?; // at 108951064
|
||||||
|
storage.branch(&branches[0], branches[1])?; // at 108951064
|
||||||
|
storage.modify_branch(&branches[1], NO_OP, 15560408, -1851392)?; // at 124511472
|
||||||
|
storage.modify_branch(&branches[0], NO_OP, 174464360, -1531904)?; // at 283415424
|
||||||
|
storage.branch(&branches[0], branches[2])?; // at 283415424
|
||||||
|
storage.modify_branch(&branches[2], NO_OP, 15906192, 8192)?; // at 299321616
|
||||||
|
storage.modify_branch(&branches[0], NO_OP, 18909976, 32768)?; // at 302325400
|
||||||
|
|
||||||
|
let size = storage.calculate(100_000)?;
|
||||||
|
|
||||||
|
Ok((storage.into_segments(), size))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
|
let scenario = if args.len() < 2 { "1" } else { &args[1] };
|
||||||
|
|
||||||
|
let (segments, size) = match scenario {
|
||||||
|
"1" => scenario_1(),
|
||||||
|
"2" => scenario_2(),
|
||||||
|
"3" => scenario_3(),
|
||||||
|
"4" => scenario_4(),
|
||||||
|
"5" => scenario_5(),
|
||||||
|
"6" => scenario_6(),
|
||||||
|
other => {
|
||||||
|
eprintln!("invalid scenario {}", other);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
graphviz_tree(&segments, &size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graphviz_recurse(segments: &[Segment], node: &SegmentSize) {
|
||||||
|
use tenant_size_model::SegmentMethod::*;
|
||||||
|
|
||||||
|
let seg_id = node.seg_id;
|
||||||
|
let seg = segments.get(seg_id).unwrap();
|
||||||
|
let lsn = seg.end_lsn;
|
||||||
|
let size = seg.end_size.unwrap_or(0);
|
||||||
|
let method = node.method;
|
||||||
|
|
||||||
|
println!(" {{");
|
||||||
|
println!(" node [width=0.1 height=0.1 shape=oval]");
|
||||||
|
|
||||||
|
let tenant_size = node.total_children();
|
||||||
|
|
||||||
|
let penwidth = if seg.needed { 6 } else { 3 };
|
||||||
|
let x = match method {
|
||||||
|
SnapshotAfter =>
|
||||||
|
format!("label=\"lsn: {lsn}\\nsize: {size}\\ntenant_size: {tenant_size}\" style=filled penwidth={penwidth}"),
|
||||||
|
Wal =>
|
||||||
|
format!("label=\"lsn: {lsn}\\nsize: {size}\\ntenant_size: {tenant_size}\" color=\"black\" penwidth={penwidth}"),
|
||||||
|
WalNeeded =>
|
||||||
|
format!("label=\"lsn: {lsn}\\nsize: {size}\\ntenant_size: {tenant_size}\" color=\"black\" penwidth={penwidth}"),
|
||||||
|
Skipped =>
|
||||||
|
format!("label=\"lsn: {lsn}\\nsize: {size}\\ntenant_size: {tenant_size}\" color=\"gray\" penwidth={penwidth}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(" \"seg{seg_id}\" [{x}]");
|
||||||
|
println!(" }}");
|
||||||
|
|
||||||
|
// Recurse. Much of the data is actually on the edge
|
||||||
|
for child in node.children.iter() {
|
||||||
|
let child_id = child.seg_id;
|
||||||
|
graphviz_recurse(segments, child);
|
||||||
|
|
||||||
|
let edge_color = match child.method {
|
||||||
|
SnapshotAfter => "gray",
|
||||||
|
Wal => "black",
|
||||||
|
WalNeeded => "black",
|
||||||
|
Skipped => "gray",
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(" {{");
|
||||||
|
println!(" edge [] ");
|
||||||
|
print!(" \"seg{seg_id}\" -> \"seg{child_id}\" [");
|
||||||
|
print!("color={edge_color}");
|
||||||
|
if child.method == WalNeeded {
|
||||||
|
print!(" penwidth=6");
|
||||||
|
}
|
||||||
|
if child.method == Wal {
|
||||||
|
print!(" penwidth=3");
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = segments.get(child_id).unwrap();
|
||||||
|
|
||||||
|
if next.op.is_empty() {
|
||||||
|
print!(
|
||||||
|
" label=\"{} / {}\"",
|
||||||
|
next.end_lsn - seg.end_lsn,
|
||||||
|
(next.end_size.unwrap_or(0) as i128 - seg.end_size.unwrap_or(0) as i128)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print!(" label=\"{}: {}\"", next.op, next.end_lsn - seg.end_lsn);
|
||||||
|
}
|
||||||
|
println!("]");
|
||||||
|
println!(" }}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graphviz_tree(segments: &[Segment], tree: &SegmentSize) {
|
||||||
|
println!("digraph G {{");
|
||||||
|
println!(" fontname=\"Helvetica,Arial,sans-serif\"");
|
||||||
|
println!(" node [fontname=\"Helvetica,Arial,sans-serif\"]");
|
||||||
|
println!(" edge [fontname=\"Helvetica,Arial,sans-serif\"]");
|
||||||
|
println!(" graph [center=1 rankdir=LR]");
|
||||||
|
println!(" edge [dir=none]");
|
||||||
|
|
||||||
|
graphviz_recurse(segments, tree);
|
||||||
|
|
||||||
|
println!("}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scenarios_return_same_size() {
|
||||||
|
type ScenarioFn = fn() -> anyhow::Result<(Vec<Segment>, SegmentSize)>;
|
||||||
|
let truths: &[(u32, ScenarioFn, _)] = &[
|
||||||
|
(line!(), scenario_1, 8000),
|
||||||
|
(line!(), scenario_2, 9000),
|
||||||
|
(line!(), scenario_3, 13000),
|
||||||
|
(line!(), scenario_4, 16000),
|
||||||
|
(line!(), scenario_5, 17000),
|
||||||
|
(line!(), scenario_6, 333_792_000),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (line, scenario, expected) in truths {
|
||||||
|
let (_, size) = scenario().unwrap();
|
||||||
|
assert_eq!(*expected, size.total_children(), "scenario on line {line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
use crate::{SegmentMethod, SegmentSizeResult, SizeResult, StorageModel};
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
const SVG_WIDTH: f32 = 500.0;
|
|
||||||
|
|
||||||
struct SvgDraw<'a> {
|
|
||||||
storage: &'a StorageModel,
|
|
||||||
branches: &'a [String],
|
|
||||||
seg_to_branch: &'a [usize],
|
|
||||||
sizes: &'a [SegmentSizeResult],
|
|
||||||
|
|
||||||
// layout
|
|
||||||
xscale: f32,
|
|
||||||
min_lsn: u64,
|
|
||||||
seg_coordinates: Vec<(f32, f32)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_legend(result: &mut String) -> anyhow::Result<()> {
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<circle cx=\"10\" cy=\"10\" r=\"5\" stroke=\"red\"/>"
|
|
||||||
)?;
|
|
||||||
writeln!(result, "<text x=\"20\" y=\"15\">logical snapshot</text>")?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<line x1=\"5\" y1=\"30\" x2=\"15\" y2=\"30\" stroke-width=\"6\" stroke=\"black\" />"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<text x=\"20\" y=\"35\">WAL within retention period</text>"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<line x1=\"5\" y1=\"50\" x2=\"15\" y2=\"50\" stroke-width=\"3\" stroke=\"black\" />"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<text x=\"20\" y=\"55\">WAL retained to avoid copy</text>"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<line x1=\"5\" y1=\"70\" x2=\"15\" y2=\"70\" stroke-width=\"1\" stroke=\"gray\" />"
|
|
||||||
)?;
|
|
||||||
writeln!(result, "<text x=\"20\" y=\"75\">WAL not retained</text>")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_svg(
|
|
||||||
storage: &StorageModel,
|
|
||||||
branches: &[String],
|
|
||||||
seg_to_branch: &[usize],
|
|
||||||
sizes: &SizeResult,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let mut draw = SvgDraw {
|
|
||||||
storage,
|
|
||||||
branches,
|
|
||||||
seg_to_branch,
|
|
||||||
sizes: &sizes.segments,
|
|
||||||
|
|
||||||
xscale: 0.0,
|
|
||||||
min_lsn: 0,
|
|
||||||
seg_coordinates: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result = String::new();
|
|
||||||
|
|
||||||
writeln!(result, "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" height=\"300\" width=\"500\">")?;
|
|
||||||
|
|
||||||
draw.calculate_svg_layout();
|
|
||||||
|
|
||||||
// Draw the tree
|
|
||||||
for (seg_id, _seg) in storage.segments.iter().enumerate() {
|
|
||||||
draw.draw_seg_phase1(seg_id, &mut result)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw snapshots
|
|
||||||
for (seg_id, _seg) in storage.segments.iter().enumerate() {
|
|
||||||
draw.draw_seg_phase2(seg_id, &mut result)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_legend(&mut result)?;
|
|
||||||
|
|
||||||
write!(result, "</svg>")?;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> SvgDraw<'a> {
|
|
||||||
fn calculate_svg_layout(&mut self) {
|
|
||||||
// Find x scale
|
|
||||||
let segments = &self.storage.segments;
|
|
||||||
let min_lsn = segments.iter().map(|s| s.lsn).fold(u64::MAX, std::cmp::min);
|
|
||||||
let max_lsn = segments.iter().map(|s| s.lsn).fold(0, std::cmp::max);
|
|
||||||
|
|
||||||
// Start with 1 pixel = 1 byte. Double the scale until it fits into the image
|
|
||||||
let mut xscale = 1.0;
|
|
||||||
while (max_lsn - min_lsn) as f32 / xscale > SVG_WIDTH {
|
|
||||||
xscale *= 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout the timelines on Y dimension.
|
|
||||||
// TODO
|
|
||||||
let mut y = 100.0;
|
|
||||||
let mut branch_y_coordinates = Vec::new();
|
|
||||||
for _branch in self.branches {
|
|
||||||
branch_y_coordinates.push(y);
|
|
||||||
y += 40.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate coordinates for each point
|
|
||||||
let seg_coordinates = std::iter::zip(segments, self.seg_to_branch)
|
|
||||||
.map(|(seg, branch_id)| {
|
|
||||||
let x = (seg.lsn - min_lsn) as f32 / xscale;
|
|
||||||
let y = branch_y_coordinates[*branch_id];
|
|
||||||
(x, y)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
self.xscale = xscale;
|
|
||||||
self.min_lsn = min_lsn;
|
|
||||||
self.seg_coordinates = seg_coordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws lines between points
|
|
||||||
fn draw_seg_phase1(&self, seg_id: usize, result: &mut String) -> anyhow::Result<()> {
|
|
||||||
let seg = &self.storage.segments[seg_id];
|
|
||||||
|
|
||||||
let wal_bytes = if let Some(parent_id) = seg.parent {
|
|
||||||
seg.lsn - self.storage.segments[parent_id].lsn
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = match self.sizes[seg_id].method {
|
|
||||||
SegmentMethod::SnapshotHere => "stroke-width=\"1\" stroke=\"gray\"",
|
|
||||||
SegmentMethod::Wal if seg.needed && wal_bytes > 0 => {
|
|
||||||
"stroke-width=\"6\" stroke=\"black\""
|
|
||||||
}
|
|
||||||
SegmentMethod::Wal => "stroke-width=\"3\" stroke=\"black\"",
|
|
||||||
SegmentMethod::Skipped => "stroke-width=\"1\" stroke=\"gray\"",
|
|
||||||
};
|
|
||||||
if let Some(parent_id) = seg.parent {
|
|
||||||
let (x1, y1) = self.seg_coordinates[parent_id];
|
|
||||||
let (x2, y2) = self.seg_coordinates[seg_id];
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" {style}>",
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
" <title>{wal_bytes} bytes of WAL (seg {seg_id})</title>"
|
|
||||||
)?;
|
|
||||||
writeln!(result, "</line>")?;
|
|
||||||
} else {
|
|
||||||
// draw a little dash to mark the starting point of this branch
|
|
||||||
let (x, y) = self.seg_coordinates[seg_id];
|
|
||||||
let (x1, y1) = (x, y - 5.0);
|
|
||||||
let (x2, y2) = (x, y + 5.0);
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" {style}>",
|
|
||||||
)?;
|
|
||||||
writeln!(result, " <title>(seg {seg_id})</title>")?;
|
|
||||||
writeln!(result, "</line>")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw circles where snapshots are taken
|
|
||||||
fn draw_seg_phase2(&self, seg_id: usize, result: &mut String) -> anyhow::Result<()> {
|
|
||||||
let seg = &self.storage.segments[seg_id];
|
|
||||||
|
|
||||||
// draw a snapshot point if it's needed
|
|
||||||
let (coord_x, coord_y) = self.seg_coordinates[seg_id];
|
|
||||||
if self.sizes[seg_id].method == SegmentMethod::SnapshotHere {
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
"<circle cx=\"{coord_x}\" cy=\"{coord_y}\" r=\"5\" stroke=\"red\">",
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
result,
|
|
||||||
" <title>logical size {}</title>",
|
|
||||||
seg.size.unwrap()
|
|
||||||
)?;
|
|
||||||
write!(result, "</circle>")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
//! Tenant size model tests.
|
|
||||||
|
|
||||||
use tenant_size_model::{Segment, SizeResult, StorageModel};
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
struct ScenarioBuilder {
|
|
||||||
segments: Vec<Segment>,
|
|
||||||
|
|
||||||
/// Mapping from the branch name to the index of a segment describing its latest state.
|
|
||||||
branches: HashMap<String, usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScenarioBuilder {
|
|
||||||
/// Creates a new storage with the given default branch name.
|
|
||||||
pub fn new(initial_branch: &str) -> ScenarioBuilder {
|
|
||||||
let init_segment = Segment {
|
|
||||||
parent: None,
|
|
||||||
lsn: 0,
|
|
||||||
size: Some(0),
|
|
||||||
needed: false, // determined later
|
|
||||||
};
|
|
||||||
|
|
||||||
ScenarioBuilder {
|
|
||||||
segments: vec![init_segment],
|
|
||||||
branches: HashMap::from([(initial_branch.into(), 0)]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advances the branch with the named operation, by the relative LSN and logical size bytes.
|
|
||||||
pub fn modify_branch(&mut self, branch: &str, lsn_bytes: u64, size_bytes: i64) {
|
|
||||||
let lastseg_id = *self.branches.get(branch).unwrap();
|
|
||||||
let newseg_id = self.segments.len();
|
|
||||||
let lastseg = &mut self.segments[lastseg_id];
|
|
||||||
|
|
||||||
let newseg = Segment {
|
|
||||||
parent: Some(lastseg_id),
|
|
||||||
lsn: lastseg.lsn + lsn_bytes,
|
|
||||||
size: Some((lastseg.size.unwrap() as i64 + size_bytes) as u64),
|
|
||||||
needed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.segments.push(newseg);
|
|
||||||
*self.branches.get_mut(branch).expect("read already") = newseg_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert(&mut self, branch: &str, bytes: u64) {
|
|
||||||
self.modify_branch(branch, bytes, bytes as i64);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, branch: &str, bytes: u64) {
|
|
||||||
self.modify_branch(branch, bytes, 0i64);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _delete(&mut self, branch: &str, bytes: u64) {
|
|
||||||
self.modify_branch(branch, bytes, -(bytes as i64));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Panics if the parent branch cannot be found.
|
|
||||||
pub fn branch(&mut self, parent: &str, name: &str) {
|
|
||||||
// Find the right segment
|
|
||||||
let branchseg_id = *self
|
|
||||||
.branches
|
|
||||||
.get(parent)
|
|
||||||
.expect("should had found the parent by key");
|
|
||||||
let _branchseg = &mut self.segments[branchseg_id];
|
|
||||||
|
|
||||||
// Create branch name for it
|
|
||||||
self.branches.insert(name.to_string(), branchseg_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate(&mut self, retention_period: u64) -> (StorageModel, SizeResult) {
|
|
||||||
// Phase 1: Mark all the segments that need to be retained
|
|
||||||
for (_branch, &last_seg_id) in self.branches.iter() {
|
|
||||||
let last_seg = &self.segments[last_seg_id];
|
|
||||||
let cutoff_lsn = last_seg.lsn.saturating_sub(retention_period);
|
|
||||||
let mut seg_id = last_seg_id;
|
|
||||||
loop {
|
|
||||||
let seg = &mut self.segments[seg_id];
|
|
||||||
if seg.lsn <= cutoff_lsn {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
seg.needed = true;
|
|
||||||
if let Some(prev_seg_id) = seg.parent {
|
|
||||||
seg_id = prev_seg_id;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the calculation
|
|
||||||
let storage_model = StorageModel {
|
|
||||||
segments: self.segments.clone(),
|
|
||||||
};
|
|
||||||
let size_result = storage_model.calculate();
|
|
||||||
(storage_model, size_result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main branch only. Some updates on it.
|
|
||||||
#[test]
|
|
||||||
fn scenario_1() {
|
|
||||||
// Create main branch
|
|
||||||
let mut scenario = ScenarioBuilder::new("main");
|
|
||||||
|
|
||||||
// Bulk load 5 GB of data to it
|
|
||||||
scenario.insert("main", 5_000);
|
|
||||||
|
|
||||||
// Stream of updates
|
|
||||||
for _ in 0..5 {
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the synthetic size with retention horizon 1000
|
|
||||||
let (_model, result) = scenario.calculate(1000);
|
|
||||||
|
|
||||||
// The end of the branch is at LSN 10000. Need to retain
|
|
||||||
// a logical snapshot at LSN 9000, plus the WAL between 9000-10000.
|
|
||||||
// The logical snapshot has size 5000.
|
|
||||||
assert_eq!(result.total_size, 5000 + 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main branch only. Some updates on it.
|
|
||||||
#[test]
|
|
||||||
fn scenario_2() {
|
|
||||||
// Create main branch
|
|
||||||
let mut scenario = ScenarioBuilder::new("main");
|
|
||||||
|
|
||||||
// Bulk load 5 GB of data to it
|
|
||||||
scenario.insert("main", 5_000);
|
|
||||||
|
|
||||||
// Stream of updates
|
|
||||||
for _ in 0..5 {
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Branch
|
|
||||||
scenario.branch("main", "child");
|
|
||||||
scenario.update("child", 1_000);
|
|
||||||
|
|
||||||
// More updates on parent
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
|
|
||||||
//
|
|
||||||
// The history looks like this now:
|
|
||||||
//
|
|
||||||
// 10000 11000
|
|
||||||
// *----*----*--------------* main
|
|
||||||
// |
|
|
||||||
// | 11000
|
|
||||||
// +-------------- child
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// With retention horizon 1000, we need to retain logical snapshot
|
|
||||||
// at the branch point, size 5000, and the WAL from 10000-11000 on
|
|
||||||
// both branches.
|
|
||||||
let (_model, result) = scenario.calculate(1000);
|
|
||||||
|
|
||||||
assert_eq!(result.total_size, 5000 + 1000 + 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Like 2, but more updates on main
|
|
||||||
#[test]
|
|
||||||
fn scenario_3() {
|
|
||||||
// Create main branch
|
|
||||||
let mut scenario = ScenarioBuilder::new("main");
|
|
||||||
|
|
||||||
// Bulk load 5 GB of data to it
|
|
||||||
scenario.insert("main", 5_000);
|
|
||||||
|
|
||||||
// Stream of updates
|
|
||||||
for _ in 0..5 {
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Branch
|
|
||||||
scenario.branch("main", "child");
|
|
||||||
scenario.update("child", 1_000);
|
|
||||||
|
|
||||||
// More updates on parent
|
|
||||||
for _ in 0..5 {
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// The history looks like this now:
|
|
||||||
//
|
|
||||||
// 10000 15000
|
|
||||||
// *----*----*------------------------------------* main
|
|
||||||
// |
|
|
||||||
// | 11000
|
|
||||||
// +-------------- child
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// With retention horizon 1000, it's still cheapest to retain
|
|
||||||
// - snapshot at branch point (size 5000)
|
|
||||||
// - WAL on child between 10000-11000
|
|
||||||
// - WAL on main between 10000-15000
|
|
||||||
//
|
|
||||||
// This is in total 5000 + 1000 + 5000
|
|
||||||
//
|
|
||||||
let (_model, result) = scenario.calculate(1000);
|
|
||||||
|
|
||||||
assert_eq!(result.total_size, 5000 + 1000 + 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diverged branches
|
|
||||||
#[test]
|
|
||||||
fn scenario_4() {
|
|
||||||
// Create main branch
|
|
||||||
let mut scenario = ScenarioBuilder::new("main");
|
|
||||||
|
|
||||||
// Bulk load 5 GB of data to it
|
|
||||||
scenario.insert("main", 5_000);
|
|
||||||
|
|
||||||
// Stream of updates
|
|
||||||
for _ in 0..5 {
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Branch
|
|
||||||
scenario.branch("main", "child");
|
|
||||||
scenario.update("child", 1_000);
|
|
||||||
|
|
||||||
// More updates on parent
|
|
||||||
for _ in 0..8 {
|
|
||||||
scenario.update("main", 1_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// The history looks like this now:
|
|
||||||
//
|
|
||||||
// 10000 18000
|
|
||||||
// *----*----*------------------------------------* main
|
|
||||||
// |
|
|
||||||
// | 11000
|
|
||||||
// +-------------- child
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// With retention horizon 1000, it's now cheapest to retain
|
|
||||||
// separate snapshots on both branches:
|
|
||||||
// - snapshot on main branch at LSN 17000 (size 5000)
|
|
||||||
// - WAL on main between 17000-18000
|
|
||||||
// - snapshot on child branch at LSN 10000 (size 5000)
|
|
||||||
// - WAL on child between 10000-11000
|
|
||||||
//
|
|
||||||
// This is in total 5000 + 1000 + 5000 + 1000 = 12000
|
|
||||||
//
|
|
||||||
// (If we used the the method from the previous scenario, and
|
|
||||||
// kept only snapshot at the branch point, we'd need to keep
|
|
||||||
// all the WAL between 10000-18000 on the main branch, so
|
|
||||||
// the total size would be 5000 + 1000 + 8000 = 14000. The
|
|
||||||
// calculation always picks the cheapest alternative)
|
|
||||||
|
|
||||||
let (_model, result) = scenario.calculate(1000);
|
|
||||||
|
|
||||||
assert_eq!(result.total_size, 5000 + 1000 + 5000 + 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scenario_5() {
|
|
||||||
let mut scenario = ScenarioBuilder::new("a");
|
|
||||||
scenario.insert("a", 5000);
|
|
||||||
scenario.branch("a", "b");
|
|
||||||
scenario.update("b", 4000);
|
|
||||||
scenario.update("a", 2000);
|
|
||||||
scenario.branch("a", "c");
|
|
||||||
scenario.insert("c", 4000);
|
|
||||||
scenario.insert("a", 2000);
|
|
||||||
|
|
||||||
let (_model, result) = scenario.calculate(1000);
|
|
||||||
|
|
||||||
assert_eq!(result.total_size, 17000);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scenario_6() {
|
|
||||||
let branches = [
|
|
||||||
"7ff1edab8182025f15ae33482edb590a",
|
|
||||||
"b1719e044db05401a05a2ed588a3ad3f",
|
|
||||||
"0xb68d6691c895ad0a70809470020929ef",
|
|
||||||
];
|
|
||||||
|
|
||||||
// compared to other scenarios, this one uses bytes instead of kB
|
|
||||||
|
|
||||||
let mut scenario = ScenarioBuilder::new("");
|
|
||||||
|
|
||||||
scenario.branch("", branches[0]); // at 0
|
|
||||||
scenario.modify_branch(branches[0], 108951064, 43696128); // at 108951064
|
|
||||||
scenario.branch(branches[0], branches[1]); // at 108951064
|
|
||||||
scenario.modify_branch(branches[1], 15560408, -1851392); // at 124511472
|
|
||||||
scenario.modify_branch(branches[0], 174464360, -1531904); // at 283415424
|
|
||||||
scenario.branch(branches[0], branches[2]); // at 283415424
|
|
||||||
scenario.modify_branch(branches[2], 15906192, 8192); // at 299321616
|
|
||||||
scenario.modify_branch(branches[0], 18909976, 32768); // at 302325400
|
|
||||||
|
|
||||||
let (model, result) = scenario.calculate(100_000);
|
|
||||||
|
|
||||||
// FIXME: We previously calculated 333_792_000. But with this PR, we get
|
|
||||||
// a much lower number. At a quick look at the model output and the
|
|
||||||
// calculations here, the new result seems correct to me.
|
|
||||||
eprintln!(
|
|
||||||
" MODEL: {}",
|
|
||||||
serde_json::to_string(&model.segments).unwrap()
|
|
||||||
);
|
|
||||||
eprintln!(
|
|
||||||
"RESULT: {}",
|
|
||||||
serde_json::to_string(&result.segments).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(result.total_size, 136_236_928);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "tracing-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
hyper.workspace = true
|
|
||||||
opentelemetry = { workspace = true, features=["rt-tokio"] }
|
|
||||||
opentelemetry-otlp = { workspace = true, default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
|
||||||
opentelemetry-semantic-conventions.workspace = true
|
|
||||||
reqwest = { workspace = true, default-features = false, features = ["rustls-tls"] }
|
|
||||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
|
||||||
tracing.workspace = true
|
|
||||||
tracing-opentelemetry.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
|
||||||
workspace_hack = { version = "0.1", path = "../../workspace_hack" }
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
//! Tracing wrapper for Hyper HTTP server
|
|
||||||
|
|
||||||
use hyper::HeaderMap;
|
|
||||||
use hyper::{Body, Request, Response};
|
|
||||||
use std::future::Future;
|
|
||||||
use tracing::Instrument;
|
|
||||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
|
||||||
|
|
||||||
/// Configuration option for what to use as the "otel.name" field in the traces.
|
|
||||||
pub enum OtelName<'a> {
|
|
||||||
/// Use a constant string
|
|
||||||
Constant(&'a str),
|
|
||||||
|
|
||||||
/// Use the path from the request.
|
|
||||||
///
|
|
||||||
/// That's very useful information, but is not appropriate if the
|
|
||||||
/// path contains parameters that differ on ever request, or worse,
|
|
||||||
/// sensitive information like usernames or email addresses.
|
|
||||||
///
|
|
||||||
/// See <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name>
|
|
||||||
UriPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle an incoming HTTP request using the given handler function,
|
|
||||||
/// with OpenTelemetry tracing.
|
|
||||||
///
|
|
||||||
/// This runs 'handler' on the request in a new span, with fields filled in
|
|
||||||
/// from the request. Notably, if the request contains tracing information,
|
|
||||||
/// it is propagated to the span, so that this request is traced as part of
|
|
||||||
/// the same trace.
|
|
||||||
///
|
|
||||||
/// XXX: Usually, this is handled by existing libraries, or built
|
|
||||||
/// directly into HTTP servers. However, I couldn't find one for Hyper,
|
|
||||||
/// so I had to write our own. OpenTelemetry website has a registry of
|
|
||||||
/// instrumentation libraries at:
|
|
||||||
/// https://opentelemetry.io/registry/?language=rust&component=instrumentation
|
|
||||||
/// If a Hyper crate appears, consider switching to that.
|
|
||||||
pub async fn tracing_handler<F, R>(
|
|
||||||
req: Request<Body>,
|
|
||||||
handler: F,
|
|
||||||
otel_name: OtelName<'_>,
|
|
||||||
) -> Response<Body>
|
|
||||||
where
|
|
||||||
F: Fn(Request<Body>) -> R,
|
|
||||||
R: Future<Output = Response<Body>>,
|
|
||||||
{
|
|
||||||
// Create a tracing span, with context propagated from the incoming
|
|
||||||
// request if any.
|
|
||||||
//
|
|
||||||
// See list of standard fields defined for HTTP requests at
|
|
||||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md
|
|
||||||
// We only fill in a few of the most useful ones here.
|
|
||||||
let otel_name = match otel_name {
|
|
||||||
OtelName::Constant(s) => s,
|
|
||||||
OtelName::UriPath => req.uri().path(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let span = tracing::info_span!(
|
|
||||||
"http request",
|
|
||||||
otel.name= %otel_name,
|
|
||||||
http.method = %req.method(),
|
|
||||||
http.status_code = tracing::field::Empty,
|
|
||||||
);
|
|
||||||
let parent_ctx = extract_remote_context(req.headers());
|
|
||||||
span.set_parent(parent_ctx);
|
|
||||||
|
|
||||||
// Handle the request within the span
|
|
||||||
let response = handler(req).instrument(span.clone()).await;
|
|
||||||
|
|
||||||
// Fill in the fields from the response code
|
|
||||||
let status = response.status();
|
|
||||||
span.record("http.status_code", status.as_str());
|
|
||||||
span.record(
|
|
||||||
"otel.status_code",
|
|
||||||
if status.is_success() { "OK" } else { "ERROR" },
|
|
||||||
);
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract remote tracing context from the HTTP headers
|
|
||||||
fn extract_remote_context(headers: &HeaderMap) -> opentelemetry::Context {
|
|
||||||
struct HeaderExtractor<'a>(&'a HeaderMap);
|
|
||||||
|
|
||||||
impl<'a> opentelemetry::propagation::Extractor for HeaderExtractor<'a> {
|
|
||||||
fn get(&self, key: &str) -> Option<&str> {
|
|
||||||
self.0.get(key).and_then(|value| value.to_str().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keys(&self) -> Vec<&str> {
|
|
||||||
self.0.keys().map(|value| value.as_str()).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let extractor = HeaderExtractor(headers);
|
|
||||||
opentelemetry::global::get_text_map_propagator(|propagator| propagator.extract(&extractor))
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
//! Helper functions to set up OpenTelemetry tracing.
|
|
||||||
//!
|
|
||||||
//! This comes in two variants, depending on whether you have a Tokio runtime available.
|
|
||||||
//! If you do, call `init_tracing()`. It sets up the trace processor and exporter to use
|
|
||||||
//! the current tokio runtime. If you don't have a runtime available, or you don't want
|
|
||||||
//! to share the runtime with the tracing tasks, call `init_tracing_without_runtime()`
|
|
||||||
//! instead. It sets up a dedicated single-threaded Tokio runtime for the tracing tasks.
|
|
||||||
//!
|
|
||||||
//! Example:
|
|
||||||
//!
|
|
||||||
//! ```rust,no_run
|
|
||||||
//! use tracing_subscriber::prelude::*;
|
|
||||||
//! use tracing_opentelemetry::OpenTelemetryLayer;
|
|
||||||
//!
|
|
||||||
//! #[tokio::main]
|
|
||||||
//! async fn main() {
|
|
||||||
//! // Set up logging to stderr
|
|
||||||
//! let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
|
||||||
//! .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
|
||||||
//! let fmt_layer = tracing_subscriber::fmt::layer()
|
|
||||||
//! .with_target(false)
|
|
||||||
//! .with_writer(std::io::stderr);
|
|
||||||
//!
|
|
||||||
//! // Initialize OpenTelemetry. Exports tracing spans as OpenTelemetry traces
|
|
||||||
//! let otlp_layer = tracing_utils::init_tracing("my_application").await.map(OpenTelemetryLayer::new);
|
|
||||||
//!
|
|
||||||
//! // Put it all together
|
|
||||||
//! tracing_subscriber::registry()
|
|
||||||
//! .with(env_filter)
|
|
||||||
//! .with(otlp_layer)
|
|
||||||
//! .with(fmt_layer)
|
|
||||||
//! .init();
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use opentelemetry::sdk::Resource;
|
|
||||||
use opentelemetry::KeyValue;
|
|
||||||
use opentelemetry_otlp::WithExportConfig;
|
|
||||||
use opentelemetry_otlp::{OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT};
|
|
||||||
|
|
||||||
pub use tracing_opentelemetry::OpenTelemetryLayer;
|
|
||||||
|
|
||||||
pub mod http;
|
|
||||||
|
|
||||||
/// Set up OpenTelemetry exporter, using configuration from environment variables.
|
|
||||||
///
|
|
||||||
/// `service_name` is set as the OpenTelemetry 'service.name' resource (see
|
|
||||||
/// <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service>)
|
|
||||||
///
|
|
||||||
/// We try to follow the conventions for the environment variables specified in
|
|
||||||
/// <https://opentelemetry.io/docs/reference/specification/sdk-environment-variables/>
|
|
||||||
///
|
|
||||||
/// However, we only support a subset of those options:
|
|
||||||
///
|
|
||||||
/// - OTEL_SDK_DISABLED is supported. The default is "false", meaning tracing
|
|
||||||
/// is enabled by default. Set it to "true" to disable.
|
|
||||||
///
|
|
||||||
/// - We use the OTLP exporter, with HTTP protocol. Most of the OTEL_EXPORTER_OTLP_*
|
|
||||||
/// settings specified in
|
|
||||||
/// <https://opentelemetry.io/docs/reference/specification/protocol/exporter/>
|
|
||||||
/// are supported, as they are handled by the `opentelemetry-otlp` crate.
|
|
||||||
/// Settings related to other exporters have no effect.
|
|
||||||
///
|
|
||||||
/// - Some other settings are supported by the `opentelemetry` crate.
|
|
||||||
///
|
|
||||||
/// If you need some other setting, please test if it works first. And perhaps
|
|
||||||
/// add a comment in the list above to save the effort of testing for the next
|
|
||||||
/// person.
|
|
||||||
///
|
|
||||||
/// This doesn't block, but is marked as 'async' to hint that this must be called in
|
|
||||||
/// asynchronous execution context.
|
|
||||||
pub async fn init_tracing(service_name: &str) -> Option<opentelemetry::sdk::trace::Tracer> {
|
|
||||||
if std::env::var("OTEL_SDK_DISABLED") == Ok("true".to_string()) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
Some(init_tracing_internal(service_name.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like `init_tracing`, but creates a separate tokio Runtime for the tracing
|
|
||||||
/// tasks.
|
|
||||||
pub fn init_tracing_without_runtime(
|
|
||||||
service_name: &str,
|
|
||||||
) -> Option<opentelemetry::sdk::trace::Tracer> {
|
|
||||||
if std::env::var("OTEL_SDK_DISABLED") == Ok("true".to_string()) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The opentelemetry batch processor and the OTLP exporter needs a Tokio
|
|
||||||
// runtime. Create a dedicated runtime for them. One thread should be
|
|
||||||
// enough.
|
|
||||||
//
|
|
||||||
// (Alternatively, instead of batching, we could use the "simple
|
|
||||||
// processor", which doesn't need Tokio, and use "reqwest-blocking"
|
|
||||||
// feature for the OTLP exporter, which also doesn't need Tokio. However,
|
|
||||||
// batching is considered best practice, and also I have the feeling that
|
|
||||||
// the non-Tokio codepaths in the opentelemetry crate are less used and
|
|
||||||
// might be more buggy, so better to stay on the well-beaten path.)
|
|
||||||
//
|
|
||||||
// We leak the runtime so that it keeps running after we exit the
|
|
||||||
// function.
|
|
||||||
let runtime = Box::leak(Box::new(
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.thread_name("otlp runtime thread")
|
|
||||||
.worker_threads(1)
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
));
|
|
||||||
let _guard = runtime.enter();
|
|
||||||
|
|
||||||
Some(init_tracing_internal(service_name.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_tracing_internal(service_name: String) -> opentelemetry::sdk::trace::Tracer {
|
|
||||||
// Set up exporter from the OTEL_EXPORTER_* environment variables
|
|
||||||
let mut exporter = opentelemetry_otlp::new_exporter().http().with_env();
|
|
||||||
|
|
||||||
// XXX opentelemetry-otlp v0.18.0 has a bug in how it uses the
|
|
||||||
// OTEL_EXPORTER_OTLP_ENDPOINT env variable. According to the
|
|
||||||
// OpenTelemetry spec at
|
|
||||||
// <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp>,
|
|
||||||
// the full exporter URL is formed by appending "/v1/traces" to the value
|
|
||||||
// of OTEL_EXPORTER_OTLP_ENDPOINT. However, opentelemetry-otlp only does
|
|
||||||
// that with the grpc-tonic exporter. Other exporters, like the HTTP
|
|
||||||
// exporter, use the URL from OTEL_EXPORTER_OTLP_ENDPOINT as is, without
|
|
||||||
// appending "/v1/traces".
|
|
||||||
//
|
|
||||||
// See https://github.com/open-telemetry/opentelemetry-rust/pull/950
|
|
||||||
//
|
|
||||||
// Work around that by checking OTEL_EXPORTER_OTLP_ENDPOINT, and setting
|
|
||||||
// the endpoint url with the "/v1/traces" path ourselves. If the bug is
|
|
||||||
// fixed in a later version, we can remove this code. But if we don't
|
|
||||||
// remember to remove this, it won't do any harm either, as the crate will
|
|
||||||
// just ignore the OTEL_EXPORTER_OTLP_ENDPOINT setting when the endpoint
|
|
||||||
// is set directly with `with_endpoint`.
|
|
||||||
if std::env::var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT).is_err() {
|
|
||||||
if let Ok(mut endpoint) = std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
||||||
if !endpoint.ends_with('/') {
|
|
||||||
endpoint.push('/');
|
|
||||||
}
|
|
||||||
endpoint.push_str("v1/traces");
|
|
||||||
exporter = exporter.with_endpoint(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Propagate trace information in the standard W3C TraceContext format.
|
|
||||||
opentelemetry::global::set_text_map_propagator(
|
|
||||||
opentelemetry::sdk::propagation::TraceContextPropagator::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
opentelemetry_otlp::new_pipeline()
|
|
||||||
.tracing()
|
|
||||||
.with_exporter(exporter)
|
|
||||||
.with_trace_config(
|
|
||||||
opentelemetry::sdk::trace::config().with_resource(Resource::new(vec![KeyValue::new(
|
|
||||||
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
|
|
||||||
service_name,
|
|
||||||
)])),
|
|
||||||
)
|
|
||||||
.install_batch(opentelemetry::runtime::Tokio)
|
|
||||||
.expect("could not initialize opentelemetry exporter")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown trace pipeline gracefully, so that it has a chance to send any
|
|
||||||
// pending traces before we exit.
|
|
||||||
pub fn shutdown_tracing() {
|
|
||||||
opentelemetry::global::shutdown_tracer_provider();
|
|
||||||
}
|
|
||||||
@@ -11,38 +11,40 @@ async-trait.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
bincode.workspace = true
|
bincode.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
heapless.workspace = true
|
|
||||||
hex = { workspace = true, features = ["serde"] }
|
|
||||||
hyper = { workspace = true, features = ["full"] }
|
hyper = { workspace = true, features = ["full"] }
|
||||||
futures = { workspace = true}
|
|
||||||
jsonwebtoken.workspace = true
|
|
||||||
nix.workspace = true
|
|
||||||
once_cell.workspace = true
|
|
||||||
pin-project-lite.workspace = true
|
|
||||||
routerify.workspace = true
|
routerify.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
signal-hook.workspace = true
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
tokio-rustls.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { workspace = true, features = ["json"] }
|
tracing-subscriber = { workspace = true, features = ["json"] }
|
||||||
|
nix.workspace = true
|
||||||
|
signal-hook.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
jsonwebtoken.workspace = true
|
||||||
|
hex = { workspace = true, features = ["serde"] }
|
||||||
|
rustls.workspace = true
|
||||||
|
rustls-split.workspace = true
|
||||||
|
git-version.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
|
once_cell.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
url.workspace = true
|
|
||||||
uuid = { version = "1.2", features = ["v4", "serde"] }
|
|
||||||
|
|
||||||
metrics.workspace = true
|
metrics.workspace = true
|
||||||
|
pq_proto.workspace = true
|
||||||
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
byteorder.workspace = true
|
byteorder.workspace = true
|
||||||
bytes.workspace = true
|
bytes.workspace = true
|
||||||
criterion.workspace = true
|
|
||||||
hex-literal.workspace = true
|
hex-literal.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
criterion.workspace = true
|
||||||
|
rustls-pemfile.workspace = true
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "benchmarks"
|
name = "benchmarks"
|
||||||
|
|||||||
@@ -9,28 +9,16 @@ use std::path::Path;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use jsonwebtoken::{
|
use jsonwebtoken::{
|
||||||
decode, encode, Algorithm, Algorithm::*, DecodingKey, EncodingKey, Header, TokenData,
|
decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation,
|
||||||
Validation,
|
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
|
|
||||||
use crate::id::TenantId;
|
use crate::id::TenantId;
|
||||||
|
|
||||||
/// Algorithms accepted during validation.
|
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
|
||||||
///
|
|
||||||
/// Accept all RSA-based algorithms. We pass this list to jsonwebtoken::decode,
|
|
||||||
/// which checks that the algorithm in the token is one of these.
|
|
||||||
///
|
|
||||||
/// XXX: It also fails the validation if there are any algorithms in this list that belong
|
|
||||||
/// to different family than the token's algorithm. In other words, we can *not* list any
|
|
||||||
/// non-RSA algorithms here, or the validation always fails with InvalidAlgorithm error.
|
|
||||||
const ACCEPTED_ALGORITHMS: &[Algorithm] = &[RS256, RS384, RS512];
|
|
||||||
|
|
||||||
/// Algorithm to use when generating a new token in [`encode_from_key_file`]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
const ENCODE_ALGORITHM: Algorithm = Algorithm::RS256;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Scope {
|
pub enum Scope {
|
||||||
// Provides access to all data for a specific tenant (specified in `struct Claims` below)
|
// Provides access to all data for a specific tenant (specified in `struct Claims` below)
|
||||||
@@ -45,9 +33,8 @@ pub enum Scope {
|
|||||||
SafekeeperData,
|
SafekeeperData,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JWT payload. See docs/authentication.md for the format
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||||
@@ -68,8 +55,7 @@ pub struct JwtAuth {
|
|||||||
|
|
||||||
impl JwtAuth {
|
impl JwtAuth {
|
||||||
pub fn new(decoding_key: DecodingKey) -> Self {
|
pub fn new(decoding_key: DecodingKey) -> Self {
|
||||||
let mut validation = Validation::default();
|
let mut validation = Validation::new(JWT_ALGORITHM);
|
||||||
validation.algorithms = ACCEPTED_ALGORITHMS.into();
|
|
||||||
// The default 'required_spec_claims' is 'exp'. But we don't want to require
|
// The default 'required_spec_claims' is 'exp'. But we don't want to require
|
||||||
// expiration.
|
// expiration.
|
||||||
validation.required_spec_claims = [].into();
|
validation.required_spec_claims = [].into();
|
||||||
@@ -100,113 +86,5 @@ impl std::fmt::Debug for JwtAuth {
|
|||||||
// this function is used only for testing purposes in CLI e g generate tokens during init
|
// this function is used only for testing purposes in CLI e g generate tokens during init
|
||||||
pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> {
|
pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> {
|
||||||
let key = EncodingKey::from_rsa_pem(key_data)?;
|
let key = EncodingKey::from_rsa_pem(key_data)?;
|
||||||
Ok(encode(&Header::new(ENCODE_ALGORITHM), claims, &key)?)
|
Ok(encode(&Header::new(JWT_ALGORITHM), claims, &key)?)
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
// generated with:
|
|
||||||
//
|
|
||||||
// openssl genpkey -algorithm rsa -out storage-auth-priv.pem
|
|
||||||
// openssl pkey -in storage-auth-priv.pem -pubout -out storage-auth-pub.pem
|
|
||||||
const TEST_PUB_KEY_RSA: &[u8] = br#"
|
|
||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy6OZ+/kQXcueVJA/KTzO
|
|
||||||
v4ljxylc/Kcb0sXWuXg1GB8k3nDA1gK66LFYToH0aTnqrnqG32Vu6wrhwuvqsZA7
|
|
||||||
jQvP0ZePAbWhpEqho7EpNunDPcxZ/XDy5TQlB1P58F9I3lkJXDC+DsHYLuuzwhAv
|
|
||||||
vo2MtWRdYlVHblCVLyZtANHhUMp2HUhgjHnJh5UrLIKOl4doCBxkM3rK0wjKsNCt
|
|
||||||
M92PCR6S9rvYzldfeAYFNppBkEQrXt2CgUqZ4KaS4LXtjTRUJxljijA4HWffhxsr
|
|
||||||
euRu3ufq8kVqie7fum0rdZZSkONmce0V0LesQ4aE2jB+2Sn48h6jb4dLXGWdq8TV
|
|
||||||
wQIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
"#;
|
|
||||||
const TEST_PRIV_KEY_RSA: &[u8] = br#"
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLo5n7+RBdy55U
|
|
||||||
kD8pPM6/iWPHKVz8pxvSxda5eDUYHyTecMDWArrosVhOgfRpOequeobfZW7rCuHC
|
|
||||||
6+qxkDuNC8/Rl48BtaGkSqGjsSk26cM9zFn9cPLlNCUHU/nwX0jeWQlcML4Owdgu
|
|
||||||
67PCEC++jYy1ZF1iVUduUJUvJm0A0eFQynYdSGCMecmHlSssgo6Xh2gIHGQzesrT
|
|
||||||
CMqw0K0z3Y8JHpL2u9jOV194BgU2mkGQRCte3YKBSpngppLgte2NNFQnGWOKMDgd
|
|
||||||
Z9+HGyt65G7e5+ryRWqJ7t+6bSt1llKQ42Zx7RXQt6xDhoTaMH7ZKfjyHqNvh0tc
|
|
||||||
ZZ2rxNXBAgMBAAECggEAVz3u4Wlx3o02dsoZlSQs+xf0PEX3RXKeU+1YMbtTG9Nz
|
|
||||||
6yxpIQaoZrpbt76rJE2gwkFR+PEu1NmjoOuLb6j4KlQuI4AHz1auOoGSwFtM6e66
|
|
||||||
K4aZ4x95oEJ3vqz2fkmEIWYJwYpMUmwvnuJx76kZm0xvROMLsu4QHS2+zCVtO5Tr
|
|
||||||
hvS05IMVuZ2TdQBZw0+JaFdwXbgDjQnQGY5n9MoTWSx1a4s/FF4Eby65BbDutcpn
|
|
||||||
Vt3jQAOmO1X2kbPeWSGuPJRzyUs7Kg8qfeglBIR3ppGP3vPYAdWX+ho00bmsVkSp
|
|
||||||
Q8vjul6C3WiM+kjwDxotHSDgbl/xldAl7OqPh0bfAQKBgQDnycXuq14Vg8nZvyn9
|
|
||||||
rTnvucO8RBz5P6G+FZ+44cAS2x79+85onARmMnm+9MKYLSMo8fOvsK034NDI68XM
|
|
||||||
04QQ/vlfouvFklMTGJIurgEImTZbGCmlMYCvFyIxaEWixon8OpeI4rFe4Hmbiijh
|
|
||||||
PxhxWg221AwvBS2sco8J/ylEkQKBgQDg6Rh2QYb/j0Wou1rJPbuy3NhHofd5Rq35
|
|
||||||
4YV3f2lfVYcPrgRhwe3T9SVII7Dx8LfwzsX5TAlf48ESlI3Dzv40uOCDM+xdtBRI
|
|
||||||
r96SfSm+jup6gsXU3AsdNkrRK3HoOG9Z/TkrUp213QAIlVnvIx65l4ckFMlpnPJ0
|
|
||||||
lo1LDXZWMQKBgFArzjZ7N5OhfdO+9zszC3MLgdRAivT7OWqR+CjujIz5FYMr8Xzl
|
|
||||||
WfAvTUTrS9Nu6VZkObFvHrrRG+YjBsuN7YQjbQXTSFGSBwH34bgbn2fl9pMTjHQC
|
|
||||||
50uoaL9GHa/rlBaV/YvvPQJgCi/uXa1rMX0jdNLkDULGO8IF7cu7Yf7BAoGBAIUU
|
|
||||||
J29BkpmAst0GDs/ogTlyR18LTR0rXyHt+UUd1MGeH859TwZw80JpWWf4BmkB4DTS
|
|
||||||
hH3gKePdJY7S65ci0XNsuRupC4DeXuorde0DtkGU2tUmr9wlX0Ynq9lcdYfMbMa4
|
|
||||||
eK1TsxG69JwfkxlWlIWITWRiEFM3lJa7xlrUWmLhAoGAFpKWF/hn4zYg3seU9gai
|
|
||||||
EYHKSbhxA4mRb+F0/9IlCBPMCqFrL5yftUsYIh2XFKn8+QhO97Nmk8wJSK6TzQ5t
|
|
||||||
ZaSRmgySrUUhx4nZ/MgqWCFv8VUbLM5MBzwxPKhXkSTfR4z2vLYLJwVY7Tb4kZtp
|
|
||||||
8ismApXVGHpOCstzikV9W7k=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_decode() -> Result<(), anyhow::Error> {
|
|
||||||
let expected_claims = Claims {
|
|
||||||
tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081")?),
|
|
||||||
scope: Scope::Tenant,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Here are tokens containing the following payload, signed using TEST_PRIV_KEY_RSA
|
|
||||||
// using RS512, RS384 and RS256 algorithms:
|
|
||||||
//
|
|
||||||
// ```
|
|
||||||
// {
|
|
||||||
// "scope": "tenant",
|
|
||||||
// "tenant_id": "3d1f7595b468230304e0b73cecbcb081",
|
|
||||||
// "iss": "neon.controlplane",
|
|
||||||
// "exp": 1709200879,
|
|
||||||
// "iat": 1678442479
|
|
||||||
// }
|
|
||||||
// ```
|
|
||||||
//
|
|
||||||
// These were encoded with the online debugger at https://jwt.io
|
|
||||||
//
|
|
||||||
let encoded_rs512 = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.QmqfteDQmDGoxQ5EFkasbt35Lx0W0Nh63muQnYZvFq93DSh4ZbOG9Mc4yaiXZoiS5HgeKtFKv3mbWkDqjz3En06aY17hWwguBtAsGASX48lYeCPADYGlGAuaWnOnVRwe3iiOC7tvPFvwX_45S84X73sNUXyUiXv6nLdcDqVXudtNrGST_DnZDnjuUJX11w7sebtKqQQ8l9-iGHiXOl5yevpMCoB1OcTWcT6DfDtffoNuMHDC3fyhmEGG5oKAt1qBybqAIiyC9-UBAowRZXhdfxrzUl-I9jzKWvk85c5ulhVRwbPeP6TTTlPKwFzBNHg1i2U-1GONew5osQ3aoptwsA";
|
|
||||||
|
|
||||||
let encoded_rs384 = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.qqk4nkxKzOJP38c_g57_w_SfdQVmCsDT_bsLmdFj_N6LIB22gr6U6_P_5mvk3pIAsp0VCTDwPrCU908TxqjibEkwvQoJwbogHamSGHpD7eJBxGblSnA-Nr3MlEMxpFtec8QokSm6C5mH7DoBYjB2xzeOlxAmpR2GAzInKiMkU4kZ_OcqqrmVcMXY_6VnbxZWMekuw56zE1-PP_qNF1HvYOH-P08ONP8qdo5UPtBG7QBEFlCqZXJZCFihQaI4Vzil9rDuZGCm3I7xQJ8-yh1PX3BTbGo8EzqLdRyBeTpr08UTuRbp_MJDWevHpP3afvJetAItqZXIoZQrbJjcByHqKw";
|
|
||||||
|
|
||||||
let encoded_rs256 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.dF2N9KXG8ftFKHYbd5jQtXMQqv0Ej8FISGp1b_dmqOCotXj5S1y2AWjwyB_EXHM77JXfbEoJPAPrFFBNfd8cWtkCSTvpxWoHaecGzegDFGv5ZSc5AECFV1Daahc3PI3jii9wEiGkFOiwiBNfZ5INomOAsV--XXxlqIwKbTcgSYI7lrOTfecXAbAHiMKQlQYiIBSGnytRCgafhRkyGzPAL8ismthFJ9RHfeejyskht-9GbVHURw02bUyijuHEulpf9eEY3ZiB28de6jnCdU7ftIYaUMaYWt0nZQGkzxKPSfSLZNy14DTOYLDS04DVstWQPqnCUW_ojg0wJETOOfo9Zw";
|
|
||||||
|
|
||||||
// Check that RS512, RS384 and RS256 tokens can all be validated
|
|
||||||
let auth = JwtAuth::new(DecodingKey::from_rsa_pem(TEST_PUB_KEY_RSA)?);
|
|
||||||
|
|
||||||
for encoded in [encoded_rs512, encoded_rs384, encoded_rs256] {
|
|
||||||
let claims_from_token = auth.decode(encoded)?.claims;
|
|
||||||
assert_eq!(claims_from_token, expected_claims);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_encode() -> Result<(), anyhow::Error> {
|
|
||||||
let claims = Claims {
|
|
||||||
tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081")?),
|
|
||||||
scope: Scope::Tenant,
|
|
||||||
};
|
|
||||||
|
|
||||||
let encoded = encode_from_key_file(&claims, TEST_PRIV_KEY_RSA)?;
|
|
||||||
|
|
||||||
// decode it back
|
|
||||||
let auth = JwtAuth::new(DecodingKey::from_rsa_pem(TEST_PUB_KEY_RSA)?);
|
|
||||||
let decoded = auth.decode(&encoded)?;
|
|
||||||
|
|
||||||
assert_eq!(decoded.claims, claims);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ where
|
|||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
fn is_empty_dir(&self) -> io::Result<bool> {
|
fn is_empty_dir(&self) -> io::Result<bool> {
|
||||||
Ok(fs::read_dir(self)?.next().is_none())
|
Ok(fs::read_dir(self)?.into_iter().next().is_none())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
//! A heapless buffer for events of sorts.
|
|
||||||
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use heapless::HistoryBuffer;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct HistoryBufferWithDropCounter<T, const L: usize> {
|
|
||||||
buffer: HistoryBuffer<T, L>,
|
|
||||||
drop_count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, const L: usize> HistoryBufferWithDropCounter<T, L> {
|
|
||||||
pub fn write(&mut self, data: T) {
|
|
||||||
let len_before = self.buffer.len();
|
|
||||||
self.buffer.write(data);
|
|
||||||
let len_after = self.buffer.len();
|
|
||||||
self.drop_count += u64::from(len_before == len_after);
|
|
||||||
}
|
|
||||||
pub fn drop_count(&self) -> u64 {
|
|
||||||
self.drop_count
|
|
||||||
}
|
|
||||||
pub fn map<U, F: Fn(&T) -> U>(&self, f: F) -> HistoryBufferWithDropCounter<U, L> {
|
|
||||||
let mut buffer = HistoryBuffer::new();
|
|
||||||
buffer.extend(self.buffer.oldest_ordered().map(f));
|
|
||||||
HistoryBufferWithDropCounter::<U, L> {
|
|
||||||
buffer,
|
|
||||||
drop_count: self.drop_count,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, const L: usize> Default for HistoryBufferWithDropCounter<T, L> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
buffer: HistoryBuffer::default(),
|
|
||||||
drop_count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, const L: usize> ops::Deref for HistoryBufferWithDropCounter<T, L> {
|
|
||||||
type Target = HistoryBuffer<T, L>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct SerdeRepr<T> {
|
|
||||||
buffer: Vec<T>,
|
|
||||||
drop_count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T, const L: usize> From<&'a HistoryBufferWithDropCounter<T, L>> for SerdeRepr<T>
|
|
||||||
where
|
|
||||||
T: Clone + serde::Serialize,
|
|
||||||
{
|
|
||||||
fn from(value: &'a HistoryBufferWithDropCounter<T, L>) -> Self {
|
|
||||||
let HistoryBufferWithDropCounter { buffer, drop_count } = value;
|
|
||||||
SerdeRepr {
|
|
||||||
buffer: buffer.iter().cloned().collect(),
|
|
||||||
drop_count: *drop_count,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, const L: usize> serde::Serialize for HistoryBufferWithDropCounter<T, L>
|
|
||||||
where
|
|
||||||
T: Clone + serde::Serialize,
|
|
||||||
{
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
SerdeRepr::from(self).serialize(serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::HistoryBufferWithDropCounter;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basics() {
|
|
||||||
let mut b = HistoryBufferWithDropCounter::<_, 2>::default();
|
|
||||||
b.write(1);
|
|
||||||
b.write(2);
|
|
||||||
b.write(3);
|
|
||||||
assert!(b.iter().any(|e| *e == 2));
|
|
||||||
assert!(b.iter().any(|e| *e == 3));
|
|
||||||
assert!(!b.iter().any(|e| *e == 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_drop_count_works() {
|
|
||||||
let mut b = HistoryBufferWithDropCounter::<_, 2>::default();
|
|
||||||
b.write(1);
|
|
||||||
assert_eq!(b.drop_count(), 0);
|
|
||||||
b.write(2);
|
|
||||||
assert_eq!(b.drop_count(), 0);
|
|
||||||
b.write(3);
|
|
||||||
assert_eq!(b.drop_count(), 1);
|
|
||||||
b.write(4);
|
|
||||||
assert_eq!(b.drop_count(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_clone_works() {
|
|
||||||
let mut b = HistoryBufferWithDropCounter::<_, 2>::default();
|
|
||||||
b.write(1);
|
|
||||||
b.write(2);
|
|
||||||
b.write(3);
|
|
||||||
assert_eq!(b.drop_count(), 1);
|
|
||||||
let mut c = b.clone();
|
|
||||||
assert_eq!(c.drop_count(), 1);
|
|
||||||
assert!(c.iter().any(|e| *e == 2));
|
|
||||||
assert!(c.iter().any(|e| *e == 3));
|
|
||||||
assert!(!c.iter().any(|e| *e == 1));
|
|
||||||
|
|
||||||
c.write(4);
|
|
||||||
assert!(c.iter().any(|e| *e == 4));
|
|
||||||
assert!(!b.iter().any(|e| *e == 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_map() {
|
|
||||||
let mut b = HistoryBufferWithDropCounter::<_, 2>::default();
|
|
||||||
|
|
||||||
b.write(1);
|
|
||||||
assert_eq!(b.drop_count(), 0);
|
|
||||||
{
|
|
||||||
let c = b.map(|i| i + 10);
|
|
||||||
assert_eq!(c.oldest_ordered().cloned().collect::<Vec<_>>(), vec![11]);
|
|
||||||
assert_eq!(c.drop_count(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
b.write(2);
|
|
||||||
assert_eq!(b.drop_count(), 0);
|
|
||||||
{
|
|
||||||
let c = b.map(|i| i + 10);
|
|
||||||
assert_eq!(
|
|
||||||
c.oldest_ordered().cloned().collect::<Vec<_>>(),
|
|
||||||
vec![11, 12]
|
|
||||||
);
|
|
||||||
assert_eq!(c.drop_count(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
b.write(3);
|
|
||||||
assert_eq!(b.drop_count(), 1);
|
|
||||||
{
|
|
||||||
let c = b.map(|i| i + 10);
|
|
||||||
assert_eq!(
|
|
||||||
c.oldest_ordered().cloned().collect::<Vec<_>>(),
|
|
||||||
vec![12, 13]
|
|
||||||
);
|
|
||||||
assert_eq!(c.drop_count(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
use crate::auth::{Claims, JwtAuth};
|
use crate::auth::{Claims, JwtAuth};
|
||||||
use crate::http::error;
|
use crate::http::error;
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::anyhow;
|
||||||
use hyper::header::{HeaderName, AUTHORIZATION};
|
use hyper::header::AUTHORIZATION;
|
||||||
use hyper::http::HeaderValue;
|
|
||||||
use hyper::Method;
|
|
||||||
use hyper::{header::CONTENT_TYPE, Body, Request, Response, Server};
|
use hyper::{header::CONTENT_TYPE, Body, Request, Response, Server};
|
||||||
use metrics::{register_int_counter, Encoder, IntCounter, TextEncoder};
|
use metrics::{register_int_counter, Encoder, IntCounter, TextEncoder};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use routerify::ext::RequestExt;
|
use routerify::ext::RequestExt;
|
||||||
use routerify::{Middleware, RequestInfo, Router, RouterBuilder, RouterService};
|
use routerify::RequestInfo;
|
||||||
|
use routerify::{Middleware, Router, RouterBuilder, RouterService};
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
use tracing::{self, debug, info, info_span, warn, Instrument};
|
use tracing::info;
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use super::error::ApiError;
|
use super::error::ApiError;
|
||||||
|
|
||||||
@@ -26,83 +24,9 @@ static SERVE_METRICS_COUNT: Lazy<IntCounter> = Lazy::new(|| {
|
|||||||
.expect("failed to define a metric")
|
.expect("failed to define a metric")
|
||||||
});
|
});
|
||||||
|
|
||||||
static X_REQUEST_ID_HEADER_STR: &str = "x-request-id";
|
async fn logger(res: Response<Body>, info: RequestInfo) -> Result<Response<Body>, ApiError> {
|
||||||
|
info!("{} {} {}", info.method(), info.uri().path(), res.status(),);
|
||||||
static X_REQUEST_ID_HEADER: HeaderName = HeaderName::from_static(X_REQUEST_ID_HEADER_STR);
|
Ok(res)
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
struct RequestId(String);
|
|
||||||
|
|
||||||
/// Adds a tracing info_span! instrumentation around the handler events,
|
|
||||||
/// logs the request start and end events for non-GET requests and non-200 responses.
|
|
||||||
///
|
|
||||||
/// Use this to distinguish between logs of different HTTP requests: every request handler wrapped
|
|
||||||
/// in this type will get request info logged in the wrapping span, including the unique request ID.
|
|
||||||
///
|
|
||||||
/// There could be other ways to implement similar functionality:
|
|
||||||
///
|
|
||||||
/// * procmacros placed on top of all handler methods
|
|
||||||
/// With all the drawbacks of procmacros, brings no difference implementation-wise,
|
|
||||||
/// and little code reduction compared to the existing approach.
|
|
||||||
///
|
|
||||||
/// * Another `TraitExt` with e.g. the `get_with_span`, `post_with_span` methods to do similar logic,
|
|
||||||
/// implemented for [`RouterBuilder`].
|
|
||||||
/// Could be simpler, but we don't want to depend on [`routerify`] more, targeting to use other library later.
|
|
||||||
///
|
|
||||||
/// * In theory, a span guard could've been created in a pre-request middleware and placed into a global collection, to be dropped
|
|
||||||
/// later, in a post-response middleware.
|
|
||||||
/// Due to suspendable nature of the futures, would give contradictive results which is exactly the opposite of what `tracing-futures`
|
|
||||||
/// tries to achive with its `.instrument` used in the current approach.
|
|
||||||
///
|
|
||||||
/// If needed, a declarative macro to substitute the |r| ... closure boilerplate could be introduced.
|
|
||||||
pub struct RequestSpan<E, R, H>(pub H)
|
|
||||||
where
|
|
||||||
E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
|
|
||||||
R: Future<Output = Result<Response<Body>, E>> + Send + 'static,
|
|
||||||
H: Fn(Request<Body>) -> R + Send + Sync + 'static;
|
|
||||||
|
|
||||||
impl<E, R, H> RequestSpan<E, R, H>
|
|
||||||
where
|
|
||||||
E: Into<Box<dyn std::error::Error + Send + Sync>> + 'static,
|
|
||||||
R: Future<Output = Result<Response<Body>, E>> + Send + 'static,
|
|
||||||
H: Fn(Request<Body>) -> R + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
/// Creates a tracing span around inner request handler and executes the request handler in the contex of that span.
|
|
||||||
/// Use as `|r| RequestSpan(my_handler).handle(r)` instead of `my_handler` as the request handler to get the span enabled.
|
|
||||||
pub async fn handle(self, request: Request<Body>) -> Result<Response<Body>, E> {
|
|
||||||
let request_id = request.context::<RequestId>().unwrap_or_default().0;
|
|
||||||
let method = request.method();
|
|
||||||
let path = request.uri().path();
|
|
||||||
let request_span = info_span!("request", %method, %path, %request_id);
|
|
||||||
|
|
||||||
let log_quietly = method == Method::GET;
|
|
||||||
async move {
|
|
||||||
if log_quietly {
|
|
||||||
debug!("Handling request");
|
|
||||||
} else {
|
|
||||||
info!("Handling request");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that we reuse `error::handler` here and not returning and error at all,
|
|
||||||
// yet cannot use `!` directly in the method signature due to `routerify::RouterBuilder` limitation.
|
|
||||||
// Usage of the error handler also means that we expect only the `ApiError` errors to be raised in this call.
|
|
||||||
//
|
|
||||||
// Panics are not handled separately, there's a `tracing_panic_hook` from another module to do that globally.
|
|
||||||
match (self.0)(request).await {
|
|
||||||
Ok(response) => {
|
|
||||||
let response_status = response.status();
|
|
||||||
if log_quietly && response_status.is_success() {
|
|
||||||
debug!("Request handled, status: {response_status}");
|
|
||||||
} else {
|
|
||||||
info!("Request handled, status: {response_status}");
|
|
||||||
}
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
Err(e) => Ok(error::handler(e.into()).await),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.instrument(request_span)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
|
||||||
@@ -129,48 +53,10 @@ async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_request_id_middleware<B: hyper::body::HttpBody + Send + Sync + 'static>(
|
|
||||||
) -> Middleware<B, ApiError> {
|
|
||||||
Middleware::pre(move |req| async move {
|
|
||||||
let request_id = match req.headers().get(&X_REQUEST_ID_HEADER) {
|
|
||||||
Some(request_id) => request_id
|
|
||||||
.to_str()
|
|
||||||
.expect("extract request id value")
|
|
||||||
.to_owned(),
|
|
||||||
None => {
|
|
||||||
let request_id = uuid::Uuid::new_v4();
|
|
||||||
request_id.to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
req.set_context(RequestId(request_id));
|
|
||||||
|
|
||||||
Ok(req)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_request_id_header_to_response(
|
|
||||||
mut res: Response<Body>,
|
|
||||||
req_info: RequestInfo,
|
|
||||||
) -> Result<Response<Body>, ApiError> {
|
|
||||||
if let Some(request_id) = req_info.context::<RequestId>() {
|
|
||||||
if let Ok(request_header_value) = HeaderValue::from_str(&request_id.0) {
|
|
||||||
res.headers_mut()
|
|
||||||
.insert(&X_REQUEST_ID_HEADER, request_header_value);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_router() -> RouterBuilder<hyper::Body, ApiError> {
|
pub fn make_router() -> RouterBuilder<hyper::Body, ApiError> {
|
||||||
Router::builder()
|
Router::builder()
|
||||||
.middleware(add_request_id_middleware())
|
.middleware(Middleware::post_with_info(logger))
|
||||||
.middleware(Middleware::post_with_info(
|
.get("/metrics", prometheus_metrics_handler)
|
||||||
add_request_id_header_to_response,
|
|
||||||
))
|
|
||||||
.get("/metrics", |r| {
|
|
||||||
RequestSpan(prometheus_metrics_handler).handle(r)
|
|
||||||
})
|
|
||||||
.err_handler(error::handler)
|
.err_handler(error::handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,43 +66,40 @@ pub fn attach_openapi_ui(
|
|||||||
spec_mount_path: &'static str,
|
spec_mount_path: &'static str,
|
||||||
ui_mount_path: &'static str,
|
ui_mount_path: &'static str,
|
||||||
) -> RouterBuilder<hyper::Body, ApiError> {
|
) -> RouterBuilder<hyper::Body, ApiError> {
|
||||||
router_builder
|
router_builder.get(spec_mount_path, move |_| async move {
|
||||||
.get(spec_mount_path, move |r| {
|
Ok(Response::builder().body(Body::from(spec)).unwrap())
|
||||||
RequestSpan(move |_| async move { Ok(Response::builder().body(Body::from(spec)).unwrap()) })
|
}).get(ui_mount_path, move |_| async move {
|
||||||
.handle(r)
|
Ok(Response::builder().body(Body::from(format!(r#"
|
||||||
})
|
<!DOCTYPE html>
|
||||||
.get(ui_mount_path, move |r| RequestSpan( move |_| async move {
|
<html lang="en">
|
||||||
Ok(Response::builder().body(Body::from(format!(r#"
|
<head>
|
||||||
<!DOCTYPE html>
|
<title>rweb</title>
|
||||||
<html lang="en">
|
<link href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css" rel="stylesheet">
|
||||||
<head>
|
</head>
|
||||||
<title>rweb</title>
|
<body>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css" rel="stylesheet">
|
<div id="swagger-ui"></div>
|
||||||
</head>
|
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||||
<body>
|
<script>
|
||||||
<div id="swagger-ui"></div>
|
window.onload = function() {{
|
||||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script>
|
const ui = SwaggerUIBundle({{
|
||||||
<script>
|
"dom_id": "\#swagger-ui",
|
||||||
window.onload = function() {{
|
presets: [
|
||||||
const ui = SwaggerUIBundle({{
|
SwaggerUIBundle.presets.apis,
|
||||||
"dom_id": "\#swagger-ui",
|
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||||
presets: [
|
],
|
||||||
SwaggerUIBundle.presets.apis,
|
layout: "BaseLayout",
|
||||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
deepLinking: true,
|
||||||
],
|
showExtensions: true,
|
||||||
layout: "BaseLayout",
|
showCommonExtensions: true,
|
||||||
deepLinking: true,
|
url: "{}",
|
||||||
showExtensions: true,
|
}})
|
||||||
showCommonExtensions: true,
|
window.ui = ui;
|
||||||
url: "{}",
|
}};
|
||||||
}})
|
</script>
|
||||||
window.ui = ui;
|
</body>
|
||||||
}};
|
</html>
|
||||||
</script>
|
"#, spec_mount_path))).unwrap())
|
||||||
</body>
|
})
|
||||||
</html>
|
|
||||||
"#, spec_mount_path))).unwrap())
|
|
||||||
}).handle(r))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_token(header_value: &str) -> Result<&str, ApiError> {
|
fn parse_token(header_value: &str) -> Result<&str, ApiError> {
|
||||||
@@ -260,38 +143,6 @@ pub fn auth_middleware<B: hyper::body::HttpBody + Send + Sync + 'static>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_response_header_middleware<B>(
|
|
||||||
header: &str,
|
|
||||||
value: &str,
|
|
||||||
) -> anyhow::Result<Middleware<B, ApiError>>
|
|
||||||
where
|
|
||||||
B: hyper::body::HttpBody + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
let name =
|
|
||||||
HeaderName::from_str(header).with_context(|| format!("invalid header name: {header}"))?;
|
|
||||||
let value =
|
|
||||||
HeaderValue::from_str(value).with_context(|| format!("invalid header value: {value}"))?;
|
|
||||||
Ok(Middleware::post_with_info(
|
|
||||||
move |mut response, request_info| {
|
|
||||||
let name = name.clone();
|
|
||||||
let value = value.clone();
|
|
||||||
async move {
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
if headers.contains_key(&name) {
|
|
||||||
warn!(
|
|
||||||
"{} response already contains header {:?}",
|
|
||||||
request_info.uri(),
|
|
||||||
&name,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
headers.insert(name, value);
|
|
||||||
}
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_permission_with(
|
pub fn check_permission_with(
|
||||||
req: &Request<Body>,
|
req: &Request<Body>,
|
||||||
check_permission: impl Fn(&Claims) -> Result<(), anyhow::Error>,
|
check_permission: impl Fn(&Claims) -> Result<(), anyhow::Error>,
|
||||||
@@ -338,48 +189,3 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use futures::future::poll_fn;
|
|
||||||
use hyper::service::Service;
|
|
||||||
use routerify::RequestServiceBuilder;
|
|
||||||
use std::net::{IpAddr, SocketAddr};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_request_id_returned() {
|
|
||||||
let builder = RequestServiceBuilder::new(make_router().build().unwrap()).unwrap();
|
|
||||||
let remote_addr = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 80);
|
|
||||||
let mut service = builder.build(remote_addr);
|
|
||||||
if let Err(e) = poll_fn(|ctx| service.poll_ready(ctx)).await {
|
|
||||||
panic!("request service is not ready: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut req: Request<Body> = Request::default();
|
|
||||||
req.headers_mut()
|
|
||||||
.append(&X_REQUEST_ID_HEADER, HeaderValue::from_str("42").unwrap());
|
|
||||||
|
|
||||||
let resp: Response<hyper::body::Body> = service.call(req).await.unwrap();
|
|
||||||
|
|
||||||
let header_val = resp.headers().get(&X_REQUEST_ID_HEADER).unwrap();
|
|
||||||
|
|
||||||
assert!(header_val == "42", "response header mismatch");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_request_id_empty() {
|
|
||||||
let builder = RequestServiceBuilder::new(make_router().build().unwrap()).unwrap();
|
|
||||||
let remote_addr = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 80);
|
|
||||||
let mut service = builder.build(remote_addr);
|
|
||||||
if let Err(e) = poll_fn(|ctx| service.poll_ready(ctx)).await {
|
|
||||||
panic!("request service is not ready: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let req: Request<Body> = Request::default();
|
|
||||||
let resp: Response<hyper::body::Body> = service.call(req).await.unwrap();
|
|
||||||
|
|
||||||
let header_val = resp.headers().get(&X_REQUEST_ID_HEADER);
|
|
||||||
|
|
||||||
assert_ne!(header_val, None, "response header should NOT be empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use hyper::{header, Body, Response, StatusCode};
|
use hyper::{header, Body, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
@@ -77,16 +76,8 @@ impl HttpErrorBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handler(err: routerify::RouteError) -> Response<Body> {
|
pub async fn handler(err: routerify::RouteError) -> Response<Body> {
|
||||||
let api_error = err
|
tracing::error!("Error processing HTTP request: {:?}", err);
|
||||||
.downcast::<ApiError>()
|
err.downcast::<ApiError>()
|
||||||
.expect("handler should always return api error");
|
.expect("handler should always return api error")
|
||||||
|
.into_response()
|
||||||
// Print a stack trace for Internal Server errors
|
|
||||||
if let ApiError::InternalServerError(_) = api_error.as_ref() {
|
|
||||||
error!("Error processing HTTP request: {api_error:?}");
|
|
||||||
} else {
|
|
||||||
error!("Error processing HTTP request: {api_error:#}");
|
|
||||||
}
|
|
||||||
|
|
||||||
api_error.into_response()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
use hyper::{header, Body, Request, Response, StatusCode};
|
use hyper::{header, Body, Request, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::error::ApiError;
|
use super::error::ApiError;
|
||||||
|
|
||||||
@@ -33,12 +31,3 @@ pub fn json_response<T: Serialize>(
|
|||||||
.map_err(|e| ApiError::InternalServerError(e.into()))?;
|
.map_err(|e| ApiError::InternalServerError(e.into()))?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize through Display trait.
|
|
||||||
pub fn display_serialize<S, F>(z: &F, s: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
F: Display,
|
|
||||||
{
|
|
||||||
s.serialize_str(&format!("{}", z))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use core::fmt;
|
use std::str::FromStr;
|
||||||
use std::{borrow::Cow, str::FromStr};
|
|
||||||
|
|
||||||
use super::error::ApiError;
|
use super::error::ApiError;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@@ -30,50 +29,6 @@ pub fn parse_request_param<T: FromStr>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_query_param<'a>(
|
|
||||||
request: &'a Request<Body>,
|
|
||||||
param_name: &str,
|
|
||||||
) -> Result<Option<Cow<'a, str>>, ApiError> {
|
|
||||||
let query = match request.uri().query() {
|
|
||||||
Some(q) => q,
|
|
||||||
None => return Ok(None),
|
|
||||||
};
|
|
||||||
let mut values = url::form_urlencoded::parse(query.as_bytes())
|
|
||||||
.filter_map(|(k, v)| if k == param_name { Some(v) } else { None })
|
|
||||||
// we call .next() twice below. If it's None the first time, .fuse() ensures it's None afterwards
|
|
||||||
.fuse();
|
|
||||||
|
|
||||||
let value1 = values.next();
|
|
||||||
if values.next().is_some() {
|
|
||||||
return Err(ApiError::BadRequest(anyhow!(
|
|
||||||
"param {param_name} specified more than once"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(value1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn must_get_query_param<'a>(
|
|
||||||
request: &'a Request<Body>,
|
|
||||||
param_name: &str,
|
|
||||||
) -> Result<Cow<'a, str>, ApiError> {
|
|
||||||
get_query_param(request, param_name)?.ok_or_else(|| {
|
|
||||||
ApiError::BadRequest(anyhow!("no {param_name} specified in query parameters"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_query_param<E: fmt::Display, T: FromStr<Err = E>>(
|
|
||||||
request: &Request<Body>,
|
|
||||||
param_name: &str,
|
|
||||||
) -> Result<Option<T>, ApiError> {
|
|
||||||
get_query_param(request, param_name)?
|
|
||||||
.map(|v| {
|
|
||||||
v.parse().map_err(|e| {
|
|
||||||
ApiError::BadRequest(anyhow!("cannot parse query param {param_name}: {e}"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.transpose()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn ensure_no_body(request: &mut Request<Body>) -> Result<(), ApiError> {
|
pub async fn ensure_no_body(request: &mut Request<Body>) -> Result<(), ApiError> {
|
||||||
match request.body_mut().data().await {
|
match request.body_mut().data().await {
|
||||||
Some(_) => Err(ApiError::BadRequest(anyhow!("Unexpected request body"))),
|
Some(_) => Err(ApiError::BadRequest(anyhow!("Unexpected request body"))),
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ pub mod simple_rcu;
|
|||||||
pub mod vec_map;
|
pub mod vec_map;
|
||||||
|
|
||||||
pub mod bin_ser;
|
pub mod bin_ser;
|
||||||
|
pub mod postgres_backend;
|
||||||
|
pub mod postgres_backend_async;
|
||||||
|
|
||||||
// helper functions for creating and fsyncing
|
// helper functions for creating and fsyncing
|
||||||
pub mod crashsafe;
|
pub mod crashsafe;
|
||||||
@@ -25,6 +27,9 @@ pub mod id;
|
|||||||
// http endpoint utils
|
// http endpoint utils
|
||||||
pub mod http;
|
pub mod http;
|
||||||
|
|
||||||
|
// socket splitting utils
|
||||||
|
pub mod sock_split;
|
||||||
|
|
||||||
// common log initialisation routine
|
// common log initialisation routine
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
|
||||||
@@ -47,10 +52,6 @@ pub mod signals;
|
|||||||
|
|
||||||
pub mod fs_ext;
|
pub mod fs_ext;
|
||||||
|
|
||||||
pub mod history_buffer;
|
|
||||||
|
|
||||||
pub mod measured_stream;
|
|
||||||
|
|
||||||
/// use with fail::cfg("$name", "return(2000)")
|
/// use with fail::cfg("$name", "return(2000)")
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! failpoint_sleep_millis_async {
|
macro_rules! failpoint_sleep_millis_async {
|
||||||
|
|||||||
@@ -45,115 +45,3 @@ pub fn init(log_format: LogFormat) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable the default rust panic hook by using `set_hook`.
|
|
||||||
///
|
|
||||||
/// For neon binaries, the assumption is that tracing is configured before with [`init`], after
|
|
||||||
/// that sentry is configured (if needed). sentry will install it's own on top of this, always
|
|
||||||
/// processing the panic before we log it.
|
|
||||||
///
|
|
||||||
/// When the return value is dropped, the hook is reverted to std default hook (prints to stderr).
|
|
||||||
/// If the assumptions about the initialization order are not held, use
|
|
||||||
/// [`TracingPanicHookGuard::disarm`] but keep in mind, if tracing is stopped, then panics will be
|
|
||||||
/// lost.
|
|
||||||
#[must_use]
|
|
||||||
pub fn replace_panic_hook_with_tracing_panic_hook() -> TracingPanicHookGuard {
|
|
||||||
std::panic::set_hook(Box::new(tracing_panic_hook));
|
|
||||||
TracingPanicHookGuard::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drop guard which restores the std panic hook on drop.
|
|
||||||
///
|
|
||||||
/// Tracing should not be used when it's not configured, but we cannot really latch on to any
|
|
||||||
/// imaginary lifetime of tracing.
|
|
||||||
pub struct TracingPanicHookGuard {
|
|
||||||
act: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TracingPanicHookGuard {
|
|
||||||
fn new() -> Self {
|
|
||||||
TracingPanicHookGuard { act: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make this hook guard not do anything when dropped.
|
|
||||||
pub fn forget(&mut self) {
|
|
||||||
self.act = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for TracingPanicHookGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.act {
|
|
||||||
let _ = std::panic::take_hook();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Named symbol for our panic hook, which logs the panic.
|
|
||||||
fn tracing_panic_hook(info: &std::panic::PanicInfo) {
|
|
||||||
// following rust 1.66.1 std implementation:
|
|
||||||
// https://github.com/rust-lang/rust/blob/90743e7298aca107ddaa0c202a4d3604e29bfeb6/library/std/src/panicking.rs#L235-L288
|
|
||||||
let location = info.location();
|
|
||||||
|
|
||||||
let msg = match info.payload().downcast_ref::<&'static str>() {
|
|
||||||
Some(s) => *s,
|
|
||||||
None => match info.payload().downcast_ref::<String>() {
|
|
||||||
Some(s) => &s[..],
|
|
||||||
None => "Box<dyn Any>",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let thread = std::thread::current();
|
|
||||||
let thread = thread.name().unwrap_or("<unnamed>");
|
|
||||||
let backtrace = std::backtrace::Backtrace::capture();
|
|
||||||
|
|
||||||
let _entered = if let Some(location) = location {
|
|
||||||
tracing::error_span!("panic", %thread, location = %PrettyLocation(location))
|
|
||||||
} else {
|
|
||||||
// very unlikely to hit here, but the guarantees of std could change
|
|
||||||
tracing::error_span!("panic", %thread)
|
|
||||||
}
|
|
||||||
.entered();
|
|
||||||
|
|
||||||
if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
|
|
||||||
// this has an annoying extra '\n' in the end which anyhow doesn't do, but we cannot really
|
|
||||||
// get rid of it as we cannot get in between of std::fmt::Formatter<'_>; we could format to
|
|
||||||
// string, maybe even to a TLS one but tracing already does that.
|
|
||||||
tracing::error!("{msg}\n\nStack backtrace:\n{backtrace}");
|
|
||||||
} else {
|
|
||||||
tracing::error!("{msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure that we log something on the panic if this hook is left after tracing has been
|
|
||||||
// unconfigured. worst case when teardown is racing the panic is to log the panic twice.
|
|
||||||
tracing::dispatcher::get_default(|d| {
|
|
||||||
if let Some(_none) = d.downcast_ref::<tracing::subscriber::NoSubscriber>() {
|
|
||||||
let location = location.map(PrettyLocation);
|
|
||||||
log_panic_to_stderr(thread, msg, location, &backtrace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cold]
|
|
||||||
fn log_panic_to_stderr(
|
|
||||||
thread: &str,
|
|
||||||
msg: &str,
|
|
||||||
location: Option<PrettyLocation<'_, '_>>,
|
|
||||||
backtrace: &std::backtrace::Backtrace,
|
|
||||||
) {
|
|
||||||
eprintln!("panic while tracing is unconfigured: thread '{thread}' panicked at '{msg}', {location:?}\nStack backtrace:\n{backtrace}");
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PrettyLocation<'a, 'b>(&'a std::panic::Location<'b>);
|
|
||||||
|
|
||||||
impl std::fmt::Display for PrettyLocation<'_, '_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}:{}:{}", self.0.file(), self.0.line(), self.0.column())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for PrettyLocation<'_, '_> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
<Self as std::fmt::Display>::fmt(self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
use pin_project_lite::pin_project;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::{io, task};
|
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
|
||||||
|
|
||||||
pin_project! {
|
|
||||||
/// This stream tracks all writes and calls user provided
|
|
||||||
/// callback when the underlying stream is flushed.
|
|
||||||
pub struct MeasuredStream<S, R, W> {
|
|
||||||
#[pin]
|
|
||||||
stream: S,
|
|
||||||
write_count: usize,
|
|
||||||
inc_read_count: R,
|
|
||||||
inc_write_count: W,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, R, W> MeasuredStream<S, R, W> {
|
|
||||||
pub fn new(stream: S, inc_read_count: R, inc_write_count: W) -> Self {
|
|
||||||
Self {
|
|
||||||
stream,
|
|
||||||
write_count: 0,
|
|
||||||
inc_read_count,
|
|
||||||
inc_write_count,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncRead + Unpin, R: FnMut(usize), W> AsyncRead for MeasuredStream<S, R, W> {
|
|
||||||
fn poll_read(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
context: &mut task::Context<'_>,
|
|
||||||
buf: &mut ReadBuf<'_>,
|
|
||||||
) -> task::Poll<io::Result<()>> {
|
|
||||||
let this = self.project();
|
|
||||||
let filled = buf.filled().len();
|
|
||||||
this.stream.poll_read(context, buf).map_ok(|()| {
|
|
||||||
let cnt = buf.filled().len() - filled;
|
|
||||||
// Increment the read count.
|
|
||||||
(this.inc_read_count)(cnt);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: AsyncWrite + Unpin, R, W: FnMut(usize)> AsyncWrite for MeasuredStream<S, R, W> {
|
|
||||||
fn poll_write(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
context: &mut task::Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> task::Poll<io::Result<usize>> {
|
|
||||||
let this = self.project();
|
|
||||||
this.stream.poll_write(context, buf).map_ok(|cnt| {
|
|
||||||
// Increment the write count.
|
|
||||||
*this.write_count += cnt;
|
|
||||||
cnt
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
context: &mut task::Context<'_>,
|
|
||||||
) -> task::Poll<io::Result<()>> {
|
|
||||||
let this = self.project();
|
|
||||||
this.stream.poll_flush(context).map_ok(|()| {
|
|
||||||
// Call the user provided callback and reset the write count.
|
|
||||||
(this.inc_write_count)(*this.write_count);
|
|
||||||
*this.write_count = 0;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_shutdown(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
context: &mut task::Context<'_>,
|
|
||||||
) -> task::Poll<io::Result<()>> {
|
|
||||||
self.project().stream.poll_shutdown(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
485
libs/utils/src/postgres_backend.rs
Normal file
485
libs/utils/src/postgres_backend.rs
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
//! Server-side synchronous Postgres connection, as limited as we need.
|
||||||
|
//! To use, create PostgresBackend and run() it, passing the Handler
|
||||||
|
//! implementation determining how to process the queries. Currently its API
|
||||||
|
//! is rather narrow, but we can extend it once required.
|
||||||
|
|
||||||
|
use crate::postgres_backend_async::{log_query_error, short_error, QueryError};
|
||||||
|
use crate::sock_split::{BidiStream, ReadStream, WriteStream};
|
||||||
|
use anyhow::Context;
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use pq_proto::{BeMessage, FeMessage, FeStartupPacket};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::net::{Shutdown, SocketAddr, TcpStream};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::*;
|
||||||
|
|
||||||
|
pub trait Handler {
|
||||||
|
/// Handle single query.
|
||||||
|
/// postgres_backend will issue ReadyForQuery after calling this (this
|
||||||
|
/// might be not what we want after CopyData streaming, but currently we don't
|
||||||
|
/// care).
|
||||||
|
fn process_query(
|
||||||
|
&mut self,
|
||||||
|
pgb: &mut PostgresBackend,
|
||||||
|
query_string: &str,
|
||||||
|
) -> Result<(), QueryError>;
|
||||||
|
|
||||||
|
/// Called on startup packet receival, allows to process params.
|
||||||
|
///
|
||||||
|
/// If Ok(false) is returned postgres_backend will skip auth -- that is needed for new users
|
||||||
|
/// creation is the proxy code. That is quite hacky and ad-hoc solution, may be we could allow
|
||||||
|
/// to override whole init logic in implementations.
|
||||||
|
fn startup(
|
||||||
|
&mut self,
|
||||||
|
_pgb: &mut PostgresBackend,
|
||||||
|
_sm: &FeStartupPacket,
|
||||||
|
) -> Result<(), QueryError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check auth jwt
|
||||||
|
fn check_auth_jwt(
|
||||||
|
&mut self,
|
||||||
|
_pgb: &mut PostgresBackend,
|
||||||
|
_jwt_response: &[u8],
|
||||||
|
) -> Result<(), QueryError> {
|
||||||
|
Err(QueryError::Other(anyhow::anyhow!("JWT auth failed")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_shutdown_requested(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PostgresBackend protocol state.
|
||||||
|
/// XXX: The order of the constructors matters.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd)]
|
||||||
|
pub enum ProtoState {
|
||||||
|
Initialization,
|
||||||
|
Encrypted,
|
||||||
|
Authentication,
|
||||||
|
Established,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum AuthType {
|
||||||
|
Trust,
|
||||||
|
// This mimics postgres's AuthenticationCleartextPassword but instead of password expects JWT
|
||||||
|
NeonJWT,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AuthType {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"Trust" => Ok(Self::Trust),
|
||||||
|
"NeonJWT" => Ok(Self::NeonJWT),
|
||||||
|
_ => anyhow::bail!("invalid value \"{s}\" for auth type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AuthType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
AuthType::Trust => "Trust",
|
||||||
|
AuthType::NeonJWT => "NeonJWT",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum ProcessMsgResult {
|
||||||
|
Continue,
|
||||||
|
Break,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Always-writeable sock_split stream.
|
||||||
|
/// May not be readable. See [`PostgresBackend::take_stream_in`]
|
||||||
|
pub enum Stream {
|
||||||
|
Bidirectional(BidiStream),
|
||||||
|
WriteOnly(WriteStream),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stream {
|
||||||
|
fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Bidirectional(bidi_stream) => bidi_stream.shutdown(how),
|
||||||
|
Self::WriteOnly(write_stream) => write_stream.shutdown(how),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for Stream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Bidirectional(bidi_stream) => bidi_stream.write(buf),
|
||||||
|
Self::WriteOnly(write_stream) => write_stream.write(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Bidirectional(bidi_stream) => bidi_stream.flush(),
|
||||||
|
Self::WriteOnly(write_stream) => write_stream.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresBackend {
|
||||||
|
stream: Option<Stream>,
|
||||||
|
// Output buffer. c.f. BeMessage::write why we are using BytesMut here.
|
||||||
|
buf_out: BytesMut,
|
||||||
|
|
||||||
|
pub state: ProtoState,
|
||||||
|
|
||||||
|
auth_type: AuthType,
|
||||||
|
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
pub tls_config: Option<Arc<rustls::ServerConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_from_cstring(query_string: Bytes) -> Vec<u8> {
|
||||||
|
let mut query_string = query_string.to_vec();
|
||||||
|
if let Some(ch) = query_string.last() {
|
||||||
|
if *ch == 0 {
|
||||||
|
query_string.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query_string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for socket read loops
|
||||||
|
pub fn is_socket_read_timed_out(error: &anyhow::Error) -> bool {
|
||||||
|
for cause in error.chain() {
|
||||||
|
if let Some(io_error) = cause.downcast_ref::<io::Error>() {
|
||||||
|
if io_error.kind() == std::io::ErrorKind::WouldBlock {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast a byte slice to a string slice, dropping null terminator if there's one.
|
||||||
|
fn cstr_to_str(bytes: &[u8]) -> anyhow::Result<&str> {
|
||||||
|
let without_null = bytes.strip_suffix(&[0]).unwrap_or(bytes);
|
||||||
|
std::str::from_utf8(without_null).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresBackend {
|
||||||
|
pub fn new(
|
||||||
|
socket: TcpStream,
|
||||||
|
auth_type: AuthType,
|
||||||
|
tls_config: Option<Arc<rustls::ServerConfig>>,
|
||||||
|
set_read_timeout: bool,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
let peer_addr = socket.peer_addr()?;
|
||||||
|
if set_read_timeout {
|
||||||
|
socket
|
||||||
|
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
stream: Some(Stream::Bidirectional(BidiStream::from_tcp(socket))),
|
||||||
|
buf_out: BytesMut::with_capacity(10 * 1024),
|
||||||
|
state: ProtoState::Initialization,
|
||||||
|
auth_type,
|
||||||
|
tls_config,
|
||||||
|
peer_addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_stream(self) -> Stream {
|
||||||
|
self.stream.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get direct reference (into the Option) to the read stream.
|
||||||
|
fn get_stream_in(&mut self) -> anyhow::Result<&mut BidiStream> {
|
||||||
|
match &mut self.stream {
|
||||||
|
Some(Stream::Bidirectional(stream)) => Ok(stream),
|
||||||
|
_ => anyhow::bail!("reader taken"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_peer_addr(&self) -> &SocketAddr {
|
||||||
|
&self.peer_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_stream_in(&mut self) -> Option<ReadStream> {
|
||||||
|
let stream = self.stream.take();
|
||||||
|
match stream {
|
||||||
|
Some(Stream::Bidirectional(bidi_stream)) => {
|
||||||
|
let (read, write) = bidi_stream.split();
|
||||||
|
self.stream = Some(Stream::WriteOnly(write));
|
||||||
|
Some(read)
|
||||||
|
}
|
||||||
|
stream => {
|
||||||
|
self.stream = stream;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read full message or return None if connection is closed.
|
||||||
|
pub fn read_message(&mut self) -> Result<Option<FeMessage>, QueryError> {
|
||||||
|
let (state, stream) = (self.state, self.get_stream_in()?);
|
||||||
|
|
||||||
|
use ProtoState::*;
|
||||||
|
match state {
|
||||||
|
Initialization | Encrypted => FeStartupPacket::read(stream),
|
||||||
|
Authentication | Established => FeMessage::read(stream),
|
||||||
|
}
|
||||||
|
.map_err(QueryError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write message into internal output buffer.
|
||||||
|
pub fn write_message_noflush(&mut self, message: &BeMessage) -> io::Result<&mut Self> {
|
||||||
|
BeMessage::write(&mut self.buf_out, message)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush output buffer into the socket.
|
||||||
|
pub fn flush(&mut self) -> io::Result<&mut Self> {
|
||||||
|
let stream = self.stream.as_mut().unwrap();
|
||||||
|
stream.write_all(&self.buf_out)?;
|
||||||
|
self.buf_out.clear();
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write message into internal buffer and flush it.
|
||||||
|
pub fn write_message(&mut self, message: &BeMessage) -> io::Result<&mut Self> {
|
||||||
|
self.write_message_noflush(message)?;
|
||||||
|
self.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper for run_message_loop() that shuts down socket when we are done
|
||||||
|
pub fn run(mut self, handler: &mut impl Handler) -> Result<(), QueryError> {
|
||||||
|
let ret = self.run_message_loop(handler);
|
||||||
|
if let Some(stream) = self.stream.as_mut() {
|
||||||
|
let _ = stream.shutdown(Shutdown::Both);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_message_loop(&mut self, handler: &mut impl Handler) -> Result<(), QueryError> {
|
||||||
|
trace!("postgres backend to {:?} started", self.peer_addr);
|
||||||
|
|
||||||
|
let mut unnamed_query_string = Bytes::new();
|
||||||
|
|
||||||
|
while !handler.is_shutdown_requested() {
|
||||||
|
match self.read_message() {
|
||||||
|
Ok(message) => {
|
||||||
|
if let Some(msg) = message {
|
||||||
|
trace!("got message {msg:?}");
|
||||||
|
|
||||||
|
match self.process_message(handler, msg, &mut unnamed_query_string)? {
|
||||||
|
ProcessMsgResult::Continue => continue,
|
||||||
|
ProcessMsgResult::Break => break,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let QueryError::Other(e) = &e {
|
||||||
|
if is_socket_read_timed_out(e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("postgres backend to {:?} exited", self.peer_addr);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_tls(&mut self) -> anyhow::Result<()> {
|
||||||
|
match self.stream.take() {
|
||||||
|
Some(Stream::Bidirectional(bidi_stream)) => {
|
||||||
|
let conn = rustls::ServerConnection::new(self.tls_config.clone().unwrap())?;
|
||||||
|
self.stream = Some(Stream::Bidirectional(bidi_stream.start_tls(conn)?));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
stream => {
|
||||||
|
self.stream = stream;
|
||||||
|
anyhow::bail!("can't start TLs without bidi stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_message(
|
||||||
|
&mut self,
|
||||||
|
handler: &mut impl Handler,
|
||||||
|
msg: FeMessage,
|
||||||
|
unnamed_query_string: &mut Bytes,
|
||||||
|
) -> Result<ProcessMsgResult, QueryError> {
|
||||||
|
// Allow only startup and password messages during auth. Otherwise client would be able to bypass auth
|
||||||
|
// TODO: change that to proper top-level match of protocol state with separate message handling for each state
|
||||||
|
if self.state < ProtoState::Established
|
||||||
|
&& !matches!(
|
||||||
|
msg,
|
||||||
|
FeMessage::PasswordMessage(_) | FeMessage::StartupPacket(_)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!("protocol violation")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let have_tls = self.tls_config.is_some();
|
||||||
|
match msg {
|
||||||
|
FeMessage::StartupPacket(m) => {
|
||||||
|
trace!("got startup message {m:?}");
|
||||||
|
|
||||||
|
match m {
|
||||||
|
FeStartupPacket::SslRequest => {
|
||||||
|
debug!("SSL requested");
|
||||||
|
|
||||||
|
self.write_message(&BeMessage::EncryptionResponse(have_tls))?;
|
||||||
|
if have_tls {
|
||||||
|
self.start_tls()?;
|
||||||
|
self.state = ProtoState::Encrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FeStartupPacket::GssEncRequest => {
|
||||||
|
debug!("GSS requested");
|
||||||
|
self.write_message(&BeMessage::EncryptionResponse(false))?;
|
||||||
|
}
|
||||||
|
FeStartupPacket::StartupMessage { .. } => {
|
||||||
|
if have_tls && !matches!(self.state, ProtoState::Encrypted) {
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
"must connect with TLS",
|
||||||
|
None,
|
||||||
|
))?;
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!(
|
||||||
|
"client did not connect with TLS"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: startup() may change self.auth_type -- we are using that in proxy code
|
||||||
|
// to bypass auth for new users.
|
||||||
|
handler.startup(self, &m)?;
|
||||||
|
|
||||||
|
match self.auth_type {
|
||||||
|
AuthType::Trust => {
|
||||||
|
self.write_message_noflush(&BeMessage::AuthenticationOk)?
|
||||||
|
.write_message_noflush(&BeMessage::CLIENT_ENCODING)?
|
||||||
|
// The async python driver requires a valid server_version
|
||||||
|
.write_message_noflush(&BeMessage::server_version("14.1"))?
|
||||||
|
.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
self.state = ProtoState::Established;
|
||||||
|
}
|
||||||
|
AuthType::NeonJWT => {
|
||||||
|
self.write_message(&BeMessage::AuthenticationCleartextPassword)?;
|
||||||
|
self.state = ProtoState::Authentication;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FeStartupPacket::CancelRequest { .. } => {
|
||||||
|
return Ok(ProcessMsgResult::Break);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::PasswordMessage(m) => {
|
||||||
|
trace!("got password message '{:?}'", m);
|
||||||
|
|
||||||
|
assert!(self.state == ProtoState::Authentication);
|
||||||
|
|
||||||
|
match self.auth_type {
|
||||||
|
AuthType::Trust => unreachable!(),
|
||||||
|
AuthType::NeonJWT => {
|
||||||
|
let (_, jwt_response) = m.split_last().context("protocol violation")?;
|
||||||
|
|
||||||
|
if let Err(e) = handler.check_auth_jwt(self, jwt_response) {
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
&e.to_string(),
|
||||||
|
Some(e.pg_error_code()),
|
||||||
|
))?;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.write_message_noflush(&BeMessage::AuthenticationOk)?
|
||||||
|
.write_message_noflush(&BeMessage::CLIENT_ENCODING)?
|
||||||
|
.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
self.state = ProtoState::Established;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Query(body) => {
|
||||||
|
// remove null terminator
|
||||||
|
let query_string = cstr_to_str(&body)?;
|
||||||
|
|
||||||
|
trace!("got query {query_string:?}");
|
||||||
|
if let Err(e) = handler.process_query(self, query_string) {
|
||||||
|
log_query_error(query_string, &e);
|
||||||
|
let short_error = short_error(&e);
|
||||||
|
self.write_message_noflush(&BeMessage::ErrorResponse(
|
||||||
|
&short_error,
|
||||||
|
Some(e.pg_error_code()),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
self.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Parse(m) => {
|
||||||
|
*unnamed_query_string = m.query_string;
|
||||||
|
self.write_message(&BeMessage::ParseComplete)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Describe(_) => {
|
||||||
|
self.write_message_noflush(&BeMessage::ParameterDescription)?
|
||||||
|
.write_message(&BeMessage::NoData)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Bind(_) => {
|
||||||
|
self.write_message(&BeMessage::BindComplete)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Close(_) => {
|
||||||
|
self.write_message(&BeMessage::CloseComplete)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Execute(_) => {
|
||||||
|
let query_string = cstr_to_str(unnamed_query_string)?;
|
||||||
|
trace!("got execute {query_string:?}");
|
||||||
|
if let Err(e) = handler.process_query(self, query_string) {
|
||||||
|
log_query_error(query_string, &e);
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
&e.to_string(),
|
||||||
|
Some(e.pg_error_code()),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
// NOTE there is no ReadyForQuery message. This handler is used
|
||||||
|
// for basebackup and it uses CopyOut which doesn't require
|
||||||
|
// ReadyForQuery message and backend just switches back to
|
||||||
|
// processing mode after sending CopyDone or ErrorResponse.
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Sync => {
|
||||||
|
self.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Terminate => {
|
||||||
|
return Ok(ProcessMsgResult::Break);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We prefer explicit pattern matching to wildcards, because
|
||||||
|
// this helps us spot the places where new variants are missing
|
||||||
|
FeMessage::CopyData(_) | FeMessage::CopyDone | FeMessage::CopyFail => {
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!(
|
||||||
|
"unexpected message type: {msg:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ProcessMsgResult::Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
634
libs/utils/src/postgres_backend_async.rs
Normal file
634
libs/utils/src/postgres_backend_async.rs
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
//! Server-side asynchronous Postgres connection, as limited as we need.
|
||||||
|
//! To use, create PostgresBackend and run() it, passing the Handler
|
||||||
|
//! implementation determining how to process the queries. Currently its API
|
||||||
|
//! is rather narrow, but we can extend it once required.
|
||||||
|
|
||||||
|
use crate::postgres_backend::AuthType;
|
||||||
|
use anyhow::Context;
|
||||||
|
use bytes::{Buf, Bytes, BytesMut};
|
||||||
|
use pq_proto::{BeMessage, ConnectionError, FeMessage, FeStartupPacket, SQLSTATE_INTERNAL_ERROR};
|
||||||
|
use std::io;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::Poll;
|
||||||
|
use std::{future::Future, task::ready};
|
||||||
|
use tracing::{debug, error, info, trace};
|
||||||
|
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
|
||||||
|
pub fn is_expected_io_error(e: &io::Error) -> bool {
|
||||||
|
use io::ErrorKind::*;
|
||||||
|
matches!(
|
||||||
|
e.kind(),
|
||||||
|
ConnectionRefused | ConnectionAborted | ConnectionReset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error, occurred during query processing:
|
||||||
|
/// either during the connection ([`ConnectionError`]) or before/after it.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum QueryError {
|
||||||
|
/// The connection was lost while processing the query.
|
||||||
|
#[error(transparent)]
|
||||||
|
Disconnected(#[from] ConnectionError),
|
||||||
|
/// Some other error
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for QueryError {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
|
Self::Disconnected(ConnectionError::Socket(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryError {
|
||||||
|
pub fn pg_error_code(&self) -> &'static [u8; 5] {
|
||||||
|
match self {
|
||||||
|
Self::Disconnected(_) => b"08006", // connection failure
|
||||||
|
Self::Other(_) => SQLSTATE_INTERNAL_ERROR, // internal error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Handler {
|
||||||
|
/// Handle single query.
|
||||||
|
/// postgres_backend will issue ReadyForQuery after calling this (this
|
||||||
|
/// might be not what we want after CopyData streaming, but currently we don't
|
||||||
|
/// care).
|
||||||
|
async fn process_query(
|
||||||
|
&mut self,
|
||||||
|
pgb: &mut PostgresBackend,
|
||||||
|
query_string: &str,
|
||||||
|
) -> Result<(), QueryError>;
|
||||||
|
|
||||||
|
/// Called on startup packet receival, allows to process params.
|
||||||
|
///
|
||||||
|
/// If Ok(false) is returned postgres_backend will skip auth -- that is needed for new users
|
||||||
|
/// creation is the proxy code. That is quite hacky and ad-hoc solution, may be we could allow
|
||||||
|
/// to override whole init logic in implementations.
|
||||||
|
fn startup(
|
||||||
|
&mut self,
|
||||||
|
_pgb: &mut PostgresBackend,
|
||||||
|
_sm: &FeStartupPacket,
|
||||||
|
) -> Result<(), QueryError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check auth jwt
|
||||||
|
fn check_auth_jwt(
|
||||||
|
&mut self,
|
||||||
|
_pgb: &mut PostgresBackend,
|
||||||
|
_jwt_response: &[u8],
|
||||||
|
) -> Result<(), QueryError> {
|
||||||
|
Err(QueryError::Other(anyhow::anyhow!("JWT auth failed")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PostgresBackend protocol state.
|
||||||
|
/// XXX: The order of the constructors matters.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd)]
|
||||||
|
pub enum ProtoState {
|
||||||
|
Initialization,
|
||||||
|
Encrypted,
|
||||||
|
Authentication,
|
||||||
|
Established,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum ProcessMsgResult {
|
||||||
|
Continue,
|
||||||
|
Break,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Always-writeable sock_split stream.
|
||||||
|
/// May not be readable. See [`PostgresBackend::take_stream_in`]
|
||||||
|
pub enum Stream {
|
||||||
|
Unencrypted(BufReader<tokio::net::TcpStream>),
|
||||||
|
Tls(Box<tokio_rustls::server::TlsStream<BufReader<tokio::net::TcpStream>>>),
|
||||||
|
Broken,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for Stream {
|
||||||
|
fn poll_write(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
match self.get_mut() {
|
||||||
|
Self::Unencrypted(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||||
|
Self::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
|
||||||
|
Self::Broken => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
match self.get_mut() {
|
||||||
|
Self::Unencrypted(stream) => Pin::new(stream).poll_flush(cx),
|
||||||
|
Self::Tls(stream) => Pin::new(stream).poll_flush(cx),
|
||||||
|
Self::Broken => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn poll_shutdown(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
match self.get_mut() {
|
||||||
|
Self::Unencrypted(stream) => Pin::new(stream).poll_shutdown(cx),
|
||||||
|
Self::Tls(stream) => Pin::new(stream).poll_shutdown(cx),
|
||||||
|
Self::Broken => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AsyncRead for Stream {
|
||||||
|
fn poll_read(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
match self.get_mut() {
|
||||||
|
Self::Unencrypted(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||||
|
Self::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
|
||||||
|
Self::Broken => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostgresBackend {
|
||||||
|
stream: Stream,
|
||||||
|
|
||||||
|
// Output buffer. c.f. BeMessage::write why we are using BytesMut here.
|
||||||
|
// The data between 0 and "current position" as tracked by the bytes::Buf
|
||||||
|
// implementation of BytesMut, have already been written.
|
||||||
|
buf_out: BytesMut,
|
||||||
|
|
||||||
|
pub state: ProtoState,
|
||||||
|
|
||||||
|
auth_type: AuthType,
|
||||||
|
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
pub tls_config: Option<Arc<rustls::ServerConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_from_cstring(query_string: Bytes) -> Vec<u8> {
|
||||||
|
let mut query_string = query_string.to_vec();
|
||||||
|
if let Some(ch) = query_string.last() {
|
||||||
|
if *ch == 0 {
|
||||||
|
query_string.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query_string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cast a byte slice to a string slice, dropping null terminator if there's one.
|
||||||
|
fn cstr_to_str(bytes: &[u8]) -> anyhow::Result<&str> {
|
||||||
|
let without_null = bytes.strip_suffix(&[0]).unwrap_or(bytes);
|
||||||
|
std::str::from_utf8(without_null).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresBackend {
|
||||||
|
pub fn new(
|
||||||
|
socket: tokio::net::TcpStream,
|
||||||
|
auth_type: AuthType,
|
||||||
|
tls_config: Option<Arc<rustls::ServerConfig>>,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
let peer_addr = socket.peer_addr()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
stream: Stream::Unencrypted(BufReader::new(socket)),
|
||||||
|
buf_out: BytesMut::with_capacity(10 * 1024),
|
||||||
|
state: ProtoState::Initialization,
|
||||||
|
auth_type,
|
||||||
|
tls_config,
|
||||||
|
peer_addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_peer_addr(&self) -> &SocketAddr {
|
||||||
|
&self.peer_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read full message or return None if connection is closed.
|
||||||
|
pub async fn read_message(&mut self) -> Result<Option<FeMessage>, QueryError> {
|
||||||
|
use ProtoState::*;
|
||||||
|
match self.state {
|
||||||
|
Initialization | Encrypted => FeStartupPacket::read_fut(&mut self.stream).await,
|
||||||
|
Authentication | Established => FeMessage::read_fut(&mut self.stream).await,
|
||||||
|
Closed => Ok(None),
|
||||||
|
}
|
||||||
|
.map_err(QueryError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush output buffer into the socket.
|
||||||
|
pub async fn flush(&mut self) -> io::Result<()> {
|
||||||
|
while self.buf_out.has_remaining() {
|
||||||
|
let bytes_written = self.stream.write(self.buf_out.chunk()).await?;
|
||||||
|
self.buf_out.advance(bytes_written);
|
||||||
|
}
|
||||||
|
self.buf_out.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write message into internal output buffer.
|
||||||
|
pub fn write_message(&mut self, message: &BeMessage<'_>) -> io::Result<&mut Self> {
|
||||||
|
BeMessage::write(&mut self.buf_out, message)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an AsyncWrite implementation that wraps all the data written
|
||||||
|
/// to it in CopyData messages, and writes them to the connection
|
||||||
|
///
|
||||||
|
/// The caller is responsible for sending CopyOutResponse and CopyDone messages.
|
||||||
|
pub fn copyout_writer(&mut self) -> CopyDataWriter {
|
||||||
|
CopyDataWriter { pgb: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A polling function that tries to write all the data from 'buf_out' to the
|
||||||
|
/// underlying stream.
|
||||||
|
fn poll_write_buf(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> Poll<Result<(), std::io::Error>> {
|
||||||
|
while self.buf_out.has_remaining() {
|
||||||
|
match ready!(Pin::new(&mut self.stream).poll_write(cx, self.buf_out.chunk())) {
|
||||||
|
Ok(bytes_written) => self.buf_out.advance(bytes_written),
|
||||||
|
Err(err) => return Poll::Ready(Err(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), std::io::Error>> {
|
||||||
|
Pin::new(&mut self.stream).poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper for run_message_loop() that shuts down socket when we are done
|
||||||
|
pub async fn run<F, S>(
|
||||||
|
mut self,
|
||||||
|
handler: &mut impl Handler,
|
||||||
|
shutdown_watcher: F,
|
||||||
|
) -> Result<(), QueryError>
|
||||||
|
where
|
||||||
|
F: Fn() -> S,
|
||||||
|
S: Future,
|
||||||
|
{
|
||||||
|
let ret = self.run_message_loop(handler, shutdown_watcher).await;
|
||||||
|
let _ = self.stream.shutdown();
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_message_loop<F, S>(
|
||||||
|
&mut self,
|
||||||
|
handler: &mut impl Handler,
|
||||||
|
shutdown_watcher: F,
|
||||||
|
) -> Result<(), QueryError>
|
||||||
|
where
|
||||||
|
F: Fn() -> S,
|
||||||
|
S: Future,
|
||||||
|
{
|
||||||
|
trace!("postgres backend to {:?} started", self.peer_addr);
|
||||||
|
|
||||||
|
tokio::select!(
|
||||||
|
biased;
|
||||||
|
|
||||||
|
_ = shutdown_watcher() => {
|
||||||
|
// We were requested to shut down.
|
||||||
|
tracing::info!("shutdown request received during handshake");
|
||||||
|
return Ok(())
|
||||||
|
},
|
||||||
|
|
||||||
|
result = async {
|
||||||
|
while self.state < ProtoState::Established {
|
||||||
|
if let Some(msg) = self.read_message().await? {
|
||||||
|
trace!("got message {msg:?} during handshake");
|
||||||
|
|
||||||
|
match self.process_handshake_message(handler, msg).await? {
|
||||||
|
ProcessMsgResult::Continue => {
|
||||||
|
self.flush().await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ProcessMsgResult::Break => {
|
||||||
|
trace!("postgres backend to {:?} exited during handshake", self.peer_addr);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!("postgres backend to {:?} exited during handshake", self.peer_addr);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok::<(), QueryError>(())
|
||||||
|
} => {
|
||||||
|
// Handshake complete.
|
||||||
|
result?;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Authentication completed
|
||||||
|
let mut query_string = Bytes::new();
|
||||||
|
while let Some(msg) = tokio::select!(
|
||||||
|
biased;
|
||||||
|
_ = shutdown_watcher() => {
|
||||||
|
// We were requested to shut down.
|
||||||
|
tracing::info!("shutdown request received in run_message_loop");
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
msg = self.read_message() => { msg },
|
||||||
|
)? {
|
||||||
|
trace!("got message {:?}", msg);
|
||||||
|
|
||||||
|
let result = self.process_message(handler, msg, &mut query_string).await;
|
||||||
|
self.flush().await?;
|
||||||
|
match result? {
|
||||||
|
ProcessMsgResult::Continue => {
|
||||||
|
self.flush().await?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ProcessMsgResult::Break => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("postgres backend to {:?} exited", self.peer_addr);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_tls(&mut self) -> anyhow::Result<()> {
|
||||||
|
if let Stream::Unencrypted(plain_stream) =
|
||||||
|
std::mem::replace(&mut self.stream, Stream::Broken)
|
||||||
|
{
|
||||||
|
let acceptor = TlsAcceptor::from(self.tls_config.clone().unwrap());
|
||||||
|
let tls_stream = acceptor.accept(plain_stream).await?;
|
||||||
|
|
||||||
|
self.stream = Stream::Tls(Box::new(tls_stream));
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
anyhow::bail!("TLS already started");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_handshake_message(
|
||||||
|
&mut self,
|
||||||
|
handler: &mut impl Handler,
|
||||||
|
msg: FeMessage,
|
||||||
|
) -> Result<ProcessMsgResult, QueryError> {
|
||||||
|
assert!(self.state < ProtoState::Established);
|
||||||
|
let have_tls = self.tls_config.is_some();
|
||||||
|
match msg {
|
||||||
|
FeMessage::StartupPacket(m) => {
|
||||||
|
trace!("got startup message {m:?}");
|
||||||
|
|
||||||
|
match m {
|
||||||
|
FeStartupPacket::SslRequest => {
|
||||||
|
debug!("SSL requested");
|
||||||
|
|
||||||
|
self.write_message(&BeMessage::EncryptionResponse(have_tls))?;
|
||||||
|
if have_tls {
|
||||||
|
self.start_tls().await?;
|
||||||
|
self.state = ProtoState::Encrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FeStartupPacket::GssEncRequest => {
|
||||||
|
debug!("GSS requested");
|
||||||
|
self.write_message(&BeMessage::EncryptionResponse(false))?;
|
||||||
|
}
|
||||||
|
FeStartupPacket::StartupMessage { .. } => {
|
||||||
|
if have_tls && !matches!(self.state, ProtoState::Encrypted) {
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
"must connect with TLS",
|
||||||
|
None,
|
||||||
|
))?;
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!(
|
||||||
|
"client did not connect with TLS"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: startup() may change self.auth_type -- we are using that in proxy code
|
||||||
|
// to bypass auth for new users.
|
||||||
|
handler.startup(self, &m)?;
|
||||||
|
|
||||||
|
match self.auth_type {
|
||||||
|
AuthType::Trust => {
|
||||||
|
self.write_message(&BeMessage::AuthenticationOk)?
|
||||||
|
.write_message(&BeMessage::CLIENT_ENCODING)?
|
||||||
|
// The async python driver requires a valid server_version
|
||||||
|
.write_message(&BeMessage::server_version("14.1"))?
|
||||||
|
.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
self.state = ProtoState::Established;
|
||||||
|
}
|
||||||
|
AuthType::NeonJWT => {
|
||||||
|
self.write_message(&BeMessage::AuthenticationCleartextPassword)?;
|
||||||
|
self.state = ProtoState::Authentication;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FeStartupPacket::CancelRequest { .. } => {
|
||||||
|
self.state = ProtoState::Closed;
|
||||||
|
return Ok(ProcessMsgResult::Break);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::PasswordMessage(m) => {
|
||||||
|
trace!("got password message '{:?}'", m);
|
||||||
|
|
||||||
|
assert!(self.state == ProtoState::Authentication);
|
||||||
|
|
||||||
|
match self.auth_type {
|
||||||
|
AuthType::Trust => unreachable!(),
|
||||||
|
AuthType::NeonJWT => {
|
||||||
|
let (_, jwt_response) = m.split_last().context("protocol violation")?;
|
||||||
|
|
||||||
|
if let Err(e) = handler.check_auth_jwt(self, jwt_response) {
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
&e.to_string(),
|
||||||
|
Some(e.pg_error_code()),
|
||||||
|
))?;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.write_message(&BeMessage::AuthenticationOk)?
|
||||||
|
.write_message(&BeMessage::CLIENT_ENCODING)?
|
||||||
|
.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
self.state = ProtoState::Established;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
self.state = ProtoState::Closed;
|
||||||
|
return Ok(ProcessMsgResult::Break);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ProcessMsgResult::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_message(
|
||||||
|
&mut self,
|
||||||
|
handler: &mut impl Handler,
|
||||||
|
msg: FeMessage,
|
||||||
|
unnamed_query_string: &mut Bytes,
|
||||||
|
) -> Result<ProcessMsgResult, QueryError> {
|
||||||
|
// Allow only startup and password messages during auth. Otherwise client would be able to bypass auth
|
||||||
|
// TODO: change that to proper top-level match of protocol state with separate message handling for each state
|
||||||
|
assert!(self.state == ProtoState::Established);
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
FeMessage::StartupPacket(_) | FeMessage::PasswordMessage(_) => {
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!("protocol violation")));
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Query(body) => {
|
||||||
|
// remove null terminator
|
||||||
|
let query_string = cstr_to_str(&body)?;
|
||||||
|
|
||||||
|
trace!("got query {query_string:?}");
|
||||||
|
if let Err(e) = handler.process_query(self, query_string).await {
|
||||||
|
log_query_error(query_string, &e);
|
||||||
|
let short_error = short_error(&e);
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
&short_error,
|
||||||
|
Some(e.pg_error_code()),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
self.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Parse(m) => {
|
||||||
|
*unnamed_query_string = m.query_string;
|
||||||
|
self.write_message(&BeMessage::ParseComplete)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Describe(_) => {
|
||||||
|
self.write_message(&BeMessage::ParameterDescription)?
|
||||||
|
.write_message(&BeMessage::NoData)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Bind(_) => {
|
||||||
|
self.write_message(&BeMessage::BindComplete)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Close(_) => {
|
||||||
|
self.write_message(&BeMessage::CloseComplete)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Execute(_) => {
|
||||||
|
let query_string = cstr_to_str(unnamed_query_string)?;
|
||||||
|
trace!("got execute {query_string:?}");
|
||||||
|
if let Err(e) = handler.process_query(self, query_string).await {
|
||||||
|
log_query_error(query_string, &e);
|
||||||
|
self.write_message(&BeMessage::ErrorResponse(
|
||||||
|
&e.to_string(),
|
||||||
|
Some(e.pg_error_code()),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
// NOTE there is no ReadyForQuery message. This handler is used
|
||||||
|
// for basebackup and it uses CopyOut which doesn't require
|
||||||
|
// ReadyForQuery message and backend just switches back to
|
||||||
|
// processing mode after sending CopyDone or ErrorResponse.
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Sync => {
|
||||||
|
self.write_message(&BeMessage::ReadyForQuery)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeMessage::Terminate => {
|
||||||
|
return Ok(ProcessMsgResult::Break);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We prefer explicit pattern matching to wildcards, because
|
||||||
|
// this helps us spot the places where new variants are missing
|
||||||
|
FeMessage::CopyData(_) | FeMessage::CopyDone | FeMessage::CopyFail => {
|
||||||
|
return Err(QueryError::Other(anyhow::anyhow!(
|
||||||
|
"unexpected message type: {:?}",
|
||||||
|
msg
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ProcessMsgResult::Continue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// A futures::AsyncWrite implementation that wraps all data written to it in CopyData
|
||||||
|
/// messages.
|
||||||
|
///
|
||||||
|
|
||||||
|
pub struct CopyDataWriter<'a> {
|
||||||
|
pgb: &'a mut PostgresBackend,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AsyncWrite for CopyDataWriter<'a> {
|
||||||
|
fn poll_write(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<Result<usize, std::io::Error>> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
|
||||||
|
// It's not strictly required to flush between each message, but makes it easier
|
||||||
|
// to view in wireshark, and usually the messages that the callers write are
|
||||||
|
// decently-sized anyway.
|
||||||
|
match ready!(this.pgb.poll_write_buf(cx)) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => return Poll::Ready(Err(err)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyData
|
||||||
|
// XXX: if the input is large, we should split it into multiple messages.
|
||||||
|
// Not sure what the threshold should be, but the ultimate hard limit is that
|
||||||
|
// the length cannot exceed u32.
|
||||||
|
this.pgb.write_message(&BeMessage::CopyData(buf))?;
|
||||||
|
|
||||||
|
Poll::Ready(Ok(buf.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> Poll<Result<(), std::io::Error>> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
match ready!(this.pgb.poll_write_buf(cx)) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => return Poll::Ready(Err(err)),
|
||||||
|
}
|
||||||
|
this.pgb.poll_flush(cx)
|
||||||
|
}
|
||||||
|
fn poll_shutdown(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> Poll<Result<(), std::io::Error>> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
match ready!(this.pgb.poll_write_buf(cx)) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => return Poll::Ready(Err(err)),
|
||||||
|
}
|
||||||
|
this.pgb.poll_flush(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn short_error(e: &QueryError) -> String {
|
||||||
|
match e {
|
||||||
|
QueryError::Disconnected(connection_error) => connection_error.to_string(),
|
||||||
|
QueryError::Other(e) => format!("{e:#}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn log_query_error(query: &str, e: &QueryError) {
|
||||||
|
match e {
|
||||||
|
QueryError::Disconnected(ConnectionError::Socket(io_error)) => {
|
||||||
|
if is_expected_io_error(io_error) {
|
||||||
|
info!("query handler for '{query}' failed with expected io error: {io_error}");
|
||||||
|
} else {
|
||||||
|
error!("query handler for '{query}' failed with io error: {io_error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QueryError::Disconnected(other_connection_error) => {
|
||||||
|
error!("query handler for '{query}' failed with connection error: {other_connection_error:?}")
|
||||||
|
}
|
||||||
|
QueryError::Other(e) => {
|
||||||
|
error!("query handler for '{query}' failed: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
libs/utils/src/sock_split.rs
Normal file
206
libs/utils/src/sock_split.rs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, BufReader, Write},
|
||||||
|
net::{Shutdown, TcpStream},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rustls::Connection;
|
||||||
|
|
||||||
|
/// Wrapper supporting reads of a shared TcpStream.
|
||||||
|
pub struct ArcTcpRead(Arc<TcpStream>);
|
||||||
|
|
||||||
|
impl io::Read for ArcTcpRead {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
(&*self.0).read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for ArcTcpRead {
|
||||||
|
type Target = TcpStream;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around a TCP Stream supporting buffered reads.
|
||||||
|
pub struct BufStream(BufReader<ArcTcpRead>);
|
||||||
|
|
||||||
|
impl io::Read for BufStream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
self.0.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for BufStream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.get_ref().write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.get_ref().flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufStream {
|
||||||
|
/// Unwrap into the internal BufReader.
|
||||||
|
fn into_reader(self) -> BufReader<ArcTcpRead> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the underlying TcpStream.
|
||||||
|
fn get_ref(&self) -> &TcpStream {
|
||||||
|
&self.0.get_ref().0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ReadStream {
|
||||||
|
Tcp(BufReader<ArcTcpRead>),
|
||||||
|
Tls(rustls_split::ReadHalf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for ReadStream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(reader) => reader.read(buf),
|
||||||
|
Self::Tls(read_half) => read_half.read(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadStream {
|
||||||
|
pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.get_ref().shutdown(how),
|
||||||
|
Self::Tls(write_half) => write_half.shutdown(how),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum WriteStream {
|
||||||
|
Tcp(Arc<TcpStream>),
|
||||||
|
Tls(rustls_split::WriteHalf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteStream {
|
||||||
|
pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.shutdown(how),
|
||||||
|
Self::Tls(write_half) => write_half.shutdown(how),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for WriteStream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.as_ref().write(buf),
|
||||||
|
Self::Tls(write_half) => write_half.write(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.as_ref().flush(),
|
||||||
|
Self::Tls(write_half) => write_half.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TlsStream<T> = rustls::StreamOwned<rustls::ServerConnection, T>;
|
||||||
|
|
||||||
|
pub enum BidiStream {
|
||||||
|
Tcp(BufStream),
|
||||||
|
/// This variant is boxed, because [`rustls::ServerConnection`] is quite larger than [`BufStream`].
|
||||||
|
Tls(Box<TlsStream<BufStream>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BidiStream {
|
||||||
|
pub fn from_tcp(stream: TcpStream) -> Self {
|
||||||
|
Self::Tcp(BufStream(BufReader::new(ArcTcpRead(Arc::new(stream)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.get_ref().shutdown(how),
|
||||||
|
Self::Tls(tls_boxed) => {
|
||||||
|
if how == Shutdown::Read {
|
||||||
|
tls_boxed.sock.get_ref().shutdown(how)
|
||||||
|
} else {
|
||||||
|
tls_boxed.conn.send_close_notify();
|
||||||
|
let res = tls_boxed.flush();
|
||||||
|
tls_boxed.sock.get_ref().shutdown(how)?;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split the bi-directional stream into two owned read and write halves.
|
||||||
|
pub fn split(self) -> (ReadStream, WriteStream) {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => {
|
||||||
|
let reader = stream.into_reader();
|
||||||
|
let stream: Arc<TcpStream> = reader.get_ref().0.clone();
|
||||||
|
|
||||||
|
(ReadStream::Tcp(reader), WriteStream::Tcp(stream))
|
||||||
|
}
|
||||||
|
Self::Tls(tls_boxed) => {
|
||||||
|
let reader = tls_boxed.sock.into_reader();
|
||||||
|
let buffer_data = reader.buffer().to_owned();
|
||||||
|
let read_buf_cfg = rustls_split::BufCfg::with_data(buffer_data, 8192);
|
||||||
|
let write_buf_cfg = rustls_split::BufCfg::with_capacity(8192);
|
||||||
|
|
||||||
|
// TODO would be nice to avoid the Arc here
|
||||||
|
let socket = Arc::try_unwrap(reader.into_inner().0).unwrap();
|
||||||
|
|
||||||
|
let (read_half, write_half) = rustls_split::split(
|
||||||
|
socket,
|
||||||
|
Connection::Server(tls_boxed.conn),
|
||||||
|
read_buf_cfg,
|
||||||
|
write_buf_cfg,
|
||||||
|
);
|
||||||
|
(ReadStream::Tls(read_half), WriteStream::Tls(write_half))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_tls(self, mut conn: rustls::ServerConnection) -> io::Result<Self> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(mut stream) => {
|
||||||
|
conn.complete_io(&mut stream)?;
|
||||||
|
assert!(!conn.is_handshaking());
|
||||||
|
Ok(Self::Tls(Box::new(TlsStream::new(conn, stream))))
|
||||||
|
}
|
||||||
|
Self::Tls { .. } => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"TLS is already started on this stream",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for BidiStream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.read(buf),
|
||||||
|
Self::Tls(tls_boxed) => tls_boxed.read(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for BidiStream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.write(buf),
|
||||||
|
Self::Tls(tls_boxed) => tls_boxed.write(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Tcp(stream) => stream.flush(),
|
||||||
|
Self::Tls(tls_boxed) => tls_boxed.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user