Compare commits

...

65 Commits

Author SHA1 Message Date
Joonas Koivunen ab0ecf5548 feat: request logging 2023-04-29 16:57:41 +03:00
Joonas Koivunen b0ea9175fb fix: exiting from process while running drops
calling std::process::exit on signals or when one task exits will not
run any drops nor drop the only runtime.
2023-04-29 14:25:54 +03:00
Joonas Koivunen ed4fd15b7c fix: strip extra runtimes 2023-04-29 12:56:08 +03:00
Arseny Sher 2356a24379 Switch safekeepers to async.
This is a full switch, fs io operations are also tokio ones, working through
thread pool. Similar to pageserver, we have multiple runtimes for easier `top`
usage and isolation.

Notable points:
- Now that guts of safekeeper.rs are full of .await's, we need to be very
  careful not to drop task at random point, leaving timeline in unclear
  state. Currently the only writer is walreceiver and we don't have top
  level cancellation there, so we are good. But to be safe probably we should
  add a fuse panicking if task is being dropped while operation on a timeline
  is in progress.
- Timeline lock is Tokio one now, as we do disk IO under it.
- Collecting metrics got a crutch: since prometheus Collector is
  synchronous, there is now a special task copying once in a scrape period
  data from under async lock to sync one where collector can take it.
- Anything involving closures becomes significantly more complicated, as
  async fns are already kinda closures + 'async closures are unstable'.
- Main thread now tracks other main tasks, which got much easier.
- The only sync place left is initial data loading, as otherwise clippy
  complains on timeline map lock being held across await points -- which is
  not bad here as it happens only in single threaded runtime of main thread.
  But having it sync doesn't hurt either.

I'm concerned about performance of thread pool io offloading, async traits and
many await points; but we can try and see how it goes.

fixes https://github.com/neondatabase/neon/issues/3036
fixes https://github.com/neondatabase/neon/issues/3966
2023-04-29 07:00:24 +04:00
Arthur Petukhovsky 8543485e92 Pull clone timeline from peer safekeepers (#4089)
Add HTTP endpoint to initialize safekeeper timeline from peer
safekeepers. This is useful for initializing new safekeeper to replace
failed safekeeper. Not fully "correct" in all cases, but should work in
most.

This code is not suitable for production workloads but can be tested on
staging to get started. New endpoint is separated from usual cases and
should not affect anything if no one explicitly uses a new endpoint. We
can rollback this commit in case of issues.
2023-04-28 14:20:46 +00:00
Joonas Koivunen ec53c5ca2e revert: "Add check for duplicates of generated image layers" (#4104)
This reverts commit 732acc5.

Reverted PR: #3869

As noted in PR #4094, we do in fact try to insert duplicates to the
layer map, if L0->L1 compaction is interrupted. We do not have a proper
fix for that right now, and we are in a hurry to make a release to
production, so revert the changes related to this to the state that we
have in production currently. We know that we have a bug here, but
better to live with the bug that we've had in production for a long
time, than rush a fix to production without testing it in staging first.

Cc: #4094, #4088
2023-04-28 17:20:18 +03:00
Stas Kelvich 94d612195a bump rust-postgres version, after merging PR in rust-postgres 2023-04-28 17:15:43 +03:00
Stas Kelvich b1329db495 fix sigterm handling 2023-04-28 17:15:43 +03:00
Stas Kelvich 5bb971d64e fix more python tests 2023-04-28 17:15:43 +03:00
Stas Kelvich 0364f77b9a fix python styling 2023-04-28 17:15:43 +03:00
Stas Kelvich 4ac6a9f089 add backward compatibility to proxy 2023-04-28 17:15:43 +03:00
Stas Kelvich 9486d76b2a Add tests for link auth to compute connection 2023-04-28 17:15:43 +03:00
Stas Kelvich 040f736909 remove changes in main proxy that are now not needed 2023-04-28 17:15:43 +03:00
Stas Kelvich 645e4f6ab9 use TLS in link proxy 2023-04-28 17:15:43 +03:00
Heikki Linnakangas e947cc119b Add a small test case for pg_sni_router 2023-04-28 17:15:43 +03:00
Heikki Linnakangas 53e5d18da5 Start passthrough earlier
As soon as we have received the SSLRequest packet, and have figured
out the hostname to connect to from the SNI, we can start passing
through data. We don't need to parse the StartupPacket that the client
will send next.
2023-04-28 17:15:43 +03:00
Heikki Linnakangas 3813c703c9 Add an option for destination port.
Makes it easier to test locally.
2023-04-28 17:15:43 +03:00
Heikki Linnakangas b15204fa8c Fix --help, and required args 2023-04-28 17:15:43 +03:00
Alexey Kondratov 81c75586ab Take port from SNI, formatting, make clippy happy 2023-04-28 17:15:43 +03:00
Anton Chaporgin 556fb1642a fixed the way hostname is parsed 2023-04-28 17:15:43 +03:00
Stas Kelvich 23aca81943 Add SNI-based proxy router
In order to not to create NodePorts for each compute we can setup
services that accept connections on wildcard domains and then use
information from domain name to route connection to some internal
service. There are ready solutions for HTTPS and TLS connections
but postgresql protocol uses opportunistic TLS and we haven't found
any ready solutions.

This patch introduces `pg_sni_router` which routes connections to
`aaa--bbb--123.external.domain` to `aaa.bbb.123.internal.domain`.

In the long run we can avoid console -> compute psql communications,
but now this router seems to be the easier way forward.
2023-04-28 17:15:43 +03:00
Arseny Sher 42798e6adc Increase connection_timeout to PG in find end of WAL test.
And log postgres to stdout.

Probably fixes https://github.com/neondatabase/neon/issues/3778
2023-04-28 16:17:23 +04:00
Arthur Petukhovsky b03143dfc8 Use serde_as DisplayFromStr everywhere (#4103)
We used `display_serialize` previously, but it works only for Serialize.
`DisplayFromStr` does the same, but also works for Deserialize.
2023-04-28 13:55:07 +03:00
Arseny Sher fdacfaabfd Move PageserverFeedback to utils.
It allows to replace u64 with proper Lsn and pretty print PageserverFeedback
with serde(_json). Now walsenders on safekeepers queried with debug_dump look
like

"walsenders": [
  {
    "ttid": "fafe0cf39a99c608c872706149de9d2a/b4fb3be6f576935e7f0fcb84bdb909a1",
    "addr": "127.0.0.1:48774",
    "conn_id": 3,
    "appname": "pageserver",
    "feedback": {
      "Pageserver": {
	"current_timeline_size": 32096256,
	"last_received_lsn": "0/2415298",
	"disk_consistent_lsn": "0/1696628",
	"remote_consistent_lsn": "0/0",
	"replytime": "2023-04-12T13:54:53.958856+00:00"
      }
    }
  }
],
2023-04-28 06:22:13 +04:00
Arseny Sher b2a3981ead Move tracking of walsenders out of Timeline.
Refactors walsenders out of timeline.rs to makes it less convoluted into
separate WalSenders with its own lock, but otherwise having the same structure.
Tracking of in-memory remote_consistent_lsn is also moved there as it is mainly
received from pageserver.

State of walsender (feedback) is also restructured to be cleaner; now it is
either PageserverFeedback or StandbyFeedback(StandbyReply, HotStandbyFeedback),
but not both.
2023-04-28 06:22:13 +04:00
Joonas Koivunen fe0b616299 feat(page_service): read timeouts (#4093)
Introduce read timeouts to our `page_service` connections. Without read
timeouts, we essentially leak connections.

This is a port of #3995. Split the refactorings to the other PR: #4097.

Fixes #4028.
2023-04-27 17:55:35 +00:00
Alexander Bayandin c4e1cafb63 scripts/flaky_tests.py: handle connection error (#4096)
- Increase `connect_timeout` to 30s, which should be enough for 
most of the cases
- If the script cannot connect to the DB (or any other
`psycopg2.OperationalError` occur) — do not fail the script, log
the error and proceed. Problems with fetching flaky tests shouldn't
block the PR
2023-04-27 17:08:00 +01:00
Joonas Koivunen fdf5e4db5e refactor: Cleanup page service (#4097)
Refactoring part of #4093.

Numerious `Send + Sync` bounds were a distraction, that were not needed
at all. The proper `Bytes` usage and one `"error_message".to_string()`
are just drive-by fixes.

Not using the `PostgresBackendTCP` allows us to start setting read
timeouts (and more). `PostgresBackendTCP` is still used from proxy, so
it cannot be removed.
2023-04-27 18:51:57 +03:00
Heikki Linnakangas d1e86d65dc Run rustfmt to fix whitespace.
Commit e6ec2400fc introduced some trivial whitespace issues.
2023-04-27 18:45:22 +03:00
Arseny Sher f5b4697c90 Log session_id when proxy per client task errors out. 2023-04-27 19:08:22 +04:00
Christian Schwarz 3be81dd36b fix clippy --release failure introduced in #4030 (#4095)
PR `build: run clippy for powerset of features (#4077)` brought us a
`clippy --release` pass.

It was merged after #4030, which fails under `clippy --release` with

```
error: static `TENANT_ID_EXTRACTOR` is never used
    --> pageserver/src/tenant/timeline.rs:4270:16
     |
4270 |     pub static TENANT_ID_EXTRACTOR: once_cell::sync::Lazy<
     |                ^^^^^^^^^^^^^^^^^^^
     |
     = note: `-D dead-code` implied by `-D warnings`

error: static `TIMELINE_ID_EXTRACTOR` is never used
    --> pageserver/src/tenant/timeline.rs:4276:16
     |
4276 |     pub static TIMELINE_ID_EXTRACTOR: once_cell::sync::Lazy<
     |                ^^^^^^^^^^^^^^^^^^^^^
```

A merge queue would have prevented this.
2023-04-27 17:07:25 +03:00
MMeent e6ec2400fc Enable hot standby PostgreSQL replicas.
Notes:
 - This still needs UI support from the Console
 - I've not tuned any GUCs for PostgreSQL to make this work better
 - Safekeeper has gotten a tweak in which WAL is sent and how: It now
sends zero-ed WAL data from the start of the timeline's first segment up to
the first byte of the timeline to be compatible with normal PostgreSQL
WAL streaming.
 - This includes the commits of #3714 

Fixes one part of https://github.com/neondatabase/neon/issues/769

Co-authored-by: Anastasia Lubennikova <anastasia@neon.tech>
2023-04-27 15:26:44 +02:00
Christian Schwarz 5b911e1f9f build: run clippy for powerset of features (#4077)
This will catch compiler & clippy warnings in all feature combinations.

We should probably use cargo hack for build and test as well, but,
that's quite expensive and would add to overall CI wait times.

obsoletes https://github.com/neondatabase/neon/pull/4073
refs https://github.com/neondatabase/neon/pull/4070
2023-04-27 15:01:27 +03:00
Christian Schwarz 9ea7b5dd38 clean up logging around on-demand downloads (#4030)
- Remove repeated tenant & timeline from span
- Demote logging of the path to debug level
- Log completion at info level, in the same function where we log errors
- distinguish between layer file download success & on-demand download
succeeding as a whole in the log message wording
- Assert that the span contains a tenant id and a timeline id

fixes https://github.com/neondatabase/neon/issues/3945

Before:

```
  INFO compaction_loop{tenant_id=$TENANT_ID}:compact_timeline{timeline=$TIMELINE_ID}:download_remote_layer{tenant_id=$TENANT_ID timeline_id=$TIMELINE_ID layer=000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000020C8A71-00000000020CAF91}: download complete: /storage/pageserver/data/tenants/$TENANT_ID/timelines/$TIMELINE_ID/000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000020C8A71-00000000020CAF91
  INFO compaction_loop{tenant_id=$TENANT_ID}:compact_timeline{timeline=$TIMELINE_ID}:download_remote_layer{tenant_id=$TENANT_ID timeline_id=$TIMELINE_ID layer=000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000020C8A71-00000000020CAF91}: Rebuilt layer map. Did 9 insertions to process a batch of 1 updates.
```

After:

```
  INFO compaction_loop{tenant_id=$TENANT_ID}:compact_timeline{timeline=$TIMELINE_ID}:download_remote_layer{layer=000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000020C8A71-00000000020CAF91}: layer file download finished
  INFO compaction_loop{tenant_id=$TENANT_ID}:compact_timeline{timeline=$TIMELINE_ID}:download_remote_layer{layer=000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000020C8A71-00000000020CAF91}: Rebuilt layer map. Did 9 insertions to process a batch of 1 updates.
  INFO compaction_loop{tenant_id=$TENANT_ID}:compact_timeline{timeline=$TIMELINE_ID}:download_remote_layer{layer=000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000020C8A71-00000000020CAF91}: on-demand download successful
```
2023-04-27 11:54:48 +02:00
Arseny Sher 0112a602e1 Add timeout on proxy -> compute connection establishment.
Otherwise we sit up to default tcp_syn_retries (about 2+ min) before gettings os
error 110 if compute has been migrated to another pod.
2023-04-27 09:50:52 +04:00
Anastasia Lubennikova 92214578af Fix proxy_io_bytes_per_client metric: use branch_id identifier properly. (#4084)
It fixes the miscalculation of the metric for projects that use multiple
branches for the same endpoint.
We were under billing users with such projects. So we need to
communicate the change in Release Notes.
2023-04-26 17:47:54 +03:00
Christian Schwarz 6861259be7 add global metric for unexpected on-demand downloads (#4069)
Until we have toned down the prod logs to zero WARN and ERROR, we want a
dedicated metric for which we can have a dedicated alert.

fixes https://github.com/neondatabase/neon/issues/3924
2023-04-26 15:18:26 +02:00
Sergey Melnikov 11df2ee5d7 Add safekeeper-3.us-east-2.aws.neon.build (#4085) 2023-04-26 14:40:36 +03:00
Arseny Sher 31a3910fd9 Remove wait_for_sk_commit_lsn_to_reach_remote_storage.
It had a couple of inherent races:

1) Even if compute is killed before the call, some more data might still arrive
to safekeepers after commit_lsn on them is polled, advancing it. Then checkpoint
on pageserver might not include this tail, and so upload of expected LSN won't
happen until one more checkpoint.

2) commit_lsn is updated asynchronously -- compute can commit transaction before
communicating commit_lsn to even single safekeeper (sync-safekeepers can be used
to forces the advancement). This makes semantics of
wait_for_sk_commit_lsn_to_reach_remote_storage quite complicated.

Replace it with last_flush_lsn_upload which
1) Learns last flush LSN on compute;
2) Waits for it to arrive to pageserver;
3) Checkpoints it;
4) Waits for the upload.

In some tests this keeps compute alive longer than before, but this doesn't seem
to be important.

There is a chance this fixes https://github.com/neondatabase/neon/issues/3209
2023-04-26 13:46:33 +04:00
Joonas Koivunen 381c8fca4f feat: log how long tenant activation takes (#4080)
Adds just a counter counting up from the creation to the tenant, logged
after activation. Might help guide us with the investigation of #4025.
2023-04-26 12:39:17 +03:00
Joonas Koivunen 4625da3164 build: remove busted sk-1.us-east-2 from staging hosts (#4082)
this should give us complete deployments while a new one is being
brought up.
2023-04-26 09:07:45 +00:00
Joonas Koivunen 850f6b1cb9 refactor: drop pageserver_ondisk_layers (#4071)
I didn't get through #3775 fast enough so we wanted to remove this
metric.

Fixes #3705.
2023-04-26 11:49:29 +03:00
Sergey Melnikov f19b70b379 Configure extra domain for us-east-1 (#4078) 2023-04-26 09:36:26 +02:00
Sergey Melnikov 9d0cf08d5f Fix new storage-broker deploy for eu-central-1 (#4079) 2023-04-26 10:29:44 +03:00
Alexander Bayandin 2d6fd72177 GitHub Workflows: Fix crane for several registries (#4076)
Follow-up fix after https://github.com/neondatabase/neon/pull/4067

```
+ crane tag neondatabase/vm-compute-node-v14:3064 latest
Error: fetching "neondatabase/vm-compute-node-v14:3064": GET https://index.docker.io/v2/neondatabase/vm-compute-node-v14/manifests/3064: MANIFEST_UNKNOWN: manifest unknown; unknown tag=3064
```

I reverted back the previous approach for promoting images
(login to one registry, save images to local fs, logout and login to
another registry, and push images from local fs). It turns out what
works for one Google project (kaniko), doesn't work for another (crane)
[sigh]
2023-04-25 23:58:59 +01:00
Heikki Linnakangas 8945fbdb31 Enable OpenTelemetry tracing in proxy in staging. (#4065)
Depends on https://github.com/neondatabase/helm-charts/pull/32

Co-authored-by: Lassi Pölönen <lassi.polonen@iki.fi>
2023-04-25 20:45:36 +03:00
Alexander Bayandin 05ac0e2493 Login to ECR and Docker Hub at once (#4067)
- Update kaniko to 1.9.2 (from 1.7.0), problem with reproducible build is fixed
- Login to ECR and Docker Hub at once, so we can push to several
registries, it makes job `push-docker-hub` unneeded
- `push-docker-hub` replaced with `promote-images` in `needs:` clause,
Pushing images to production ECR moved to `promote-images` job
2023-04-25 17:54:10 +01:00
Joonas Koivunen bfd45dd671 test_tenant_config: allow ERROR from eviction task (#4074) 2023-04-25 18:41:09 +03:00
Joonas Koivunen 7f80230fd2 fix: stop dead_code rustc lint (#4070)
only happens without `--all-features` which is what `./run_clippy.sh`
uses.
2023-04-25 17:07:04 +02:00
Sergey Melnikov 78bbbccadb Deploy proxies for preview enviroments (#4052)
## Describe your changes
Deploy `main` proxies to the preview environments
We don't deploy storage there yet, as it's tricky.

## Issue ticket number and link
https://github.com/neondatabase/cloud/issues/4737
2023-04-25 16:46:52 +02:00
Christian Schwarz dbbe032c39 neon_local: fix tenant create -c eviction_policy:... (#4004)
And add corresponding unit test.

The fix is to use `.remove()` instead of `.get()` when processing the
arugments hash map.
The code uses emptiness of the hash map to determine whether all
arguments have been processed.
This was likely a copy-paste error.

    
refs https://github.com/neondatabase/neon/issues/3942
2023-04-25 15:33:30 +02:00
Joonas Koivunen cb9473928d feat: add rough timings for basebackup (#4062)
just record the time needed for waiting the lsn and then the basebackup
in a log message in millis. this is related to ongoing investigations to
cold start performance.

this could also be a a counter. it cannot be added next to smgr
histograms, because we don't want another histogram per timeline.

the aim is to allow drilling deeper into which timelines were slow, and
to understand why some need two basebackups.
2023-04-25 13:22:16 +00:00
Christian Schwarz fa20e37574 add gauge for in-flight layer uploads (#3951)
For the "worst-case /storage usage panel", we need to compute
```
remote size + local-only size
```

We currently don't have a metric for local-only layers.

The number of in-flight layers in the upload queue is just that, so, let
Prometheus scrape it.

The metric is two counters (started and finished).
The delta is the amount of in-flight uploads in the queue.

The metrics are incremented in the respective `call_unfinished_metric_*`
functions.
These track ongoing operations by file_kind and op_kind.
We only need this metric for layer uploads, so, there's the new
RemoteTimelineClientMetricsCallTrackSize type that forces all call sites
to decide whether they want the size tracked or not.
If we find that other file_kinds or op_kinds are interesting (metadata
uploads, layer downloads, layer deletes) are interesting, we can just
enable them, and they'll be just another label combination within the
metrics that this PR adds.

fixes https://github.com/neondatabase/neon/issues/3922
2023-04-25 14:22:48 +02:00
Joonas Koivunen 4911d7ce6f feat: warn when requests get cancelled (#4064)
Add a simple disarmable dropguard to log if request is cancelled before
it is completed. We currently don't have this, and it makes for
difficult to know when the request was dropped.
2023-04-25 15:22:23 +03:00
Christian Schwarz e83684b868 add libmetric metric for each logged log message (#4055)
This patch extends the libmetrics logging setup functionality with a
`tracing` layer that increments a Prometheus counter each time we log a
log message. We have the counter per tracing event level. This allows
for monitoring WARN and ERR log volume without parsing the log. Also, it
would allow cross-checking whether logs got dropped on the way into
Loki.

It would be nicer if we could hook deeper into the tracing logging
layer, to avoid evaluating the filter twice.
But I don't know how to do it.
2023-04-25 14:10:18 +02:00
Eduard Dyckman afbbc61036 Adding synthetic size to pageserver swagger (#4049)
## Describe your changes

I added synthetic size response to the console swagger. Now I am syncing
it back to neon
2023-04-24 16:19:25 +03:00
Alexey Kondratov 7ba5c286b7 [compute_ctl] Improve 'empty' compute startup sequence (#4034)
Do several attempts to get spec from the control-plane and retry network
errors and all reasonable HTTP response codes. Do not hang waiting for
spec without confirmation from the control-plane that compute is known
and is in the `Empty` state.

Adjust the way we track `total_startup_ms` metric, it should be
calculated since the moment we received spec, not from the moment
`compute_ctl` started. Also introduce a new `wait_for_spec_ms` metric
to track the time spent sleeping and waiting for spec to be delivered
from control-plane.

Part of neondatabase/cloud#3533
2023-04-21 11:10:48 +02:00
sharnoff 02b28ae0b1 fix vm-informant dbname: "neondb" -> "postgres" (#4046)
Changes the vm-informant's postgres connection string's dbname from
"neondb" (which sometimes doesn't exist) to "postgres" (which
_hopefully_ should exist more often?).

Currently there are a handful of VMs in prod that aren't working with
autoscaling because they don't have the "neondb" database.

The vm-informant doesn't require any database in particular; it's just
connecting as `cloud_admin` to be able to adjust the file cache
settings.
2023-04-18 18:54:32 +03:00
Cihan Demirci 0bfbae2d73 Add storage broker deployment to us-east-1 (#4048) 2023-04-18 18:41:09 +03:00
fcdm f1b7dc4064 Update pageserver instances in us-east-1 2023-04-18 14:08:12 +01:00
Alexander Bayandin e2a5177e89 Bump h2 from 0.3.17 to 0.3.18 (#4045) 2023-04-18 16:04:10 +03:00
Cihan Demirci 0c083564ce Add us-east-1 hosts file and update regions (#4042)
## Describe your changes

## Issue ticket number and link

## Checklist before requesting a review

- [x] I have performed a self-review of my code.
- [ ] If it is a core feature, I have added thorough tests.
- [ ] Do we need to implement analytics? if so did you add the relevant
metrics to the dashboard?
- [ ] If this PR requires public announcement, mark it with
/release-notes label and add several sentences in this section.

## Checklist before merging

- [ ] Do not forget to reformat commit message to not include the above
checklist
2023-04-17 15:25:27 +03:00
fcdm d8dd60dc81 Add helm values for us-east-1 2023-04-17 11:59:38 +01:00
Arthur Petukhovsky 73f34eaa5e Send AppendResponse keepalive once per second (#4036)
Walproposer sends AppendRequest at least once per second. This patch
adds a response to these requests once per second.

Fixes https://github.com/neondatabase/neon/issues/4017
2023-04-17 11:24:57 +03:00
Matt Nappo c2496c7ef2 Added black_box in layer_map benches (fix #3396) 2023-04-16 16:33:37 +03:00
124 changed files with 5501 additions and 1718 deletions
+50
View File
@@ -0,0 +1,50 @@
storage:
vars:
bucket_name: neon-prod-storage-us-east-1
bucket_region: us-east-1
console_mgmt_base_url: http://neon-internal-api.aws.neon.tech
broker_endpoint: http://storage-broker-lb.theta.us-east-1.internal.aws.neon.tech:50051
pageserver_config_stub:
pg_distrib_dir: /usr/local
metric_collection_endpoint: http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events
metric_collection_interval: 10min
disk_usage_based_eviction:
max_usage_pct: 85 # TODO: decrease to 80 after all pageservers are below 80
min_avail_bytes: 0
period: "10s"
tenant_config:
eviction_policy:
kind: "LayerAccessThreshold"
period: "10m"
threshold: &default_eviction_threshold "24h"
evictions_low_residence_duration_metric_threshold: *default_eviction_threshold
remote_storage:
bucket_name: "{{ bucket_name }}"
bucket_region: "{{ bucket_region }}"
prefix_in_bucket: "pageserver/v1"
safekeeper_s3_prefix: safekeeper/v1/wal
hostname_suffix: ""
remote_user: ssm-user
ansible_aws_ssm_region: us-east-1
ansible_aws_ssm_bucket_name: neon-prod-storage-us-east-1
console_region_id: aws-us-east-1
sentry_environment: production
children:
pageservers:
hosts:
pageserver-0.us-east-1.aws.neon.tech:
ansible_host: i-085222088b0d2e0c7
pageserver-1.us-east-1.aws.neon.tech:
ansible_host: i-0969d4f684d23a21e
pageserver-2.us-east-1.aws.neon.tech:
ansible_host: i-05dee87895da58dad
safekeepers:
hosts:
safekeeper-0.us-east-1.aws.neon.tech:
ansible_host: i-04ce739e88793d864
safekeeper-1.us-east-1.aws.neon.tech:
ansible_host: i-0e9e6c9227fb81410
safekeeper-2.us-east-1.aws.neon.tech:
ansible_host: i-072f4dd86a327d52f
@@ -0,0 +1,47 @@
storage:
vars:
bucket_name: neon-dev-storage-eu-central-1
bucket_region: eu-central-1
# We only register/update storage in one preview console and manually copy to other instances
console_mgmt_base_url: http://neon-internal-api.helium.aws.neon.build
broker_endpoint: http://storage-broker-lb.alpha.eu-central-1.internal.aws.neon.build:50051
pageserver_config_stub:
pg_distrib_dir: /usr/local
metric_collection_endpoint: http://neon-internal-api.helium.aws.neon.build/billing/api/v1/usage_events
metric_collection_interval: 10min
disk_usage_based_eviction:
max_usage_pct: 80
min_avail_bytes: 0
period: "10s"
tenant_config:
eviction_policy:
kind: "LayerAccessThreshold"
period: "20m"
threshold: &default_eviction_threshold "20m"
evictions_low_residence_duration_metric_threshold: *default_eviction_threshold
remote_storage:
bucket_name: "{{ bucket_name }}"
bucket_region: "{{ bucket_region }}"
prefix_in_bucket: "pageserver/v1"
safekeeper_s3_prefix: safekeeper/v1/wal
hostname_suffix: ""
remote_user: ssm-user
ansible_aws_ssm_region: eu-central-1
ansible_aws_ssm_bucket_name: neon-dev-storage-eu-central-1
console_region_id: aws-eu-central-1
sentry_environment: staging
children:
pageservers:
hosts:
pageserver-0.eu-central-1.aws.neon.build:
ansible_host: i-011f93ec26cfba2d4
safekeepers:
hosts:
safekeeper-0.eu-central-1.aws.neon.build:
ansible_host: i-0ff026d27babf8ddd
safekeeper-1.eu-central-1.aws.neon.build:
ansible_host: i-03983a49ee54725d9
safekeeper-2.eu-central-1.aws.neon.build:
ansible_host: i-0bd025ecdb61b0db3
+2 -2
View File
@@ -48,9 +48,9 @@ storage:
hosts: hosts:
safekeeper-0.us-east-2.aws.neon.build: safekeeper-0.us-east-2.aws.neon.build:
ansible_host: i-027662bd552bf5db0 ansible_host: i-027662bd552bf5db0
safekeeper-1.us-east-2.aws.neon.build:
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-3.us-east-2.aws.neon.build:
ansible_host: i-05f8ba2cda243bd18
safekeeper-99.us-east-2.aws.neon.build: safekeeper-99.us-east-2.aws.neon.build:
ansible_host: i-0d61b6a2ea32028d5 ansible_host: i-0d61b6a2ea32028d5
@@ -0,0 +1,52 @@
# Helm chart values for neon-storage-broker
podLabels:
neon_env: staging
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-lb.alpha.eu-central-1.internal.aws.neon.build
# service.type -- Service type
type: LoadBalancer
# service.port -- broker listen port
port: 50051
ingress:
enabled: false
metrics:
enabled: false
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: "staging"
@@ -23,6 +23,7 @@ settings:
authBackend: "console" authBackend: "console"
authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2" authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2"
domain: "*.eu-west-1.aws.neon.build" domain: "*.eu-west-1.aws.neon.build"
otelExporterOtlpEndpoint: "https://otel-collector.zeta.eu-west-1.internal.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://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
@@ -9,6 +9,7 @@ settings:
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" domain: "pg.neon.build"
otelExporterOtlpEndpoint: "https://otel-collector.beta.us-east-2.internal.aws.neon.build"
sentryEnvironment: "staging" sentryEnvironment: "staging"
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events" metricCollectionEndpoint: "http://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
metricCollectionInterval: "1min" metricCollectionInterval: "1min"
@@ -24,6 +24,7 @@ settings:
authBackend: "console" authBackend: "console"
authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2" authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2"
domain: "*.cloud.stage.neon.tech" domain: "*.cloud.stage.neon.tech"
otelExporterOtlpEndpoint: "https://otel-collector.beta.us-east-2.internal.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://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
@@ -25,6 +25,7 @@ settings:
authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2" authEndpoint: "http://neon-internal-api.aws.neon.build/management/api/v2"
domain: "*.us-east-2.aws.neon.build" domain: "*.us-east-2.aws.neon.build"
extraDomains: ["*.us-east-2.postgres.zenith.tech", "*.us-east-2.retooldb-staging.com"] extraDomains: ["*.us-east-2.postgres.zenith.tech", "*.us-east-2.retooldb-staging.com"]
otelExporterOtlpEndpoint: "https://otel-collector.beta.us-east-2.internal.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://neon-internal-api.aws.neon.build/billing/api/v1/usage_events"
@@ -0,0 +1,67 @@
# Helm chart values for neon-proxy-scram.
# This is a YAML-formatted file.
deploymentStrategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 100%
maxUnavailable: 50%
image:
repository: neondatabase/neon
settings:
authBackend: "console"
authEndpoint: "http://neon-internal-api.${PREVIEW_NAME}.aws.neon.build/management/api/v2"
domain: "*.cloud.${PREVIEW_NAME}.aws.neon.build"
sentryEnvironment: "staging"
wssPort: 8443
metricCollectionEndpoint: "http://neon-internal-api.${PREVIEW_NAME}.aws.neon.build/billing/api/v1/usage_events"
metricCollectionInterval: "1min"
# -- Additional labels for neon-proxy pods
podLabels:
neon_service: proxy-scram
neon_env: test
neon_region: ${PREVIEW_NAME}.eu-central-1
exposedService:
annotations:
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-scheme: internet-facing
external-dns.alpha.kubernetes.io/hostname: cloud.${PREVIEW_NAME}.aws.neon.build
httpsPort: 443
#metrics:
# enabled: true
# serviceMonitor:
# enabled: true
# selector:
# release: kube-prometheus-stack
extraManifests:
- apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: "{{ include \"neon-proxy.fullname\" . }}"
labels:
helm.sh/chart: neon-proxy-{{ .Chart.Version }}
app.kubernetes.io/name: neon-proxy
app.kubernetes.io/instance: "{{ include \"neon-proxy.fullname\" . }}"
app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
app.kubernetes.io/managed-by: Helm
namespace: "{{ .Release.Namespace }}"
spec:
selector:
matchLabels:
app.kubernetes.io/name: "neon-proxy"
endpoints:
- port: http
path: /metrics
interval: 10s
scrapeTimeout: 10s
namespaceSelector:
matchNames:
- "{{ .Release.Namespace }}"
@@ -0,0 +1,69 @@
# 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 5 minutes (5 * 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 300"]
terminationGracePeriodSeconds: 604800
image:
repository: neondatabase/neon
settings:
authBackend: "console"
authEndpoint: "http://neon-internal-api.aws.neon.tech/management/api/v2"
domain: "*.us-east-1.aws.neon.tech"
# *.us-east-1.retooldb.com hasn't been delegated yet.
extraDomains: ["*.us-east-1.postgres.vercel-storage.com"]
sentryEnvironment: "production"
wssPort: 8443
metricCollectionEndpoint: "http://neon-internal-api.aws.neon.tech/billing/api/v1/usage_events"
metricCollectionInterval: "10min"
podLabels:
neon_service: proxy-scram
neon_env: prod
neon_region: us-east-1
exposedService:
annotations:
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-scheme: internet-facing
external-dns.alpha.kubernetes.io/hostname: us-east-1.aws.neon.tech
httpsPort: 443
extraManifests:
- apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: "{{ include \"neon-proxy.fullname\" . }}"
labels:
helm.sh/chart: neon-proxy-{{ .Chart.Version }}
app.kubernetes.io/name: neon-proxy
app.kubernetes.io/instance: "{{ include \"neon-proxy.fullname\" . }}"
app.kubernetes.io/version: "{{ .Chart.AppVersion }}"
app.kubernetes.io/managed-by: Helm
namespace: "{{ .Release.Namespace }}"
spec:
selector:
matchLabels:
app.kubernetes.io/name: "neon-proxy"
endpoints:
- port: http
path: /metrics
interval: 10s
scrapeTimeout: 10s
namespaceSelector:
matchNames:
- "{{ .Release.Namespace }}"
@@ -0,0 +1,52 @@
# 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-lb.theta.us-east-1.internal.aws.neon.tech
# service.type -- Service type
type: LoadBalancer
# service.port -- broker listen port
port: 50051
ingress:
enabled: false
metrics:
enabled: false
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"
+113 -86
View File
@@ -111,8 +111,21 @@ jobs:
- name: Get postgres headers - name: Get postgres headers
run: make postgres-headers -j$(nproc) run: make postgres-headers -j$(nproc)
- name: Run cargo clippy # cargo hack runs the given cargo subcommand (clippy in this case) for all feature combinations.
run: ./run_clippy.sh # This will catch compiler & clippy warnings in all feature combinations.
# TODO: use cargo hack for build and test as well, but, that's quite expensive.
# NB: keep clippy args in sync with ./run_clippy.sh
- run: |
CLIPPY_COMMON_ARGS="$( source .neon_clippy_args; echo "$CLIPPY_COMMON_ARGS")"
if [ "$CLIPPY_COMMON_ARGS" = "" ]; then
echo "No clippy args found in .neon_clippy_args"
exit 1
fi
echo "CLIPPY_COMMON_ARGS=${CLIPPY_COMMON_ARGS}" >> $GITHUB_ENV
- name: Run cargo clippy (debug)
run: cargo hack --feature-powerset clippy $CLIPPY_COMMON_ARGS
- name: Run cargo clippy (release)
run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS
# Use `${{ !cancelled() }}` to run quck tests after the longer clippy run # Use `${{ !cancelled() }}` to run quck tests after the longer clippy run
- name: Check formatting - name: Check formatting
@@ -541,7 +554,7 @@ jobs:
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
needs: [ push-docker-hub, tag ] needs: [ promote-images, tag ]
steps: steps:
- name: Set PR's status to pending and request a remote CI test - name: Set PR's status to pending and request a remote CI test
run: | run: |
@@ -584,8 +597,7 @@ jobs:
neon-image: neon-image:
runs-on: [ self-hosted, gen3, large ] runs-on: [ self-hosted, gen3, large ]
needs: [ tag ] needs: [ tag ]
# https://github.com/GoogleContainerTools/kaniko/issues/2005 container: gcr.io/kaniko-project/executor:v1.9.2-debug
container: gcr.io/kaniko-project/executor:v1.7.0-debug
defaults: defaults:
run: run:
shell: sh -eu {0} shell: sh -eu {0}
@@ -597,11 +609,32 @@ jobs:
submodules: true submodules: true
fetch-depth: 0 fetch-depth: 0
- name: Configure ECR login - name: Configure ECR and Docker Hub login
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json run: |
DOCKERHUB_AUTH=$(echo -n "${{ secrets.NEON_DOCKERHUB_USERNAME }}:${{ secrets.NEON_DOCKERHUB_PASSWORD }}" | base64)
echo "::add-mask::${DOCKERHUB_AUTH}"
cat <<-EOF > /kaniko/.docker/config.json
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "${DOCKERHUB_AUTH}"
}
},
"credHelpers": {
"369495373322.dkr.ecr.eu-central-1.amazonaws.com": "ecr-login"
}
}
EOF
- 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 --reproducible --snapshot-mode=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}}
--destination neondatabase/neon:${{needs.tag.outputs.build-tag}}
# Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied # Cleanup script fails otherwise - rm: cannot remove '/nvme/actions-runner/_work/_temp/_github_home/.ecr': Permission denied
- name: Cleanup ECR folder - name: Cleanup ECR folder
@@ -652,7 +685,7 @@ jobs:
compute-tools-image: compute-tools-image:
runs-on: [ self-hosted, gen3, large ] runs-on: [ self-hosted, gen3, large ]
needs: [ tag ] needs: [ tag ]
container: gcr.io/kaniko-project/executor:v1.7.0-debug container: gcr.io/kaniko-project/executor:v1.9.2-debug
defaults: defaults:
run: run:
shell: sh -eu {0} shell: sh -eu {0}
@@ -661,18 +694,41 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v1 # v3 won't work with kaniko uses: actions/checkout@v1 # v3 won't work with kaniko
- name: Configure ECR login - name: Configure ECR and Docker Hub login
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json run: |
DOCKERHUB_AUTH=$(echo -n "${{ secrets.NEON_DOCKERHUB_USERNAME }}:${{ secrets.NEON_DOCKERHUB_PASSWORD }}" | base64)
echo "::add-mask::${DOCKERHUB_AUTH}"
cat <<-EOF > /kaniko/.docker/config.json
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "${DOCKERHUB_AUTH}"
}
},
"credHelpers": {
"369495373322.dkr.ecr.eu-central-1.amazonaws.com": "ecr-login"
}
}
EOF
- 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 --reproducible --snapshot-mode=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}}
--destination neondatabase/compute-tools:${{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 - name: Cleanup ECR folder
run: rm -rf ~/.ecr run: rm -rf ~/.ecr
compute-node-image: compute-node-image:
runs-on: [ self-hosted, gen3, large ] runs-on: [ self-hosted, gen3, large ]
container: gcr.io/kaniko-project/executor:v1.7.0-debug container: gcr.io/kaniko-project/executor:v1.9.2-debug
needs: [ tag ] needs: [ tag ]
strategy: strategy:
fail-fast: false fail-fast: false
@@ -689,12 +745,36 @@ jobs:
submodules: true submodules: true
fetch-depth: 0 fetch-depth: 0
- name: Configure ECR login - name: Configure ECR and Docker Hub login
run: echo "{\"credsStore\":\"ecr-login\"}" > /kaniko/.docker/config.json run: |
DOCKERHUB_AUTH=$(echo -n "${{ secrets.NEON_DOCKERHUB_USERNAME }}:${{ secrets.NEON_DOCKERHUB_PASSWORD }}" | base64)
echo "::add-mask::${DOCKERHUB_AUTH}"
cat <<-EOF > /kaniko/.docker/config.json
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "${DOCKERHUB_AUTH}"
}
},
"credHelpers": {
"369495373322.dkr.ecr.eu-central-1.amazonaws.com": "ecr-login"
}
}
EOF
- 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 --reproducible --snapshot-mode=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}}
--destination neondatabase/compute-node-${{ matrix.version }}:${{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 - name: Cleanup ECR folder
run: rm -rf ~/.ecr run: rm -rf ~/.ecr
@@ -786,13 +866,11 @@ jobs:
runs-on: [ self-hosted, gen3, small ] runs-on: [ self-hosted, gen3, small ]
needs: [ tag, test-images, vm-compute-node-image ] needs: [ tag, test-images, vm-compute-node-image ]
container: golang:1.19-bullseye container: golang:1.19-bullseye
if: github.event_name != 'workflow_dispatch' # Don't add if-condition here.
# The job should always be run because we have dependant other jobs that shouldn't be skipped
steps: steps:
- name: Install Crane & ECR helper - name: Install Crane & ECR helper
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 go install github.com/google/go-containerregistry/cmd/crane@31786c6cbb82d6ec4fb8eb79cd9387905130534e # v0.11.0
go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0 go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
@@ -802,10 +880,15 @@ jobs:
mkdir /github/home/.docker/ mkdir /github/home/.docker/
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
- name: Copy vm-compute-node images to Docker Hub
run: |
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} vm-compute-node-v14
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} vm-compute-node-v15
- name: Add latest tag to images - name: Add latest tag to images
if: | if: |
(github.ref_name == 'main' || github.ref_name == 'release') && (github.ref_name == 'main' || github.ref_name == 'release') &&
github.event_name != 'workflow_dispatch' github.event_name != 'workflow_dispatch'
run: | 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/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-tools:${{needs.tag.outputs.build-tag}} latest
@@ -814,50 +897,10 @@ jobs:
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/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 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:
runs-on: [ self-hosted, dev, x64 ]
needs: [ promote-images, tag ]
container: golang:1.19-bullseye
steps:
- name: Install Crane & ECR helper
run: |
go install github.com/google/go-containerregistry/cmd/crane@31786c6cbb82d6ec4fb8eb79cd9387905130534e # v0.11.0
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: Pull neon image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} neon
- name: Pull compute tools image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} compute-tools
- name: Pull compute node v14 image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} compute-node-v14
- name: Pull vm compute node v14 image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} vm-compute-node-v14
- name: Pull compute node v15 image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} compute-node-v15
- name: Pull vm compute node v15 image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} vm-compute-node-v15
- name: Pull rust image from ECR
run: crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/rust:pinned rust
- name: Push images to production ECR - name: Push images to production ECR
if: | if: |
(github.ref_name == 'main' || github.ref_name == 'release') && (github.ref_name == 'main' || github.ref_name == 'release') &&
github.event_name != 'workflow_dispatch' github.event_name != 'workflow_dispatch'
run: | run: |
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/neon:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/neon:latest
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest
@@ -872,28 +915,12 @@ jobs:
echo "" > /github/home/.docker/config.json echo "" > /github/home/.docker/config.json
crane auth login -u ${{ secrets.NEON_DOCKERHUB_USERNAME }} -p ${{ secrets.NEON_DOCKERHUB_PASSWORD }} index.docker.io crane auth login -u ${{ secrets.NEON_DOCKERHUB_USERNAME }} -p ${{ secrets.NEON_DOCKERHUB_PASSWORD }} index.docker.io
- name: Push neon image to Docker Hub - name: Push vm-compute-node to Docker Hub
run: crane push neon neondatabase/neon:${{needs.tag.outputs.build-tag}} run: |
crane push vm-compute-node-v14 neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}}
crane push vm-compute-node-v15 neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}}
- name: Push compute tools image to Docker Hub - name: Push latest tags to Docker Hub
run: crane push compute-tools neondatabase/compute-tools:${{needs.tag.outputs.build-tag}}
- name: Push compute node v14 image to Docker Hub
run: crane push compute-node-v14 neondatabase/compute-node-v14:${{needs.tag.outputs.build-tag}}
- name: Push vm compute node v14 image to Docker Hub
run: crane push vm-compute-node-v14 neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}}
- name: Push compute node v15 image to Docker Hub
run: crane push compute-node-v15 neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}}
- name: Push vm compute node v15 image to Docker Hub
run: crane push vm-compute-node-v15 neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}}
- name: Push rust image to Docker Hub
run: crane push rust neondatabase/rust:pinned
- name: Add latest tag to images in Docker Hub
if: | if: |
(github.ref_name == 'main' || github.ref_name == 'release') && (github.ref_name == 'main' || github.ref_name == 'release') &&
github.event_name != 'workflow_dispatch' github.event_name != 'workflow_dispatch'
@@ -913,7 +940,7 @@ jobs:
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
needs: [ push-docker-hub, tag, regress-tests ] needs: [ promote-images, tag, regress-tests ]
if: | if: |
contains(github.event.pull_request.labels.*.name, 'deploy-test-storage') && contains(github.event.pull_request.labels.*.name, 'deploy-test-storage') &&
github.event_name != 'workflow_dispatch' github.event_name != 'workflow_dispatch'
@@ -947,7 +974,7 @@ jobs:
deploy: deploy:
runs-on: [ self-hosted, gen3, small ] runs-on: [ self-hosted, gen3, small ]
container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest container: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/ansible:latest
needs: [ push-docker-hub, tag, regress-tests ] needs: [ promote-images, tag, regress-tests ]
if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch' if: ( github.ref_name == 'main' || github.ref_name == 'release' ) && github.event_name != 'workflow_dispatch'
steps: steps:
- name: Fix git ownership - name: Fix git ownership
@@ -984,7 +1011,7 @@ jobs:
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: [ promote-images, tag, regress-tests ]
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
+51 -1
View File
@@ -48,7 +48,8 @@ jobs:
shell: bash shell: bash
strategy: strategy:
matrix: matrix:
target_region: [ eu-west-1, us-east-2 ] # TODO(sergey): Fix storage deploy in eu-central-1
target_region: [ eu-west-1, us-east-2]
environment: environment:
name: dev-${{ matrix.target_region }} name: dev-${{ matrix.target_region }}
steps: steps:
@@ -133,6 +134,53 @@ jobs:
- name: Cleanup helm folder - name: Cleanup helm folder
run: rm -rf ~/.cache run: rm -rf ~/.cache
deploy-preview-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: eu-central-1
target_cluster: dev-eu-central-1-alpha
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 preview proxies
run: |
DOCKER_TAG=${{ inputs.dockerTag }}
for PREVIEW_NAME in helium argon krypton xenon radon oganesson hydrogen nitrogen oxygen fluorine chlorine; do
export PREVIEW_NAME
envsubst <.github/helm-values/preview-template.neon-proxy-scram.yaml >preview-${PREVIEW_NAME}.neon-proxy-scram.yaml
helm upgrade neon-proxy-scram-${PREVIEW_NAME} neondatabase/neon-proxy --namespace neon-proxy-${PREVIEW_NAME} --create-namespace --install --atomic -f preview-${PREVIEW_NAME}.neon-proxy-scram.yaml --set image.tag=${DOCKER_TAG} --set settings.sentryUrl=${{ secrets.SENTRY_URL_PROXY }} --wait --timeout 15m0s
done
- name: Cleanup helm folder
run: rm -rf ~/.cache
deploy-storage-broker-new: deploy-storage-broker-new:
runs-on: [ self-hosted, gen3, small ] runs-on: [ self-hosted, gen3, small ]
@@ -148,6 +196,8 @@ jobs:
target_cluster: dev-us-east-2-beta target_cluster: dev-us-east-2-beta
- target_region: eu-west-1 - target_region: eu-west-1
target_cluster: dev-eu-west-1-zeta target_cluster: dev-eu-west-1-zeta
- target_region: eu-central-1
target_cluster: dev-eu-central-1-alpha
environment: environment:
name: dev-${{ matrix.target_region }} name: dev-${{ matrix.target_region }}
steps: steps:
+7 -1
View File
@@ -49,7 +49,7 @@ jobs:
shell: bash shell: bash
strategy: strategy:
matrix: matrix:
target_region: [ us-east-2, us-west-2, eu-central-1, ap-southeast-1 ] target_region: [ us-east-2, us-west-2, eu-central-1, ap-southeast-1, us-east-1 ]
environment: environment:
name: prod-${{ matrix.target_region }} name: prod-${{ matrix.target_region }}
steps: steps:
@@ -97,6 +97,10 @@ jobs:
target_cluster: prod-ap-southeast-1-epsilon target_cluster: prod-ap-southeast-1-epsilon
deploy_link_proxy: false deploy_link_proxy: false
deploy_legacy_scram_proxy: false deploy_legacy_scram_proxy: false
- target_region: us-east-1
target_cluster: prod-us-east-1-theta
deploy_link_proxy: false
deploy_legacy_scram_proxy: false
environment: environment:
name: prod-${{ matrix.target_region }} name: prod-${{ matrix.target_region }}
steps: steps:
@@ -147,6 +151,8 @@ jobs:
target_cluster: prod-eu-central-1-gamma target_cluster: prod-eu-central-1-gamma
- target_region: ap-southeast-1 - target_region: ap-southeast-1
target_cluster: prod-ap-southeast-1-epsilon target_cluster: prod-ap-southeast-1-epsilon
- target_region: us-east-1
target_cluster: prod-us-east-1-theta
environment: environment:
name: prod-${{ matrix.target_region }} name: prod-${{ matrix.target_region }}
steps: steps:
+4
View File
@@ -0,0 +1,4 @@
# * `-A unknown_lints` do not warn about unknown lint suppressions
# that people with newer toolchains might use
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status)
export CLIPPY_COMMON_ARGS="--locked --workspace --all-targets -- -A unknown_lints -D warnings"
Generated
+128 -9
View File
@@ -1574,6 +1574,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.1.0" version = "1.1.0"
@@ -1756,9 +1771,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.17" version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -2361,6 +2376,24 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.26.2" version = "0.26.2"
@@ -2483,12 +2516,50 @@ version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "openssl"
version = "0.10.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "opentelemetry" name = "opentelemetry"
version = "0.18.0" version = "0.18.0"
@@ -2681,6 +2752,7 @@ dependencies = [
"tenant_size_model", "tenant_size_model",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-io-timeout",
"tokio-postgres", "tokio-postgres",
"tokio-tar", "tokio-tar",
"tokio-util", "tokio-util",
@@ -2815,6 +2887,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]] [[package]]
name = "plotters" name = "plotters"
version = "0.3.4" version = "0.3.4"
@@ -2846,7 +2924,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres" name = "postgres"
version = "0.19.4" version = "0.19.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=43e6db254a97fdecbce33d8bc0890accfd74495e#43e6db254a97fdecbce33d8bc0890accfd74495e" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=0bc41d8503c092b040142214aac3cf7d11d0c19f#0bc41d8503c092b040142214aac3cf7d11d0c19f"
dependencies = [ dependencies = [
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@@ -2856,10 +2934,21 @@ dependencies = [
"tokio-postgres", "tokio-postgres",
] ]
[[package]]
name = "postgres-native-tls"
version = "0.5.0"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=0bc41d8503c092b040142214aac3cf7d11d0c19f#0bc41d8503c092b040142214aac3cf7d11d0c19f"
dependencies = [
"native-tls",
"tokio",
"tokio-native-tls",
"tokio-postgres",
]
[[package]] [[package]]
name = "postgres-protocol" name = "postgres-protocol"
version = "0.6.4" version = "0.6.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=43e6db254a97fdecbce33d8bc0890accfd74495e#43e6db254a97fdecbce33d8bc0890accfd74495e" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=0bc41d8503c092b040142214aac3cf7d11d0c19f#0bc41d8503c092b040142214aac3cf7d11d0c19f"
dependencies = [ dependencies = [
"base64 0.20.0", "base64 0.20.0",
"byteorder", "byteorder",
@@ -2877,7 +2966,7 @@ dependencies = [
[[package]] [[package]]
name = "postgres-types" name = "postgres-types"
version = "0.2.4" version = "0.2.4"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=43e6db254a97fdecbce33d8bc0890accfd74495e#43e6db254a97fdecbce33d8bc0890accfd74495e" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=0bc41d8503c092b040142214aac3cf7d11d0c19f#0bc41d8503c092b040142214aac3cf7d11d0c19f"
dependencies = [ dependencies = [
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@@ -2958,7 +3047,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"postgres-protocol", "postgres-protocol",
"rand", "rand",
"serde",
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
@@ -3109,10 +3197,12 @@ dependencies = [
"itertools", "itertools",
"md5", "md5",
"metrics", "metrics",
"native-tls",
"once_cell", "once_cell",
"opentelemetry", "opentelemetry",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"postgres-native-tls",
"postgres_backend", "postgres_backend",
"pq_proto", "pq_proto",
"prometheus", "prometheus",
@@ -3567,6 +3657,7 @@ dependencies = [
"const_format", "const_format",
"crc32c", "crc32c",
"fs2", "fs2",
"futures",
"git-version", "git-version",
"hex", "hex",
"humantime", "humantime",
@@ -3581,6 +3672,7 @@ dependencies = [
"pq_proto", "pq_proto",
"regex", "regex",
"remote_storage", "remote_storage",
"reqwest",
"safekeeper_api", "safekeeper_api",
"serde", "serde",
"serde_json", "serde_json",
@@ -4319,10 +4411,20 @@ dependencies = [
"syn 2.0.15", "syn 2.0.15",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-postgres" name = "tokio-postgres"
version = "0.7.7" version = "0.7.7"
source = "git+https://github.com/neondatabase/rust-postgres.git?rev=43e6db254a97fdecbce33d8bc0890accfd74495e#43e6db254a97fdecbce33d8bc0890accfd74495e" source = "git+https://github.com/neondatabase/rust-postgres.git?rev=0bc41d8503c092b040142214aac3cf7d11d0c19f#0bc41d8503c092b040142214aac3cf7d11d0c19f"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"byteorder", "byteorder",
@@ -4629,6 +4731,16 @@ dependencies = [
"valuable", "valuable",
] ]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-futures" name = "tracing-futures"
version = "0.2.5" version = "0.2.5"
@@ -4854,6 +4966,7 @@ dependencies = [
"bincode", "bincode",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"criterion", "criterion",
"futures", "futures",
"heapless", "heapless",
@@ -4865,6 +4978,7 @@ dependencies = [
"nix", "nix",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"pq_proto",
"rand", "rand",
"regex", "regex",
"routerify", "routerify",
@@ -4879,6 +4993,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"tracing-error",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
"uuid", "uuid",
@@ -4901,6 +5016,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@@ -5279,13 +5400,11 @@ name = "workspace_hack"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder",
"bytes", "bytes",
"chrono", "chrono",
"clap 4.2.2", "clap 4.2.2",
"clap_builder", "clap_builder",
"crossbeam-utils", "crossbeam-utils",
"digest",
"either", "either",
"fail", "fail",
"futures", "futures",
+8 -5
View File
@@ -62,6 +62,7 @@ jsonwebtoken = "8"
libc = "0.2" libc = "0.2"
md5 = "0.7.0" md5 = "0.7.0"
memoffset = "0.8" memoffset = "0.8"
native-tls = "0.2"
nix = "0.26" nix = "0.26"
notify = "5.0.0" notify = "5.0.0"
num_cpus = "1.15" num_cpus = "1.15"
@@ -110,6 +111,7 @@ toml = "0.7"
toml_edit = "0.19" toml_edit = "0.19"
tonic = {version = "0.9", features = ["tls", "tls-roots"]} tonic = {version = "0.9", features = ["tls", "tls-roots"]}
tracing = "0.1" tracing = "0.1"
tracing-error = "0.2.0"
tracing-opentelemetry = "0.18.0" 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"
@@ -123,10 +125,11 @@ env_logger = "0.10"
log = "0.4" log = "0.4"
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed ## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" } postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="0bc41d8503c092b040142214aac3cf7d11d0c19f" }
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" } postgres-native-tls = { git = "https://github.com/neondatabase/rust-postgres.git", rev="0bc41d8503c092b040142214aac3cf7d11d0c19f" }
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" } postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", rev="0bc41d8503c092b040142214aac3cf7d11d0c19f" }
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" } postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", rev="0bc41d8503c092b040142214aac3cf7d11d0c19f" }
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="0bc41d8503c092b040142214aac3cf7d11d0c19f" }
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 ## Other git libraries
@@ -161,7 +164,7 @@ tonic-build = "0.9"
# This is only needed for proxy's tests. # This is only needed for proxy's tests.
# TODO: we should probably fork `tokio-postgres-rustls` instead. # TODO: we should probably fork `tokio-postgres-rustls` instead.
[patch.crates-io] [patch.crates-io]
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="43e6db254a97fdecbce33d8bc0890accfd74495e" } tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", rev="0bc41d8503c092b040142214aac3cf7d11d0c19f" }
################# Binary contents sections ################# Binary contents sections
+1 -1
View File
@@ -54,7 +54,7 @@ RUN set -e \
RUN set -e \ RUN set -e \
&& echo "::sysinit:cgconfigparser -l /etc/cgconfig.conf -s 1664" >> /etc/inittab \ && echo "::sysinit:cgconfigparser -l /etc/cgconfig.conf -s 1664" >> /etc/inittab \
&& CONNSTR="dbname=neondb user=cloud_admin sslmode=disable" \ && CONNSTR="dbname=postgres user=cloud_admin sslmode=disable" \
&& ARGS="--auto-restart --cgroup=neon-postgres --pgconnstr=\"$CONNSTR\"" \ && ARGS="--auto-restart --cgroup=neon-postgres --pgconnstr=\"$CONNSTR\"" \
&& echo "::respawn:su vm-informant -c '/usr/local/bin/vm-informant $ARGS'" >> /etc/inittab && echo "::respawn:su vm-informant -c '/usr/local/bin/vm-informant $ARGS'" >> /etc/inittab
+19 -5
View File
@@ -73,7 +73,7 @@ fn main() -> Result<()> {
// 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();
let mut spec = None; let spec;
let mut live_config_allowed = false; let mut live_config_allowed = false;
match spec_json { match spec_json {
// First, try to get cluster spec from the cli argument // First, try to get cluster spec from the cli argument
@@ -89,9 +89,13 @@ fn main() -> Result<()> {
} else if let Some(id) = compute_id { } else if let Some(id) = compute_id {
if let Some(cp_base) = control_plane_uri { if let Some(cp_base) = control_plane_uri {
live_config_allowed = true; live_config_allowed = true;
if let Ok(s) = get_spec_from_control_plane(cp_base, id) { spec = match get_spec_from_control_plane(cp_base, id) {
spec = Some(s); Ok(s) => s,
} Err(e) => {
error!("cannot get response from control plane: {}", e);
panic!("neither spec nor confirmation that compute is in the Empty state was received");
}
};
} else { } else {
panic!("must specify both --control-plane-uri and --compute-id or none"); panic!("must specify both --control-plane-uri and --compute-id or none");
} }
@@ -114,7 +118,6 @@ fn main() -> Result<()> {
spec_set = false; spec_set = false;
} }
let compute_node = ComputeNode { let compute_node = ComputeNode {
start_time: Utc::now(),
connstr: Url::parse(connstr).context("cannot parse connstr as a URL")?, connstr: Url::parse(connstr).context("cannot parse connstr as a URL")?,
pgdata: pgdata.to_string(), pgdata: pgdata.to_string(),
pgbin: pgbin.to_string(), pgbin: pgbin.to_string(),
@@ -147,6 +150,17 @@ fn main() -> Result<()> {
let mut state = compute.state.lock().unwrap(); let mut state = compute.state.lock().unwrap();
let pspec = state.pspec.as_ref().expect("spec must be set"); let pspec = state.pspec.as_ref().expect("spec must be set");
let startup_tracing_context = pspec.spec.startup_tracing_context.clone(); let startup_tracing_context = pspec.spec.startup_tracing_context.clone();
// Record for how long we slept waiting for the spec.
state.metrics.wait_for_spec_ms = Utc::now()
.signed_duration_since(state.start_time)
.to_std()
.unwrap()
.as_millis() as u64;
// Reset start time to the actual start of the configuration, so that
// total startup time was properly measured at the end.
state.start_time = Utc::now();
state.status = ComputeStatus::Init; state.status = ComputeStatus::Init;
compute.state_changed.notify_all(); compute.state_changed.notify_all();
drop(state); drop(state);
+60 -7
View File
@@ -38,7 +38,6 @@ use crate::spec::*;
/// Compute node info shared across several `compute_ctl` threads. /// Compute node info shared across several `compute_ctl` threads.
pub struct ComputeNode { pub struct ComputeNode {
pub start_time: DateTime<Utc>,
// Url type maintains proper escaping // Url type maintains proper escaping
pub connstr: url::Url, pub connstr: url::Url,
pub pgdata: String, pub pgdata: String,
@@ -66,6 +65,7 @@ pub struct ComputeNode {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ComputeState { pub struct ComputeState {
pub start_time: DateTime<Utc>,
pub status: ComputeStatus, pub status: ComputeStatus,
/// Timestamp of the last Postgres activity /// Timestamp of the last Postgres activity
pub last_active: DateTime<Utc>, pub last_active: DateTime<Utc>,
@@ -77,6 +77,7 @@ pub struct ComputeState {
impl ComputeState { impl ComputeState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
start_time: Utc::now(),
status: ComputeStatus::Empty, status: ComputeStatus::Empty,
last_active: Utc::now(), last_active: Utc::now(),
error: None, error: None,
@@ -248,18 +249,63 @@ impl ComputeNode {
/// safekeepers sync, basebackup, etc. /// safekeepers sync, basebackup, etc.
#[instrument(skip(self, compute_state))] #[instrument(skip(self, compute_state))]
pub fn prepare_pgdata(&self, compute_state: &ComputeState) -> Result<()> { pub fn prepare_pgdata(&self, compute_state: &ComputeState) -> Result<()> {
#[derive(Clone)]
enum Replication {
Primary,
Static { lsn: Lsn },
HotStandby,
}
let pspec = compute_state.pspec.as_ref().expect("spec must be set"); let pspec = compute_state.pspec.as_ref().expect("spec must be set");
let spec = &pspec.spec;
let pgdata_path = Path::new(&self.pgdata); let pgdata_path = Path::new(&self.pgdata);
let hot_replica = if let Some(option) = spec.cluster.settings.find_ref("hot_standby") {
if let Some(value) = &option.value {
anyhow::ensure!(option.vartype == "bool");
matches!(value.as_str(), "on" | "yes" | "true")
} else {
false
}
} else {
false
};
let replication = if hot_replica {
Replication::HotStandby
} else if let Some(lsn) = spec.cluster.settings.find("recovery_target_lsn") {
Replication::Static {
lsn: Lsn::from_str(&lsn)?,
}
} else {
Replication::Primary
};
// Remove/create an empty pgdata directory and put configuration there. // Remove/create an empty pgdata directory and put configuration there.
self.create_pgdata()?; self.create_pgdata()?;
config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), &pspec.spec)?; config::write_postgres_conf(&pgdata_path.join("postgresql.conf"), &pspec.spec)?;
info!("starting safekeepers syncing"); // Syncing safekeepers is only safe with primary nodes: if a primary
let lsn = self // is already connected it will be kicked out, so a secondary (standby)
.sync_safekeepers(pspec.storage_auth_token.clone()) // cannot sync safekeepers.
.with_context(|| "failed to sync safekeepers")?; let lsn = match &replication {
info!("safekeepers synced at LSN {}", lsn); Replication::Primary => {
info!("starting safekeepers syncing");
let lsn = self
.sync_safekeepers(pspec.storage_auth_token.clone())
.with_context(|| "failed to sync safekeepers")?;
info!("safekeepers synced at LSN {}", lsn);
lsn
}
Replication::Static { lsn } => {
info!("Starting read-only node at static LSN {}", lsn);
*lsn
}
Replication::HotStandby => {
info!("Initializing standby from latest Pageserver LSN");
Lsn(0)
}
};
info!( info!(
"getting basebackup@{} from pageserver {}", "getting basebackup@{} from pageserver {}",
@@ -275,6 +321,13 @@ impl ComputeNode {
// Update pg_hba.conf received with basebackup. // Update pg_hba.conf received with basebackup.
update_pg_hba(pgdata_path)?; update_pg_hba(pgdata_path)?;
match &replication {
Replication::Primary | Replication::Static { .. } => {}
Replication::HotStandby => {
add_standby_signal(pgdata_path)?;
}
}
Ok(()) Ok(())
} }
@@ -425,7 +478,7 @@ impl ComputeNode {
.unwrap() .unwrap()
.as_millis() as u64; .as_millis() as u64;
state.metrics.total_startup_ms = startup_end_time state.metrics.total_startup_ms = startup_end_time
.signed_duration_since(self.start_time) .signed_duration_since(compute_state.start_time)
.to_std() .to_std()
.unwrap() .unwrap()
.as_millis() as u64; .as_millis() as u64;
+1
View File
@@ -18,6 +18,7 @@ use tracing_utils::http::OtelName;
fn status_response_from_state(state: &ComputeState) -> ComputeStatusResponse { fn status_response_from_state(state: &ComputeState) -> ComputeStatusResponse {
ComputeStatusResponse { ComputeStatusResponse {
start_time: state.start_time,
tenant: state tenant: state
.pspec .pspec
.as_ref() .as_ref()
+10
View File
@@ -152,11 +152,14 @@ components:
type: object type: object
description: Compute startup metrics. description: Compute startup metrics.
required: required:
- wait_for_spec_ms
- sync_safekeepers_ms - sync_safekeepers_ms
- basebackup_ms - basebackup_ms
- config_ms - config_ms
- total_startup_ms - total_startup_ms
properties: properties:
wait_for_spec_ms:
type: integer
sync_safekeepers_ms: sync_safekeepers_ms:
type: integer type: integer
basebackup_ms: basebackup_ms:
@@ -181,6 +184,13 @@ components:
- status - status
- last_active - last_active
properties: properties:
start_time:
type: string
description: |
Time when compute was started. If initially compute was started in the `empty`
state and then provided with valid spec, `start_time` will be reset to the
moment, when spec was received.
example: "2022-10-12T07:20:50.52Z"
status: status:
$ref: '#/components/schemas/ComputeStatus' $ref: '#/components/schemas/ComputeStatus'
last_active: last_active:
+7
View File
@@ -94,6 +94,7 @@ impl PgOptionsSerialize for GenericOptions {
pub trait GenericOptionsSearch { pub trait GenericOptionsSearch {
fn find(&self, name: &str) -> Option<String>; fn find(&self, name: &str) -> Option<String>;
fn find_ref(&self, name: &str) -> Option<&GenericOption>;
} }
impl GenericOptionsSearch for GenericOptions { impl GenericOptionsSearch for GenericOptions {
@@ -103,6 +104,12 @@ impl GenericOptionsSearch for GenericOptions {
let op = ops.iter().find(|s| s.name == name)?; let op = ops.iter().find(|s| s.name == name)?;
op.value.clone() op.value.clone()
} }
/// Lookup option by name, returning ref
fn find_ref(&self, name: &str) -> Option<&GenericOption> {
let ops = self.as_ref()?;
ops.iter().find(|s| s.name == name)
}
} }
pub trait RoleExt { pub trait RoleExt {
+110 -19
View File
@@ -1,45 +1,121 @@
use std::fs::File;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use postgres::config::Config; use postgres::config::Config;
use postgres::{Client, NoTls}; use postgres::{Client, NoTls};
use tracing::{info, info_span, instrument, span_enabled, warn, Level}; use reqwest::StatusCode;
use tracing::{error, info, info_span, instrument, span_enabled, warn, Level};
use crate::config; use crate::config;
use crate::params::PG_HBA_ALL_MD5; use crate::params::PG_HBA_ALL_MD5;
use crate::pg_helpers::*; use crate::pg_helpers::*;
use compute_api::responses::ControlPlaneSpecResponse; use compute_api::responses::{ControlPlaneComputeStatus, ControlPlaneSpecResponse};
use compute_api::spec::{ComputeSpec, Database, PgIdent, Role}; use compute_api::spec::{ComputeSpec, Database, PgIdent, Role};
// Do control plane request and return response if any. In case of error it
// returns a bool flag indicating whether it makes sense to retry the request
// and a string with error message.
fn do_control_plane_request(
uri: &str,
jwt: &str,
) -> Result<ControlPlaneSpecResponse, (bool, String)> {
let resp = reqwest::blocking::Client::new()
.get(uri)
.header("Authorization", jwt)
.send()
.map_err(|e| {
(
true,
format!("could not perform spec request to control plane: {}", e),
)
})?;
match resp.status() {
StatusCode::OK => match resp.json::<ControlPlaneSpecResponse>() {
Ok(spec_resp) => Ok(spec_resp),
Err(e) => Err((
true,
format!("could not deserialize control plane response: {}", e),
)),
},
StatusCode::SERVICE_UNAVAILABLE => {
Err((true, "control plane is temporarily unavailable".to_string()))
}
StatusCode::BAD_GATEWAY => {
// We have a problem with intermittent 502 errors now
// https://github.com/neondatabase/cloud/issues/2353
// It's fine to retry GET request in this case.
Err((true, "control plane request failed with 502".to_string()))
}
// Another code, likely 500 or 404, means that compute is unknown to the control plane
// or some internal failure happened. Doesn't make much sense to retry in this case.
_ => Err((
false,
format!(
"unexpected control plane response status code: {}",
resp.status()
),
)),
}
}
/// Request spec from the control-plane by compute_id. If `NEON_CONSOLE_JWT` /// Request spec from the control-plane by compute_id. If `NEON_CONSOLE_JWT`
/// env variable is set, it will be used for authorization. /// env variable is set, it will be used for authorization.
pub fn get_spec_from_control_plane(base_uri: &str, compute_id: &str) -> Result<ComputeSpec> { pub fn get_spec_from_control_plane(
base_uri: &str,
compute_id: &str,
) -> Result<Option<ComputeSpec>> {
let cp_uri = format!("{base_uri}/management/api/v2/computes/{compute_id}/spec"); let cp_uri = format!("{base_uri}/management/api/v2/computes/{compute_id}/spec");
let jwt: String = match std::env::var("NEON_CONSOLE_JWT") { let jwt: String = match std::env::var("NEON_CONTROL_PLANE_TOKEN") {
Ok(v) => v, Ok(v) => v,
Err(_) => "".to_string(), Err(_) => "".to_string(),
}; };
let mut attempt = 1;
let mut spec: Result<Option<ComputeSpec>> = Ok(None);
info!("getting spec from control plane: {}", cp_uri); info!("getting spec from control plane: {}", cp_uri);
// TODO: check the response. We should distinguish cases when it's // Do 3 attempts to get spec from the control plane using the following logic:
// - network error, then retry // - network error -> then retry
// - no spec for compute yet, then wait // - compute id is unknown or any other error -> bail out
// - compute id is unknown or any other error, then bail out // - no spec for compute yet (Empty state) -> return Ok(None)
let resp: ControlPlaneSpecResponse = reqwest::blocking::Client::new() // - got spec -> return Ok(Some(spec))
.get(cp_uri) while attempt < 4 {
.header("Authorization", jwt) spec = match do_control_plane_request(&cp_uri, &jwt) {
.send() Ok(spec_resp) => match spec_resp.status {
.map_err(|e| anyhow!("could not send spec request to control plane: {}", e))? ControlPlaneComputeStatus::Empty => Ok(None),
.json() ControlPlaneComputeStatus::Attached => {
.map_err(|e| anyhow!("could not get compute spec from control plane: {}", e))?; if let Some(spec) = spec_resp.spec {
Ok(Some(spec))
} else {
bail!("compute is attached, but spec is empty")
}
}
},
Err((retry, msg)) => {
if retry {
Err(anyhow!(msg))
} else {
bail!(msg);
}
}
};
if let Some(spec) = resp.spec { if let Err(e) = &spec {
Ok(spec) error!("attempt {} to get spec failed with: {}", attempt, e);
} else { } else {
bail!("could not get compute spec from control plane") return spec;
}
attempt += 1;
std::thread::sleep(std::time::Duration::from_millis(100));
} }
// All attempts failed, return error.
spec
} }
/// It takes cluster specification and does the following: /// It takes cluster specification and does the following:
@@ -70,6 +146,21 @@ pub fn update_pg_hba(pgdata_path: &Path) -> Result<()> {
Ok(()) Ok(())
} }
/// Create a standby.signal file
pub fn add_standby_signal(pgdata_path: &Path) -> Result<()> {
// XXX: consider making it a part of spec.json
info!("adding standby.signal");
let signalfile = pgdata_path.join("standby.signal");
if !signalfile.exists() {
info!("created standby.signal");
File::create(signalfile)?;
} else {
info!("reused pre-existing standby.signal");
}
Ok(())
}
/// Given a cluster spec json and open transaction it handles roles creation, /// Given a cluster spec json and open transaction it handles roles creation,
/// deletion and update. /// deletion and update.
#[instrument(skip_all)] #[instrument(skip_all)]
+68 -11
View File
@@ -8,6 +8,7 @@
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use control_plane::endpoint::ComputeControlPlane; use control_plane::endpoint::ComputeControlPlane;
use control_plane::endpoint::Replication;
use control_plane::local_env::LocalEnv; use control_plane::local_env::LocalEnv;
use control_plane::pageserver::PageServerNode; use control_plane::pageserver::PageServerNode;
use control_plane::safekeeper::SafekeeperNode; use control_plane::safekeeper::SafekeeperNode;
@@ -474,7 +475,14 @@ fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) -
env.register_branch_mapping(name.to_string(), tenant_id, timeline_id)?; env.register_branch_mapping(name.to_string(), tenant_id, timeline_id)?;
println!("Creating endpoint for imported timeline ..."); println!("Creating endpoint for imported timeline ...");
cplane.new_endpoint(tenant_id, name, timeline_id, None, None, pg_version)?; cplane.new_endpoint(
tenant_id,
name,
timeline_id,
None,
pg_version,
Replication::Primary,
)?;
println!("Done"); println!("Done");
} }
Some(("branch", branch_match)) => { Some(("branch", branch_match)) => {
@@ -560,20 +568,20 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
.iter() .iter()
.filter(|(_, endpoint)| endpoint.tenant_id == tenant_id) .filter(|(_, endpoint)| endpoint.tenant_id == tenant_id)
{ {
let lsn_str = match endpoint.lsn { let lsn_str = match endpoint.replication {
None => { Replication::Static(lsn) => {
// -> primary endpoint // -> read-only endpoint
// Use the node's LSN.
lsn.to_string()
}
_ => {
// -> primary endpoint or hot replica
// Use the LSN at the end of the timeline. // Use the LSN at the end of the timeline.
timeline_infos timeline_infos
.get(&endpoint.timeline_id) .get(&endpoint.timeline_id)
.map(|bi| bi.last_record_lsn.to_string()) .map(|bi| bi.last_record_lsn.to_string())
.unwrap_or_else(|| "?".to_string()) .unwrap_or_else(|| "?".to_string())
} }
Some(lsn) => {
// -> read-only endpoint
// Use the endpoint's LSN.
lsn.to_string()
}
}; };
let branch_name = timeline_name_mappings let branch_name = timeline_name_mappings
@@ -619,7 +627,26 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
.copied() .copied()
.context("Failed to parse postgres version from the argument string")?; .context("Failed to parse postgres version from the argument string")?;
cplane.new_endpoint(tenant_id, &endpoint_id, timeline_id, lsn, port, pg_version)?; let hot_standby = sub_args
.get_one::<bool>("hot-standby")
.copied()
.unwrap_or(false);
let replication = match (lsn, hot_standby) {
(Some(lsn), false) => Replication::Static(lsn),
(None, true) => Replication::Replica,
(None, false) => Replication::Primary,
(Some(_), true) => anyhow::bail!("cannot specify both lsn and hot-standby"),
};
cplane.new_endpoint(
tenant_id,
&endpoint_id,
timeline_id,
port,
pg_version,
replication,
)?;
} }
"start" => { "start" => {
let port: Option<u16> = sub_args.get_one::<u16>("port").copied(); let port: Option<u16> = sub_args.get_one::<u16>("port").copied();
@@ -637,7 +664,21 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
None None
}; };
let hot_standby = sub_args
.get_one::<bool>("hot-standby")
.copied()
.unwrap_or(false);
if let Some(endpoint) = endpoint { if let Some(endpoint) = endpoint {
match (&endpoint.replication, hot_standby) {
(Replication::Static(_), true) => {
bail!("Cannot start a node in hot standby mode when it is already configured as a static replica")
}
(Replication::Primary, true) => {
bail!("Cannot start a node as a hot standby replica, it is already configured as primary node")
}
_ => {}
}
println!("Starting existing endpoint {endpoint_id}..."); println!("Starting existing endpoint {endpoint_id}...");
endpoint.start(&auth_token)?; endpoint.start(&auth_token)?;
} else { } else {
@@ -659,6 +700,14 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
.get_one::<u32>("pg-version") .get_one::<u32>("pg-version")
.copied() .copied()
.context("Failed to `pg-version` from the argument string")?; .context("Failed to `pg-version` from the argument string")?;
let replication = match (lsn, hot_standby) {
(Some(lsn), false) => Replication::Static(lsn),
(None, true) => Replication::Replica,
(None, false) => Replication::Primary,
(Some(_), true) => anyhow::bail!("cannot specify both lsn and hot-standby"),
};
// when used with custom port this results in non obvious behaviour // when used with custom port this results in non obvious behaviour
// port is remembered from first start command, i e // port is remembered from first start command, i e
// start --port X // start --port X
@@ -670,9 +719,9 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
tenant_id, tenant_id,
endpoint_id, endpoint_id,
timeline_id, timeline_id,
lsn,
port, port,
pg_version, pg_version,
replication,
)?; )?;
ep.start(&auth_token)?; ep.start(&auth_token)?;
} }
@@ -928,6 +977,12 @@ fn cli() -> Command {
.help("Specify Lsn on the timeline to start from. By default, end of the timeline would be used.") .help("Specify Lsn on the timeline to start from. By default, end of the timeline would be used.")
.required(false); .required(false);
let hot_standby_arg = Arg::new("hot-standby")
.value_parser(value_parser!(bool))
.long("hot-standby")
.help("If set, the node will be a hot replica on the specified timeline")
.required(false);
Command::new("Neon CLI") Command::new("Neon CLI")
.arg_required_else_help(true) .arg_required_else_help(true)
.version(GIT_VERSION) .version(GIT_VERSION)
@@ -1052,6 +1107,7 @@ fn cli() -> Command {
.long("config-only") .long("config-only")
.required(false)) .required(false))
.arg(pg_version_arg.clone()) .arg(pg_version_arg.clone())
.arg(hot_standby_arg.clone())
) )
.subcommand(Command::new("start") .subcommand(Command::new("start")
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.") .about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
@@ -1062,6 +1118,7 @@ fn cli() -> Command {
.arg(lsn_arg) .arg(lsn_arg)
.arg(port_arg) .arg(port_arg)
.arg(pg_version_arg) .arg(pg_version_arg)
.arg(hot_standby_arg)
) )
.subcommand( .subcommand(
Command::new("stop") Command::new("stop")
+121 -61
View File
@@ -68,18 +68,19 @@ impl ComputeControlPlane {
tenant_id: TenantId, tenant_id: TenantId,
name: &str, name: &str,
timeline_id: TimelineId, timeline_id: TimelineId,
lsn: Option<Lsn>,
port: Option<u16>, port: Option<u16>,
pg_version: u32, pg_version: u32,
replication: Replication,
) -> Result<Arc<Endpoint>> { ) -> Result<Arc<Endpoint>> {
let port = port.unwrap_or_else(|| self.get_port()); let port = port.unwrap_or_else(|| self.get_port());
let ep = Arc::new(Endpoint { let ep = Arc::new(Endpoint {
name: name.to_owned(), name: name.to_owned(),
address: SocketAddr::new("127.0.0.1".parse().unwrap(), port), address: SocketAddr::new("127.0.0.1".parse().unwrap(), port),
env: self.env.clone(), env: self.env.clone(),
pageserver: Arc::clone(&self.pageserver), pageserver: Arc::clone(&self.pageserver),
timeline_id, timeline_id,
lsn, replication,
tenant_id, tenant_id,
pg_version, pg_version,
}); });
@@ -95,6 +96,18 @@ impl ComputeControlPlane {
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Replication {
// Regular read-write node
Primary,
// if recovery_target_lsn is provided, and we want to pin the node to a specific LSN
Static(Lsn),
// Hot standby; read-only replica.
// Future versions may want to distinguish between replicas with hot standby
// feedback and other kinds of replication configurations.
Replica,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Endpoint { pub struct Endpoint {
/// used as the directory name /// used as the directory name
@@ -102,7 +115,7 @@ pub struct Endpoint {
pub tenant_id: TenantId, pub tenant_id: TenantId,
pub timeline_id: TimelineId, pub timeline_id: TimelineId,
// Some(lsn) if this is a read-only endpoint anchored at 'lsn'. None for the primary. // Some(lsn) if this is a read-only endpoint anchored at 'lsn'. None for the primary.
pub lsn: Option<Lsn>, pub replication: Replication,
// port and address of the Postgres server // port and address of the Postgres server
pub address: SocketAddr, pub address: SocketAddr,
@@ -153,9 +166,17 @@ impl Endpoint {
fs::read_to_string(pg_version_path).unwrap_or_else(|_| DEFAULT_PG_VERSION.to_string()); fs::read_to_string(pg_version_path).unwrap_or_else(|_| DEFAULT_PG_VERSION.to_string());
let pg_version = u32::from_str(&pg_version_str)?; let pg_version = u32::from_str(&pg_version_str)?;
// parse recovery_target_lsn, if any // parse recovery_target_lsn and primary_conninfo into Recovery Target, if any
let recovery_target_lsn: Option<Lsn> = let replication = if let Some(lsn_str) = conf.get("recovery_target_lsn") {
conf.parse_field_optional("recovery_target_lsn", &context)?; Replication::Static(Lsn::from_str(lsn_str)?)
} else if let Some(slot_name) = conf.get("primary_slot_name") {
let slot_name = slot_name.to_string();
let prefix = format!("repl_{}_", timeline_id);
assert!(slot_name.starts_with(&prefix));
Replication::Replica
} else {
Replication::Primary
};
// ok now // ok now
Ok(Endpoint { Ok(Endpoint {
@@ -164,7 +185,7 @@ impl Endpoint {
env: env.clone(), env: env.clone(),
pageserver: Arc::clone(pageserver), pageserver: Arc::clone(pageserver),
timeline_id, timeline_id,
lsn: recovery_target_lsn, replication,
tenant_id, tenant_id,
pg_version, pg_version,
}) })
@@ -299,50 +320,83 @@ impl Endpoint {
conf.append("neon.pageserver_connstring", &pageserver_connstr); conf.append("neon.pageserver_connstring", &pageserver_connstr);
conf.append("neon.tenant_id", &self.tenant_id.to_string()); conf.append("neon.tenant_id", &self.tenant_id.to_string());
conf.append("neon.timeline_id", &self.timeline_id.to_string()); conf.append("neon.timeline_id", &self.timeline_id.to_string());
if let Some(lsn) = self.lsn {
conf.append("recovery_target_lsn", &lsn.to_string());
}
conf.append_line(""); conf.append_line("");
// Configure backpressure // Replication-related configurations, such as WAL sending
// - Replication write lag depends on how fast the walreceiver can process incoming WAL. match &self.replication {
// This lag determines latency of get_page_at_lsn. Speed of applying WAL is about 10MB/sec, Replication::Primary => {
// so to avoid expiration of 1 minute timeout, this lag should not be larger than 600MB. // Configure backpressure
// Actually latency should be much smaller (better if < 1sec). But we assume that recently // - Replication write lag depends on how fast the walreceiver can process incoming WAL.
// updates pages are not requested from pageserver. // This lag determines latency of get_page_at_lsn. Speed of applying WAL is about 10MB/sec,
// - Replication flush lag depends on speed of persisting data by checkpointer (creation of // so to avoid expiration of 1 minute timeout, this lag should not be larger than 600MB.
// delta/image layers) and advancing disk_consistent_lsn. Safekeepers are able to // Actually latency should be much smaller (better if < 1sec). But we assume that recently
// remove/archive WAL only beyond disk_consistent_lsn. Too large a lag can cause long // updates pages are not requested from pageserver.
// recovery time (in case of pageserver crash) and disk space overflow at safekeepers. // - Replication flush lag depends on speed of persisting data by checkpointer (creation of
// - Replication apply lag depends on speed of uploading changes to S3 by uploader thread. // delta/image layers) and advancing disk_consistent_lsn. Safekeepers are able to
// To be able to restore database in case of pageserver node crash, safekeeper should not // remove/archive WAL only beyond disk_consistent_lsn. Too large a lag can cause long
// remove WAL beyond this point. Too large lag can cause space exhaustion in safekeepers // recovery time (in case of pageserver crash) and disk space overflow at safekeepers.
// (if they are not able to upload WAL to S3). // - Replication apply lag depends on speed of uploading changes to S3 by uploader thread.
conf.append("max_replication_write_lag", "15MB"); // To be able to restore database in case of pageserver node crash, safekeeper should not
conf.append("max_replication_flush_lag", "10GB"); // remove WAL beyond this point. Too large lag can cause space exhaustion in safekeepers
// (if they are not able to upload WAL to S3).
conf.append("max_replication_write_lag", "15MB");
conf.append("max_replication_flush_lag", "10GB");
if !self.env.safekeepers.is_empty() { if !self.env.safekeepers.is_empty() {
// Configure Postgres to connect to the safekeepers // Configure Postgres to connect to the safekeepers
conf.append("synchronous_standby_names", "walproposer"); conf.append("synchronous_standby_names", "walproposer");
let safekeepers = self let safekeepers = self
.env .env
.safekeepers .safekeepers
.iter() .iter()
.map(|sk| format!("localhost:{}", sk.pg_port)) .map(|sk| format!("localhost:{}", sk.pg_port))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(","); .join(",");
conf.append("neon.safekeepers", &safekeepers); conf.append("neon.safekeepers", &safekeepers);
} else { } else {
// We only use setup without safekeepers for tests, // We only use setup without safekeepers for tests,
// and don't care about data durability on pageserver, // and don't care about data durability on pageserver,
// so set more relaxed synchronous_commit. // so set more relaxed synchronous_commit.
conf.append("synchronous_commit", "remote_write"); conf.append("synchronous_commit", "remote_write");
// Configure the node to stream WAL directly to the pageserver // Configure the node to stream WAL directly to the pageserver
// This isn't really a supported configuration, but can be useful for // This isn't really a supported configuration, but can be useful for
// testing. // testing.
conf.append("synchronous_standby_names", "pageserver"); conf.append("synchronous_standby_names", "pageserver");
}
}
Replication::Static(lsn) => {
conf.append("recovery_target_lsn", &lsn.to_string());
}
Replication::Replica => {
assert!(!self.env.safekeepers.is_empty());
// TODO: use future host field from safekeeper spec
// Pass the list of safekeepers to the replica so that it can connect to any of them,
// whichever is availiable.
let sk_ports = self
.env
.safekeepers
.iter()
.map(|x| x.pg_port.to_string())
.collect::<Vec<_>>()
.join(",");
let sk_hosts = vec!["localhost"; self.env.safekeepers.len()].join(",");
let connstr = format!(
"host={} port={} options='-c timeline_id={} tenant_id={}' application_name=replica replication=true",
sk_hosts,
sk_ports,
&self.timeline_id.to_string(),
&self.tenant_id.to_string(),
);
let slot_name = format!("repl_{}_", self.timeline_id);
conf.append("primary_conninfo", connstr.as_str());
conf.append("primary_slot_name", slot_name.as_str());
conf.append("hot_standby", "on");
}
} }
let mut file = File::create(self.pgdata().join("postgresql.conf"))?; let mut file = File::create(self.pgdata().join("postgresql.conf"))?;
@@ -355,21 +409,27 @@ impl Endpoint {
} }
fn load_basebackup(&self, auth_token: &Option<String>) -> Result<()> { fn load_basebackup(&self, auth_token: &Option<String>) -> Result<()> {
let backup_lsn = if let Some(lsn) = self.lsn { let backup_lsn = match &self.replication {
Some(lsn) Replication::Primary => {
} else if !self.env.safekeepers.is_empty() { if !self.env.safekeepers.is_empty() {
// LSN 0 means that it is bootstrap and we need to download just // LSN 0 means that it is bootstrap and we need to download just
// latest data from the pageserver. That is a bit clumsy but whole bootstrap // latest data from the pageserver. That is a bit clumsy but whole bootstrap
// procedure evolves quite actively right now, so let's think about it again // procedure evolves quite actively right now, so let's think about it again
// when things would be more stable (TODO). // when things would be more stable (TODO).
let lsn = self.sync_safekeepers(auth_token, self.pg_version)?; let lsn = self.sync_safekeepers(auth_token, self.pg_version)?;
if lsn == Lsn(0) { if lsn == Lsn(0) {
None None
} else { } else {
Some(lsn) Some(lsn)
}
} else {
None
}
}
Replication::Static(lsn) => Some(*lsn),
Replication::Replica => {
None // Take the latest snapshot available to start with
} }
} else {
None
}; };
self.do_basebackup(backup_lsn)?; self.do_basebackup(backup_lsn)?;
@@ -466,7 +526,7 @@ impl Endpoint {
// 3. Load basebackup // 3. Load basebackup
self.load_basebackup(auth_token)?; self.load_basebackup(auth_token)?;
if self.lsn.is_some() { if self.replication != Replication::Primary {
File::create(self.pgdata().join("standby.signal"))?; File::create(self.pgdata().join("standby.signal"))?;
} }
+2 -2
View File
@@ -359,8 +359,8 @@ impl PageServerNode {
.transpose() .transpose()
.context("Failed to parse 'trace_read_requests' as bool")?, .context("Failed to parse 'trace_read_requests' as bool")?,
eviction_policy: settings eviction_policy: settings
.get("eviction_policy") .remove("eviction_policy")
.map(|x| serde_json::from_str(x)) .map(serde_json::from_str)
.transpose() .transpose()
.context("Failed to parse 'eviction_policy' json")?, .context("Failed to parse 'eviction_policy' json")?,
min_resident_size_override: settings min_resident_size_override: settings
+1 -1
View File
@@ -13,7 +13,7 @@ use std::io::BufRead;
use std::str::FromStr; use std::str::FromStr;
/// In-memory representation of a postgresql.conf file /// In-memory representation of a postgresql.conf file
#[derive(Default)] #[derive(Default, Debug)]
pub struct PostgresConf { pub struct PostgresConf {
lines: Vec<String>, lines: Vec<String>,
hash: HashMap<String, String>, hash: HashMap<String, String>,
@@ -28,11 +28,6 @@
"value": "replica", "value": "replica",
"vartype": "enum" "vartype": "enum"
}, },
{
"name": "hot_standby",
"value": "on",
"vartype": "bool"
},
{ {
"name": "wal_log_hints", "name": "wal_log_hints",
"value": "on", "value": "on",
+14
View File
@@ -14,6 +14,7 @@ pub struct GenericAPIError {
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct ComputeStatusResponse { pub struct ComputeStatusResponse {
pub start_time: DateTime<Utc>,
pub tenant: Option<String>, pub tenant: Option<String>,
pub timeline: Option<String>, pub timeline: Option<String>,
pub status: ComputeStatus, pub status: ComputeStatus,
@@ -63,6 +64,7 @@ where
/// Response of the /metrics.json API /// Response of the /metrics.json API
#[derive(Clone, Debug, Default, Serialize)] #[derive(Clone, Debug, Default, Serialize)]
pub struct ComputeMetrics { pub struct ComputeMetrics {
pub wait_for_spec_ms: u64,
pub sync_safekeepers_ms: u64, pub sync_safekeepers_ms: u64,
pub basebackup_ms: u64, pub basebackup_ms: u64,
pub config_ms: u64, pub config_ms: u64,
@@ -75,4 +77,16 @@ pub struct ComputeMetrics {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct ControlPlaneSpecResponse { pub struct ControlPlaneSpecResponse {
pub spec: Option<ComputeSpec>, pub spec: Option<ComputeSpec>,
pub status: ControlPlaneComputeStatus,
}
#[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ControlPlaneComputeStatus {
// Compute is known to control-plane, but it's not
// yet attached to any timeline / endpoint.
Empty,
// Compute is attached to some timeline / endpoint and
// should be able to start with provided spec.
Attached,
} }
+5 -2
View File
@@ -95,10 +95,13 @@ pub fn generate_wal_segment(
segno: u64, segno: u64,
system_id: u64, system_id: u64,
pg_version: u32, pg_version: u32,
lsn: Lsn,
) -> Result<Bytes, SerializeError> { ) -> Result<Bytes, SerializeError> {
assert_eq!(segno, lsn.segment_number(WAL_SEGMENT_SIZE));
match pg_version { match pg_version {
14 => v14::xlog_utils::generate_wal_segment(segno, system_id), 14 => v14::xlog_utils::generate_wal_segment(segno, system_id, lsn),
15 => v15::xlog_utils::generate_wal_segment(segno, system_id), 15 => v15::xlog_utils::generate_wal_segment(segno, system_id, lsn),
_ => Err(SerializeError::BadInput), _ => Err(SerializeError::BadInput),
} }
} }
+1
View File
@@ -195,6 +195,7 @@ pub const FIRST_NORMAL_OBJECT_ID: u32 = 16384;
pub const XLOG_CHECKPOINT_SHUTDOWN: u8 = 0x00; pub const XLOG_CHECKPOINT_SHUTDOWN: u8 = 0x00;
pub const XLOG_CHECKPOINT_ONLINE: u8 = 0x10; pub const XLOG_CHECKPOINT_ONLINE: u8 = 0x10;
pub const XLP_FIRST_IS_CONTRECORD: u16 = 0x0001;
pub const XLP_LONG_HEADER: u16 = 0x0002; pub const XLP_LONG_HEADER: u16 = 0x0002;
/* From fsm_internals.h */ /* From fsm_internals.h */
+49 -7
View File
@@ -270,6 +270,11 @@ impl XLogPageHeaderData {
use utils::bin_ser::LeSer; use utils::bin_ser::LeSer;
XLogPageHeaderData::des_from(&mut buf.reader()) XLogPageHeaderData::des_from(&mut buf.reader())
} }
pub fn encode(&self) -> Result<Bytes, SerializeError> {
use utils::bin_ser::LeSer;
self.ser().map(|b| b.into())
}
} }
impl XLogLongPageHeaderData { impl XLogLongPageHeaderData {
@@ -328,22 +333,32 @@ impl CheckPoint {
} }
} }
// /// Generate new, empty WAL segment, with correct block headers at the first
// Generate new, empty WAL segment. /// page of the segment and the page that contains the given LSN.
// We need this segment to start compute node. /// We need this segment to start compute node.
// pub fn generate_wal_segment(segno: u64, system_id: u64, lsn: Lsn) -> Result<Bytes, SerializeError> {
pub fn generate_wal_segment(segno: u64, system_id: u64) -> Result<Bytes, SerializeError> {
let mut seg_buf = BytesMut::with_capacity(WAL_SEGMENT_SIZE); let mut seg_buf = BytesMut::with_capacity(WAL_SEGMENT_SIZE);
let pageaddr = XLogSegNoOffsetToRecPtr(segno, 0, WAL_SEGMENT_SIZE); let pageaddr = XLogSegNoOffsetToRecPtr(segno, 0, WAL_SEGMENT_SIZE);
let page_off = lsn.block_offset();
let seg_off = lsn.segment_offset(WAL_SEGMENT_SIZE);
let first_page_only = seg_off < XLOG_BLCKSZ;
let (shdr_rem_len, infoflags) = if first_page_only {
(seg_off, pg_constants::XLP_FIRST_IS_CONTRECORD)
} else {
(0, 0)
};
let hdr = XLogLongPageHeaderData { let hdr = XLogLongPageHeaderData {
std: { std: {
XLogPageHeaderData { XLogPageHeaderData {
xlp_magic: XLOG_PAGE_MAGIC as u16, xlp_magic: XLOG_PAGE_MAGIC as u16,
xlp_info: pg_constants::XLP_LONG_HEADER, xlp_info: pg_constants::XLP_LONG_HEADER | infoflags,
xlp_tli: PG_TLI, xlp_tli: PG_TLI,
xlp_pageaddr: pageaddr, xlp_pageaddr: pageaddr,
xlp_rem_len: 0, xlp_rem_len: shdr_rem_len as u32,
..Default::default() // Put 0 in padding fields. ..Default::default() // Put 0 in padding fields.
} }
}, },
@@ -357,6 +372,33 @@ pub fn generate_wal_segment(segno: u64, system_id: u64) -> Result<Bytes, Seriali
//zero out the rest of the file //zero out the rest of the file
seg_buf.resize(WAL_SEGMENT_SIZE, 0); seg_buf.resize(WAL_SEGMENT_SIZE, 0);
if !first_page_only {
let block_offset = lsn.page_offset_in_segment(WAL_SEGMENT_SIZE) as usize;
let header = XLogPageHeaderData {
xlp_magic: XLOG_PAGE_MAGIC as u16,
xlp_info: if page_off >= pg_constants::SIZE_OF_PAGE_HEADER as u64 {
pg_constants::XLP_FIRST_IS_CONTRECORD
} else {
0
},
xlp_tli: PG_TLI,
xlp_pageaddr: lsn.page_lsn().0,
xlp_rem_len: if page_off >= pg_constants::SIZE_OF_PAGE_HEADER as u64 {
page_off as u32
} else {
0u32
},
..Default::default() // Put 0 in padding fields.
};
let hdr_bytes = header.encode()?;
debug_assert!(seg_buf.len() > block_offset + hdr_bytes.len());
debug_assert_ne!(block_offset, 0);
seg_buf[block_offset..block_offset + hdr_bytes.len()].copy_from_slice(&hdr_bytes[..]);
}
Ok(seg_buf.freeze()) Ok(seg_buf.freeze())
} }
+2 -11
View File
@@ -6,9 +6,8 @@ use postgres::Client;
use postgres_ffi::{WAL_SEGMENT_SIZE, XLOG_BLCKSZ}; use postgres_ffi::{WAL_SEGMENT_SIZE, XLOG_BLCKSZ};
use postgres_ffi::{XLOG_SIZE_OF_XLOG_RECORD, XLOG_SIZE_OF_XLOG_SHORT_PHD}; use postgres_ffi::{XLOG_SIZE_OF_XLOG_RECORD, XLOG_SIZE_OF_XLOG_SHORT_PHD};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::Command;
use std::time::Instant; use std::time::Instant;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
@@ -95,12 +94,6 @@ impl Conf {
pub fn start_server(&self) -> Result<PostgresServer> { pub fn start_server(&self) -> Result<PostgresServer> {
info!("Starting Postgres server in {:?}", self.datadir); info!("Starting Postgres server in {:?}", self.datadir);
let log_file = fs::File::create(self.datadir.join("pg.log")).with_context(|| {
format!(
"Failed to create pg.log file in directory {}",
self.datadir.display()
)
})?;
let unix_socket_dir = tempdir()?; // We need a directory with a short name for Unix socket (up to 108 symbols) let unix_socket_dir = tempdir()?; // We need a directory with a short name for Unix socket (up to 108 symbols)
let unix_socket_dir_path = unix_socket_dir.path().to_owned(); let unix_socket_dir_path = unix_socket_dir.path().to_owned();
let server_process = self let server_process = self
@@ -110,9 +103,7 @@ impl Conf {
.arg(unix_socket_dir_path.as_os_str()) .arg(unix_socket_dir_path.as_os_str())
.arg("-D") .arg("-D")
.arg(self.datadir.as_os_str()) .arg(self.datadir.as_os_str())
.args(["-c", "logging_collector=on"]) // stderr will mess up with tests output
.args(REQUIRED_POSTGRES_CONFIG.iter().flat_map(|cfg| ["-c", cfg])) .args(REQUIRED_POSTGRES_CONFIG.iter().flat_map(|cfg| ["-c", cfg]))
.stderr(Stdio::from(log_file))
.spawn()?; .spawn()?;
let server = PostgresServer { let server = PostgresServer {
process: server_process, process: server_process,
@@ -121,7 +112,7 @@ impl Conf {
let mut c = postgres::Config::new(); let mut c = postgres::Config::new();
c.host_path(&unix_socket_dir_path); c.host_path(&unix_socket_dir_path);
c.user("postgres"); c.user("postgres");
c.connect_timeout(Duration::from_millis(1000)); c.connect_timeout(Duration::from_millis(10000));
c c
}, },
}; };
-1
View File
@@ -10,7 +10,6 @@ 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
serde.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
thiserror.workspace = true thiserror.workspace = true
+5 -175
View File
@@ -6,15 +6,10 @@ pub mod framed;
use byteorder::{BigEndian, ReadBytesExt}; use byteorder::{BigEndian, ReadBytesExt};
use bytes::{Buf, BufMut, Bytes, BytesMut}; use bytes::{Buf, BufMut, Bytes, BytesMut};
use postgres_protocol::PG_EPOCH; use std::{borrow::Cow, collections::HashMap, fmt, io, str};
use serde::{Deserialize, Serialize};
use std::{ // re-export for use in utils pageserver_feedback.rs
borrow::Cow, pub use postgres_protocol::PG_EPOCH;
collections::HashMap,
fmt, io, str,
time::{Duration, SystemTime},
};
use tracing::{trace, warn};
pub type Oid = u32; pub type Oid = u32;
pub type SystemId = u64; pub type SystemId = u64;
@@ -664,7 +659,7 @@ fn write_cstr(s: impl AsRef<[u8]>, buf: &mut BytesMut) -> Result<(), ProtocolErr
} }
/// Read cstring from buf, advancing it. /// Read cstring from buf, advancing it.
fn read_cstr(buf: &mut Bytes) -> Result<Bytes, ProtocolError> { pub fn read_cstr(buf: &mut Bytes) -> Result<Bytes, ProtocolError> {
let pos = buf let pos = buf
.iter() .iter()
.position(|x| *x == 0) .position(|x| *x == 0)
@@ -939,175 +934,10 @@ impl<'a> BeMessage<'a> {
} }
} }
/// Feedback pageserver sends to safekeeper and safekeeper resends to compute.
/// Serialized in custom flexible key/value format. In replication protocol, it
/// is marked with NEON_STATUS_UPDATE_TAG_BYTE to differentiate from postgres
/// Standby status update / Hot standby feedback messages.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PageserverFeedback {
/// Last known size of the timeline. Used to enforce timeline size limit.
pub current_timeline_size: u64,
/// LSN last received and ingested by the pageserver.
pub last_received_lsn: u64,
/// LSN up to which data is persisted by the pageserver to its local disc.
pub disk_consistent_lsn: u64,
/// LSN up to which data is persisted by the pageserver on s3; safekeepers
/// consider WAL before it can be removed.
pub remote_consistent_lsn: u64,
pub replytime: SystemTime,
}
// NOTE: Do not forget to increment this number when adding new fields to PageserverFeedback.
// Do not remove previously available fields because this might be backwards incompatible.
pub const PAGESERVER_FEEDBACK_FIELDS_NUMBER: u8 = 5;
impl PageserverFeedback {
pub fn empty() -> PageserverFeedback {
PageserverFeedback {
current_timeline_size: 0,
last_received_lsn: 0,
remote_consistent_lsn: 0,
disk_consistent_lsn: 0,
replytime: SystemTime::now(),
}
}
// Serialize PageserverFeedback using custom format
// to support protocol extensibility.
//
// Following layout is used:
// char - number of key-value pairs that follow.
//
// key-value pairs:
// null-terminated string - key,
// uint32 - value length in bytes
// value itself
//
// TODO: change serialized fields names once all computes migrate to rename.
pub fn serialize(&self, buf: &mut BytesMut) {
buf.put_u8(PAGESERVER_FEEDBACK_FIELDS_NUMBER); // # of keys
buf.put_slice(b"current_timeline_size\0");
buf.put_i32(8);
buf.put_u64(self.current_timeline_size);
buf.put_slice(b"ps_writelsn\0");
buf.put_i32(8);
buf.put_u64(self.last_received_lsn);
buf.put_slice(b"ps_flushlsn\0");
buf.put_i32(8);
buf.put_u64(self.disk_consistent_lsn);
buf.put_slice(b"ps_applylsn\0");
buf.put_i32(8);
buf.put_u64(self.remote_consistent_lsn);
let timestamp = self
.replytime
.duration_since(*PG_EPOCH)
.expect("failed to serialize pg_replytime earlier than PG_EPOCH")
.as_micros() as i64;
buf.put_slice(b"ps_replytime\0");
buf.put_i32(8);
buf.put_i64(timestamp);
}
// Deserialize PageserverFeedback message
// TODO: change serialized fields names once all computes migrate to rename.
pub fn parse(mut buf: Bytes) -> PageserverFeedback {
let mut rf = PageserverFeedback::empty();
let nfields = buf.get_u8();
for _ in 0..nfields {
let key = read_cstr(&mut buf).unwrap();
match key.as_ref() {
b"current_timeline_size" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.current_timeline_size = buf.get_u64();
}
b"ps_writelsn" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.last_received_lsn = buf.get_u64();
}
b"ps_flushlsn" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.disk_consistent_lsn = buf.get_u64();
}
b"ps_applylsn" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.remote_consistent_lsn = buf.get_u64();
}
b"ps_replytime" => {
let len = buf.get_i32();
assert_eq!(len, 8);
let raw_time = buf.get_i64();
if raw_time > 0 {
rf.replytime = *PG_EPOCH + Duration::from_micros(raw_time as u64);
} else {
rf.replytime = *PG_EPOCH - Duration::from_micros(-raw_time as u64);
}
}
_ => {
let len = buf.get_i32();
warn!(
"PageserverFeedback parse. unknown key {} of len {len}. Skip it.",
String::from_utf8_lossy(key.as_ref())
);
buf.advance(len as usize);
}
}
}
trace!("PageserverFeedback parsed is {:?}", rf);
rf
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_replication_feedback_serialization() {
let mut rf = PageserverFeedback::empty();
// Fill rf with some values
rf.current_timeline_size = 12345678;
// Set rounded time to be able to compare it with deserialized value,
// because it is rounded up to microseconds during serialization.
rf.replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
let mut data = BytesMut::new();
rf.serialize(&mut data);
let rf_parsed = PageserverFeedback::parse(data.freeze());
assert_eq!(rf, rf_parsed);
}
#[test]
fn test_replication_feedback_unknown_key() {
let mut rf = PageserverFeedback::empty();
// Fill rf with some values
rf.current_timeline_size = 12345678;
// Set rounded time to be able to compare it with deserialized value,
// because it is rounded up to microseconds during serialization.
rf.replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
let mut data = BytesMut::new();
rf.serialize(&mut data);
// Add an extra field to the buffer and adjust number of keys
if let Some(first) = data.first_mut() {
*first = PAGESERVER_FEEDBACK_FIELDS_NUMBER + 1;
}
data.put_slice(b"new_field_one\0");
data.put_i32(8);
data.put_u64(42);
// Parse serialized data and check that new field is not parsed
let rf_parsed = PageserverFeedback::parse(data.freeze());
assert_eq!(rf, rf_parsed);
}
#[test] #[test]
fn test_startup_message_params_options_escaped() { fn test_startup_message_params_options_escaped() {
fn split_options(params: &StartupMessageParams) -> Vec<Cow<'_, str>> { fn split_options(params: &StartupMessageParams) -> Vec<Cow<'_, str>> {
@@ -99,7 +99,11 @@ struct S3WithTestBlobs {
#[async_trait::async_trait] #[async_trait::async_trait]
impl AsyncTestContext for MaybeEnabledS3 { impl AsyncTestContext for MaybeEnabledS3 {
async fn setup() -> Self { async fn setup() -> Self {
utils::logging::init(utils::logging::LogFormat::Test).expect("logging init failed"); utils::logging::init(
utils::logging::LogFormat::Test,
utils::logging::TracingErrorLayerEnablement::Disabled,
)
.expect("logging init failed");
if env::var(ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME).is_err() { if env::var(ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME).is_err() {
info!( info!(
"`{}` env variable is not set, skipping the test", "`{}` env variable is not set, skipping the test",
+4 -1
View File
@@ -11,6 +11,7 @@ async-trait.workspace = true
anyhow.workspace = true anyhow.workspace = true
bincode.workspace = true bincode.workspace = true
bytes.workspace = true bytes.workspace = true
chrono.workspace = true
heapless.workspace = true heapless.workspace = true
hex = { workspace = true, features = ["serde"] } hex = { workspace = true, features = ["serde"] }
hyper = { workspace = true, features = ["full"] } hyper = { workspace = true, features = ["full"] }
@@ -27,7 +28,8 @@ signal-hook.workspace = true
thiserror.workspace = true thiserror.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["json"] } tracing-error.workspace = true
tracing-subscriber = { workspace = true, features = ["json", "registry"] }
rand.workspace = true rand.workspace = true
serde_with.workspace = true serde_with.workspace = true
strum.workspace = true strum.workspace = true
@@ -35,6 +37,7 @@ strum_macros.workspace = true
url.workspace = true url.workspace = true
uuid.workspace = true uuid.workspace = true
pq_proto.workspace = true
metrics.workspace = true metrics.workspace = true
workspace_hack.workspace = true workspace_hack.workspace = true
+41 -39
View File
@@ -1,19 +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::Context;
use hyper::header::{HeaderName, AUTHORIZATION}; use hyper::header::{HeaderName, AUTHORIZATION};
use hyper::http::HeaderValue; use hyper::http::HeaderValue;
use hyper::Method; use hyper::Method;
use hyper::{header::CONTENT_TYPE, Body, Request, Response, Server}; use hyper::{header::CONTENT_TYPE, Body, Request, Response};
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::{Middleware, RequestInfo, Router, RouterBuilder};
use tokio::task::JoinError; use tokio::task::JoinError;
use tracing::{self, debug, info, info_span, warn, Instrument}; use tracing::{self, debug, info, info_span, warn, Instrument};
use std::future::Future; use std::future::Future;
use std::net::TcpListener;
use std::str::FromStr; use std::str::FromStr;
use super::error::ApiError; use super::error::ApiError;
@@ -76,6 +75,7 @@ where
let log_quietly = method == Method::GET; let log_quietly = method == Method::GET;
async move { async move {
let cancellation_guard = RequestCancelled::warn_when_dropped_without_responding();
if log_quietly { if log_quietly {
debug!("Handling request"); debug!("Handling request");
} else { } else {
@@ -87,7 +87,11 @@ where
// Usage of the error handler also means that we expect only the `ApiError` errors to be raised in this call. // 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. // Panics are not handled separately, there's a `tracing_panic_hook` from another module to do that globally.
match (self.0)(request).await { let res = (self.0)(request).await;
cancellation_guard.disarm();
match res {
Ok(response) => { Ok(response) => {
let response_status = response.status(); let response_status = response.status();
if log_quietly && response_status.is_success() { if log_quietly && response_status.is_success() {
@@ -105,6 +109,38 @@ where
} }
} }
/// Drop guard to WARN in case the request was dropped before completion.
struct RequestCancelled {
warn: Option<tracing::Span>,
}
impl RequestCancelled {
/// Create the drop guard using the [`tracing::Span::current`] as the span.
fn warn_when_dropped_without_responding() -> Self {
RequestCancelled {
warn: Some(tracing::Span::current()),
}
}
/// Consume the drop guard without logging anything.
fn disarm(mut self) {
self.warn = None;
}
}
impl Drop for RequestCancelled {
fn drop(&mut self) {
if let Some(span) = self.warn.take() {
// the span has all of the info already, but the outer `.instrument(span)` has already
// been dropped, so we need to manually re-enter it for this message.
//
// this is what the instrument would do before polling so it is fine.
let _g = span.entered();
warn!("request was dropped before completing");
}
}
}
async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> { async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
SERVE_METRICS_COUNT.inc(); SERVE_METRICS_COUNT.inc();
@@ -304,40 +340,6 @@ pub fn check_permission_with(
} }
} }
///
/// Start listening for HTTP requests on given socket.
///
/// 'shutdown_future' can be used to stop. If the Future becomes
/// ready, we stop listening for new requests, and the function returns.
///
pub fn serve_thread_main<S>(
router_builder: RouterBuilder<hyper::Body, ApiError>,
listener: TcpListener,
shutdown_future: S,
) -> anyhow::Result<()>
where
S: Future<Output = ()> + Send + Sync,
{
info!("Starting an HTTP endpoint at {}", listener.local_addr()?);
// Create a Service from the router above to handle incoming requests.
let service = RouterService::new(router_builder.build().map_err(|err| anyhow!(err))?).unwrap();
// Enter a single-threaded tokio runtime bound to the current thread
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let _guard = runtime.enter();
let server = Server::from_tcp(listener)?
.serve(service)
.with_graceful_shutdown(shutdown_future);
runtime.block_on(server)?;
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+1 -12
View File
@@ -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))
}
+20
View File
@@ -265,6 +265,26 @@ impl fmt::Display for TenantTimelineId {
} }
} }
impl FromStr for TenantTimelineId {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('/');
let tenant_id = parts
.next()
.ok_or_else(|| anyhow::anyhow!("TenantTimelineId must contain tenant_id"))?
.parse()?;
let timeline_id = parts
.next()
.ok_or_else(|| anyhow::anyhow!("TenantTimelineId must contain timeline_id"))?
.parse()?;
if parts.next().is_some() {
anyhow::bail!("TenantTimelineId must contain only tenant_id and timeline_id");
}
Ok(TenantTimelineId::new(tenant_id, timeline_id))
}
}
// Unique ID of a storage node (safekeeper or pageserver). Supposed to be issued // Unique ID of a storage node (safekeeper or pageserver). Supposed to be issued
// by the console. // by the console.
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
+4
View File
@@ -54,6 +54,10 @@ pub mod measured_stream;
pub mod serde_percent; pub mod serde_percent;
pub mod serde_regex; pub mod serde_regex;
pub mod pageserver_feedback;
pub mod tracing_span_assert;
/// 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 {
+102 -14
View File
@@ -1,6 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use anyhow::Context; use anyhow::Context;
use once_cell::sync::Lazy;
use strum_macros::{EnumString, EnumVariantNames}; use strum_macros::{EnumString, EnumVariantNames};
#[derive(EnumString, EnumVariantNames, Eq, PartialEq, Debug, Clone, Copy)] #[derive(EnumString, EnumVariantNames, Eq, PartialEq, Debug, Clone, Copy)]
@@ -23,24 +24,81 @@ impl LogFormat {
} }
} }
pub fn init(log_format: LogFormat) -> anyhow::Result<()> { static TRACING_EVENT_COUNT: Lazy<metrics::IntCounterVec> = Lazy::new(|| {
let default_filter_str = "info"; metrics::register_int_counter_vec!(
"libmetrics_tracing_event_count",
"Number of tracing events, by level",
&["level"]
)
.expect("failed to define metric")
});
struct TracingEventCountLayer(&'static metrics::IntCounterVec);
impl<S> tracing_subscriber::layer::Layer<S> for TracingEventCountLayer
where
S: tracing::Subscriber,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let level = event.metadata().level();
let level = match *level {
tracing::Level::ERROR => "error",
tracing::Level::WARN => "warn",
tracing::Level::INFO => "info",
tracing::Level::DEBUG => "debug",
tracing::Level::TRACE => "trace",
};
self.0.with_label_values(&[level]).inc();
}
}
/// Whether to add the `tracing_error` crate's `ErrorLayer`
/// to the global tracing subscriber.
///
pub enum TracingErrorLayerEnablement {
/// Do not add the `ErrorLayer`.
Disabled,
/// Add the `ErrorLayer` with the filter specified by RUST_LOG, defaulting to `info` if `RUST_LOG` is unset.
EnableWithRustLogFilter,
}
pub fn init(
log_format: LogFormat,
tracing_error_layer_enablement: TracingErrorLayerEnablement,
) -> anyhow::Result<()> {
// We fall back to printing all spans at info-level or above if // We fall back to printing all spans at info-level or above if
// the RUST_LOG environment variable is not set. // the RUST_LOG environment variable is not set.
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() let rust_log_env_filter = || {
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter_str)); tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
};
let base_logger = tracing_subscriber::fmt() // NB: the order of the with() calls does not matter.
.with_env_filter(env_filter) // See https://docs.rs/tracing-subscriber/0.3.16/tracing_subscriber/layer/index.html#per-layer-filtering
.with_target(false) use tracing_subscriber::prelude::*;
.with_ansi(atty::is(atty::Stream::Stdout)) let r = tracing_subscriber::registry();
.with_writer(std::io::stdout); let r = r.with({
let log_layer = tracing_subscriber::fmt::layer()
match log_format { .with_target(false)
LogFormat::Json => base_logger.json().init(), .with_ansi(atty::is(atty::Stream::Stdout))
LogFormat::Plain => base_logger.init(), .with_writer(std::io::stdout);
LogFormat::Test => base_logger.with_test_writer().init(), let log_layer = match log_format {
LogFormat::Json => log_layer.json().boxed(),
LogFormat::Plain => log_layer.boxed(),
LogFormat::Test => log_layer.with_test_writer().boxed(),
};
log_layer.with_filter(rust_log_env_filter())
});
let r = r.with(TracingEventCountLayer(&TRACING_EVENT_COUNT).with_filter(rust_log_env_filter()));
match tracing_error_layer_enablement {
TracingErrorLayerEnablement::EnableWithRustLogFilter => r
.with(tracing_error::ErrorLayer::default().with_filter(rust_log_env_filter()))
.init(),
TracingErrorLayerEnablement::Disabled => r.init(),
} }
Ok(()) Ok(())
@@ -157,3 +215,33 @@ impl std::fmt::Debug for PrettyLocation<'_, '_> {
<Self as std::fmt::Display>::fmt(self, f) <Self as std::fmt::Display>::fmt(self, f)
} }
} }
#[cfg(test)]
mod tests {
use metrics::{core::Opts, IntCounterVec};
use super::TracingEventCountLayer;
#[test]
fn tracing_event_count_metric() {
let counter_vec =
IntCounterVec::new(Opts::new("testmetric", "testhelp"), &["level"]).unwrap();
let counter_vec = Box::leak(Box::new(counter_vec)); // make it 'static
let layer = TracingEventCountLayer(counter_vec);
use tracing_subscriber::prelude::*;
tracing::subscriber::with_default(tracing_subscriber::registry().with(layer), || {
tracing::trace!("foo");
tracing::debug!("foo");
tracing::info!("foo");
tracing::warn!("foo");
tracing::error!("foo");
});
assert_eq!(counter_vec.with_label_values(&["trace"]).get(), 1);
assert_eq!(counter_vec.with_label_values(&["debug"]).get(), 1);
assert_eq!(counter_vec.with_label_values(&["info"]).get(), 1);
assert_eq!(counter_vec.with_label_values(&["warn"]).get(), 1);
assert_eq!(counter_vec.with_label_values(&["error"]).get(), 1);
}
}
+19
View File
@@ -62,29 +62,48 @@ impl Lsn {
} }
/// Compute the offset into a segment /// Compute the offset into a segment
#[inline]
pub fn segment_offset(self, seg_sz: usize) -> usize { pub fn segment_offset(self, seg_sz: usize) -> usize {
(self.0 % seg_sz as u64) as usize (self.0 % seg_sz as u64) as usize
} }
/// Compute LSN of the segment start. /// Compute LSN of the segment start.
#[inline]
pub fn segment_lsn(self, seg_sz: usize) -> Lsn { pub fn segment_lsn(self, seg_sz: usize) -> Lsn {
Lsn(self.0 - (self.0 % seg_sz as u64)) Lsn(self.0 - (self.0 % seg_sz as u64))
} }
/// Compute the segment number /// Compute the segment number
#[inline]
pub fn segment_number(self, seg_sz: usize) -> u64 { pub fn segment_number(self, seg_sz: usize) -> u64 {
self.0 / seg_sz as u64 self.0 / seg_sz as u64
} }
/// Compute the offset into a block /// Compute the offset into a block
#[inline]
pub fn block_offset(self) -> u64 { pub fn block_offset(self) -> u64 {
const BLCKSZ: u64 = XLOG_BLCKSZ as u64; const BLCKSZ: u64 = XLOG_BLCKSZ as u64;
self.0 % BLCKSZ self.0 % BLCKSZ
} }
/// Compute the block offset of the first byte of this Lsn within this
/// segment
#[inline]
pub fn page_lsn(self) -> Lsn {
Lsn(self.0 - self.block_offset())
}
/// Compute the block offset of the first byte of this Lsn within this
/// segment
#[inline]
pub fn page_offset_in_segment(self, seg_sz: usize) -> u64 {
(self.0 - self.block_offset()) - self.segment_lsn(seg_sz).0
}
/// Compute the bytes remaining in this block /// Compute the bytes remaining in this block
/// ///
/// If the LSN is already at the block boundary, it will return `XLOG_BLCKSZ`. /// If the LSN is already at the block boundary, it will return `XLOG_BLCKSZ`.
#[inline]
pub fn remaining_in_block(self) -> u64 { pub fn remaining_in_block(self) -> u64 {
const BLCKSZ: u64 = XLOG_BLCKSZ as u64; const BLCKSZ: u64 = XLOG_BLCKSZ as u64;
BLCKSZ - (self.0 % BLCKSZ) BLCKSZ - (self.0 % BLCKSZ)
+214
View File
@@ -0,0 +1,214 @@
use std::time::{Duration, SystemTime};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use pq_proto::{read_cstr, PG_EPOCH};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use tracing::{trace, warn};
use crate::lsn::Lsn;
/// Feedback pageserver sends to safekeeper and safekeeper resends to compute.
/// Serialized in custom flexible key/value format. In replication protocol, it
/// is marked with NEON_STATUS_UPDATE_TAG_BYTE to differentiate from postgres
/// Standby status update / Hot standby feedback messages.
///
/// serde Serialize is used only for human readable dump to json (e.g. in
/// safekeepers debug_dump).
#[serde_as]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PageserverFeedback {
/// Last known size of the timeline. Used to enforce timeline size limit.
pub current_timeline_size: u64,
/// LSN last received and ingested by the pageserver. Controls backpressure.
#[serde_as(as = "DisplayFromStr")]
pub last_received_lsn: Lsn,
/// LSN up to which data is persisted by the pageserver to its local disc.
/// Controls backpressure.
#[serde_as(as = "DisplayFromStr")]
pub disk_consistent_lsn: Lsn,
/// LSN up to which data is persisted by the pageserver on s3; safekeepers
/// consider WAL before it can be removed.
#[serde_as(as = "DisplayFromStr")]
pub remote_consistent_lsn: Lsn,
// Serialize with RFC3339 format.
#[serde(with = "serde_systemtime")]
pub replytime: SystemTime,
}
// NOTE: Do not forget to increment this number when adding new fields to PageserverFeedback.
// Do not remove previously available fields because this might be backwards incompatible.
pub const PAGESERVER_FEEDBACK_FIELDS_NUMBER: u8 = 5;
impl PageserverFeedback {
pub fn empty() -> PageserverFeedback {
PageserverFeedback {
current_timeline_size: 0,
last_received_lsn: Lsn::INVALID,
remote_consistent_lsn: Lsn::INVALID,
disk_consistent_lsn: Lsn::INVALID,
replytime: *PG_EPOCH,
}
}
// Serialize PageserverFeedback using custom format
// to support protocol extensibility.
//
// Following layout is used:
// char - number of key-value pairs that follow.
//
// key-value pairs:
// null-terminated string - key,
// uint32 - value length in bytes
// value itself
//
// TODO: change serialized fields names once all computes migrate to rename.
pub fn serialize(&self, buf: &mut BytesMut) {
buf.put_u8(PAGESERVER_FEEDBACK_FIELDS_NUMBER); // # of keys
buf.put_slice(b"current_timeline_size\0");
buf.put_i32(8);
buf.put_u64(self.current_timeline_size);
buf.put_slice(b"ps_writelsn\0");
buf.put_i32(8);
buf.put_u64(self.last_received_lsn.0);
buf.put_slice(b"ps_flushlsn\0");
buf.put_i32(8);
buf.put_u64(self.disk_consistent_lsn.0);
buf.put_slice(b"ps_applylsn\0");
buf.put_i32(8);
buf.put_u64(self.remote_consistent_lsn.0);
let timestamp = self
.replytime
.duration_since(*PG_EPOCH)
.expect("failed to serialize pg_replytime earlier than PG_EPOCH")
.as_micros() as i64;
buf.put_slice(b"ps_replytime\0");
buf.put_i32(8);
buf.put_i64(timestamp);
}
// Deserialize PageserverFeedback message
// TODO: change serialized fields names once all computes migrate to rename.
pub fn parse(mut buf: Bytes) -> PageserverFeedback {
let mut rf = PageserverFeedback::empty();
let nfields = buf.get_u8();
for _ in 0..nfields {
let key = read_cstr(&mut buf).unwrap();
match key.as_ref() {
b"current_timeline_size" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.current_timeline_size = buf.get_u64();
}
b"ps_writelsn" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.last_received_lsn = Lsn(buf.get_u64());
}
b"ps_flushlsn" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.disk_consistent_lsn = Lsn(buf.get_u64());
}
b"ps_applylsn" => {
let len = buf.get_i32();
assert_eq!(len, 8);
rf.remote_consistent_lsn = Lsn(buf.get_u64());
}
b"ps_replytime" => {
let len = buf.get_i32();
assert_eq!(len, 8);
let raw_time = buf.get_i64();
if raw_time > 0 {
rf.replytime = *PG_EPOCH + Duration::from_micros(raw_time as u64);
} else {
rf.replytime = *PG_EPOCH - Duration::from_micros(-raw_time as u64);
}
}
_ => {
let len = buf.get_i32();
warn!(
"PageserverFeedback parse. unknown key {} of len {len}. Skip it.",
String::from_utf8_lossy(key.as_ref())
);
buf.advance(len as usize);
}
}
}
trace!("PageserverFeedback parsed is {:?}", rf);
rf
}
}
mod serde_systemtime {
use std::time::SystemTime;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(ts: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let chrono_dt: DateTime<Utc> = (*ts).into();
serializer.serialize_str(&chrono_dt.to_rfc3339())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
where
D: Deserializer<'de>,
{
let time: String = Deserialize::deserialize(deserializer)?;
Ok(DateTime::parse_from_rfc3339(&time)
.map_err(serde::de::Error::custom)?
.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replication_feedback_serialization() {
let mut rf = PageserverFeedback::empty();
// Fill rf with some values
rf.current_timeline_size = 12345678;
// Set rounded time to be able to compare it with deserialized value,
// because it is rounded up to microseconds during serialization.
rf.replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
let mut data = BytesMut::new();
rf.serialize(&mut data);
let rf_parsed = PageserverFeedback::parse(data.freeze());
assert_eq!(rf, rf_parsed);
}
#[test]
fn test_replication_feedback_unknown_key() {
let mut rf = PageserverFeedback::empty();
// Fill rf with some values
rf.current_timeline_size = 12345678;
// Set rounded time to be able to compare it with deserialized value,
// because it is rounded up to microseconds during serialization.
rf.replytime = *PG_EPOCH + Duration::from_secs(100_000_000);
let mut data = BytesMut::new();
rf.serialize(&mut data);
// Add an extra field to the buffer and adjust number of keys
if let Some(first) = data.first_mut() {
*first = PAGESERVER_FEEDBACK_FIELDS_NUMBER + 1;
}
data.put_slice(b"new_field_one\0");
data.put_i32(8);
data.put_u64(42);
// Parse serialized data and check that new field is not parsed
let rf_parsed = PageserverFeedback::parse(data.freeze());
assert_eq!(rf, rf_parsed);
}
}
+287
View File
@@ -0,0 +1,287 @@
//! Assert that the current [`tracing::Span`] has a given set of fields.
//!
//! # Usage
//!
//! ```
//! use tracing_subscriber::prelude::*;
//! let registry = tracing_subscriber::registry()
//! .with(tracing_error::ErrorLayer::default());
//!
//! // Register the registry as the global subscriber.
//! // In this example, we'll only use it as a thread-local subscriber.
//! let _guard = tracing::subscriber::set_default(registry);
//!
//! // Then, in the main code:
//!
//! let span = tracing::info_span!("TestSpan", test_id = 1);
//! let _guard = span.enter();
//!
//! // ... down the call stack
//!
//! use utils::tracing_span_assert::{check_fields_present, MultiNameExtractor};
//! let extractor = MultiNameExtractor::new("TestExtractor", ["test", "test_id"]);
//! match check_fields_present([&extractor]) {
//! Ok(()) => {},
//! Err(missing) => {
//! panic!("Missing fields: {:?}", missing.into_iter().map(|f| f.name() ).collect::<Vec<_>>());
//! }
//! }
//! ```
//!
//! Recommended reading: https://docs.rs/tracing-subscriber/0.3.16/tracing_subscriber/layer/index.html#per-layer-filtering
//!
use std::{
collections::HashSet,
fmt::{self},
hash::{Hash, Hasher},
};
pub enum ExtractionResult {
Present,
Absent,
}
pub trait Extractor: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn extract(&self, fields: &tracing::field::FieldSet) -> ExtractionResult;
}
#[derive(Debug)]
pub struct MultiNameExtractor<const L: usize> {
name: &'static str,
field_names: [&'static str; L],
}
impl<const L: usize> MultiNameExtractor<L> {
pub fn new(name: &'static str, field_names: [&'static str; L]) -> MultiNameExtractor<L> {
MultiNameExtractor { name, field_names }
}
}
impl<const L: usize> Extractor for MultiNameExtractor<L> {
fn name(&self) -> &str {
self.name
}
fn extract(&self, fields: &tracing::field::FieldSet) -> ExtractionResult {
if fields.iter().any(|f| self.field_names.contains(&f.name())) {
ExtractionResult::Present
} else {
ExtractionResult::Absent
}
}
}
struct MemoryIdentity<'a>(&'a dyn Extractor);
impl<'a> MemoryIdentity<'a> {
fn as_ptr(&self) -> *const () {
self.0 as *const _ as *const ()
}
}
impl<'a> PartialEq for MemoryIdentity<'a> {
fn eq(&self, other: &Self) -> bool {
self.as_ptr() == other.as_ptr()
}
}
impl<'a> Eq for MemoryIdentity<'a> {}
impl<'a> Hash for MemoryIdentity<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_ptr().hash(state);
}
}
impl<'a> fmt::Debug for MemoryIdentity<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:p}: {}", self.as_ptr(), self.0.name())
}
}
/// The extractor names passed as keys to [`new`].
pub fn check_fields_present<const L: usize>(
must_be_present: [&dyn Extractor; L],
) -> Result<(), Vec<&dyn Extractor>> {
let mut missing: HashSet<MemoryIdentity> =
HashSet::from_iter(must_be_present.into_iter().map(|r| MemoryIdentity(r)));
let trace = tracing_error::SpanTrace::capture();
trace.with_spans(|md, _formatted_fields| {
missing.retain(|extractor| match extractor.0.extract(md.fields()) {
ExtractionResult::Present => false,
ExtractionResult::Absent => true,
});
!missing.is_empty() // continue walking up until we've found all missing
});
if missing.is_empty() {
Ok(())
} else {
Err(missing.into_iter().map(|mi| mi.0).collect())
}
}
#[cfg(test)]
mod tests {
use tracing_subscriber::prelude::*;
use super::*;
struct Setup {
_current_thread_subscriber_guard: tracing::subscriber::DefaultGuard,
tenant_extractor: MultiNameExtractor<2>,
timeline_extractor: MultiNameExtractor<2>,
}
fn setup_current_thread() -> Setup {
let tenant_extractor = MultiNameExtractor::new("TenantId", ["tenant_id", "tenant"]);
let timeline_extractor = MultiNameExtractor::new("TimelineId", ["timeline_id", "timeline"]);
let registry = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(tracing_error::ErrorLayer::default());
let guard = tracing::subscriber::set_default(registry);
Setup {
_current_thread_subscriber_guard: guard,
tenant_extractor,
timeline_extractor,
}
}
fn assert_missing(missing: Vec<&dyn Extractor>, expected: Vec<&dyn Extractor>) {
let missing: HashSet<MemoryIdentity> =
HashSet::from_iter(missing.into_iter().map(MemoryIdentity));
let expected: HashSet<MemoryIdentity> =
HashSet::from_iter(expected.into_iter().map(MemoryIdentity));
assert_eq!(missing, expected);
}
#[test]
fn positive_one_level() {
let setup = setup_current_thread();
let span = tracing::info_span!("root", tenant_id = "tenant-1", timeline_id = "timeline-1");
let _guard = span.enter();
check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap();
}
#[test]
fn negative_one_level() {
let setup = setup_current_thread();
let span = tracing::info_span!("root", timeline_id = "timeline-1");
let _guard = span.enter();
let missing =
check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]);
}
#[test]
fn positive_multiple_levels() {
let setup = setup_current_thread();
let span = tracing::info_span!("root");
let _guard = span.enter();
let span = tracing::info_span!("child", tenant_id = "tenant-1");
let _guard = span.enter();
let span = tracing::info_span!("grandchild", timeline_id = "timeline-1");
let _guard = span.enter();
check_fields_present([&setup.tenant_extractor, &setup.timeline_extractor]).unwrap();
}
#[test]
fn negative_multiple_levels() {
let setup = setup_current_thread();
let span = tracing::info_span!("root");
let _guard = span.enter();
let span = tracing::info_span!("child", timeline_id = "timeline-1");
let _guard = span.enter();
let missing = check_fields_present([&setup.tenant_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]);
}
#[test]
fn positive_subset_one_level() {
let setup = setup_current_thread();
let span = tracing::info_span!("root", tenant_id = "tenant-1", timeline_id = "timeline-1");
let _guard = span.enter();
check_fields_present([&setup.tenant_extractor]).unwrap();
}
#[test]
fn positive_subset_multiple_levels() {
let setup = setup_current_thread();
let span = tracing::info_span!("root");
let _guard = span.enter();
let span = tracing::info_span!("child", tenant_id = "tenant-1");
let _guard = span.enter();
let span = tracing::info_span!("grandchild", timeline_id = "timeline-1");
let _guard = span.enter();
check_fields_present([&setup.tenant_extractor]).unwrap();
}
#[test]
fn negative_subset_one_level() {
let setup = setup_current_thread();
let span = tracing::info_span!("root", timeline_id = "timeline-1");
let _guard = span.enter();
let missing = check_fields_present([&setup.tenant_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]);
}
#[test]
fn negative_subset_multiple_levels() {
let setup = setup_current_thread();
let span = tracing::info_span!("root");
let _guard = span.enter();
let span = tracing::info_span!("child", timeline_id = "timeline-1");
let _guard = span.enter();
let missing = check_fields_present([&setup.tenant_extractor]).unwrap_err();
assert_missing(missing, vec![&setup.tenant_extractor]);
}
#[test]
fn tracing_error_subscriber_not_set_up() {
// no setup
let span = tracing::info_span!("foo", e = "some value");
let _guard = span.enter();
let extractor = MultiNameExtractor::new("E", ["e"]);
let missing = check_fields_present([&extractor]).unwrap_err();
assert_missing(missing, vec![&extractor]);
}
#[test]
#[should_panic]
fn panics_if_tracing_error_subscriber_has_wrong_filter() {
let r = tracing_subscriber::registry().with({
tracing_error::ErrorLayer::default().with_filter(
tracing_subscriber::filter::dynamic_filter_fn(|md, _| {
if md.is_span() && *md.level() == tracing::Level::INFO {
return false;
}
true
}),
)
});
let _guard = tracing::subscriber::set_default(r);
let span = tracing::info_span!("foo", e = "some value");
let _guard = span.enter();
let extractor = MultiNameExtractor::new("E", ["e"]);
let missing = check_fields_present([&extractor]).unwrap_err();
assert_missing(missing, vec![&extractor]);
}
}
+1
View File
@@ -52,6 +52,7 @@ sync_wrapper.workspace = true
tokio-tar.workspace = true tokio-tar.workspace = true
thiserror.workspace = true thiserror.workspace = true
tokio = { workspace = true, features = ["process", "sync", "fs", "rt", "io-util", "time"] } tokio = { workspace = true, features = ["process", "sync", "fs", "rt", "io-util", "time"] }
tokio-io-timeout.workspace = true
tokio-postgres.workspace = true tokio-postgres.workspace = true
tokio-util.workspace = true tokio-util.workspace = true
toml_edit = { workspace = true, features = [ "serde" ] } toml_edit = { workspace = true, features = [ "serde" ] }
+8 -8
View File
@@ -13,7 +13,7 @@ use std::time::Instant;
use utils::lsn::Lsn; use utils::lsn::Lsn;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> { fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
let mut layer_map = LayerMap::<LayerDescriptor>::default(); let mut layer_map = LayerMap::<LayerDescriptor>::default();
@@ -33,7 +33,7 @@ fn build_layer_map(filename_dump: PathBuf) -> LayerMap<LayerDescriptor> {
min_lsn = min(min_lsn, lsn_range.start); min_lsn = min(min_lsn, lsn_range.start);
max_lsn = max(max_lsn, Lsn(lsn_range.end.0 - 1)); max_lsn = max(max_lsn, Lsn(lsn_range.end.0 - 1));
updates.insert_historic(Arc::new(layer)).unwrap(); updates.insert_historic(Arc::new(layer));
} }
println!("min: {min_lsn}, max: {max_lsn}"); println!("min: {min_lsn}, max: {max_lsn}");
@@ -114,7 +114,7 @@ fn bench_from_captest_env(c: &mut Criterion) {
c.bench_function("captest_uniform_queries", |b| { c.bench_function("captest_uniform_queries", |b| {
b.iter(|| { b.iter(|| {
for q in queries.clone().into_iter() { for q in queries.clone().into_iter() {
layer_map.search(q.0, q.1); black_box(layer_map.search(q.0, q.1));
} }
}); });
}); });
@@ -122,11 +122,11 @@ fn bench_from_captest_env(c: &mut Criterion) {
// test with a key that corresponds to the RelDir entry. See pgdatadir_mapping.rs. // test with a key that corresponds to the RelDir entry. See pgdatadir_mapping.rs.
c.bench_function("captest_rel_dir_query", |b| { c.bench_function("captest_rel_dir_query", |b| {
b.iter(|| { b.iter(|| {
let result = layer_map.search( let result = black_box(layer_map.search(
Key::from_hex("000000067F00008000000000000000000001").unwrap(), Key::from_hex("000000067F00008000000000000000000001").unwrap(),
// This LSN is higher than any of the LSNs in the tree // This LSN is higher than any of the LSNs in the tree
Lsn::from_str("D0/80208AE1").unwrap(), Lsn::from_str("D0/80208AE1").unwrap(),
); ));
result.unwrap(); result.unwrap();
}); });
}); });
@@ -183,7 +183,7 @@ fn bench_from_real_project(c: &mut Criterion) {
group.bench_function("uniform_queries", |b| { group.bench_function("uniform_queries", |b| {
b.iter(|| { b.iter(|| {
for q in queries.clone().into_iter() { for q in queries.clone().into_iter() {
layer_map.search(q.0, q.1); black_box(layer_map.search(q.0, q.1));
} }
}); });
}); });
@@ -215,7 +215,7 @@ fn bench_sequential(c: &mut Criterion) {
is_incremental: false, is_incremental: false,
short_id: format!("Layer {}", i), short_id: format!("Layer {}", i),
}; };
updates.insert_historic(Arc::new(layer)).unwrap(); updates.insert_historic(Arc::new(layer));
} }
updates.flush(); updates.flush();
println!("Finished layer map init in {:?}", now.elapsed()); println!("Finished layer map init in {:?}", now.elapsed());
@@ -232,7 +232,7 @@ fn bench_sequential(c: &mut Criterion) {
group.bench_function("uniform_queries", |b| { group.bench_function("uniform_queries", |b| {
b.iter(|| { b.iter(|| {
for q in queries.clone().into_iter() { for q in queries.clone().into_iter() {
layer_map.search(q.0, q.1); black_box(layer_map.search(q.0, q.1));
} }
}); });
}); });
+7 -3
View File
@@ -463,9 +463,13 @@ where
let wal_file_path = format!("pg_wal/{}", wal_file_name); let wal_file_path = format!("pg_wal/{}", wal_file_name);
let header = new_tar_header(&wal_file_path, WAL_SEGMENT_SIZE as u64)?; let header = new_tar_header(&wal_file_path, WAL_SEGMENT_SIZE as u64)?;
let wal_seg = let wal_seg = postgres_ffi::generate_wal_segment(
postgres_ffi::generate_wal_segment(segno, system_identifier, self.timeline.pg_version) segno,
.map_err(|e| anyhow!(e).context("Failed generating wal segment"))?; system_identifier,
self.timeline.pg_version,
self.lsn,
)
.map_err(|e| anyhow!(e).context("Failed generating wal segment"))?;
ensure!(wal_seg.len() == WAL_SEGMENT_SIZE); ensure!(wal_seg.len() == WAL_SEGMENT_SIZE);
self.ar.append(&header, &wal_seg[..]).await?; self.ar.append(&header, &wal_seg[..]).await?;
Ok(()) Ok(())
+15 -2
View File
@@ -25,6 +25,7 @@ use pageserver::{
virtual_file, virtual_file,
}; };
use postgres_backend::AuthType; use postgres_backend::AuthType;
use utils::logging::TracingErrorLayerEnablement;
use utils::signals::ShutdownSignals; use utils::signals::ShutdownSignals;
use utils::{ use utils::{
auth::JwtAuth, logging, project_git_version, sentry_init::init_sentry, signals::Signal, auth::JwtAuth, logging, project_git_version, sentry_init::init_sentry, signals::Signal,
@@ -86,8 +87,19 @@ fn main() -> anyhow::Result<()> {
} }
}; };
// Initialize logging, which must be initialized before the custom panic hook is installed. // Initialize logging.
logging::init(conf.log_format)?; //
// It must be initialized before the custom panic hook is installed below.
//
// Regarding tracing_error enablement: at this time, we only use the
// tracing_error crate to debug_assert that log spans contain tenant and timeline ids.
// See `debug_assert_current_span_has_tenant_and_timeline_id` in the timeline module
let tracing_error_layer_enablement = if cfg!(debug_assertions) {
TracingErrorLayerEnablement::EnableWithRustLogFilter
} else {
TracingErrorLayerEnablement::Disabled
};
logging::init(conf.log_format, tracing_error_layer_enablement)?;
// mind the order required here: 1. logging, 2. panic_hook, 3. sentry. // mind the order required here: 1. logging, 2. panic_hook, 3. sentry.
// disarming this hook on pageserver, because we never tear down tracing. // disarming this hook on pageserver, because we never tear down tracing.
@@ -226,6 +238,7 @@ fn start_pageserver(
); );
set_build_info_metric(GIT_VERSION); set_build_info_metric(GIT_VERSION);
set_launch_timestamp_metric(launch_ts); set_launch_timestamp_metric(launch_ts);
pageserver::preinitialize_metrics();
// If any failpoints were set from FAILPOINTS environment variable, // If any failpoints were set from FAILPOINTS environment variable,
// print them to the log for debugging purposes // print them to the log for debugging purposes
+115
View File
@@ -520,6 +520,43 @@ paths:
schema: schema:
$ref: "#/components/schemas/Error" $ref: "#/components/schemas/Error"
/v1/tenant/{tenant_id}/synthetic_size:
parameters:
- name: tenant_id
in: path
required: true
schema:
type: string
format: hex
get:
description: |
Calculate tenant's synthetic size
responses:
"200":
description: Tenant's synthetic size
content:
application/json:
schema:
$ref: "#/components/schemas/SyntheticSizeResponse"
"401":
description: Unauthorized Error
content:
application/json:
schema:
$ref: "#/components/schemas/UnauthorizedError"
"403":
description: Forbidden Error
content:
application/json:
schema:
$ref: "#/components/schemas/ForbiddenError"
"500":
description: Generic operation error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/tenant/{tenant_id}/size: /v1/tenant/{tenant_id}/size:
parameters: parameters:
- name: tenant_id - name: tenant_id
@@ -948,6 +985,84 @@ components:
latest_gc_cutoff_lsn: latest_gc_cutoff_lsn:
type: string type: string
format: hex format: hex
SyntheticSizeResponse:
type: object
required:
- id
- size
- segment_sizes
- inputs
properties:
id:
type: string
format: hex
size:
type: integer
segment_sizes:
type: array
items:
$ref: "#/components/schemas/SegmentSize"
inputs:
type: object
properties:
segments:
type: array
items:
$ref: "#/components/schemas/SegmentData"
timeline_inputs:
type: array
items:
$ref: "#/components/schemas/TimelineInput"
SegmentSize:
type: object
required:
- method
- accum_size
properties:
method:
type: string
accum_size:
type: integer
SegmentData:
type: object
required:
- segment
properties:
segment:
type: object
required:
- lsn
properties:
parent:
type: integer
lsn:
type: integer
size:
type: integer
needed:
type: boolean
timeline_id:
type: string
format: hex
kind:
type: string
TimelineInput:
type: object
required:
- timeline_id
properties:
ancestor_id:
type: string
ancestor_lsn:
type: string
timeline_id:
type: string
format: hex
Error: Error:
type: object type: object
required: required:
+35
View File
@@ -1201,6 +1201,37 @@ async fn handler_404(_: Request<Body>) -> Result<Response<Body>, ApiError> {
) )
} }
#[cfg(feature = "testing")]
async fn post_tracing_event_handler(mut r: Request<Body>) -> Result<Response<Body>, ApiError> {
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
enum Level {
Error,
Warn,
Info,
Debug,
Trace,
}
#[derive(Debug, serde::Deserialize)]
struct Request {
level: Level,
message: String,
}
let body: Request = json_request(&mut r)
.await
.map_err(|_| ApiError::BadRequest(anyhow::anyhow!("invalid JSON body")))?;
match body.level {
Level::Error => tracing::error!(?body.message),
Level::Warn => tracing::warn!(?body.message),
Level::Info => tracing::info!(?body.message),
Level::Debug => tracing::debug!(?body.message),
Level::Trace => tracing::trace!(?body.message),
}
json_response(StatusCode::OK, ())
}
pub fn make_router( pub fn make_router(
conf: &'static PageServerConf, conf: &'static PageServerConf,
launch_ts: &'static LaunchTimestamp, launch_ts: &'static LaunchTimestamp,
@@ -1341,5 +1372,9 @@ pub fn make_router(
testing_api!("set tenant state to broken", handle_tenant_break), testing_api!("set tenant state to broken", handle_tenant_break),
) )
.get("/v1/panic", |r| RequestSpan(always_panic_handler).handle(r)) .get("/v1/panic", |r| RequestSpan(always_panic_handler).handle(r))
.post(
"/v1/tracing/event",
testing_api!("emit a tracing event", post_tracing_event_handler),
)
.any(handler_404)) .any(handler_404))
} }
+4 -4
View File
@@ -114,7 +114,7 @@ async fn import_rel(
path: &Path, path: &Path,
spcoid: Oid, spcoid: Oid,
dboid: Oid, dboid: Oid,
reader: &mut (impl AsyncRead + Send + Sync + Unpin), reader: &mut (impl AsyncRead + Unpin),
len: usize, len: usize,
ctx: &RequestContext, ctx: &RequestContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@@ -200,7 +200,7 @@ async fn import_slru(
modification: &mut DatadirModification<'_>, modification: &mut DatadirModification<'_>,
slru: SlruKind, slru: SlruKind,
path: &Path, path: &Path,
reader: &mut (impl AsyncRead + Send + Sync + Unpin), reader: &mut (impl AsyncRead + Unpin),
len: usize, len: usize,
ctx: &RequestContext, ctx: &RequestContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@@ -612,8 +612,8 @@ async fn import_file(
Ok(None) Ok(None)
} }
async fn read_all_bytes(reader: &mut (impl AsyncRead + Send + Sync + Unpin)) -> Result<Bytes> { async fn read_all_bytes(reader: &mut (impl AsyncRead + Unpin)) -> Result<Bytes> {
let mut buf: Vec<u8> = vec![]; let mut buf: Vec<u8> = vec![];
reader.read_to_end(&mut buf).await?; reader.read_to_end(&mut buf).await?;
Ok(Bytes::copy_from_slice(&buf[..])) Ok(Bytes::from(buf))
} }
+2
View File
@@ -44,6 +44,8 @@ pub const DELTA_FILE_MAGIC: u16 = 0x5A61;
static ZERO_PAGE: bytes::Bytes = bytes::Bytes::from_static(&[0u8; 8192]); static ZERO_PAGE: bytes::Bytes = bytes::Bytes::from_static(&[0u8; 8192]);
pub use crate::metrics::preinitialize_metrics;
pub async fn shutdown_pageserver(exit_code: i32) { pub async fn shutdown_pageserver(exit_code: i32) {
// Shut down the libpq endpoint task. This prevents new connections from // Shut down the libpq endpoint task. This prevents new connections from
// being accepted. // being accepted.
+198 -24
View File
@@ -1,9 +1,9 @@
use metrics::core::{AtomicU64, GenericCounter}; use metrics::core::{AtomicU64, GenericCounter};
use metrics::{ use metrics::{
register_counter_vec, register_histogram, register_histogram_vec, register_int_counter, register_counter_vec, register_histogram, register_histogram_vec, register_int_counter,
register_int_counter_vec, register_int_gauge, register_int_gauge_vec, register_uint_gauge_vec, register_int_counter_vec, register_int_gauge_vec, register_uint_gauge_vec, Counter, CounterVec,
Counter, CounterVec, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, UIntGauge,
UIntGauge, UIntGaugeVec, UIntGaugeVec,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pageserver_api::models::TenantState; use pageserver_api::models::TenantState;
@@ -205,6 +205,15 @@ static EVICTIONS_WITH_LOW_RESIDENCE_DURATION: Lazy<IntCounterVec> = Lazy::new(||
.expect("failed to define a metric") .expect("failed to define a metric")
}); });
pub static UNEXPECTED_ONDEMAND_DOWNLOADS: Lazy<IntCounter> = Lazy::new(|| {
register_int_counter!(
"pageserver_unexpected_ondemand_downloads_count",
"Number of unexpected on-demand downloads. \
We log more context for each increment, so, forgo any labels in this metric.",
)
.expect("failed to define a metric")
});
/// Each [`Timeline`]'s [`EVICTIONS_WITH_LOW_RESIDENCE_DURATION`] metric. /// Each [`Timeline`]'s [`EVICTIONS_WITH_LOW_RESIDENCE_DURATION`] metric.
#[derive(Debug)] #[derive(Debug)]
pub struct EvictionsWithLowResidenceDuration { pub struct EvictionsWithLowResidenceDuration {
@@ -350,11 +359,6 @@ pub static LIVE_CONNECTIONS_COUNT: Lazy<IntGaugeVec> = Lazy::new(|| {
.expect("failed to define a metric") .expect("failed to define a metric")
}); });
pub static NUM_ONDISK_LAYERS: Lazy<IntGauge> = Lazy::new(|| {
register_int_gauge!("pageserver_ondisk_layers", "Number of layers on-disk")
.expect("failed to define a metric")
});
// remote storage metrics // remote storage metrics
/// NB: increment _after_ recording the current value into [`REMOTE_TIMELINE_CLIENT_CALLS_STARTED_HIST`]. /// NB: increment _after_ recording the current value into [`REMOTE_TIMELINE_CLIENT_CALLS_STARTED_HIST`].
@@ -385,6 +389,26 @@ static REMOTE_TIMELINE_CLIENT_CALLS_STARTED_HIST: Lazy<HistogramVec> = Lazy::new
.expect("failed to define a metric") .expect("failed to define a metric")
}); });
static REMOTE_TIMELINE_CLIENT_BYTES_STARTED_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| {
register_int_counter_vec!(
"pageserver_remote_timeline_client_bytes_started",
"Incremented by the number of bytes associated with a remote timeline client operation. \
The increment happens when the operation is scheduled.",
&["tenant_id", "timeline_id", "file_kind", "op_kind"],
)
.expect("failed to define a metric")
});
static REMOTE_TIMELINE_CLIENT_BYTES_FINISHED_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| {
register_int_counter_vec!(
"pageserver_remote_timeline_client_bytes_finished",
"Incremented by the number of bytes associated with a remote timeline client operation. \
The increment happens when the operation finishes (regardless of success/failure/shutdown).",
&["tenant_id", "timeline_id", "file_kind", "op_kind"],
)
.expect("failed to define a metric")
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RemoteOpKind { pub enum RemoteOpKind {
Upload, Upload,
@@ -739,6 +763,8 @@ pub struct RemoteTimelineClientMetrics {
remote_operation_time: Mutex<HashMap<(&'static str, &'static str, &'static str), Histogram>>, remote_operation_time: Mutex<HashMap<(&'static str, &'static str, &'static str), Histogram>>,
calls_unfinished_gauge: Mutex<HashMap<(&'static str, &'static str), IntGauge>>, calls_unfinished_gauge: Mutex<HashMap<(&'static str, &'static str), IntGauge>>,
calls_started_hist: Mutex<HashMap<(&'static str, &'static str), Histogram>>, calls_started_hist: Mutex<HashMap<(&'static str, &'static str), Histogram>>,
bytes_started_counter: Mutex<HashMap<(&'static str, &'static str), IntCounter>>,
bytes_finished_counter: Mutex<HashMap<(&'static str, &'static str), IntCounter>>,
} }
impl RemoteTimelineClientMetrics { impl RemoteTimelineClientMetrics {
@@ -749,6 +775,8 @@ impl RemoteTimelineClientMetrics {
remote_operation_time: Mutex::new(HashMap::default()), remote_operation_time: Mutex::new(HashMap::default()),
calls_unfinished_gauge: Mutex::new(HashMap::default()), calls_unfinished_gauge: Mutex::new(HashMap::default()),
calls_started_hist: Mutex::new(HashMap::default()), calls_started_hist: Mutex::new(HashMap::default()),
bytes_started_counter: Mutex::new(HashMap::default()),
bytes_finished_counter: Mutex::new(HashMap::default()),
remote_physical_size_gauge: Mutex::new(None), remote_physical_size_gauge: Mutex::new(None),
} }
} }
@@ -787,6 +815,7 @@ impl RemoteTimelineClientMetrics {
}); });
metric.clone() metric.clone()
} }
fn calls_unfinished_gauge( fn calls_unfinished_gauge(
&self, &self,
file_kind: &RemoteOpFileKind, file_kind: &RemoteOpFileKind,
@@ -828,32 +857,125 @@ impl RemoteTimelineClientMetrics {
}); });
metric.clone() metric.clone()
} }
fn bytes_started_counter(
&self,
file_kind: &RemoteOpFileKind,
op_kind: &RemoteOpKind,
) -> IntCounter {
// XXX would be nice to have an upgradable RwLock
let mut guard = self.bytes_started_counter.lock().unwrap();
let key = (file_kind.as_str(), op_kind.as_str());
let metric = guard.entry(key).or_insert_with(move || {
REMOTE_TIMELINE_CLIENT_BYTES_STARTED_COUNTER
.get_metric_with_label_values(&[
&self.tenant_id.to_string(),
&self.timeline_id.to_string(),
key.0,
key.1,
])
.unwrap()
});
metric.clone()
}
fn bytes_finished_counter(
&self,
file_kind: &RemoteOpFileKind,
op_kind: &RemoteOpKind,
) -> IntCounter {
// XXX would be nice to have an upgradable RwLock
let mut guard = self.bytes_finished_counter.lock().unwrap();
let key = (file_kind.as_str(), op_kind.as_str());
let metric = guard.entry(key).or_insert_with(move || {
REMOTE_TIMELINE_CLIENT_BYTES_FINISHED_COUNTER
.get_metric_with_label_values(&[
&self.tenant_id.to_string(),
&self.timeline_id.to_string(),
key.0,
key.1,
])
.unwrap()
});
metric.clone()
}
}
#[cfg(test)]
impl RemoteTimelineClientMetrics {
pub fn get_bytes_started_counter_value(
&self,
file_kind: &RemoteOpFileKind,
op_kind: &RemoteOpKind,
) -> Option<u64> {
let guard = self.bytes_started_counter.lock().unwrap();
let key = (file_kind.as_str(), op_kind.as_str());
guard.get(&key).map(|counter| counter.get())
}
pub fn get_bytes_finished_counter_value(
&self,
file_kind: &RemoteOpFileKind,
op_kind: &RemoteOpKind,
) -> Option<u64> {
let guard = self.bytes_finished_counter.lock().unwrap();
let key = (file_kind.as_str(), op_kind.as_str());
guard.get(&key).map(|counter| counter.get())
}
} }
/// See [`RemoteTimelineClientMetrics::call_begin`]. /// See [`RemoteTimelineClientMetrics::call_begin`].
#[must_use] #[must_use]
pub(crate) struct RemoteTimelineClientCallMetricGuard(Option<IntGauge>); pub(crate) struct RemoteTimelineClientCallMetricGuard {
/// Decremented on drop.
calls_unfinished_metric: Option<IntGauge>,
/// If Some(), this references the bytes_finished metric, and we increment it by the given `u64` on drop.
bytes_finished: Option<(IntCounter, u64)>,
}
impl RemoteTimelineClientCallMetricGuard { impl RemoteTimelineClientCallMetricGuard {
/// Consume this guard object without decrementing the metric. /// Consume this guard object without performing the metric updates it would do on `drop()`.
/// The caller vouches to do this manually, so that the prior increment of the gauge will cancel out. /// The caller vouches to do the metric updates manually.
pub fn will_decrement_manually(mut self) { pub fn will_decrement_manually(mut self) {
self.0 = None; // prevent drop() from decrementing let RemoteTimelineClientCallMetricGuard {
calls_unfinished_metric,
bytes_finished,
} = &mut self;
calls_unfinished_metric.take();
bytes_finished.take();
} }
} }
impl Drop for RemoteTimelineClientCallMetricGuard { impl Drop for RemoteTimelineClientCallMetricGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let RemoteTimelineClientCallMetricGuard(Some(guard)) = self { let RemoteTimelineClientCallMetricGuard {
calls_unfinished_metric,
bytes_finished,
} = self;
if let Some(guard) = calls_unfinished_metric.take() {
guard.dec(); guard.dec();
} }
if let Some((bytes_finished_metric, value)) = bytes_finished {
bytes_finished_metric.inc_by(*value);
}
} }
} }
/// The enum variants communicate to the [`RemoteTimelineClientMetrics`] whether to
/// track the byte size of this call in applicable metric(s).
pub(crate) enum RemoteTimelineClientMetricsCallTrackSize {
/// Do not account for this call's byte size in any metrics.
/// The `reason` field is there to make the call sites self-documenting
/// about why they don't need the metric.
DontTrackSize { reason: &'static str },
/// Track the byte size of the call in applicable metric(s).
Bytes(u64),
}
impl RemoteTimelineClientMetrics { impl RemoteTimelineClientMetrics {
/// Increment the metrics that track ongoing calls to the remote timeline client instance. /// Update the metrics that change when a call to the remote timeline client instance starts.
/// ///
/// Drop the returned guard object once the operation is finished to decrement the values. /// Drop the returned guard object once the operation is finished to updates corresponding metrics that track completions.
/// Or, use [`RemoteTimelineClientCallMetricGuard::will_decrement_manually`] and [`call_end`] if that /// Or, use [`RemoteTimelineClientCallMetricGuard::will_decrement_manually`] and [`call_end`] if that
/// is more suitable. /// is more suitable.
/// Never do both. /// Never do both.
@@ -861,24 +983,51 @@ impl RemoteTimelineClientMetrics {
&self, &self,
file_kind: &RemoteOpFileKind, file_kind: &RemoteOpFileKind,
op_kind: &RemoteOpKind, op_kind: &RemoteOpKind,
size: RemoteTimelineClientMetricsCallTrackSize,
) -> RemoteTimelineClientCallMetricGuard { ) -> RemoteTimelineClientCallMetricGuard {
let unfinished_metric = self.calls_unfinished_gauge(file_kind, op_kind); let calls_unfinished_metric = self.calls_unfinished_gauge(file_kind, op_kind);
self.calls_started_hist(file_kind, op_kind) self.calls_started_hist(file_kind, op_kind)
.observe(unfinished_metric.get() as f64); .observe(calls_unfinished_metric.get() as f64);
unfinished_metric.inc(); calls_unfinished_metric.inc(); // NB: inc after the histogram, see comment on underlying metric
RemoteTimelineClientCallMetricGuard(Some(unfinished_metric))
let bytes_finished = match size {
RemoteTimelineClientMetricsCallTrackSize::DontTrackSize { reason: _reason } => {
// nothing to do
None
}
RemoteTimelineClientMetricsCallTrackSize::Bytes(size) => {
self.bytes_started_counter(file_kind, op_kind).inc_by(size);
let finished_counter = self.bytes_finished_counter(file_kind, op_kind);
Some((finished_counter, size))
}
};
RemoteTimelineClientCallMetricGuard {
calls_unfinished_metric: Some(calls_unfinished_metric),
bytes_finished,
}
} }
/// Manually decrement the metric instead of using the guard object. /// Manually udpate the metrics that track completions, instead of using the guard object.
/// Using the guard object is generally preferable. /// Using the guard object is generally preferable.
/// See [`call_begin`] for more context. /// See [`call_begin`] for more context.
pub(crate) fn call_end(&self, file_kind: &RemoteOpFileKind, op_kind: &RemoteOpKind) { pub(crate) fn call_end(
let unfinished_metric = self.calls_unfinished_gauge(file_kind, op_kind); &self,
file_kind: &RemoteOpFileKind,
op_kind: &RemoteOpKind,
size: RemoteTimelineClientMetricsCallTrackSize,
) {
let calls_unfinished_metric = self.calls_unfinished_gauge(file_kind, op_kind);
debug_assert!( debug_assert!(
unfinished_metric.get() > 0, calls_unfinished_metric.get() > 0,
"begin and end should cancel out" "begin and end should cancel out"
); );
unfinished_metric.dec(); calls_unfinished_metric.dec();
match size {
RemoteTimelineClientMetricsCallTrackSize::DontTrackSize { reason: _reason } => {}
RemoteTimelineClientMetricsCallTrackSize::Bytes(size) => {
self.bytes_finished_counter(file_kind, op_kind).inc_by(size);
}
}
} }
} }
@@ -891,6 +1040,8 @@ impl Drop for RemoteTimelineClientMetrics {
remote_operation_time, remote_operation_time,
calls_unfinished_gauge, calls_unfinished_gauge,
calls_started_hist, calls_started_hist,
bytes_started_counter,
bytes_finished_counter,
} = self; } = self;
for ((a, b, c), _) in remote_operation_time.get_mut().unwrap().drain() { for ((a, b, c), _) in remote_operation_time.get_mut().unwrap().drain() {
let _ = REMOTE_OPERATION_TIME.remove_label_values(&[tenant_id, timeline_id, a, b, c]); let _ = REMOTE_OPERATION_TIME.remove_label_values(&[tenant_id, timeline_id, a, b, c]);
@@ -911,6 +1062,22 @@ impl Drop for RemoteTimelineClientMetrics {
b, b,
]); ]);
} }
for ((a, b), _) in bytes_started_counter.get_mut().unwrap().drain() {
let _ = REMOTE_TIMELINE_CLIENT_BYTES_STARTED_COUNTER.remove_label_values(&[
tenant_id,
timeline_id,
a,
b,
]);
}
for ((a, b), _) in bytes_finished_counter.get_mut().unwrap().drain() {
let _ = REMOTE_TIMELINE_CLIENT_BYTES_FINISHED_COUNTER.remove_label_values(&[
tenant_id,
timeline_id,
a,
b,
]);
}
{ {
let _ = remote_physical_size_gauge; // use to avoid 'unused' warning in desctructuring above let _ = remote_physical_size_gauge; // use to avoid 'unused' warning in desctructuring above
let _ = REMOTE_PHYSICAL_SIZE.remove_label_values(&[tenant_id, timeline_id]); let _ = REMOTE_PHYSICAL_SIZE.remove_label_values(&[tenant_id, timeline_id]);
@@ -974,3 +1141,10 @@ impl<F: Future<Output = Result<O, E>>, O, E> Future for MeasuredRemoteOp<F> {
poll_result poll_result
} }
} }
pub fn preinitialize_metrics() {
// We want to alert on this metric increasing.
// Initialize it eagerly, so that our alert rule can distinguish absence of the metric from metric value 0.
assert_eq!(UNEXPECTED_ONDEMAND_DOWNLOADS.get(), 0);
UNEXPECTED_ONDEMAND_DOWNLOADS.reset();
}
+66 -23
View File
@@ -20,7 +20,6 @@ use pageserver_api::models::{
PagestreamFeMessage, PagestreamGetPageRequest, PagestreamGetPageResponse, PagestreamFeMessage, PagestreamGetPageRequest, PagestreamGetPageResponse,
PagestreamNblocksRequest, PagestreamNblocksResponse, PagestreamNblocksRequest, PagestreamNblocksResponse,
}; };
use postgres_backend::PostgresBackendTCP;
use postgres_backend::{self, is_expected_io_error, AuthType, PostgresBackend, QueryError}; use postgres_backend::{self, is_expected_io_error, AuthType, PostgresBackend, QueryError};
use pq_proto::framed::ConnectionError; use pq_proto::framed::ConnectionError;
use pq_proto::FeStartupPacket; use pq_proto::FeStartupPacket;
@@ -32,6 +31,7 @@ use std::str;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::io::StreamReader; use tokio_util::io::StreamReader;
use tracing::*; use tracing::*;
use utils::id::ConnectionId; use utils::id::ConnectionId;
@@ -57,7 +57,10 @@ use crate::trace::Tracer;
use postgres_ffi::pg_constants::DEFAULTTABLESPACE_OID; use postgres_ffi::pg_constants::DEFAULTTABLESPACE_OID;
use postgres_ffi::BLCKSZ; use postgres_ffi::BLCKSZ;
fn copyin_stream(pgb: &mut PostgresBackendTCP) -> impl Stream<Item = io::Result<Bytes>> + '_ { fn copyin_stream<IO>(pgb: &mut PostgresBackend<IO>) -> impl Stream<Item = io::Result<Bytes>> + '_
where
IO: AsyncRead + AsyncWrite + Unpin,
{
async_stream::try_stream! { async_stream::try_stream! {
loop { loop {
let msg = tokio::select! { let msg = tokio::select! {
@@ -65,8 +68,8 @@ fn copyin_stream(pgb: &mut PostgresBackendTCP) -> impl Stream<Item = io::Result<
_ = task_mgr::shutdown_watcher() => { _ = task_mgr::shutdown_watcher() => {
// We were requested to shut down. // We were requested to shut down.
let msg = "pageserver is shutting down".to_string(); let msg = "pageserver is shutting down";
let _ = pgb.write_message_noflush(&BeMessage::ErrorResponse(&msg, None)); let _ = pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, None));
Err(QueryError::Other(anyhow::anyhow!(msg))) Err(QueryError::Other(anyhow::anyhow!(msg)))
} }
@@ -125,7 +128,7 @@ fn copyin_stream(pgb: &mut PostgresBackendTCP) -> impl Stream<Item = io::Result<
/// ///
/// XXX: Currently, any trailing data after the EOF marker prints a warning. /// XXX: Currently, any trailing data after the EOF marker prints a warning.
/// Perhaps it should be a hard error? /// Perhaps it should be a hard error?
async fn read_tar_eof(mut reader: (impl tokio::io::AsyncRead + Unpin)) -> anyhow::Result<()> { async fn read_tar_eof(mut reader: (impl AsyncRead + Unpin)) -> anyhow::Result<()> {
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
let mut buf = [0u8; 512]; let mut buf = [0u8; 512];
@@ -245,12 +248,23 @@ async fn page_service_conn_main(
.set_nodelay(true) .set_nodelay(true)
.context("could not set TCP_NODELAY")?; .context("could not set TCP_NODELAY")?;
let peer_addr = socket.peer_addr().context("get peer address")?;
// setup read timeout of 10 minutes. the timeout is rather arbitrary for requirements:
// - long enough for most valid compute connections
// - less than infinite to stop us from "leaking" connections to long-gone computes
//
// no write timeout is used, because the kernel is assumed to error writes after some time.
let mut socket = tokio_io_timeout::TimeoutReader::new(socket);
socket.set_timeout(Some(std::time::Duration::from_secs(60 * 10)));
let socket = std::pin::pin!(socket);
// XXX: pgbackend.run() should take the connection_ctx, // XXX: pgbackend.run() should take the connection_ctx,
// and create a child per-query context when it invokes process_query. // and create a child per-query context when it invokes process_query.
// But it's in a shared crate, so, we store connection_ctx inside PageServerHandler // But it's in a shared crate, so, we store connection_ctx inside PageServerHandler
// and create the per-query context in process_query ourselves. // and create the per-query context in process_query ourselves.
let mut conn_handler = PageServerHandler::new(conf, auth, connection_ctx); let mut conn_handler = PageServerHandler::new(conf, auth, connection_ctx);
let pgbackend = PostgresBackend::new(socket, auth_type, None)?; let pgbackend = PostgresBackend::new_from_io(socket, peer_addr, auth_type, None)?;
match pgbackend match pgbackend
.run(&mut conn_handler, task_mgr::shutdown_watcher) .run(&mut conn_handler, task_mgr::shutdown_watcher)
@@ -332,13 +346,16 @@ impl PageServerHandler {
} }
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_pagerequests( async fn handle_pagerequests<IO>(
&self, &self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend<IO>,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
ctx: RequestContext, ctx: RequestContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()>
where
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
// NOTE: pagerequests handler exits when connection is closed, // NOTE: pagerequests handler exits when connection is closed,
// so there is no need to reset the association // so there is no need to reset the association
task_mgr::associate_with(Some(tenant_id), Some(timeline_id)); task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
@@ -436,16 +453,19 @@ impl PageServerHandler {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_import_basebackup( async fn handle_import_basebackup<IO>(
&self, &self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend<IO>,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
base_lsn: Lsn, base_lsn: Lsn,
_end_lsn: Lsn, _end_lsn: Lsn,
pg_version: u32, pg_version: u32,
ctx: RequestContext, ctx: RequestContext,
) -> Result<(), QueryError> { ) -> Result<(), QueryError>
where
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
task_mgr::associate_with(Some(tenant_id), Some(timeline_id)); task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
// Create empty timeline // Create empty timeline
info!("creating new timeline"); info!("creating new timeline");
@@ -486,15 +506,18 @@ impl PageServerHandler {
} }
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_import_wal( async fn handle_import_wal<IO>(
&self, &self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend<IO>,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
start_lsn: Lsn, start_lsn: Lsn,
end_lsn: Lsn, end_lsn: Lsn,
ctx: RequestContext, ctx: RequestContext,
) -> Result<(), QueryError> { ) -> Result<(), QueryError>
where
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
task_mgr::associate_with(Some(tenant_id), Some(timeline_id)); task_mgr::associate_with(Some(tenant_id), Some(timeline_id));
let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?; let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?;
@@ -690,16 +713,21 @@ impl PageServerHandler {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[instrument(skip(self, pgb, ctx))] #[instrument(skip(self, pgb, ctx))]
async fn handle_basebackup_request( async fn handle_basebackup_request<IO>(
&mut self, &mut self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend<IO>,
tenant_id: TenantId, tenant_id: TenantId,
timeline_id: TimelineId, timeline_id: TimelineId,
lsn: Option<Lsn>, lsn: Option<Lsn>,
prev_lsn: Option<Lsn>, prev_lsn: Option<Lsn>,
full_backup: bool, full_backup: bool,
ctx: RequestContext, ctx: RequestContext,
) -> anyhow::Result<()> { ) -> anyhow::Result<()>
where
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
let started = std::time::Instant::now();
// check that the timeline exists // check that the timeline exists
let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?; let timeline = get_active_tenant_timeline(tenant_id, timeline_id, &ctx).await?;
let latest_gc_cutoff_lsn = timeline.get_latest_gc_cutoff_lsn(); let latest_gc_cutoff_lsn = timeline.get_latest_gc_cutoff_lsn();
@@ -712,6 +740,8 @@ impl PageServerHandler {
.context("invalid basebackup lsn")?; .context("invalid basebackup lsn")?;
} }
let lsn_awaited_after = started.elapsed();
// switch client to COPYOUT // switch client to COPYOUT
pgb.write_message_noflush(&BeMessage::CopyOutResponse)?; pgb.write_message_noflush(&BeMessage::CopyOutResponse)?;
pgb.flush().await?; pgb.flush().await?;
@@ -732,7 +762,17 @@ impl PageServerHandler {
pgb.write_message_noflush(&BeMessage::CopyDone)?; pgb.write_message_noflush(&BeMessage::CopyDone)?;
pgb.flush().await?; pgb.flush().await?;
info!("basebackup complete");
let basebackup_after = started
.elapsed()
.checked_sub(lsn_awaited_after)
.unwrap_or(Duration::ZERO);
info!(
lsn_await_millis = lsn_awaited_after.as_millis(),
basebackup_millis = basebackup_after.as_millis(),
"basebackup complete"
);
Ok(()) Ok(())
} }
@@ -756,10 +796,13 @@ impl PageServerHandler {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl postgres_backend::Handler<tokio::net::TcpStream> for PageServerHandler { impl<IO> postgres_backend::Handler<IO> for PageServerHandler
where
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
fn check_auth_jwt( fn check_auth_jwt(
&mut self, &mut self,
_pgb: &mut PostgresBackendTCP, _pgb: &mut PostgresBackend<IO>,
jwt_response: &[u8], jwt_response: &[u8],
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
// this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT // this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT
@@ -787,7 +830,7 @@ impl postgres_backend::Handler<tokio::net::TcpStream> for PageServerHandler {
fn startup( fn startup(
&mut self, &mut self,
_pgb: &mut PostgresBackendTCP, _pgb: &mut PostgresBackend<IO>,
_sm: &FeStartupPacket, _sm: &FeStartupPacket,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
Ok(()) Ok(())
@@ -795,7 +838,7 @@ impl postgres_backend::Handler<tokio::net::TcpStream> for PageServerHandler {
async fn process_query( async fn process_query(
&mut self, &mut self,
pgb: &mut PostgresBackendTCP, pgb: &mut PostgresBackend<IO>,
query_string: &str, query_string: &str,
) -> Result<(), QueryError> { ) -> Result<(), QueryError> {
let ctx = self.connection_ctx.attached_child(); let ctx = self.connection_ctx.attached_child();
+39 -9
View File
@@ -118,6 +118,10 @@ pub struct Tenant {
// Global pageserver config parameters // Global pageserver config parameters
pub conf: &'static PageServerConf, pub conf: &'static PageServerConf,
/// The value creation timestamp, used to measure activation delay, see:
/// <https://github.com/neondatabase/neon/issues/4025>
loading_started_at: Instant,
state: watch::Sender<TenantState>, state: watch::Sender<TenantState>,
// Overridden tenant-specific config parameters. // Overridden tenant-specific config parameters.
@@ -267,10 +271,7 @@ impl UninitializedTimeline<'_> {
.await .await
.context("Failed to flush after basebackup import")?; .context("Failed to flush after basebackup import")?;
// Initialize without loading the layer map. We started with an empty layer map, and already self.initialize(ctx)
// updated it for the layers that we created during the import.
let mut timelines = self.owning_tenant.timelines.lock().unwrap();
self.initialize_with_lock(ctx, &mut timelines, false, true)
} }
fn raw_timeline(&self) -> anyhow::Result<&Arc<Timeline>> { fn raw_timeline(&self) -> anyhow::Result<&Arc<Timeline>> {
@@ -1476,7 +1477,7 @@ impl Tenant {
TenantState::Loading | TenantState::Attaching => { TenantState::Loading | TenantState::Attaching => {
*current_state = TenantState::Active; *current_state = TenantState::Active;
info!("Activating tenant {}", self.tenant_id); debug!(tenant_id = %self.tenant_id, "Activating tenant");
let timelines_accessor = self.timelines.lock().unwrap(); let timelines_accessor = self.timelines.lock().unwrap();
let not_broken_timelines = timelines_accessor let not_broken_timelines = timelines_accessor
@@ -1487,12 +1488,17 @@ impl Tenant {
// down when they notice that the tenant is inactive. // down when they notice that the tenant is inactive.
tasks::start_background_loops(self.tenant_id); tasks::start_background_loops(self.tenant_id);
let mut activated_timelines = 0;
let mut timelines_broken_during_activation = 0;
for timeline in not_broken_timelines { for timeline in not_broken_timelines {
match timeline match timeline
.activate(ctx) .activate(ctx)
.context("timeline activation for activating tenant") .context("timeline activation for activating tenant")
{ {
Ok(()) => {} Ok(()) => {
activated_timelines += 1;
}
Err(e) => { Err(e) => {
error!( error!(
"Failed to activate timeline {}: {:#}", "Failed to activate timeline {}: {:#}",
@@ -1503,9 +1509,26 @@ impl Tenant {
"failed to activate timeline {}: {}", "failed to activate timeline {}: {}",
timeline.timeline_id, e timeline.timeline_id, e
)); ));
timelines_broken_during_activation += 1;
} }
} }
} }
let elapsed = self.loading_started_at.elapsed();
let total_timelines = timelines_accessor.len();
// log a lot of stuff, because some tenants sometimes suffer from user-visible
// times to activate. see https://github.com/neondatabase/neon/issues/4025
info!(
since_creation_millis = elapsed.as_millis(),
tenant_id = %self.tenant_id,
activated_timelines,
timelines_broken_during_activation,
total_timelines,
post_state = <&'static str>::from(&*current_state),
"activation attempt finished"
);
} }
} }
}); });
@@ -1812,6 +1835,9 @@ impl Tenant {
Tenant { Tenant {
tenant_id, tenant_id,
conf, conf,
// using now here is good enough approximation to catch tenants with really long
// activation times.
loading_started_at: Instant::now(),
tenant_conf: Arc::new(RwLock::new(tenant_conf)), tenant_conf: Arc::new(RwLock::new(tenant_conf)),
timelines: Mutex::new(HashMap::new()), timelines: Mutex::new(HashMap::new()),
gc_cs: tokio::sync::Mutex::new(()), gc_cs: tokio::sync::Mutex::new(()),
@@ -2326,8 +2352,6 @@ impl Tenant {
) )
})?; })?;
// Initialize the timeline without loading the layer map, because we already updated the layer
// map above, when we imported the datadir.
let timeline = { let timeline = {
let mut timelines = self.timelines.lock().unwrap(); let mut timelines = self.timelines.lock().unwrap();
raw_timeline.initialize_with_lock(ctx, &mut timelines, false, true)? raw_timeline.initialize_with_lock(ctx, &mut timelines, false, true)?
@@ -2857,7 +2881,13 @@ pub mod harness {
}; };
LOG_HANDLE.get_or_init(|| { LOG_HANDLE.get_or_init(|| {
logging::init(logging::LogFormat::Test).expect("Failed to init test logging") logging::init(
logging::LogFormat::Test,
// enable it in case in case the tests exercise code paths that use
// debug_assert_current_span_has_tenant_and_timeline_id
logging::TracingErrorLayerEnablement::EnableWithRustLogFilter,
)
.expect("Failed to init test logging")
}); });
let repo_dir = PageServerConf::test_repo_dir(test_name); let repo_dir = PageServerConf::test_repo_dir(test_name);
+9 -18
View File
@@ -48,11 +48,10 @@ mod layer_coverage;
use crate::context::RequestContext; use crate::context::RequestContext;
use crate::keyspace::KeyPartitioning; use crate::keyspace::KeyPartitioning;
use crate::metrics::NUM_ONDISK_LAYERS;
use crate::repository::Key; use crate::repository::Key;
use crate::tenant::storage_layer::InMemoryLayer; use crate::tenant::storage_layer::InMemoryLayer;
use crate::tenant::storage_layer::Layer; use crate::tenant::storage_layer::Layer;
use anyhow::{bail, Result}; use anyhow::Result;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
@@ -126,7 +125,7 @@ where
/// ///
/// Insert an on-disk layer. /// Insert an on-disk layer.
/// ///
pub fn insert_historic(&mut self, layer: Arc<L>) -> anyhow::Result<()> { pub fn insert_historic(&mut self, layer: Arc<L>) {
self.layer_map.insert_historic_noflush(layer) self.layer_map.insert_historic_noflush(layer)
} }
@@ -274,22 +273,16 @@ where
/// ///
/// Helper function for BatchedUpdates::insert_historic /// Helper function for BatchedUpdates::insert_historic
/// ///
pub(self) fn insert_historic_noflush(&mut self, layer: Arc<L>) -> anyhow::Result<()> { pub(self) fn insert_historic_noflush(&mut self, layer: Arc<L>) {
let key = historic_layer_coverage::LayerKey::from(&*layer); // TODO: See #3869, resulting #4088, attempted fix and repro #4094
if self.historic.contains(&key) { self.historic.insert(
bail!( historic_layer_coverage::LayerKey::from(&*layer),
"Attempt to insert duplicate layer {} in layer map", Arc::clone(&layer),
layer.short_id() );
);
}
self.historic.insert(key, Arc::clone(&layer));
if Self::is_l0(&layer) { if Self::is_l0(&layer) {
self.l0_delta_layers.push(layer); self.l0_delta_layers.push(layer);
} }
NUM_ONDISK_LAYERS.inc();
Ok(())
} }
/// ///
@@ -314,8 +307,6 @@ where
"failed to locate removed historic layer from l0_delta_layers" "failed to locate removed historic layer from l0_delta_layers"
); );
} }
NUM_ONDISK_LAYERS.dec();
} }
pub(self) fn replace_historic_noflush( pub(self) fn replace_historic_noflush(
@@ -843,7 +834,7 @@ mod tests {
let expected_in_counts = (1, usize::from(expected_l0)); let expected_in_counts = (1, usize::from(expected_l0));
map.batch_update().insert_historic(remote.clone()).unwrap(); map.batch_update().insert_historic(remote.clone());
assert_eq!(count_layer_in(&map, &remote), expected_in_counts); assert_eq!(count_layer_in(&map, &remote), expected_in_counts);
let replaced = map let replaced = map
@@ -417,14 +417,6 @@ impl<Value: Clone> BufferedHistoricLayerCoverage<Value> {
} }
} }
pub fn contains(&self, layer_key: &LayerKey) -> bool {
match self.buffer.get(layer_key) {
Some(None) => false, // layer remove was buffered
Some(_) => true, // layer insert was buffered
None => self.layers.contains_key(layer_key), // no buffered ops for this layer
}
}
pub fn insert(&mut self, layer_key: LayerKey, value: Value) { pub fn insert(&mut self, layer_key: LayerKey, value: Value) {
self.buffer.insert(layer_key, Some(value)); self.buffer.insert(layer_key, Some(value));
} }
+224 -61
View File
@@ -219,7 +219,8 @@ use utils::lsn::Lsn;
use crate::metrics::{ use crate::metrics::{
MeasureRemoteOp, RemoteOpFileKind, RemoteOpKind, RemoteTimelineClientMetrics, MeasureRemoteOp, RemoteOpFileKind, RemoteOpKind, RemoteTimelineClientMetrics,
REMOTE_ONDEMAND_DOWNLOADED_BYTES, REMOTE_ONDEMAND_DOWNLOADED_LAYERS, RemoteTimelineClientMetricsCallTrackSize, REMOTE_ONDEMAND_DOWNLOADED_BYTES,
REMOTE_ONDEMAND_DOWNLOADED_LAYERS,
}; };
use crate::tenant::remote_timeline_client::index::LayerFileMetadata; use crate::tenant::remote_timeline_client::index::LayerFileMetadata;
use crate::{ use crate::{
@@ -367,9 +368,13 @@ impl RemoteTimelineClient {
/// Download index file /// Download index file
pub async fn download_index_file(&self) -> Result<IndexPart, DownloadError> { pub async fn download_index_file(&self) -> Result<IndexPart, DownloadError> {
let _unfinished_gauge_guard = self let _unfinished_gauge_guard = self.metrics.call_begin(
.metrics &RemoteOpFileKind::Index,
.call_begin(&RemoteOpFileKind::Index, &RemoteOpKind::Download); &RemoteOpKind::Download,
crate::metrics::RemoteTimelineClientMetricsCallTrackSize::DontTrackSize {
reason: "no need for a downloads gauge",
},
);
download::download_index_part( download::download_index_part(
self.conf, self.conf,
@@ -398,9 +403,13 @@ impl RemoteTimelineClient {
layer_metadata: &LayerFileMetadata, layer_metadata: &LayerFileMetadata,
) -> anyhow::Result<u64> { ) -> anyhow::Result<u64> {
let downloaded_size = { let downloaded_size = {
let _unfinished_gauge_guard = self let _unfinished_gauge_guard = self.metrics.call_begin(
.metrics &RemoteOpFileKind::Layer,
.call_begin(&RemoteOpFileKind::Layer, &RemoteOpKind::Download); &RemoteOpKind::Download,
crate::metrics::RemoteTimelineClientMetricsCallTrackSize::DontTrackSize {
reason: "no need for a downloads gauge",
},
);
download::download_layer_file( download::download_layer_file(
self.conf, self.conf,
&self.storage_impl, &self.storage_impl,
@@ -886,11 +895,32 @@ impl RemoteTimelineClient {
fn calls_unfinished_metric_impl( fn calls_unfinished_metric_impl(
&self, &self,
op: &UploadOp, op: &UploadOp,
) -> Option<(RemoteOpFileKind, RemoteOpKind)> { ) -> Option<(
RemoteOpFileKind,
RemoteOpKind,
RemoteTimelineClientMetricsCallTrackSize,
)> {
use RemoteTimelineClientMetricsCallTrackSize::DontTrackSize;
let res = match op { let res = match op {
UploadOp::UploadLayer(_, _) => (RemoteOpFileKind::Layer, RemoteOpKind::Upload), UploadOp::UploadLayer(_, m) => (
UploadOp::UploadMetadata(_, _) => (RemoteOpFileKind::Index, RemoteOpKind::Upload), RemoteOpFileKind::Layer,
UploadOp::Delete(file_kind, _) => (*file_kind, RemoteOpKind::Delete), RemoteOpKind::Upload,
RemoteTimelineClientMetricsCallTrackSize::Bytes(m.file_size()),
),
UploadOp::UploadMetadata(_, _) => (
RemoteOpFileKind::Index,
RemoteOpKind::Upload,
DontTrackSize {
reason: "metadata uploads are tiny",
},
),
UploadOp::Delete(file_kind, _) => (
*file_kind,
RemoteOpKind::Delete,
DontTrackSize {
reason: "should we track deletes? positive or negative sign?",
},
),
UploadOp::Barrier(_) => { UploadOp::Barrier(_) => {
// we do not account these // we do not account these
return None; return None;
@@ -900,20 +930,20 @@ impl RemoteTimelineClient {
} }
fn calls_unfinished_metric_begin(&self, op: &UploadOp) { fn calls_unfinished_metric_begin(&self, op: &UploadOp) {
let (file_kind, op_kind) = match self.calls_unfinished_metric_impl(op) { let (file_kind, op_kind, track_bytes) = match self.calls_unfinished_metric_impl(op) {
Some(x) => x, Some(x) => x,
None => return, None => return,
}; };
let guard = self.metrics.call_begin(&file_kind, &op_kind); let guard = self.metrics.call_begin(&file_kind, &op_kind, track_bytes);
guard.will_decrement_manually(); // in unfinished_ops_metric_end() guard.will_decrement_manually(); // in unfinished_ops_metric_end()
} }
fn calls_unfinished_metric_end(&self, op: &UploadOp) { fn calls_unfinished_metric_end(&self, op: &UploadOp) {
let (file_kind, op_kind) = match self.calls_unfinished_metric_impl(op) { let (file_kind, op_kind, track_bytes) = match self.calls_unfinished_metric_impl(op) {
Some(x) => x, Some(x) => x,
None => return, None => return,
}; };
self.metrics.call_end(&file_kind, &op_kind); self.metrics.call_end(&file_kind, &op_kind, track_bytes);
} }
fn stop(&self) { fn stop(&self) {
@@ -981,11 +1011,19 @@ impl RemoteTimelineClient {
mod tests { mod tests {
use super::*; use super::*;
use crate::{ use crate::{
tenant::harness::{TenantHarness, TIMELINE_ID}, context::RequestContext,
tenant::{
harness::{TenantHarness, TIMELINE_ID},
Tenant,
},
DEFAULT_PG_VERSION, DEFAULT_PG_VERSION,
}; };
use remote_storage::{RemoteStorageConfig, RemoteStorageKind}; use remote_storage::{RemoteStorageConfig, RemoteStorageKind};
use std::{collections::HashSet, path::Path}; use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use tokio::runtime::EnterGuard;
use utils::lsn::Lsn; use utils::lsn::Lsn;
pub(super) fn dummy_contents(name: &str) -> Vec<u8> { pub(super) fn dummy_contents(name: &str) -> Vec<u8> {
@@ -1034,39 +1072,80 @@ mod tests {
assert_eq!(found, expected); assert_eq!(found, expected);
} }
struct TestSetup {
runtime: &'static tokio::runtime::Runtime,
entered_runtime: EnterGuard<'static>,
harness: TenantHarness<'static>,
tenant: Arc<Tenant>,
tenant_ctx: RequestContext,
remote_fs_dir: PathBuf,
client: Arc<RemoteTimelineClient>,
}
impl TestSetup {
fn new(test_name: &str) -> anyhow::Result<Self> {
// Use a current-thread runtime in the test
let runtime = Box::leak(Box::new(
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?,
));
let entered_runtime = runtime.enter();
let test_name = Box::leak(Box::new(format!("remote_timeline_client__{test_name}")));
let harness = TenantHarness::create(test_name)?;
let (tenant, ctx) = runtime.block_on(harness.load());
// create an empty timeline directory
let timeline =
tenant.create_empty_timeline(TIMELINE_ID, Lsn(0), DEFAULT_PG_VERSION, &ctx)?;
let _ = timeline.initialize(&ctx).unwrap();
let remote_fs_dir = harness.conf.workdir.join("remote_fs");
std::fs::create_dir_all(remote_fs_dir)?;
let remote_fs_dir = std::fs::canonicalize(harness.conf.workdir.join("remote_fs"))?;
let storage_config = RemoteStorageConfig {
max_concurrent_syncs: std::num::NonZeroUsize::new(
remote_storage::DEFAULT_REMOTE_STORAGE_MAX_CONCURRENT_SYNCS,
)
.unwrap(),
max_sync_errors: std::num::NonZeroU32::new(
remote_storage::DEFAULT_REMOTE_STORAGE_MAX_SYNC_ERRORS,
)
.unwrap(),
storage: RemoteStorageKind::LocalFs(remote_fs_dir.clone()),
};
let storage = GenericRemoteStorage::from_config(&storage_config).unwrap();
let client = Arc::new(RemoteTimelineClient {
conf: harness.conf,
runtime,
tenant_id: harness.tenant_id,
timeline_id: TIMELINE_ID,
storage_impl: storage,
upload_queue: Mutex::new(UploadQueue::Uninitialized),
metrics: Arc::new(RemoteTimelineClientMetrics::new(
&harness.tenant_id,
&TIMELINE_ID,
)),
});
Ok(Self {
runtime,
entered_runtime,
harness,
tenant,
tenant_ctx: ctx,
remote_fs_dir,
client,
})
}
}
// Test scheduling // Test scheduling
#[test] #[test]
fn upload_scheduling() -> anyhow::Result<()> { fn upload_scheduling() -> anyhow::Result<()> {
// Use a current-thread runtime in the test
let runtime = Box::leak(Box::new(
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?,
));
let _entered = runtime.enter();
let harness = TenantHarness::create("upload_scheduling")?;
let (tenant, ctx) = runtime.block_on(harness.load());
let _timeline =
tenant.create_empty_timeline(TIMELINE_ID, Lsn(0), DEFAULT_PG_VERSION, &ctx)?;
let timeline_path = harness.timeline_path(&TIMELINE_ID);
let remote_fs_dir = harness.conf.workdir.join("remote_fs");
std::fs::create_dir_all(remote_fs_dir)?;
let remote_fs_dir = std::fs::canonicalize(harness.conf.workdir.join("remote_fs"))?;
let storage_config = RemoteStorageConfig {
max_concurrent_syncs: std::num::NonZeroUsize::new(
remote_storage::DEFAULT_REMOTE_STORAGE_MAX_CONCURRENT_SYNCS,
)
.unwrap(),
max_sync_errors: std::num::NonZeroU32::new(
remote_storage::DEFAULT_REMOTE_STORAGE_MAX_SYNC_ERRORS,
)
.unwrap(),
storage: RemoteStorageKind::LocalFs(remote_fs_dir.clone()),
};
// Test outline: // Test outline:
// //
// Schedule upload of a bunch of layers. Check that they are started immediately, not queued // Schedule upload of a bunch of layers. Check that they are started immediately, not queued
@@ -1081,21 +1160,19 @@ mod tests {
// Schedule another deletion. Check that it's launched immediately. // Schedule another deletion. Check that it's launched immediately.
// Schedule index upload. Check that it's queued // Schedule index upload. Check that it's queued
println!("workdir: {}", harness.conf.workdir.display()); let TestSetup {
let storage_impl = GenericRemoteStorage::from_config(&storage_config)?;
let client = Arc::new(RemoteTimelineClient {
conf: harness.conf,
runtime, runtime,
tenant_id: harness.tenant_id, entered_runtime: _entered_runtime,
timeline_id: TIMELINE_ID, harness,
storage_impl, tenant: _tenant,
upload_queue: Mutex::new(UploadQueue::Uninitialized), tenant_ctx: _tenant_ctx,
metrics: Arc::new(RemoteTimelineClientMetrics::new( remote_fs_dir,
&harness.tenant_id, client,
&TIMELINE_ID, } = TestSetup::new("upload_scheduling").unwrap();
)),
}); let timeline_path = harness.timeline_path(&TIMELINE_ID);
println!("workdir: {}", harness.conf.workdir.display());
let remote_timeline_dir = let remote_timeline_dir =
remote_fs_dir.join(timeline_path.strip_prefix(&harness.conf.workdir)?); remote_fs_dir.join(timeline_path.strip_prefix(&harness.conf.workdir)?);
@@ -1216,4 +1293,90 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn bytes_unfinished_gauge_for_layer_file_uploads() -> anyhow::Result<()> {
// Setup
let TestSetup {
runtime,
harness,
client,
..
} = TestSetup::new("metrics")?;
let metadata = dummy_metadata(Lsn(0x10));
client.init_upload_queue_for_empty_remote(&metadata)?;
let timeline_path = harness.timeline_path(&TIMELINE_ID);
let layer_file_name_1: LayerFileName = "000000000000000000000000000000000000-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF__00000000016B59D8-00000000016B5A51".parse().unwrap();
let content_1 = dummy_contents("foo");
std::fs::write(
timeline_path.join(layer_file_name_1.file_name()),
&content_1,
)?;
#[derive(Debug, PartialEq)]
struct BytesStartedFinished {
started: Option<usize>,
finished: Option<usize>,
}
let get_bytes_started_stopped = || {
let started = client
.metrics
.get_bytes_started_counter_value(&RemoteOpFileKind::Layer, &RemoteOpKind::Upload)
.map(|v| v.try_into().unwrap());
let stopped = client
.metrics
.get_bytes_finished_counter_value(&RemoteOpFileKind::Layer, &RemoteOpKind::Upload)
.map(|v| v.try_into().unwrap());
BytesStartedFinished {
started,
finished: stopped,
}
};
// Test
let init = get_bytes_started_stopped();
client.schedule_layer_file_upload(
&layer_file_name_1,
&LayerFileMetadata::new(content_1.len() as u64),
)?;
let pre = get_bytes_started_stopped();
runtime.block_on(client.wait_completion())?;
let post = get_bytes_started_stopped();
// Validate
assert_eq!(
init,
BytesStartedFinished {
started: None,
finished: None
}
);
assert_eq!(
pre,
BytesStartedFinished {
started: Some(content_1.len()),
// assert that the _finished metric is created eagerly so that subtractions work on first sample
finished: Some(0),
}
);
assert_eq!(
post,
BytesStartedFinished {
started: Some(content_1.len()),
finished: Some(content_1.len())
}
);
Ok(())
}
} }
@@ -16,6 +16,7 @@ use tracing::{info, warn};
use crate::config::PageServerConf; use crate::config::PageServerConf;
use crate::tenant::storage_layer::LayerFileName; use crate::tenant::storage_layer::LayerFileName;
use crate::tenant::timeline::debug_assert_current_span_has_tenant_and_timeline_id;
use crate::{exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS}; use crate::{exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS};
use remote_storage::{DownloadError, GenericRemoteStorage}; use remote_storage::{DownloadError, GenericRemoteStorage};
use utils::crashsafe::path_with_suffix_extension; use utils::crashsafe::path_with_suffix_extension;
@@ -43,6 +44,8 @@ pub async fn download_layer_file<'a>(
layer_file_name: &'a LayerFileName, layer_file_name: &'a LayerFileName,
layer_metadata: &'a LayerFileMetadata, layer_metadata: &'a LayerFileMetadata,
) -> Result<u64, DownloadError> { ) -> Result<u64, DownloadError> {
debug_assert_current_span_has_tenant_and_timeline_id();
let timeline_path = conf.timeline_path(&timeline_id, &tenant_id); let timeline_path = conf.timeline_path(&timeline_id, &tenant_id);
let local_path = timeline_path.join(layer_file_name.file_name()); let local_path = timeline_path.join(layer_file_name.file_name());
@@ -154,7 +157,7 @@ pub async fn download_layer_file<'a>(
.with_context(|| format!("Could not fsync layer file {}", local_path.display(),)) .with_context(|| format!("Could not fsync layer file {}", local_path.display(),))
.map_err(DownloadError::Other)?; .map_err(DownloadError::Other)?;
tracing::info!("download complete: {}", local_path.display()); tracing::debug!("download complete: {}", local_path.display());
Ok(bytes_amount) Ok(bytes_amount)
} }
+53 -19
View File
@@ -48,7 +48,7 @@ use crate::tenant::{
use crate::config::PageServerConf; use crate::config::PageServerConf;
use crate::keyspace::{KeyPartitioning, KeySpace}; use crate::keyspace::{KeyPartitioning, KeySpace};
use crate::metrics::TimelineMetrics; use crate::metrics::{TimelineMetrics, UNEXPECTED_ONDEMAND_DOWNLOADS};
use crate::pgdatadir_mapping::LsnForTimestamp; use crate::pgdatadir_mapping::LsnForTimestamp;
use crate::pgdatadir_mapping::{is_rel_fsm_block_key, is_rel_vm_block_key}; use crate::pgdatadir_mapping::{is_rel_fsm_block_key, is_rel_vm_block_key};
use crate::pgdatadir_mapping::{BlockNumber, CalculateLogicalSizeError}; use crate::pgdatadir_mapping::{BlockNumber, CalculateLogicalSizeError};
@@ -936,6 +936,7 @@ impl Timeline {
} }
} }
#[instrument(skip_all, fields(tenant = %self.tenant_id, timeline = %self.timeline_id))]
pub async fn download_layer(&self, layer_file_name: &str) -> anyhow::Result<Option<bool>> { pub async fn download_layer(&self, layer_file_name: &str) -> anyhow::Result<Option<bool>> {
let Some(layer) = self.find_layer(layer_file_name) else { return Ok(None) }; let Some(layer) = self.find_layer(layer_file_name) else { return Ok(None) };
let Some(remote_layer) = layer.downcast_remote_layer() else { return Ok(Some(false)) }; let Some(remote_layer) = layer.downcast_remote_layer() else { return Ok(Some(false)) };
@@ -1483,7 +1484,7 @@ impl Timeline {
trace!("found layer {}", layer.path().display()); trace!("found layer {}", layer.path().display());
total_physical_size += file_size; total_physical_size += file_size;
updates.insert_historic(Arc::new(layer))?; updates.insert_historic(Arc::new(layer));
num_layers += 1; num_layers += 1;
} else if let Some(deltafilename) = DeltaFileName::parse_str(&fname) { } else if let Some(deltafilename) = DeltaFileName::parse_str(&fname) {
// Create a DeltaLayer struct for each delta file. // Create a DeltaLayer struct for each delta file.
@@ -1515,7 +1516,7 @@ impl Timeline {
trace!("found layer {}", layer.path().display()); trace!("found layer {}", layer.path().display());
total_physical_size += file_size; total_physical_size += file_size;
updates.insert_historic(Arc::new(layer))?; updates.insert_historic(Arc::new(layer));
num_layers += 1; num_layers += 1;
} else if fname == METADATA_FILE_NAME || fname.ends_with(".old") { } else if fname == METADATA_FILE_NAME || fname.ends_with(".old") {
// ignore these // ignore these
@@ -1589,7 +1590,7 @@ impl Timeline {
// remote index file? // remote index file?
// If so, rename_to_backup those files & replace their local layer with // If so, rename_to_backup those files & replace their local layer with
// a RemoteLayer in the layer map so that we re-download them on-demand. // a RemoteLayer in the layer map so that we re-download them on-demand.
if let Some(local_layer) = &local_layer { if let Some(local_layer) = local_layer {
let local_layer_path = local_layer let local_layer_path = local_layer
.local_path() .local_path()
.expect("caller must ensure that local_layers only contains local layers"); .expect("caller must ensure that local_layers only contains local layers");
@@ -1614,6 +1615,7 @@ impl Timeline {
anyhow::bail!("could not rename file {local_layer_path:?}: {err:?}"); anyhow::bail!("could not rename file {local_layer_path:?}: {err:?}");
} else { } else {
self.metrics.resident_physical_size_gauge.sub(local_size); self.metrics.resident_physical_size_gauge.sub(local_size);
updates.remove_historic(local_layer);
// fall-through to adding the remote layer // fall-through to adding the remote layer
} }
} else { } else {
@@ -1649,11 +1651,7 @@ impl Timeline {
); );
let remote_layer = Arc::new(remote_layer); let remote_layer = Arc::new(remote_layer);
if let Some(local_layer) = &local_layer { updates.insert_historic(remote_layer);
updates.replace_historic(local_layer, remote_layer)?;
} else {
updates.insert_historic(remote_layer)?;
}
} }
LayerFileName::Delta(deltafilename) => { LayerFileName::Delta(deltafilename) => {
// Create a RemoteLayer for the delta file. // Create a RemoteLayer for the delta file.
@@ -1677,11 +1675,7 @@ impl Timeline {
LayerAccessStats::for_loading_layer(LayerResidenceStatus::Evicted), LayerAccessStats::for_loading_layer(LayerResidenceStatus::Evicted),
); );
let remote_layer = Arc::new(remote_layer); let remote_layer = Arc::new(remote_layer);
if let Some(local_layer) = &local_layer { updates.insert_historic(remote_layer);
updates.replace_historic(local_layer, remote_layer)?;
} else {
updates.insert_historic(remote_layer)?;
}
} }
} }
} }
@@ -2355,6 +2349,7 @@ impl Timeline {
id, id,
ctx.task_kind() ctx.task_kind()
); );
UNEXPECTED_ONDEMAND_DOWNLOADS.inc();
timeline.download_remote_layer(remote_layer).await?; timeline.download_remote_layer(remote_layer).await?;
continue 'layer_map_search; continue 'layer_map_search;
} }
@@ -2728,7 +2723,7 @@ impl Timeline {
.write() .write()
.unwrap() .unwrap()
.batch_update() .batch_update()
.insert_historic(Arc::new(new_delta))?; .insert_historic(Arc::new(new_delta));
// update the timeline's physical size // update the timeline's physical size
let sz = new_delta_path.metadata()?.len(); let sz = new_delta_path.metadata()?.len();
@@ -2933,7 +2928,7 @@ impl Timeline {
self.metrics self.metrics
.resident_physical_size_gauge .resident_physical_size_gauge
.add(metadata.len()); .add(metadata.len());
updates.insert_historic(Arc::new(l))?; updates.insert_historic(Arc::new(l));
} }
updates.flush(); updates.flush();
drop(layers); drop(layers);
@@ -3366,7 +3361,7 @@ impl Timeline {
new_layer_paths.insert(new_delta_path, LayerFileMetadata::new(metadata.len())); new_layer_paths.insert(new_delta_path, LayerFileMetadata::new(metadata.len()));
let x: Arc<dyn PersistentLayer + 'static> = Arc::new(l); let x: Arc<dyn PersistentLayer + 'static> = Arc::new(l);
updates.insert_historic(x)?; updates.insert_historic(x);
} }
// Now that we have reshuffled the data to set of new delta layers, we can // Now that we have reshuffled the data to set of new delta layers, we can
@@ -3818,11 +3813,13 @@ impl Timeline {
/// If the caller has a deadline or needs a timeout, they can simply stop polling: /// If the caller has a deadline or needs a timeout, they can simply stop polling:
/// we're **cancellation-safe** because the download happens in a separate task_mgr task. /// we're **cancellation-safe** because the download happens in a separate task_mgr task.
/// So, the current download attempt will run to completion even if we stop polling. /// So, the current download attempt will run to completion even if we stop polling.
#[instrument(skip_all, fields(tenant_id=%self.tenant_id, timeline_id=%self.timeline_id, layer=%remote_layer.short_id()))] #[instrument(skip_all, fields(layer=%remote_layer.short_id()))]
pub async fn download_remote_layer( pub async fn download_remote_layer(
&self, &self,
remote_layer: Arc<RemoteLayer>, remote_layer: Arc<RemoteLayer>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
debug_assert_current_span_has_tenant_and_timeline_id();
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
let permit = match Arc::clone(&remote_layer.ongoing_download) let permit = match Arc::clone(&remote_layer.ongoing_download)
@@ -3866,6 +3863,8 @@ impl Timeline {
.await; .await;
if let Ok(size) = &result { if let Ok(size) = &result {
info!("layer file download finished");
// XXX the temp file is still around in Err() case // XXX the temp file is still around in Err() case
// and consumes space until we clean up upon pageserver restart. // and consumes space until we clean up upon pageserver restart.
self_clone.metrics.resident_physical_size_gauge.add(*size); self_clone.metrics.resident_physical_size_gauge.add(*size);
@@ -3937,6 +3936,8 @@ impl Timeline {
updates.flush(); updates.flush();
drop(layers); drop(layers);
info!("on-demand download successful");
// Now that we've inserted the download into the layer map, // Now that we've inserted the download into the layer map,
// close the semaphore. This will make other waiters for // close the semaphore. This will make other waiters for
// this download return Ok(()). // this download return Ok(()).
@@ -3944,7 +3945,7 @@ impl Timeline {
remote_layer.ongoing_download.close(); remote_layer.ongoing_download.close();
} else { } else {
// Keep semaphore open. We'll drop the permit at the end of the function. // Keep semaphore open. We'll drop the permit at the end of the function.
error!("on-demand download failed: {:?}", result.as_ref().unwrap_err()); error!("layer file download failed: {:?}", result.as_ref().unwrap_err());
} }
// Don't treat it as an error if the task that triggered the download // Don't treat it as an error if the task that triggered the download
@@ -4255,3 +4256,36 @@ fn rename_to_backup(path: &Path) -> anyhow::Result<()> {
bail!("couldn't find an unused backup number for {:?}", path) bail!("couldn't find an unused backup number for {:?}", path)
} }
#[cfg(not(debug_assertions))]
#[inline]
pub(crate) fn debug_assert_current_span_has_tenant_and_timeline_id() {}
#[cfg(debug_assertions)]
#[inline]
pub(crate) fn debug_assert_current_span_has_tenant_and_timeline_id() {
use utils::tracing_span_assert;
pub static TENANT_ID_EXTRACTOR: once_cell::sync::Lazy<
tracing_span_assert::MultiNameExtractor<2>,
> = once_cell::sync::Lazy::new(|| {
tracing_span_assert::MultiNameExtractor::new("TenantId", ["tenant_id", "tenant"])
});
pub static TIMELINE_ID_EXTRACTOR: once_cell::sync::Lazy<
tracing_span_assert::MultiNameExtractor<2>,
> = once_cell::sync::Lazy::new(|| {
tracing_span_assert::MultiNameExtractor::new("TimelineId", ["timeline_id", "timeline"])
});
match tracing_span_assert::check_fields_present([
&*TENANT_ID_EXTRACTOR,
&*TIMELINE_ID_EXTRACTOR,
]) {
Ok(()) => (),
Err(missing) => panic!(
"missing extractors: {:?}",
missing.into_iter().map(|e| e.name()).collect::<Vec<_>>()
),
}
}
@@ -348,7 +348,7 @@ impl ConnectionManagerState {
.context("walreceiver connection handling failure") .context("walreceiver connection handling failure")
} }
.instrument( .instrument(
info_span!("walreceiver_connection", id = %id, node_id = %new_sk.safekeeper_id), info_span!("walreceiver_connection", tenant_id = %id.tenant_id, timeline_id = %id.timeline_id, node_id = %new_sk.safekeeper_id),
) )
}); });
@@ -37,8 +37,8 @@ use crate::{
use postgres_backend::is_expected_io_error; use postgres_backend::is_expected_io_error;
use postgres_connection::PgConnectionConfig; use postgres_connection::PgConnectionConfig;
use postgres_ffi::waldecoder::WalStreamDecoder; use postgres_ffi::waldecoder::WalStreamDecoder;
use pq_proto::PageserverFeedback;
use utils::lsn::Lsn; use utils::lsn::Lsn;
use utils::pageserver_feedback::PageserverFeedback;
/// Status of the connection. /// Status of the connection.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -319,12 +319,12 @@ pub(super) async fn handle_walreceiver_connection(
timeline.get_remote_consistent_lsn().unwrap_or(Lsn(0)); timeline.get_remote_consistent_lsn().unwrap_or(Lsn(0));
// The last LSN we processed. It is not guaranteed to survive pageserver crash. // The last LSN we processed. It is not guaranteed to survive pageserver crash.
let last_received_lsn = u64::from(last_lsn); let last_received_lsn = last_lsn;
// `disk_consistent_lsn` is the LSN at which page server guarantees local persistence of all received data // `disk_consistent_lsn` is the LSN at which page server guarantees local persistence of all received data
let disk_consistent_lsn = u64::from(timeline.get_disk_consistent_lsn()); let disk_consistent_lsn = timeline.get_disk_consistent_lsn();
// The last LSN that is synced to remote storage and is guaranteed to survive pageserver crash // The last LSN that is synced to remote storage and is guaranteed to survive pageserver crash
// Used by safekeepers to remove WAL preceding `remote_consistent_lsn`. // Used by safekeepers to remove WAL preceding `remote_consistent_lsn`.
let remote_consistent_lsn = u64::from(timeline_remote_consistent_lsn); let remote_consistent_lsn = timeline_remote_consistent_lsn;
let ts = SystemTime::now(); let ts = SystemTime::now();
// Update the status about what we just received. This is shown in the mgmt API. // Update the status about what we just received. This is shown in the mgmt API.
+68 -1
View File
@@ -370,6 +370,74 @@ lfc_cache_contains(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno)
return found; return found;
} }
/*
* Evict a page (if present) from the local file cache
*/
void
lfc_evict(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno)
{
BufferTag tag;
FileCacheEntry* entry;
ssize_t rc;
bool found;
int chunk_offs = blkno & (BLOCKS_PER_CHUNK-1);
uint32 hash;
if (lfc_size_limit == 0) /* fast exit if file cache is disabled */
return;
INIT_BUFFERTAG(tag, rnode, forkNum, (blkno & ~(BLOCKS_PER_CHUNK-1)));
hash = get_hash_value(lfc_hash, &tag);
LWLockAcquire(lfc_lock, LW_EXCLUSIVE);
entry = hash_search_with_hash_value(lfc_hash, &tag, hash, HASH_FIND, &found);
if (!found)
{
/* nothing to do */
LWLockRelease(lfc_lock);
return;
}
/* remove the page from the cache */
entry->bitmap[chunk_offs >> 5] &= ~(1 << (chunk_offs & (32 - 1)));
/*
* If the chunk has no live entries, we can position the chunk to be
* recycled first.
*/
if (entry->bitmap[chunk_offs >> 5] == 0)
{
bool has_remaining_pages;
for (int i = 0; i < (BLOCKS_PER_CHUNK / 32); i++) {
if (entry->bitmap[i] != 0)
{
has_remaining_pages = true;
break;
}
}
/*
* Put the entry at the position that is first to be reclaimed when
* we have no cached pages remaining in the chunk
*/
if (!has_remaining_pages)
{
dlist_delete(&entry->lru_node);
dlist_push_head(&lfc_ctl->lru, &entry->lru_node);
}
}
/*
* Done: apart from empty chunks, we don't move chunks in the LRU when
* they're empty because eviction isn't usage.
*/
LWLockRelease(lfc_lock);
}
/* /*
* Try to read page from local cache. * Try to read page from local cache.
* Returns true if page is found in local cache. * Returns true if page is found in local cache.
@@ -528,7 +596,6 @@ lfc_write(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno,
LWLockRelease(lfc_lock); LWLockRelease(lfc_lock);
} }
/* /*
* Record structure holding the to be exposed cache data. * Record structure holding the to be exposed cache data.
*/ */
+6
View File
@@ -17,6 +17,8 @@
#include "pagestore_client.h" #include "pagestore_client.h"
#include "fmgr.h" #include "fmgr.h"
#include "access/xlog.h" #include "access/xlog.h"
#include "access/xlogutils.h"
#include "storage/buf_internals.h"
#include "libpq-fe.h" #include "libpq-fe.h"
#include "libpq/pqformat.h" #include "libpq/pqformat.h"
@@ -57,6 +59,8 @@ int n_unflushed_requests = 0;
int flush_every_n_requests = 8; int flush_every_n_requests = 8;
int readahead_buffer_size = 128; int readahead_buffer_size = 128;
bool (*old_redo_read_buffer_filter) (XLogReaderState *record, uint8 block_id) = NULL;
static void pageserver_flush(void); static void pageserver_flush(void);
static bool static bool
@@ -467,6 +471,8 @@ pg_init_libpagestore(void)
smgr_hook = smgr_neon; smgr_hook = smgr_neon;
smgr_init_hook = smgr_init_neon; smgr_init_hook = smgr_init_neon;
dbsize_hook = neon_dbsize; dbsize_hook = neon_dbsize;
old_redo_read_buffer_filter = redo_read_buffer_filter;
redo_read_buffer_filter = neon_redo_read_buffer_filter;
} }
lfc_init(); lfc_init();
} }
+1
View File
@@ -24,6 +24,7 @@
#include "neon.h" #include "neon.h"
#include "walproposer.h" #include "walproposer.h"
#include "pagestore_client.h"
PG_MODULE_MAGIC; PG_MODULE_MAGIC;
void _PG_init(void); void _PG_init(void);
+8
View File
@@ -11,6 +11,7 @@
#ifndef NEON_H #ifndef NEON_H
#define NEON_H #define NEON_H
#include "access/xlogreader.h"
/* GUCs */ /* GUCs */
extern char *neon_auth_token; extern char *neon_auth_token;
@@ -20,4 +21,11 @@ extern char *neon_tenant;
extern void pg_init_libpagestore(void); extern void pg_init_libpagestore(void);
extern void pg_init_walproposer(void); extern void pg_init_walproposer(void);
/*
* Returns true if we shouldn't do REDO on that block in record indicated by
* block_id; false otherwise.
*/
extern bool neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id);
extern bool (*old_redo_read_buffer_filter) (XLogReaderState *record, uint8 block_id);
#endif /* NEON_H */ #endif /* NEON_H */
+1
View File
@@ -207,6 +207,7 @@ extern void forget_cached_relsize(RelFileNode rnode, ForkNumber forknum);
extern void lfc_write(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno, char *buffer); extern void lfc_write(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno, char *buffer);
extern bool lfc_read(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno, char *buffer); extern bool lfc_read(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno, char *buffer);
extern bool lfc_cache_contains(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno); extern bool lfc_cache_contains(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno);
extern void lfc_evict(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno);
extern void lfc_init(void); extern void lfc_init(void);
+192 -2
View File
@@ -189,6 +189,7 @@ typedef struct PrfHashEntry {
#define SH_DEFINE #define SH_DEFINE
#define SH_DECLARE #define SH_DECLARE
#include "lib/simplehash.h" #include "lib/simplehash.h"
#include "neon.h"
/* /*
* PrefetchState maintains the state of (prefetch) getPage@LSN requests. * PrefetchState maintains the state of (prefetch) getPage@LSN requests.
@@ -1209,6 +1210,9 @@ neon_wallog_page(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, ch
if (ShutdownRequestPending) if (ShutdownRequestPending)
return; return;
/* Don't log any pages if we're not allowed to do so. */
if (!XLogInsertAllowed())
return;
/* /*
* Whenever a VM or FSM page is evicted, WAL-log it. FSM and (some) VM * Whenever a VM or FSM page is evicted, WAL-log it. FSM and (some) VM
@@ -1375,8 +1379,18 @@ neon_get_request_lsn(bool *latest, RelFileNode rnode, ForkNumber forknum, BlockN
if (RecoveryInProgress()) if (RecoveryInProgress())
{ {
/*
* We don't know if WAL has been generated but not yet replayed, so
* we're conservative in our estimates about latest pages.
*/
*latest = false; *latest = false;
lsn = GetXLogReplayRecPtr(NULL);
/*
* Get the last written LSN of this page.
*/
lsn = GetLastWrittenLSN(rnode, forknum, blkno);
lsn = nm_adjust_lsn(lsn);
elog(DEBUG1, "neon_get_request_lsn GetXLogReplayRecPtr %X/%X request lsn 0 ", elog(DEBUG1, "neon_get_request_lsn GetXLogReplayRecPtr %X/%X request lsn 0 ",
(uint32) ((lsn) >> 32), (uint32) (lsn)); (uint32) ((lsn) >> 32), (uint32) (lsn));
} }
@@ -1559,6 +1573,15 @@ neon_create(SMgrRelation reln, ForkNumber forkNum, bool isRedo)
/* /*
* Newly created relation is empty, remember that in the relsize cache. * Newly created relation is empty, remember that in the relsize cache.
* *
* Note that in REDO, this is called to make sure the relation fork exists,
* but it does not truncate the relation. So, we can only update the
* relsize if it didn't exist before.
*
* Also, in redo, we must make sure to update the cached size of the
* relation, as that is the primary source of truth for REDO's
* file length considerations, and as file extension isn't (perfectly)
* logged, we need to take care of that before we hit file size checks.
*
* FIXME: This is currently not just an optimization, but required for * FIXME: This is currently not just an optimization, but required for
* correctness. Postgres can call smgrnblocks() on the newly-created * correctness. Postgres can call smgrnblocks() on the newly-created
* relation. Currently, we don't call SetLastWrittenLSN() when a new * relation. Currently, we don't call SetLastWrittenLSN() when a new
@@ -1566,7 +1589,14 @@ neon_create(SMgrRelation reln, ForkNumber forkNum, bool isRedo)
* cache, we might call smgrnblocks() on the newly-created relation before * cache, we might call smgrnblocks() on the newly-created relation before
* the creation WAL record hass been received by the page server. * the creation WAL record hass been received by the page server.
*/ */
set_cached_relsize(reln->smgr_rnode.node, forkNum, 0); if (isRedo)
{
update_cached_relsize(reln->smgr_rnode.node, forkNum, 0);
get_cached_relsize(reln->smgr_rnode.node, forkNum,
&reln->smgr_cached_nblocks[forkNum]);
}
else
set_cached_relsize(reln->smgr_rnode.node, forkNum, 0);
#ifdef DEBUG_COMPARE_LOCAL #ifdef DEBUG_COMPARE_LOCAL
if (IS_LOCAL_REL(reln)) if (IS_LOCAL_REL(reln))
@@ -1831,6 +1861,26 @@ neon_read_at_lsn(RelFileNode rnode, ForkNumber forkNum, BlockNumber blkno,
.blockNum = blkno, .blockNum = blkno,
}; };
/*
* The redo process does not lock pages that it needs to replay but are
* not in the shared buffers, so a concurrent process may request the
* page after redo has decided it won't redo that page and updated the
* LwLSN for that page.
* If we're in hot standby we need to take care that we don't return
* until after REDO has finished replaying up to that LwLSN, as the page
* should have been locked up to that point.
*
* See also the description on neon_redo_read_buffer_filter below.
*
* NOTE: It is possible that the WAL redo process will still do IO due to
* concurrent failed read IOs. Those IOs should never have a request_lsn
* that is as large as the WAL record we're currently replaying, if it
* weren't for the behaviour of the LwLsn cache that uses the highest
* value of the LwLsn cache when the entry is not found.
*/
if (RecoveryInProgress() && !(MyBackendType == B_STARTUP))
XLogWaitForReplayOf(request_lsn);
/* /*
* Try to find prefetched page in the list of received pages. * Try to find prefetched page in the list of received pages.
*/ */
@@ -2584,3 +2634,143 @@ smgr_init_neon(void)
smgr_init_standard(); smgr_init_standard();
neon_init(); neon_init();
} }
/*
* Return whether we can skip the redo for this block.
*
* The conditions for skipping the IO are:
*
* - The block is not in the shared buffers, and
* - The block is not in the local file cache
*
* ... because any subsequent read of the page requires us to read
* the new version of the page from the PageServer. We do not
* check the local file cache; we instead evict the page from LFC: it
* is cheaper than going through the FS calls to read the page, and
* limits the number of lock operations used in the REDO process.
*
* We have one exception to the rules for skipping IO: We always apply
* changes to shared catalogs' pages. Although this is mostly out of caution,
* catalog updates usually result in backends rebuilding their catalog snapshot,
* which means it's quite likely the modified page is going to be used soon.
*
* It is important to note that skipping WAL redo for a page also means
* the page isn't locked by the redo process, as there is no Buffer
* being returned, nor is there a buffer descriptor to lock.
* This means that any IO that wants to read this block needs to wait
* for the WAL REDO process to finish processing the WAL record before
* it allows the system to start reading the block, as releasing the
* block early could lead to phantom reads.
*
* For example, REDO for a WAL record that modifies 3 blocks could skip
* the first block, wait for a lock on the second, and then modify the
* third block. Without skipping, all blocks would be locked and phantom
* reads would not occur, but with skipping, a concurrent process could
* read block 1 with post-REDO contents and read block 3 with pre-REDO
* contents, where with REDO locking it would wait on block 1 and see
* block 3 with post-REDO contents only.
*/
bool
neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id)
{
XLogRecPtr end_recptr = record->EndRecPtr;
XLogRecPtr prev_end_recptr = record->ReadRecPtr - 1;
RelFileNode rnode;
ForkNumber forknum;
BlockNumber blkno;
BufferTag tag;
uint32 hash;
LWLock *partitionLock;
Buffer buffer;
bool no_redo_needed;
BlockNumber relsize;
if (old_redo_read_buffer_filter && old_redo_read_buffer_filter(record, block_id))
return true;
#if PG_VERSION_NUM < 150000
if (!XLogRecGetBlockTag(record, block_id, &rnode, &forknum, &blkno))
elog(PANIC, "failed to locate backup block with ID %d", block_id);
#else
XLogRecGetBlockTag(record, block_id, &rnode, &forknum, &blkno);
#endif
/*
* Out of an abundance of caution, we always run redo on shared catalogs,
* regardless of whether the block is stored in shared buffers.
* See also this function's top comment.
*/
if (!OidIsValid(rnode.dbNode))
return false;
INIT_BUFFERTAG(tag, rnode, forknum, blkno);
hash = BufTableHashCode(&tag);
partitionLock = BufMappingPartitionLock(hash);
/*
* Lock the partition of shared_buffers so that it can't be updated
* concurrently.
*/
LWLockAcquire(partitionLock, LW_SHARED);
/* Try to find the relevant buffer */
buffer = BufTableLookup(&tag, hash);
no_redo_needed = buffer < 0;
/* we don't have the buffer in memory, update lwLsn past this record */
if (no_redo_needed)
{
SetLastWrittenLSNForBlock(end_recptr, rnode, forknum, blkno);
lfc_evict(rnode, forknum, blkno);
}
else
{
SetLastWrittenLSNForBlock(prev_end_recptr, rnode, forknum, blkno);
}
LWLockRelease(partitionLock);
/* Extend the relation if we know its size */
if (get_cached_relsize(rnode, forknum, &relsize))
{
if (relsize < blkno + 1)
update_cached_relsize(rnode, forknum, blkno + 1);
}
else
{
/*
* Size was not cached. We populate the cache now, with the size of the
* relation measured after this WAL record is applied.
*
* This length is later reused when we open the smgr to read the block,
* which is fine and expected.
*/
NeonResponse *response;
NeonNblocksResponse *nbresponse;
NeonNblocksRequest request = {
.req = (NeonRequest) {
.lsn = end_recptr,
.latest = false,
.tag = T_NeonNblocksRequest,
},
.rnode = rnode,
.forknum = forknum,
};
response = page_server_request(&request);
Assert(response->tag == T_NeonNblocksResponse);
nbresponse = (NeonNblocksResponse *) response;
Assert(nbresponse->n_blocks > blkno);
set_cached_relsize(rnode, forknum, nbresponse->n_blocks);
elog(SmgrTrace, "Set length to %d", nbresponse->n_blocks);
}
return no_redo_needed;
}
+14 -6
View File
@@ -1964,18 +1964,26 @@ CombineHotStanbyFeedbacks(HotStandbyFeedback * hs)
{ {
if (safekeeper[i].appendResponse.hs.ts != 0) if (safekeeper[i].appendResponse.hs.ts != 0)
{ {
if (FullTransactionIdPrecedes(safekeeper[i].appendResponse.hs.xmin, hs->xmin)) HotStandbyFeedback *skhs = &safekeeper[i].appendResponse.hs;
if (FullTransactionIdIsNormal(skhs->xmin)
&& FullTransactionIdPrecedes(skhs->xmin, hs->xmin))
{ {
hs->xmin = safekeeper[i].appendResponse.hs.xmin; hs->xmin = skhs->xmin;
hs->ts = safekeeper[i].appendResponse.hs.ts; hs->ts = skhs->ts;
} }
if (FullTransactionIdPrecedes(safekeeper[i].appendResponse.hs.catalog_xmin, hs->catalog_xmin)) if (FullTransactionIdIsNormal(skhs->catalog_xmin)
&& FullTransactionIdPrecedes(skhs->catalog_xmin, hs->xmin))
{ {
hs->catalog_xmin = safekeeper[i].appendResponse.hs.catalog_xmin; hs->catalog_xmin = skhs->catalog_xmin;
hs->ts = safekeeper[i].appendResponse.hs.ts; hs->ts = skhs->ts;
} }
} }
} }
if (hs->xmin.value == ~0)
hs->xmin = InvalidFullTransactionId;
if (hs->catalog_xmin.value == ~0)
hs->catalog_xmin = InvalidFullTransactionId;
} }
/* /*
+2
View File
@@ -62,6 +62,8 @@ utils.workspace = true
uuid.workspace = true uuid.workspace = true
webpki-roots.workspace = true webpki-roots.workspace = true
x509-parser.workspace = true x509-parser.workspace = true
native-tls.workspace = true
postgres-native-tls.workspace = true
workspace_hack.workspace = true workspace_hack.workspace = true
tokio-util.workspace = true tokio-util.workspace = true
+12
View File
@@ -9,6 +9,7 @@ use crate::{
use pq_proto::BeMessage as Be; use pq_proto::BeMessage as Be;
use thiserror::Error; use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tokio_postgres::config::SslMode;
use tracing::{info, info_span}; use tracing::{info, info_span};
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -87,6 +88,16 @@ pub(super) async fn authenticate(
.dbname(&db_info.dbname) .dbname(&db_info.dbname)
.user(&db_info.user); .user(&db_info.user);
// Backwards compatibility. pg_sni_proxy uses "--" in domain names
// while direct connections do not. Once we migrate to pg_sni_proxy
// everywhere, we can remove this.
if db_info.host.contains("--") {
// we need TLS connection with SNI info to properly route it
config.ssl_mode(SslMode::Require);
} else {
config.ssl_mode(SslMode::Disable);
}
if let Some(password) = db_info.password { if let Some(password) = db_info.password {
config.password(password.as_ref()); config.password(password.as_ref());
} }
@@ -96,6 +107,7 @@ pub(super) async fn authenticate(
value: NodeInfo { value: NodeInfo {
config, config,
aux: db_info.aux.into(), aux: db_info.aux.into(),
allow_self_signed_compute: false, // caller may override
}, },
}) })
} }
+250
View File
@@ -0,0 +1,250 @@
/// A stand-alone program that routes connections, e.g. from
/// `aaa--bbb--1234.external.domain` to `aaa.bbb.internal.domain:1234`.
///
/// This allows connecting to pods/services running in the same Kubernetes cluster from
/// the outside. Similar to an ingress controller for HTTPS.
use std::{net::SocketAddr, sync::Arc};
use tokio::net::TcpListener;
use anyhow::{anyhow, bail, ensure, Context};
use clap::{self, Arg};
use futures::TryFutureExt;
use proxy::console::messages::MetricsAuxInfo;
use proxy::stream::{PqStream, Stream};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::sync::CancellationToken;
use utils::{project_git_version, sentry_init::init_sentry};
use tracing::{error, info, warn};
project_git_version!(GIT_VERSION);
fn cli() -> clap::Command {
clap::Command::new("Neon proxy/router")
.version(GIT_VERSION)
.arg(
Arg::new("listen")
.short('l')
.long("listen")
.help("listen for incoming client connections on ip:port")
.default_value("127.0.0.1:4432"),
)
.arg(
Arg::new("tls-key")
.short('k')
.long("tls-key")
.help("path to TLS key for client postgres connections")
.required(true),
)
.arg(
Arg::new("tls-cert")
.short('c')
.long("tls-cert")
.help("path to TLS cert for client postgres connections")
.required(true),
)
.arg(
Arg::new("dest")
.short('d')
.long("destination")
.help("append this domain zone to the SNI hostname to get the destination address")
.required(true),
)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _logging_guard = proxy::logging::init().await?;
let _panic_hook_guard = utils::logging::replace_panic_hook_with_tracing_panic_hook();
let _sentry_guard = init_sentry(Some(GIT_VERSION.into()), &[]);
let args = cli().get_matches();
let destination: String = args.get_one::<String>("dest").unwrap().parse()?;
// Configure TLS
let tls_config: Arc<rustls::ServerConfig> = match (
args.get_one::<String>("tls-key"),
args.get_one::<String>("tls-cert"),
) {
(Some(key_path), Some(cert_path)) => {
let key = {
let key_bytes = std::fs::read(key_path).context("TLS key file")?;
let mut keys = rustls_pemfile::pkcs8_private_keys(&mut &key_bytes[..])
.context(format!("Failed to read TLS keys at '{key_path}'"))?;
ensure!(keys.len() == 1, "keys.len() = {} (should be 1)", keys.len());
keys.pop().map(rustls::PrivateKey).unwrap()
};
let cert_chain_bytes = std::fs::read(cert_path)
.context(format!("Failed to read TLS cert file at '{cert_path}.'"))?;
let cert_chain = {
rustls_pemfile::certs(&mut &cert_chain_bytes[..])
.context(format!(
"Failed to read TLS certificate chain from bytes from file at '{cert_path}'."
))?
.into_iter()
.map(rustls::Certificate)
.collect()
};
rustls::ServerConfig::builder()
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
.with_no_client_auth()
.with_single_cert(cert_chain, key)?
.into()
}
_ => bail!("tls-key and tls-cert must be specified"),
};
// Start listening for incoming client connections
let proxy_address: SocketAddr = args.get_one::<String>("listen").unwrap().parse()?;
info!("Starting sni router on {proxy_address}");
let proxy_listener = TcpListener::bind(proxy_address).await?;
let cancellation_token = CancellationToken::new();
let main = proxy::flatten_err(tokio::spawn(task_main(
Arc::new(destination),
tls_config,
proxy_listener,
cancellation_token.clone(),
)));
let signals_task = proxy::flatten_err(tokio::spawn(proxy::handle_signals(cancellation_token)));
tokio::select! {
res = main => { res?; },
res = signals_task => { res?; },
}
Ok(())
}
async fn task_main(
dest_suffix: Arc<String>,
tls_config: Arc<rustls::ServerConfig>,
listener: tokio::net::TcpListener,
cancellation_token: CancellationToken,
) -> anyhow::Result<()> {
// When set for the server socket, the keepalive setting
// will be inherited by all accepted client sockets.
socket2::SockRef::from(&listener).set_keepalive(true)?;
let mut connections = tokio::task::JoinSet::new();
loop {
tokio::select! {
accept_result = listener.accept() => {
let (socket, peer_addr) = accept_result?;
info!("accepted postgres client connection from {peer_addr}");
let session_id = uuid::Uuid::new_v4();
let tls_config = Arc::clone(&tls_config);
let dest_suffix = Arc::clone(&dest_suffix);
connections.spawn(
async move {
info!("spawned a task for {peer_addr}");
socket
.set_nodelay(true)
.context("failed to set socket option")?;
handle_client(dest_suffix, tls_config, session_id, socket).await
}
.unwrap_or_else(|e| {
// Acknowledge that the task has finished with an error.
error!("per-client task finished with an error: {e:#}");
}),
);
}
_ = cancellation_token.cancelled() => {
drop(listener);
break;
}
}
}
// Drain connections
info!("waiting for all client connections to finish");
while let Some(res) = connections.join_next().await {
if let Err(e) = res {
if !e.is_panic() && !e.is_cancelled() {
warn!("unexpected error from joined connection task: {e:?}");
}
}
}
info!("all client connections have finished");
Ok(())
}
const ERR_INSECURE_CONNECTION: &str = "connection is insecure (try using `sslmode=require`)";
async fn ssl_handshake<S: AsyncRead + AsyncWrite + Unpin>(
raw_stream: S,
tls_config: Arc<rustls::ServerConfig>,
) -> anyhow::Result<Stream<S>> {
let mut stream = PqStream::new(Stream::from_raw(raw_stream));
let msg = stream.read_startup_packet().await?;
info!("received {msg:?}");
use pq_proto::FeStartupPacket::*;
match msg {
SslRequest => {
stream
.write_message(&pq_proto::BeMessage::EncryptionResponse(true))
.await?;
// Upgrade raw stream into a secure TLS-backed stream.
// NOTE: We've consumed `tls`; this fact will be used later.
let (raw, read_buf) = stream.into_inner();
// TODO: Normally, client doesn't send any data before
// server says TLS handshake is ok and read_buf is empy.
// However, you could imagine pipelining of postgres
// SSLRequest + TLS ClientHello in one hunk similar to
// pipelining in our node js driver. We should probably
// support that by chaining read_buf with the stream.
if !read_buf.is_empty() {
bail!("data is sent before server replied with EncryptionResponse");
}
Ok(raw.upgrade(tls_config).await?)
}
_ => stream.throw_error_str(ERR_INSECURE_CONNECTION).await?,
}
}
#[tracing::instrument(fields(session_id = ?session_id), skip_all)]
async fn handle_client(
dest_suffix: Arc<String>,
tls_config: Arc<rustls::ServerConfig>,
session_id: uuid::Uuid,
stream: impl AsyncRead + AsyncWrite + Unpin,
) -> anyhow::Result<()> {
let tls_stream = ssl_handshake(stream, tls_config).await?;
// Cut off first part of the SNI domain
// We receive required destination details in the format of
// `{k8s_service_name}--{k8s_namespace}--{port}.non-sni-domain`
let sni = tls_stream.sni_hostname().ok_or(anyhow!("SNI missing"))?;
let dest: Vec<&str> = sni
.split_once('.')
.context("invalid SNI")?
.0
.splitn(3, "--")
.collect();
let port = dest[2].parse::<u16>().context("invalid port")?;
let destination = format!("{}.{}.{}:{}", dest[0], dest[1], dest_suffix, port);
info!("destination: {}", destination);
let client = tokio::net::TcpStream::connect(destination).await?;
let metrics_aux: MetricsAuxInfo = Default::default();
proxy::proxy::proxy_pass(tls_stream, client, &metrics_aux).await
}
+31 -68
View File
@@ -1,49 +1,23 @@
//! Postgres protocol proxy/router. use proxy::auth;
//! use proxy::console;
//! This service listens psql port and can check auth via external service use proxy::http;
//! (control plane API in our case) and can create new databases and accounts use proxy::metrics;
//! in somewhat transparent manner (again via communication with control plane API).
mod auth; use anyhow::bail;
mod cache;
mod cancellation;
mod compute;
mod config;
mod console;
mod error;
mod http;
mod logging;
mod metrics;
mod parse;
mod proxy;
mod sasl;
mod scram;
mod stream;
mod url;
mod waiters;
use anyhow::{bail, Context};
use clap::{self, Arg}; use clap::{self, Arg};
use config::ProxyConfig; use proxy::config::{self, ProxyConfig};
use futures::FutureExt; use std::{borrow::Cow, net::SocketAddr};
use std::{borrow::Cow, future::Future, net::SocketAddr}; use tokio::net::TcpListener;
use tokio::{net::TcpListener, task::JoinError};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{info, warn}; use tracing::info;
use tracing::warn;
use utils::{project_git_version, sentry_init::init_sentry}; use utils::{project_git_version, sentry_init::init_sentry};
project_git_version!(GIT_VERSION); project_git_version!(GIT_VERSION);
/// Flattens `Result<Result<T>>` into `Result<T>`.
async fn flatten_err(
f: impl Future<Output = Result<anyhow::Result<()>, JoinError>>,
) -> anyhow::Result<()> {
f.map(|r| r.context("join error").and_then(|x| x)).await
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let _logging_guard = logging::init().await?; let _logging_guard = proxy::logging::init().await?;
let _panic_hook_guard = utils::logging::replace_panic_hook_with_tracing_panic_hook(); let _panic_hook_guard = utils::logging::replace_panic_hook_with_tracing_panic_hook();
let _sentry_guard = init_sentry(Some(GIT_VERSION.into()), &[]); let _sentry_guard = init_sentry(Some(GIT_VERSION.into()), &[]);
@@ -69,7 +43,7 @@ async fn main() -> anyhow::Result<()> {
let proxy_listener = TcpListener::bind(proxy_address).await?; let proxy_listener = TcpListener::bind(proxy_address).await?;
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();
let mut client_tasks = vec![tokio::spawn(proxy::task_main( let mut client_tasks = vec![tokio::spawn(proxy::proxy::task_main(
config, config,
proxy_listener, proxy_listener,
cancellation_token.clone(), cancellation_token.clone(),
@@ -88,7 +62,7 @@ async fn main() -> anyhow::Result<()> {
} }
let mut tasks = vec![ let mut tasks = vec![
tokio::spawn(handle_signals(cancellation_token)), tokio::spawn(proxy::handle_signals(cancellation_token)),
tokio::spawn(http::server::task_main(http_listener)), tokio::spawn(http::server::task_main(http_listener)),
tokio::spawn(console::mgmt::task_main(mgmt_listener)), tokio::spawn(console::mgmt::task_main(mgmt_listener)),
]; ];
@@ -97,8 +71,9 @@ async fn main() -> anyhow::Result<()> {
tasks.push(tokio::spawn(metrics::task_main(metrics_config))); tasks.push(tokio::spawn(metrics::task_main(metrics_config)));
} }
let tasks = futures::future::try_join_all(tasks.into_iter().map(flatten_err)); let tasks = futures::future::try_join_all(tasks.into_iter().map(proxy::flatten_err));
let client_tasks = futures::future::try_join_all(client_tasks.into_iter().map(flatten_err)); let client_tasks =
futures::future::try_join_all(client_tasks.into_iter().map(proxy::flatten_err));
tokio::select! { tokio::select! {
// We are only expecting an error from these forever tasks // We are only expecting an error from these forever tasks
res = tasks => { res?; }, res = tasks => { res?; },
@@ -107,33 +82,6 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
/// Handle unix signals appropriately.
async fn handle_signals(token: CancellationToken) -> anyhow::Result<()> {
use tokio::signal::unix::{signal, SignalKind};
let mut hangup = signal(SignalKind::hangup())?;
let mut interrupt = signal(SignalKind::interrupt())?;
let mut terminate = signal(SignalKind::terminate())?;
loop {
tokio::select! {
// Hangup is commonly used for config reload.
_ = hangup.recv() => {
warn!("received SIGHUP; config reload is not supported");
}
// Shut down the whole application.
_ = interrupt.recv() => {
warn!("received SIGINT, exiting immediately");
bail!("interrupted");
}
_ = terminate.recv() => {
warn!("received SIGTERM, shutting down once all existing connections have closed");
token.cancel();
}
}
}
}
/// ProxyConfig is created at proxy startup, and lives forever. /// ProxyConfig is created at proxy startup, and lives forever.
fn build_config(args: &clap::ArgMatches) -> anyhow::Result<&'static ProxyConfig> { fn build_config(args: &clap::ArgMatches) -> anyhow::Result<&'static ProxyConfig> {
let tls_config = match ( let tls_config = match (
@@ -149,6 +97,14 @@ fn build_config(args: &clap::ArgMatches) -> anyhow::Result<&'static ProxyConfig>
_ => bail!("either both or neither tls-key and tls-cert must be specified"), _ => bail!("either both or neither tls-key and tls-cert must be specified"),
}; };
let allow_self_signed_compute: bool = args
.get_one::<String>("allow-self-signed-compute")
.unwrap()
.parse()?;
if allow_self_signed_compute {
warn!("allowing self-signed compute certificates");
}
let metric_collection = match ( let metric_collection = match (
args.get_one::<String>("metric-collection-endpoint"), args.get_one::<String>("metric-collection-endpoint"),
args.get_one::<String>("metric-collection-interval"), args.get_one::<String>("metric-collection-interval"),
@@ -198,6 +154,7 @@ fn build_config(args: &clap::ArgMatches) -> anyhow::Result<&'static ProxyConfig>
tls_config, tls_config,
auth_backend, auth_backend,
metric_collection, metric_collection,
allow_self_signed_compute,
})); }));
Ok(config) Ok(config)
@@ -288,6 +245,12 @@ fn cli() -> clap::Command {
.help("cache for `wake_compute` api method (use `size=0` to disable)") .help("cache for `wake_compute` api method (use `size=0` to disable)")
.default_value(config::CacheOptions::DEFAULT_OPTIONS_NODE_INFO), .default_value(config::CacheOptions::DEFAULT_OPTIONS_NODE_INFO),
) )
.arg(
Arg::new("allow-self-signed-compute")
.long("allow-self-signed-compute")
.help("Allow self-signed certificates for compute nodes (for testing)")
.default_value("false"),
)
} }
#[cfg(test)] #[cfg(test)]
+59 -15
View File
@@ -1,11 +1,11 @@
use crate::{cancellation::CancelClosure, error::UserFacingError}; use crate::{cancellation::CancelClosure, error::UserFacingError};
use futures::TryFutureExt; use futures::{FutureExt, TryFutureExt};
use itertools::Itertools; use itertools::Itertools;
use pq_proto::StartupMessageParams; use pq_proto::StartupMessageParams;
use std::{io, net::SocketAddr}; use std::{io, net::SocketAddr, time::Duration};
use thiserror::Error; use thiserror::Error;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_postgres::NoTls; use tokio_postgres::tls::MakeTlsConnect;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
const COULD_NOT_CONNECT: &str = "Couldn't connect to compute node"; const COULD_NOT_CONNECT: &str = "Couldn't connect to compute node";
@@ -19,6 +19,9 @@ pub enum ConnectionError {
#[error("{COULD_NOT_CONNECT}: {0}")] #[error("{COULD_NOT_CONNECT}: {0}")]
CouldNotConnect(#[from] io::Error), CouldNotConnect(#[from] io::Error),
#[error("{COULD_NOT_CONNECT}: {0}")]
TlsError(#[from] native_tls::Error),
} }
impl UserFacingError for ConnectionError { impl UserFacingError for ConnectionError {
@@ -125,14 +128,34 @@ impl std::ops::DerefMut for ConnCfg {
} }
} }
impl Default for ConnCfg {
fn default() -> Self {
Self::new()
}
}
impl ConnCfg { impl ConnCfg {
/// Establish a raw TCP connection to the compute node. /// Establish a raw TCP connection to the compute node.
async fn connect_raw(&self) -> io::Result<(SocketAddr, TcpStream)> { async fn connect_raw(&self) -> io::Result<(SocketAddr, TcpStream, &str)> {
use tokio_postgres::config::Host; use tokio_postgres::config::Host;
// wrap TcpStream::connect with timeout
let connect_with_timeout = |host, port| {
let connection_timeout = Duration::from_millis(10000);
tokio::time::timeout(connection_timeout, TcpStream::connect((host, port))).map(
move |res| match res {
Ok(tcpstream_connect_res) => tcpstream_connect_res,
Err(_) => Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("exceeded connection timeout {connection_timeout:?}"),
)),
},
)
};
let connect_once = |host, port| { let connect_once = |host, port| {
info!("trying to connect to compute node at {host}:{port}"); info!("trying to connect to compute node at {host}:{port}");
TcpStream::connect((host, port)).and_then(|socket| async { connect_with_timeout(host, port).and_then(|socket| async {
let socket_addr = socket.peer_addr()?; let socket_addr = socket.peer_addr()?;
// This prevents load balancer from severing the connection. // This prevents load balancer from severing the connection.
socket2::SockRef::from(&socket).set_keepalive(true)?; socket2::SockRef::from(&socket).set_keepalive(true)?;
@@ -165,9 +188,8 @@ impl ConnCfg {
Host::Unix(_) => continue, // unix sockets are not welcome here Host::Unix(_) => continue, // unix sockets are not welcome here
}; };
// TODO: maybe we should add a timeout.
match connect_once(host, *port).await { match connect_once(host, *port).await {
Ok(socket) => return Ok(socket), Ok((sockaddr, stream)) => return Ok((sockaddr, stream, host)),
Err(err) => { Err(err) => {
// We can't throw an error here, as there might be more hosts to try. // We can't throw an error here, as there might be more hosts to try.
warn!("couldn't connect to compute node at {host}:{port}: {err}"); warn!("couldn't connect to compute node at {host}:{port}: {err}");
@@ -187,7 +209,10 @@ impl ConnCfg {
pub struct PostgresConnection { pub struct PostgresConnection {
/// Socket connected to a compute node. /// Socket connected to a compute node.
pub stream: TcpStream, pub stream: tokio_postgres::maybe_tls_stream::MaybeTlsStream<
tokio::net::TcpStream,
postgres_native_tls::TlsStream<tokio::net::TcpStream>,
>,
/// PostgreSQL connection parameters. /// PostgreSQL connection parameters.
pub params: std::collections::HashMap<String, String>, pub params: std::collections::HashMap<String, String>,
/// Query cancellation token. /// Query cancellation token.
@@ -195,11 +220,27 @@ pub struct PostgresConnection {
} }
impl ConnCfg { impl ConnCfg {
async fn do_connect(&self) -> Result<PostgresConnection, ConnectionError> { async fn do_connect(
// TODO: establish a secure connection to the DB. &self,
let (socket_addr, mut stream) = self.connect_raw().await?; allow_self_signed_compute: bool,
let (client, connection) = self.0.connect_raw(&mut stream, NoTls).await?; ) -> Result<PostgresConnection, ConnectionError> {
info!("connected to compute node at {socket_addr}"); let (socket_addr, stream, host) = self.connect_raw().await?;
let tls_connector = native_tls::TlsConnector::builder()
.danger_accept_invalid_certs(allow_self_signed_compute)
.build()
.unwrap();
let mut mk_tls = postgres_native_tls::MakeTlsConnector::new(tls_connector);
let tls = MakeTlsConnect::<tokio::net::TcpStream>::make_tls_connect(&mut mk_tls, host)?;
// connect_raw() will not use TLS if sslmode is "disable"
let (client, connection) = self.0.connect_raw(stream, tls).await?;
let stream = connection.stream.into_inner();
info!(
"connected to compute node at {host} ({socket_addr}) sslmode={:?}",
self.0.get_ssl_mode()
);
// This is very ugly but as of now there's no better way to // This is very ugly but as of now there's no better way to
// extract the connection parameters from tokio-postgres' connection. // extract the connection parameters from tokio-postgres' connection.
@@ -220,8 +261,11 @@ impl ConnCfg {
} }
/// Connect to a corresponding compute node. /// Connect to a corresponding compute node.
pub async fn connect(&self) -> Result<PostgresConnection, ConnectionError> { pub async fn connect(
self.do_connect() &self,
allow_self_signed_compute: bool,
) -> Result<PostgresConnection, ConnectionError> {
self.do_connect(allow_self_signed_compute)
.inspect_err(|err| { .inspect_err(|err| {
// Immediately log the error we have at our disposal. // Immediately log the error we have at our disposal.
error!("couldn't connect to compute node: {err}"); error!("couldn't connect to compute node: {err}");
+1
View File
@@ -12,6 +12,7 @@ pub struct ProxyConfig {
pub tls_config: Option<TlsConfig>, pub tls_config: Option<TlsConfig>,
pub auth_backend: auth::BackendType<'static, ()>, pub auth_backend: auth::BackendType<'static, ()>,
pub metric_collection: Option<MetricCollectionConfig>, pub metric_collection: Option<MetricCollectionConfig>,
pub allow_self_signed_compute: bool,
} }
#[derive(Debug)] #[derive(Debug)]
+3
View File
@@ -170,6 +170,9 @@ pub struct NodeInfo {
/// Labels for proxy's metrics. /// Labels for proxy's metrics.
pub aux: Arc<MetricsAuxInfo>, pub aux: Arc<MetricsAuxInfo>,
/// Whether we should accept self-signed certificates (for testing)
pub allow_self_signed_compute: bool,
} }
pub type NodeInfoCache = TimedLru<Arc<str>, NodeInfo>; pub type NodeInfoCache = TimedLru<Arc<str>, NodeInfo>;
+4 -1
View File
@@ -8,6 +8,7 @@ use crate::{auth::ClientCredentials, compute, error::io_error, scram, url::ApiUr
use async_trait::async_trait; use async_trait::async_trait;
use futures::TryFutureExt; use futures::TryFutureExt;
use thiserror::Error; use thiserror::Error;
use tokio_postgres::config::SslMode;
use tracing::{error, info, info_span, warn, Instrument}; use tracing::{error, info, info_span, warn, Instrument};
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -86,11 +87,13 @@ impl Api {
let mut config = compute::ConnCfg::new(); let mut config = compute::ConnCfg::new();
config config
.host(self.endpoint.host_str().unwrap_or("localhost")) .host(self.endpoint.host_str().unwrap_or("localhost"))
.port(self.endpoint.port().unwrap_or(5432)); .port(self.endpoint.port().unwrap_or(5432))
.ssl_mode(SslMode::Disable);
let node = NodeInfo { let node = NodeInfo {
config, config,
aux: Default::default(), aux: Default::default(),
allow_self_signed_compute: false,
}; };
Ok(node) Ok(node)
+3 -1
View File
@@ -8,6 +8,7 @@ use super::{
use crate::{auth::ClientCredentials, compute, http, scram}; use crate::{auth::ClientCredentials, compute, http, scram};
use async_trait::async_trait; use async_trait::async_trait;
use futures::TryFutureExt; use futures::TryFutureExt;
use tokio_postgres::config::SslMode;
use tracing::{error, info, info_span, warn, Instrument}; use tracing::{error, info, info_span, warn, Instrument};
#[derive(Clone)] #[derive(Clone)]
@@ -100,11 +101,12 @@ impl Api {
// We'll set username and such later using the startup message. // We'll set username and such later using the startup message.
// TODO: add more type safety (in progress). // TODO: add more type safety (in progress).
let mut config = compute::ConnCfg::new(); let mut config = compute::ConnCfg::new();
config.host(host).port(port); config.host(host).port(port).ssl_mode(SslMode::Disable); // TLS is not configured on compute nodes.
let node = NodeInfo { let node = NodeInfo {
config, config,
aux: body.aux.into(), aux: body.aux.into(),
allow_self_signed_compute: false,
}; };
Ok(node) Ok(node)
+57
View File
@@ -0,0 +1,57 @@
use anyhow::{bail, Context};
use futures::{Future, FutureExt};
use tokio::task::JoinError;
use tokio_util::sync::CancellationToken;
use tracing::warn;
pub mod auth;
pub mod cache;
pub mod cancellation;
pub mod compute;
pub mod config;
pub mod console;
pub mod error;
pub mod http;
pub mod logging;
pub mod metrics;
pub mod parse;
pub mod proxy;
pub mod sasl;
pub mod scram;
pub mod stream;
pub mod url;
pub mod waiters;
/// Handle unix signals appropriately.
pub async fn handle_signals(token: CancellationToken) -> anyhow::Result<()> {
use tokio::signal::unix::{signal, SignalKind};
let mut hangup = signal(SignalKind::hangup())?;
let mut interrupt = signal(SignalKind::interrupt())?;
let mut terminate = signal(SignalKind::terminate())?;
loop {
tokio::select! {
// Hangup is commonly used for config reload.
_ = hangup.recv() => {
warn!("received SIGHUP; config reload is not supported");
}
// Shut down the whole application.
_ = interrupt.recv() => {
warn!("received SIGINT, exiting immediately");
bail!("interrupted");
}
_ = terminate.recv() => {
warn!("received SIGTERM, shutting down once all existing connections have closed");
token.cancel();
}
}
}
}
/// Flattens `Result<Result<T>>` into `Result<T>`.
pub async fn flatten_err(
f: impl Future<Output = Result<anyhow::Result<()>, JoinError>>,
) -> anyhow::Result<()> {
f.map(|r| r.context("join error").and_then(|x| x)).await
}
+1 -1
View File
@@ -95,7 +95,7 @@ fn gather_proxy_io_bytes_per_client() -> Vec<(Ids, (u64, DateTime<Utc>))> {
current_metrics.push(( current_metrics.push((
Ids { Ids {
endpoint_id: endpoint_id.to_string(), endpoint_id: endpoint_id.to_string(),
branch_id: "".to_string(), branch_id: branch_id.to_string(),
}, },
(value, Utc::now()), (value, Utc::now()),
)); ));
+23 -6
View File
@@ -95,9 +95,9 @@ pub async fn task_main(
handle_client(config, &cancel_map, session_id, socket).await handle_client(config, &cancel_map, session_id, socket).await
} }
.unwrap_or_else(|e| { .unwrap_or_else(move |e| {
// Acknowledge that the task has finished with an error. // Acknowledge that the task has finished with an error.
error!("per-client task finished with an error: {e:#}"); error!(?session_id, "per-client task finished with an error: {e:#}");
}), }),
); );
} }
@@ -155,7 +155,7 @@ pub async fn handle_ws_client(
async { result }.or_else(|e| stream.throw_error(e)).await? async { result }.or_else(|e| stream.throw_error(e)).await?
}; };
let client = Client::new(stream, creds, &params, session_id); let client = Client::new(stream, creds, &params, session_id, false);
cancel_map cancel_map
.with_session(|session| client.connect_to_db(session, true)) .with_session(|session| client.connect_to_db(session, true))
.await .await
@@ -194,7 +194,15 @@ async fn handle_client(
async { result }.or_else(|e| stream.throw_error(e)).await? async { result }.or_else(|e| stream.throw_error(e)).await?
}; };
let client = Client::new(stream, creds, &params, session_id); let allow_self_signed_compute = config.allow_self_signed_compute;
let client = Client::new(
stream,
creds,
&params,
session_id,
allow_self_signed_compute,
);
cancel_map cancel_map
.with_session(|session| client.connect_to_db(session, false)) .with_session(|session| client.connect_to_db(session, false))
.await .await
@@ -297,9 +305,11 @@ async fn connect_to_compute_once(
NUM_CONNECTION_FAILURES.with_label_values(&[label]).inc(); NUM_CONNECTION_FAILURES.with_label_values(&[label]).inc();
}; };
let allow_self_signed_compute = node_info.allow_self_signed_compute;
node_info node_info
.config .config
.connect() .connect(allow_self_signed_compute)
.inspect_err(invalidate_cache) .inspect_err(invalidate_cache)
.await .await
} }
@@ -378,7 +388,7 @@ async fn prepare_client_connection(
/// Forward bytes in both directions (client <-> compute). /// Forward bytes in both directions (client <-> compute).
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn proxy_pass( pub async fn proxy_pass(
client: impl AsyncRead + AsyncWrite + Unpin, client: impl AsyncRead + AsyncWrite + Unpin,
compute: impl AsyncRead + AsyncWrite + Unpin, compute: impl AsyncRead + AsyncWrite + Unpin,
aux: &MetricsAuxInfo, aux: &MetricsAuxInfo,
@@ -420,6 +430,8 @@ struct Client<'a, S> {
params: &'a StartupMessageParams, params: &'a StartupMessageParams,
/// Unique connection ID. /// Unique connection ID.
session_id: uuid::Uuid, session_id: uuid::Uuid,
/// Allow self-signed certificates (for testing).
allow_self_signed_compute: bool,
} }
impl<'a, S> Client<'a, S> { impl<'a, S> Client<'a, S> {
@@ -429,12 +441,14 @@ impl<'a, S> Client<'a, S> {
creds: auth::BackendType<'a, auth::ClientCredentials<'a>>, creds: auth::BackendType<'a, auth::ClientCredentials<'a>>,
params: &'a StartupMessageParams, params: &'a StartupMessageParams,
session_id: uuid::Uuid, session_id: uuid::Uuid,
allow_self_signed_compute: bool,
) -> Self { ) -> Self {
Self { Self {
stream, stream,
creds, creds,
params, params,
session_id, session_id,
allow_self_signed_compute,
} }
} }
} }
@@ -451,6 +465,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin> Client<'_, S> {
mut creds, mut creds,
params, params,
session_id, session_id,
allow_self_signed_compute,
} = self; } = self;
let extra = console::ConsoleReqExtra { let extra = console::ConsoleReqExtra {
@@ -473,6 +488,8 @@ impl<S: AsyncRead + AsyncWrite + Unpin> Client<'_, S> {
value: mut node_info, value: mut node_info,
} = auth_result; } = auth_result;
node_info.allow_self_signed_compute = allow_self_signed_compute;
let mut node = connect_to_compute(&mut node_info, params, &extra, &creds) let mut node = connect_to_compute(&mut node_info, params, &extra, &creds)
.or_else(|e| stream.throw_error(e)) .or_else(|e| stream.throw_error(e))
.await?; .await?;
+10 -5
View File
@@ -1,4 +1,5 @@
#!/bin/bash #!/usr/bin/env bash
set -euo pipefail
# If you save this in your path under the name "cargo-zclippy" (or whatever # If you save this in your path under the name "cargo-zclippy" (or whatever
# name you like), then you can run it as "cargo zclippy" from the shell prompt. # name you like), then you can run it as "cargo zclippy" from the shell prompt.
@@ -8,7 +9,11 @@
# warnings and errors right in the editor. # warnings and errors right in the editor.
# In vscode, this setting is Rust-analyzer>Check On Save:Command # In vscode, this setting is Rust-analyzer>Check On Save:Command
# * `-A unknown_lints` do not warn about unknown lint suppressions # NB: the CI runs the full feature powerset, so, it catches slightly more errors
# that people with newer toolchains might use # at the expense of longer runtime. This script is used by developers, so, don't
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status) # do that here.
cargo clippy --locked --all --all-targets --all-features -- -A unknown_lints -D warnings
thisscript="${BASH_SOURCE[0]}"
thisscript_dir="$(dirname "$thisscript")"
CLIPPY_COMMON_ARGS="$( source .neon_clippy_args; echo "$CLIPPY_COMMON_ARGS")"
exec cargo clippy --all-features $CLIPPY_COMMON_ARGS
+3 -3
View File
@@ -19,11 +19,13 @@ git-version.workspace = true
hex.workspace = true hex.workspace = true
humantime.workspace = true humantime.workspace = true
hyper.workspace = true hyper.workspace = true
futures.workspace = true
once_cell.workspace = true once_cell.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
postgres.workspace = true postgres.workspace = true
postgres-protocol.workspace = true postgres-protocol.workspace = true
regex.workspace = true regex.workspace = true
reqwest = { workspace = true, features = ["json"] }
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_with.workspace = true serde_with.workspace = true
@@ -33,6 +35,7 @@ tokio = { workspace = true, features = ["fs"] }
tokio-io-timeout.workspace = true tokio-io-timeout.workspace = true
tokio-postgres.workspace = true tokio-postgres.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
tempfile.workspace = true
tracing.workspace = true tracing.workspace = true
url.workspace = true url.workspace = true
metrics.workspace = true metrics.workspace = true
@@ -45,6 +48,3 @@ storage_broker.workspace = true
utils.workspace = true utils.workspace = true
workspace_hack.workspace = true workspace_hack.workspace = true
[dev-dependencies]
tempfile.workspace = true
+90 -64
View File
@@ -3,15 +3,16 @@
// //
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use clap::Parser; use clap::Parser;
use futures::FutureExt;
use remote_storage::RemoteStorageConfig; use remote_storage::RemoteStorageConfig;
use tokio::signal::unix::{signal, SignalKind};
use tokio::task::JoinError;
use toml_edit::Document; use toml_edit::Document;
use utils::signals::ShutdownSignals;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{ErrorKind, Write}; use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::thread;
use std::time::Duration; use std::time::Duration;
use storage_broker::Uri; use storage_broker::Uri;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -35,7 +36,6 @@ use safekeeper::SafeKeeperConf;
use storage_broker::DEFAULT_ENDPOINT; use storage_broker::DEFAULT_ENDPOINT;
use utils::auth::JwtAuth; use utils::auth::JwtAuth;
use utils::{ use utils::{
http::endpoint,
id::NodeId, id::NodeId,
logging::{self, LogFormat}, logging::{self, LogFormat},
project_git_version, project_git_version,
@@ -120,7 +120,8 @@ struct Args {
log_format: String, log_format: String,
} }
fn main() -> anyhow::Result<()> { #[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let args = Args::parse(); let args = Args::parse();
if let Some(addr) = args.dump_control_file { if let Some(addr) = args.dump_control_file {
@@ -134,7 +135,10 @@ fn main() -> anyhow::Result<()> {
// 1. init logging // 1. init logging
// 2. tracing panic hook // 2. tracing panic hook
// 3. sentry // 3. sentry
logging::init(LogFormat::from_config(&args.log_format)?)?; logging::init(
LogFormat::from_config(&args.log_format)?,
logging::TracingErrorLayerEnablement::Disabled,
)?;
logging::replace_panic_hook_with_tracing_panic_hook().forget(); logging::replace_panic_hook_with_tracing_panic_hook().forget();
info!("version: {GIT_VERSION}"); info!("version: {GIT_VERSION}");
@@ -177,7 +181,6 @@ fn main() -> anyhow::Result<()> {
heartbeat_timeout: args.heartbeat_timeout, heartbeat_timeout: args.heartbeat_timeout,
remote_storage: args.remote_storage, remote_storage: args.remote_storage,
max_offloader_lag_bytes: args.max_offloader_lag, max_offloader_lag_bytes: args.max_offloader_lag,
backup_runtime_threads: args.wal_backup_threads,
wal_backup_enabled: !args.disable_wal_backup, wal_backup_enabled: !args.disable_wal_backup,
auth, auth,
}; };
@@ -187,10 +190,10 @@ fn main() -> anyhow::Result<()> {
Some(GIT_VERSION.into()), Some(GIT_VERSION.into()),
&[("node_id", &conf.my_id.to_string())], &[("node_id", &conf.my_id.to_string())],
); );
start_safekeeper(conf) start_safekeeper(conf).await
} }
fn start_safekeeper(conf: SafeKeeperConf) -> Result<()> { async fn start_safekeeper(conf: SafeKeeperConf) -> anyhow::Result<()> {
// Prevent running multiple safekeepers on the same directory // Prevent running multiple safekeepers on the same directory
let lock_file_path = conf.workdir.join(PID_FILE_NAME); let lock_file_path = conf.workdir.join(PID_FILE_NAME);
let lock_file = let lock_file =
@@ -201,14 +204,18 @@ fn start_safekeeper(conf: SafeKeeperConf) -> Result<()> {
// we need to release the lock file only when the current process is gone // we need to release the lock file only when the current process is gone
std::mem::forget(lock_file); std::mem::forget(lock_file);
let http_listener = tcp_listener::bind(conf.listen_http_addr.clone()).map_err(|e| { info!("starting safekeeper WAL service on {}", conf.listen_pg_addr);
error!("failed to bind to address {}: {}", conf.listen_http_addr, e); let pg_listener = tcp_listener::bind(conf.listen_pg_addr.clone()).map_err(|e| {
error!("failed to bind to address {}: {}", conf.listen_pg_addr, e);
e e
})?; })?;
info!("starting safekeeper on {}", conf.listen_pg_addr); info!(
let pg_listener = tcp_listener::bind(conf.listen_pg_addr.clone()).map_err(|e| { "starting safekeeper HTTP service on {}",
error!("failed to bind to address {}: {}", conf.listen_pg_addr, e); conf.listen_http_addr
);
let http_listener = tcp_listener::bind(conf.listen_http_addr.clone()).map_err(|e| {
error!("failed to bind to address {}: {}", conf.listen_http_addr, e);
e e
})?; })?;
@@ -217,71 +224,90 @@ fn start_safekeeper(conf: SafeKeeperConf) -> Result<()> {
let timeline_collector = safekeeper::metrics::TimelineCollector::new(); let timeline_collector = safekeeper::metrics::TimelineCollector::new();
metrics::register_internal(Box::new(timeline_collector))?; metrics::register_internal(Box::new(timeline_collector))?;
let mut threads = vec![];
let (wal_backup_launcher_tx, wal_backup_launcher_rx) = mpsc::channel(100); let (wal_backup_launcher_tx, wal_backup_launcher_rx) = mpsc::channel(100);
// Load all timelines from disk to memory. // Load all timelines from disk to memory.
GlobalTimelines::init(conf.clone(), wal_backup_launcher_tx)?; GlobalTimelines::init(conf.clone(), wal_backup_launcher_tx)?;
let conf_ = conf.clone(); fn named_should_never_return(
threads.push( name: &'static str,
thread::Builder::new() unexpected: Result<Result<(), anyhow::Error>, JoinError>,
.name("http_endpoint_thread".into()) ) -> anyhow::Result<()> {
.spawn(|| { let res = match unexpected {
let router = http::make_router(conf_); Ok(Ok(())) => Err(anyhow::anyhow!("unexpected Ok(()) return")),
endpoint::serve_thread_main( Ok(Err(e)) => Err(e),
router, Err(e) => Err(anyhow::Error::new(e)),
http_listener, };
std::future::pending(), // never shut down
)
.unwrap();
})?,
);
let conf_cloned = conf.clone(); // was not able to get this working with `enum Void {}`
let safekeeper_thread = thread::Builder::new() res.with_context(|| format!("task {name} unexpectedly joined"))
.name("WAL service thread".into()) }
.spawn(|| wal_service::thread_main(conf_cloned, pg_listener))
.unwrap();
threads.push(safekeeper_thread);
let conf_ = conf.clone(); let conf_ = conf.clone();
threads.push( let wal_service_handle = tokio::spawn(wal_service::task_main(conf_, pg_listener))
thread::Builder::new() // wrap with task name for error reporting
.name("broker thread".into()) .map(|res| named_should_never_return("WAL service main", res));
.spawn(|| {
broker::thread_main(conf_);
})?,
);
let conf_ = conf.clone(); let conf_ = conf.clone();
threads.push( let http_handle = tokio::spawn(http::task_main(conf_, http_listener))
thread::Builder::new() .map(|res| named_should_never_return("HTTP service main", res));
.name("WAL removal thread".into())
.spawn(|| {
remove_wal::thread_main(conf_);
})?,
);
threads.push( let conf_ = conf.clone();
thread::Builder::new() let broker_task_handle =
.name("WAL backup launcher thread".into()) tokio::spawn(broker::task_main(conf_).instrument(info_span!("broker")))
.spawn(move || { .map(|res| named_should_never_return("broker main", res));
wal_backup::wal_backup_launcher_thread_main(conf, wal_backup_launcher_rx);
})?, let conf_ = conf.clone();
); let wal_remover_handle = tokio::spawn(remove_wal::task_main(conf_))
.map(|res| named_should_never_return("WAL remover", res));
let conf_ = conf.clone();
let wal_backup_handle = tokio::spawn(wal_backup::wal_backup_launcher_task_main(
conf_,
wal_backup_launcher_rx,
))
.map(|res| named_should_never_return("WAL backup launcher", res));
let metrics_shifter_handle = tokio::spawn(safekeeper::metrics::metrics_shifter())
.map(|res| named_should_never_return("metrics shifter", res));
set_build_info_metric(GIT_VERSION); set_build_info_metric(GIT_VERSION);
// TODO: put more thoughts into handling of failed threads
// We should catch & die if they are in trouble.
// On any shutdown signal, log receival and exit. Additionally, handling // TODO: update tokio-stream, convert to real async Stream with
// SIGQUIT prevents coredump. // SignalStream, map it to obtain missing signal name, combine streams into
ShutdownSignals::handle(|signal| { // single stream we can easily sit on.
info!("received {}, terminating", signal.name()); let mut sigquit_stream = signal(SignalKind::quit())?;
std::process::exit(0); let mut sigint_stream = signal(SignalKind::interrupt())?;
}) let mut sigterm_stream = signal(SignalKind::terminate())?;
let tasks = async move {
tokio::try_join!(
wal_service_handle,
http_handle,
broker_task_handle,
wal_remover_handle,
wal_backup_handle,
metrics_shifter_handle
)
};
tokio::select! {
res = tasks => {
// this will be the first reason to stop a safekeeper, but not necessarily the only one
// which will get to happen before we exit
match res {
Ok(_) => unreachable!("because of named_should_never_return, we can never end up here, cannot use ! yet"),
Err(e) => return Err(e),
}
}
// On any shutdown signal, log receival and exit. Additionally, handling
// SIGQUIT prevents coredump.
_ = sigquit_stream.recv() => info!("received SIGQUIT, terminating"),
_ = sigint_stream.recv() => info!("received SIGINT, terminating"),
_ = sigterm_stream.recv() => info!("received SIGTERM, terminating")
}
Ok(())
} }
/// Determine safekeeper id. /// Determine safekeeper id.
+15 -21
View File
@@ -15,7 +15,7 @@ use storage_broker::Request;
use std::time::Duration; use std::time::Duration;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio::{runtime, time::sleep}; use tokio::time::sleep;
use tracing::*; use tracing::*;
use crate::GlobalTimelines; use crate::GlobalTimelines;
@@ -24,20 +24,6 @@ use crate::SafeKeeperConf;
const RETRY_INTERVAL_MSEC: u64 = 1000; const RETRY_INTERVAL_MSEC: u64 = 1000;
const PUSH_INTERVAL_MSEC: u64 = 1000; const PUSH_INTERVAL_MSEC: u64 = 1000;
pub fn thread_main(conf: SafeKeeperConf) {
let runtime = runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let _enter = info_span!("broker").entered();
info!("started, broker endpoint {:?}", conf.broker_endpoint);
runtime.block_on(async {
main_loop(conf).await;
});
}
/// Push once in a while data about all active timelines to the broker. /// Push once in a while data about all active timelines to the broker.
async fn push_loop(conf: SafeKeeperConf) -> anyhow::Result<()> { async fn push_loop(conf: SafeKeeperConf) -> anyhow::Result<()> {
let mut client = BrokerServiceClient::connect(conf.broker_endpoint.clone()).await?; let mut client = BrokerServiceClient::connect(conf.broker_endpoint.clone()).await?;
@@ -49,10 +35,15 @@ async fn push_loop(conf: SafeKeeperConf) -> anyhow::Result<()> {
// is under plain mutex. That's ok, all this code is not performance // is under plain mutex. That's ok, all this code is not performance
// sensitive and there is no risk of deadlock as we don't await while // sensitive and there is no risk of deadlock as we don't await while
// lock is held. // lock is held.
let mut active_tlis = GlobalTimelines::get_all(); let all_tlis = GlobalTimelines::get_all();
active_tlis.retain(|tli| tli.is_active()); for tli in &all_tlis {
for tli in &active_tlis { // filtering alternative futures::stream::iter(all_tlis)
let sk_info = tli.get_safekeeper_info(&conf); // .filter(|tli| {let tli = tli.clone(); async move { tli.is_active().await}}).collect::<Vec<_>>().await;
// doesn't look better, and I'm not sure how to do that without collect.
if !tli.is_active().await {
continue;
}
let sk_info = tli.get_safekeeper_info(&conf).await;
yield sk_info; yield sk_info;
} }
sleep(push_interval).await; sleep(push_interval).await;
@@ -91,16 +82,19 @@ async fn pull_loop(conf: SafeKeeperConf) -> Result<()> {
// connection to the broker. // connection to the broker.
// note: there are blocking operations below, but it's considered fine for now // note: there are blocking operations below, but it's considered fine for now
tli.record_safekeeper_info(&msg).await? tli.record_safekeeper_info(msg).await?
} }
} }
bail!("end of stream"); bail!("end of stream");
} }
async fn main_loop(conf: SafeKeeperConf) { pub async fn task_main(conf: SafeKeeperConf) -> anyhow::Result<()> {
info!("started, broker endpoint {:?}", conf.broker_endpoint);
let mut ticker = tokio::time::interval(Duration::from_millis(RETRY_INTERVAL_MSEC)); let mut ticker = tokio::time::interval(Duration::from_millis(RETRY_INTERVAL_MSEC));
let mut push_handle: Option<JoinHandle<Result<(), Error>>> = None; let mut push_handle: Option<JoinHandle<Result<(), Error>>> = None;
let mut pull_handle: Option<JoinHandle<Result<(), Error>>> = None; let mut pull_handle: Option<JoinHandle<Result<(), Error>>> = None;
// Selecting on JoinHandles requires some squats; is there a better way to // Selecting on JoinHandles requires some squats; is there a better way to
// reap tasks individually? // reap tasks individually?
+59 -40
View File
@@ -2,9 +2,10 @@
use anyhow::{bail, ensure, Context, Result}; use anyhow::{bail, ensure, Context, Result};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
use std::fs::{self, File, OpenOptions}; use std::io::Read;
use std::io::{Read, Write};
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -25,9 +26,10 @@ pub const CHECKSUM_SIZE: usize = std::mem::size_of::<u32>();
/// Storage should keep actual state inside of it. It should implement Deref /// Storage should keep actual state inside of it. It should implement Deref
/// trait to access state fields and have persist method for updating that state. /// trait to access state fields and have persist method for updating that state.
#[async_trait::async_trait]
pub trait Storage: Deref<Target = SafeKeeperState> { pub trait Storage: Deref<Target = SafeKeeperState> {
/// Persist safekeeper state on disk and update internal state. /// Persist safekeeper state on disk and update internal state.
fn persist(&mut self, s: &SafeKeeperState) -> Result<()>; async fn persist(&mut self, s: &SafeKeeperState) -> Result<()>;
} }
#[derive(Debug)] #[derive(Debug)]
@@ -74,7 +76,7 @@ impl FileStorage {
/// Check the magic/version in the on-disk data and deserialize it, if possible. /// Check the magic/version in the on-disk data and deserialize it, if possible.
fn deser_sk_state(buf: &mut &[u8]) -> Result<SafeKeeperState> { fn deser_sk_state(buf: &mut &[u8]) -> Result<SafeKeeperState> {
// Read the version independent part // Read the version independent part
let magic = buf.read_u32::<LittleEndian>()?; let magic = ReadBytesExt::read_u32::<LittleEndian>(buf)?;
if magic != SK_MAGIC { if magic != SK_MAGIC {
bail!( bail!(
"bad control file magic: {:X}, expected {:X}", "bad control file magic: {:X}, expected {:X}",
@@ -82,7 +84,7 @@ impl FileStorage {
SK_MAGIC SK_MAGIC
); );
} }
let version = buf.read_u32::<LittleEndian>()?; let version = ReadBytesExt::read_u32::<LittleEndian>(buf)?;
if version == SK_FORMAT_VERSION { if version == SK_FORMAT_VERSION {
let res = SafeKeeperState::des(buf)?; let res = SafeKeeperState::des(buf)?;
return Ok(res); return Ok(res);
@@ -102,7 +104,7 @@ impl FileStorage {
/// Read in the control file. /// Read in the control file.
pub fn load_control_file<P: AsRef<Path>>(control_file_path: P) -> Result<SafeKeeperState> { pub fn load_control_file<P: AsRef<Path>>(control_file_path: P) -> Result<SafeKeeperState> {
let mut control_file = OpenOptions::new() let mut control_file = std::fs::OpenOptions::new()
.read(true) .read(true)
.write(true) .write(true)
.open(&control_file_path) .open(&control_file_path)
@@ -151,30 +153,31 @@ impl Deref for FileStorage {
} }
} }
#[async_trait::async_trait]
impl Storage for FileStorage { impl Storage for FileStorage {
/// persists state durably to underlying storage /// persists state durably to underlying storage
/// for description see https://lwn.net/Articles/457667/ /// for description see https://lwn.net/Articles/457667/
fn persist(&mut self, s: &SafeKeeperState) -> Result<()> { async fn persist(&mut self, s: &SafeKeeperState) -> Result<()> {
let _timer = PERSIST_CONTROL_FILE_SECONDS.start_timer(); let _timer = PERSIST_CONTROL_FILE_SECONDS.start_timer();
// write data to safekeeper.control.partial // write data to safekeeper.control.partial
let control_partial_path = self.timeline_dir.join(CONTROL_FILE_NAME_PARTIAL); let control_partial_path = self.timeline_dir.join(CONTROL_FILE_NAME_PARTIAL);
let mut control_partial = File::create(&control_partial_path).with_context(|| { let mut control_partial = File::create(&control_partial_path).await.with_context(|| {
format!( format!(
"failed to create partial control file at: {}", "failed to create partial control file at: {}",
&control_partial_path.display() &control_partial_path.display()
) )
})?; })?;
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
buf.write_u32::<LittleEndian>(SK_MAGIC)?; WriteBytesExt::write_u32::<LittleEndian>(&mut buf, SK_MAGIC)?;
buf.write_u32::<LittleEndian>(SK_FORMAT_VERSION)?; WriteBytesExt::write_u32::<LittleEndian>(&mut buf, SK_FORMAT_VERSION)?;
s.ser_into(&mut buf)?; s.ser_into(&mut buf)?;
// calculate checksum before resize // calculate checksum before resize
let checksum = crc32c::crc32c(&buf); let checksum = crc32c::crc32c(&buf);
buf.extend_from_slice(&checksum.to_le_bytes()); buf.extend_from_slice(&checksum.to_le_bytes());
control_partial.write_all(&buf).with_context(|| { control_partial.write_all(&buf).await.with_context(|| {
format!( format!(
"failed to write safekeeper state into control file at: {}", "failed to write safekeeper state into control file at: {}",
control_partial_path.display() control_partial_path.display()
@@ -183,7 +186,7 @@ impl Storage for FileStorage {
// fsync the file // fsync the file
if !self.conf.no_sync { if !self.conf.no_sync {
control_partial.sync_all().with_context(|| { control_partial.sync_all().await.with_context(|| {
format!( format!(
"failed to sync partial control file at {}", "failed to sync partial control file at {}",
control_partial_path.display() control_partial_path.display()
@@ -194,21 +197,22 @@ impl Storage for FileStorage {
let control_path = self.timeline_dir.join(CONTROL_FILE_NAME); let control_path = self.timeline_dir.join(CONTROL_FILE_NAME);
// rename should be atomic // rename should be atomic
fs::rename(&control_partial_path, &control_path)?; fs::rename(&control_partial_path, &control_path).await?;
// this sync is not required by any standard but postgres does this (see durable_rename) // this sync is not required by any standard but postgres does this (see durable_rename)
if !self.conf.no_sync { if !self.conf.no_sync {
File::open(&control_path) let new_f = File::open(&control_path).await?;
.and_then(|f| f.sync_all()) new_f.sync_all().await.with_context(|| {
.with_context(|| { format!(
format!( "failed to sync control file at: {}",
"failed to sync control file at: {}", &control_path.display()
&control_path.display() )
) })?;
})?;
// fsync the directory (linux specific) // fsync the directory (linux specific)
File::open(&self.timeline_dir) let tli_dir = File::open(&self.timeline_dir).await?;
.and_then(|f| f.sync_all()) tli_dir
.sync_all()
.await
.context("failed to sync control file directory")?; .context("failed to sync control file directory")?;
} }
@@ -224,7 +228,6 @@ mod test {
use super::*; use super::*;
use crate::{safekeeper::SafeKeeperState, SafeKeeperConf}; use crate::{safekeeper::SafeKeeperState, SafeKeeperConf};
use anyhow::Result; use anyhow::Result;
use std::fs;
use utils::{id::TenantTimelineId, lsn::Lsn}; use utils::{id::TenantTimelineId, lsn::Lsn};
fn stub_conf() -> SafeKeeperConf { fn stub_conf() -> SafeKeeperConf {
@@ -235,59 +238,75 @@ mod test {
} }
} }
fn load_from_control_file( async fn load_from_control_file(
conf: &SafeKeeperConf, conf: &SafeKeeperConf,
ttid: &TenantTimelineId, ttid: &TenantTimelineId,
) -> Result<(FileStorage, SafeKeeperState)> { ) -> Result<(FileStorage, SafeKeeperState)> {
fs::create_dir_all(conf.timeline_dir(ttid)).expect("failed to create timeline dir"); fs::create_dir_all(conf.timeline_dir(ttid))
.await
.expect("failed to create timeline dir");
Ok(( Ok((
FileStorage::restore_new(ttid, conf)?, FileStorage::restore_new(ttid, conf)?,
FileStorage::load_control_file_conf(conf, ttid)?, FileStorage::load_control_file_conf(conf, ttid)?,
)) ))
} }
fn create( async fn create(
conf: &SafeKeeperConf, conf: &SafeKeeperConf,
ttid: &TenantTimelineId, ttid: &TenantTimelineId,
) -> Result<(FileStorage, SafeKeeperState)> { ) -> Result<(FileStorage, SafeKeeperState)> {
fs::create_dir_all(conf.timeline_dir(ttid)).expect("failed to create timeline dir"); fs::create_dir_all(conf.timeline_dir(ttid))
.await
.expect("failed to create timeline dir");
let state = SafeKeeperState::empty(); let state = SafeKeeperState::empty();
let storage = FileStorage::create_new(ttid, conf, state.clone())?; let storage = FileStorage::create_new(ttid, conf, state.clone())?;
Ok((storage, state)) Ok((storage, state))
} }
#[test] #[tokio::test]
fn test_read_write_safekeeper_state() { async fn test_read_write_safekeeper_state() {
let conf = stub_conf(); let conf = stub_conf();
let ttid = TenantTimelineId::generate(); let ttid = TenantTimelineId::generate();
{ {
let (mut storage, mut state) = create(&conf, &ttid).expect("failed to create state"); let (mut storage, mut state) =
create(&conf, &ttid).await.expect("failed to create state");
// change something // change something
state.commit_lsn = Lsn(42); state.commit_lsn = Lsn(42);
storage.persist(&state).expect("failed to persist state"); storage
.persist(&state)
.await
.expect("failed to persist state");
} }
let (_, state) = load_from_control_file(&conf, &ttid).expect("failed to read state"); let (_, state) = load_from_control_file(&conf, &ttid)
.await
.expect("failed to read state");
assert_eq!(state.commit_lsn, Lsn(42)); assert_eq!(state.commit_lsn, Lsn(42));
} }
#[test] #[tokio::test]
fn test_safekeeper_state_checksum_mismatch() { async fn test_safekeeper_state_checksum_mismatch() {
let conf = stub_conf(); let conf = stub_conf();
let ttid = TenantTimelineId::generate(); let ttid = TenantTimelineId::generate();
{ {
let (mut storage, mut state) = create(&conf, &ttid).expect("failed to read state"); let (mut storage, mut state) =
create(&conf, &ttid).await.expect("failed to read state");
// change something // change something
state.commit_lsn = Lsn(42); state.commit_lsn = Lsn(42);
storage.persist(&state).expect("failed to persist state"); storage
.persist(&state)
.await
.expect("failed to persist state");
} }
let control_path = conf.timeline_dir(&ttid).join(CONTROL_FILE_NAME); let control_path = conf.timeline_dir(&ttid).join(CONTROL_FILE_NAME);
let mut data = fs::read(&control_path).unwrap(); let mut data = fs::read(&control_path).await.unwrap();
data[0] += 1; // change the first byte of the file to fail checksum validation data[0] += 1; // change the first byte of the file to fail checksum validation
fs::write(&control_path, &data).expect("failed to write control file"); fs::write(&control_path, &data)
.await
.expect("failed to write control file");
match load_from_control_file(&conf, &ttid) { match load_from_control_file(&conf, &ttid).await {
Err(err) => assert!(err Err(err) => assert!(err
.to_string() .to_string()
.contains("safekeeper control file checksum mismatch")), .contains("safekeeper control file checksum mismatch")),
+17 -15
View File
@@ -9,9 +9,10 @@ use std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use postgres_ffi::XLogSegNo; use postgres_ffi::XLogSegNo;
use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use utils::http::json::display_serialize; use serde_with::{serde_as, DisplayFromStr};
use utils::id::NodeId; use utils::id::NodeId;
use utils::id::TenantTimelineId; use utils::id::TenantTimelineId;
use utils::id::{TenantId, TimelineId}; use utils::id::{TenantId, TimelineId};
@@ -22,11 +23,11 @@ use crate::safekeeper::SafekeeperMemState;
use crate::safekeeper::TermHistory; use crate::safekeeper::TermHistory;
use crate::SafeKeeperConf; use crate::SafeKeeperConf;
use crate::timeline::ReplicaState; use crate::send_wal::WalSenderState;
use crate::GlobalTimelines; use crate::GlobalTimelines;
/// Various filters that influence the resulting JSON output. /// Various filters that influence the resulting JSON output.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Args { pub struct Args {
/// Dump all available safekeeper state. False by default. /// Dump all available safekeeper state. False by default.
pub dump_all: bool, pub dump_all: bool,
@@ -51,7 +52,7 @@ pub struct Args {
} }
/// Response for debug dump request. /// Response for debug dump request.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Response { pub struct Response {
pub start_time: DateTime<Utc>, pub start_time: DateTime<Utc>,
pub finish_time: DateTime<Utc>, pub finish_time: DateTime<Utc>,
@@ -61,7 +62,7 @@ pub struct Response {
} }
/// Safekeeper configuration. /// Safekeeper configuration.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub id: NodeId, pub id: NodeId,
pub workdir: PathBuf, pub workdir: PathBuf,
@@ -72,22 +73,23 @@ pub struct Config {
pub wal_backup_enabled: bool, pub wal_backup_enabled: bool,
} }
#[derive(Debug, Serialize)] #[serde_as]
#[derive(Debug, Serialize, Deserialize)]
pub struct Timeline { pub struct Timeline {
#[serde(serialize_with = "display_serialize")] #[serde_as(as = "DisplayFromStr")]
pub tenant_id: TenantId, pub tenant_id: TenantId,
#[serde(serialize_with = "display_serialize")] #[serde_as(as = "DisplayFromStr")]
pub timeline_id: TimelineId, pub timeline_id: TimelineId,
pub control_file: Option<SafeKeeperState>, pub control_file: Option<SafeKeeperState>,
pub memory: Option<Memory>, pub memory: Option<Memory>,
pub disk_content: Option<DiskContent>, pub disk_content: Option<DiskContent>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Memory { pub struct Memory {
pub is_cancelled: bool, pub is_cancelled: bool,
pub peers_info_len: usize, pub peers_info_len: usize,
pub replicas: Vec<Option<ReplicaState>>, pub walsenders: Vec<WalSenderState>,
pub wal_backup_active: bool, pub wal_backup_active: bool,
pub active: bool, pub active: bool,
pub num_computes: u32, pub num_computes: u32,
@@ -102,12 +104,12 @@ pub struct Memory {
pub file_open: bool, pub file_open: bool,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct DiskContent { pub struct DiskContent {
pub files: Vec<FileInfo>, pub files: Vec<FileInfo>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct FileInfo { pub struct FileInfo {
pub name: String, pub name: String,
pub size: u64, pub size: u64,
@@ -119,7 +121,7 @@ pub struct FileInfo {
} }
/// Build debug dump response, using the provided [`Args`] filters. /// Build debug dump response, using the provided [`Args`] filters.
pub fn build(args: Args) -> Result<Response> { pub async fn build(args: Args) -> Result<Response> {
let start_time = Utc::now(); let start_time = Utc::now();
let timelines_count = GlobalTimelines::timelines_count(); let timelines_count = GlobalTimelines::timelines_count();
@@ -153,7 +155,7 @@ pub fn build(args: Args) -> Result<Response> {
} }
let control_file = if args.dump_control_file { let control_file = if args.dump_control_file {
let mut state = tli.get_state().1; let mut state = tli.get_state().await.1;
if !args.dump_term_history { if !args.dump_term_history {
state.acceptor_state.term_history = TermHistory(vec![]); state.acceptor_state.term_history = TermHistory(vec![]);
} }
@@ -163,7 +165,7 @@ pub fn build(args: Args) -> Result<Response> {
}; };
let memory = if args.dump_memory { let memory = if args.dump_memory {
Some(tli.memory_dump()) Some(tli.memory_dump().await)
} else { } else {
None None
}; };
+9 -6
View File
@@ -3,6 +3,7 @@
use anyhow::Context; use anyhow::Context;
use std::str; use std::str;
use std::str::FromStr;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::{info, info_span, Instrument}; use tracing::{info, info_span, Instrument};
@@ -49,12 +50,14 @@ fn parse_cmd(cmd: &str) -> anyhow::Result<SafekeeperPostgresCommand> {
if cmd.starts_with("START_WAL_PUSH") { if cmd.starts_with("START_WAL_PUSH") {
Ok(SafekeeperPostgresCommand::StartWalPush) Ok(SafekeeperPostgresCommand::StartWalPush)
} else if cmd.starts_with("START_REPLICATION") { } else if cmd.starts_with("START_REPLICATION") {
let re = let re = Regex::new(
Regex::new(r"START_REPLICATION(?: PHYSICAL)? ([[:xdigit:]]+/[[:xdigit:]]+)").unwrap(); r"START_REPLICATION(?: SLOT [^ ]+)?(?: PHYSICAL)? ([[:xdigit:]]+/[[:xdigit:]]+)",
)
.unwrap();
let mut caps = re.captures_iter(cmd); let mut caps = re.captures_iter(cmd);
let start_lsn = caps let start_lsn = caps
.next() .next()
.map(|cap| cap[1].parse::<Lsn>()) .map(|cap| Lsn::from_str(&cap[1]))
.context("parse start LSN from START_REPLICATION command")??; .context("parse start LSN from START_REPLICATION command")??;
Ok(SafekeeperPostgresCommand::StartReplication { start_lsn }) Ok(SafekeeperPostgresCommand::StartReplication { start_lsn })
} else if cmd.starts_with("IDENTIFY_SYSTEM") { } else if cmd.starts_with("IDENTIFY_SYSTEM") {
@@ -238,14 +241,14 @@ impl SafekeeperPostgresHandler {
let lsn = if self.is_walproposer_recovery() { let lsn = if self.is_walproposer_recovery() {
// walproposer should get all local WAL until flush_lsn // walproposer should get all local WAL until flush_lsn
tli.get_flush_lsn() tli.get_flush_lsn().await
} else { } else {
// other clients shouldn't get any uncommitted WAL // other clients shouldn't get any uncommitted WAL
tli.get_state().0.commit_lsn tli.get_state().await.0.commit_lsn
} }
.to_string(); .to_string();
let sysid = tli.get_state().1.server.system_id.to_string(); let sysid = tli.get_state().await.1.server.system_id.to_string();
let lsn_bytes = lsn.as_bytes(); let lsn_bytes = lsn.as_bytes();
let tli = PG_TLI.to_string(); let tli = PG_TLI.to_string();
let tli_bytes = tli.as_bytes(); let tli_bytes = tli.as_bytes();
+15
View File
@@ -2,3 +2,18 @@ pub mod routes;
pub use routes::make_router; pub use routes::make_router;
pub use safekeeper_api::models; pub use safekeeper_api::models;
use crate::SafeKeeperConf;
pub async fn task_main(
conf: SafeKeeperConf,
http_listener: std::net::TcpListener,
) -> anyhow::Result<()> {
let router = make_router(conf)
.build()
.map_err(|err| anyhow::anyhow!(err))?;
let service = utils::http::RouterService::new(router).unwrap();
let server = hyper::Server::from_tcp(http_listener)?;
server.serve(service).await?;
Ok(()) // unreachable
}
+123 -72
View File
@@ -3,19 +3,20 @@ use hyper::{Body, Request, Response, StatusCode, Uri};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use postgres_ffi::WAL_SEGMENT_SIZE; use postgres_ffi::WAL_SEGMENT_SIZE;
use safekeeper_api::models::SkTimelineInfo; use safekeeper_api::models::SkTimelineInfo;
use serde::Serialize; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use storage_broker::proto::SafekeeperTimelineInfo; use storage_broker::proto::SafekeeperTimelineInfo;
use storage_broker::proto::TenantTimelineId as ProtoTenantTimelineId; use storage_broker::proto::TenantTimelineId as ProtoTenantTimelineId;
use tokio::task::JoinError; use tokio::fs::File;
use utils::http::json::display_serialize; use tokio::io::AsyncReadExt;
use crate::debug_dump;
use crate::safekeeper::ServerInfo; use crate::safekeeper::ServerInfo;
use crate::safekeeper::Term; use crate::safekeeper::Term;
use crate::{debug_dump, pull_timeline};
use crate::timelines_global_map::TimelineDeleteForceResult; use crate::timelines_global_map::TimelineDeleteForceResult;
use crate::GlobalTimelines; use crate::GlobalTimelines;
@@ -57,44 +58,46 @@ fn get_conf(request: &Request<Body>) -> &SafeKeeperConf {
/// Same as TermSwitchEntry, but serializes LSN using display serializer /// Same as TermSwitchEntry, but serializes LSN using display serializer
/// in Postgres format, i.e. 0/FFFFFFFF. Used only for the API response. /// in Postgres format, i.e. 0/FFFFFFFF. Used only for the API response.
#[derive(Debug, Serialize)] #[serde_as]
struct TermSwitchApiEntry { #[derive(Debug, Serialize, Deserialize)]
pub struct TermSwitchApiEntry {
pub term: Term, pub term: Term,
#[serde(serialize_with = "display_serialize")] #[serde_as(as = "DisplayFromStr")]
pub lsn: Lsn, pub lsn: Lsn,
} }
/// Augment AcceptorState with epoch for convenience /// Augment AcceptorState with epoch for convenience
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Deserialize)]
struct AcceptorStateStatus { pub struct AcceptorStateStatus {
term: Term, pub term: Term,
epoch: Term, pub epoch: Term,
term_history: Vec<TermSwitchApiEntry>, pub term_history: Vec<TermSwitchApiEntry>,
} }
/// Info about timeline on safekeeper ready for reporting. /// Info about timeline on safekeeper ready for reporting.
#[derive(Debug, Serialize)] #[serde_as]
struct TimelineStatus { #[derive(Debug, Serialize, Deserialize)]
#[serde(serialize_with = "display_serialize")] pub struct TimelineStatus {
tenant_id: TenantId, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub tenant_id: TenantId,
timeline_id: TimelineId, #[serde_as(as = "DisplayFromStr")]
acceptor_state: AcceptorStateStatus, pub timeline_id: TimelineId,
pg_info: ServerInfo, pub acceptor_state: AcceptorStateStatus,
#[serde(serialize_with = "display_serialize")] pub pg_info: ServerInfo,
flush_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub flush_lsn: Lsn,
timeline_start_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub timeline_start_lsn: Lsn,
local_start_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub local_start_lsn: Lsn,
commit_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub commit_lsn: Lsn,
backup_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub backup_lsn: Lsn,
peer_horizon_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
#[serde(serialize_with = "display_serialize")] pub peer_horizon_lsn: Lsn,
remote_consistent_lsn: Lsn, #[serde_as(as = "DisplayFromStr")]
pub remote_consistent_lsn: Lsn,
} }
fn check_permission(request: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> { fn check_permission(request: &Request<Body>, tenant_id: Option<TenantId>) -> Result<(), ApiError> {
@@ -112,8 +115,8 @@ async fn timeline_status_handler(request: Request<Body>) -> Result<Response<Body
check_permission(&request, Some(ttid.tenant_id))?; check_permission(&request, Some(ttid.tenant_id))?;
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?; let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
let (inmem, state) = tli.get_state(); let (inmem, state) = tli.get_state().await;
let flush_lsn = tli.get_flush_lsn(); let flush_lsn = tli.get_flush_lsn().await;
let epoch = state.acceptor_state.get_epoch(flush_lsn); let epoch = state.acceptor_state.get_epoch(flush_lsn);
let term_history = state let term_history = state
@@ -144,7 +147,7 @@ async fn timeline_status_handler(request: Request<Body>) -> Result<Response<Body
commit_lsn: inmem.commit_lsn, commit_lsn: inmem.commit_lsn,
backup_lsn: inmem.backup_lsn, backup_lsn: inmem.backup_lsn,
peer_horizon_lsn: inmem.peer_horizon_lsn, peer_horizon_lsn: inmem.peer_horizon_lsn,
remote_consistent_lsn: inmem.remote_consistent_lsn, remote_consistent_lsn: tli.get_walsenders().get_remote_consistent_lsn(),
}; };
json_response(StatusCode::OK, status) json_response(StatusCode::OK, status)
} }
@@ -175,6 +178,49 @@ async fn timeline_create_handler(mut request: Request<Body>) -> Result<Response<
json_response(StatusCode::OK, ()) json_response(StatusCode::OK, ())
} }
/// Pull timeline from peer safekeeper instances.
async fn timeline_pull_handler(mut request: Request<Body>) -> Result<Response<Body>, ApiError> {
check_permission(&request, None)?;
let data: pull_timeline::Request = json_request(&mut request).await?;
let resp = pull_timeline::handle_request(data)
.await
.map_err(ApiError::InternalServerError)?;
json_response(StatusCode::OK, resp)
}
/// Download a file from the timeline directory.
// TODO: figure out a better way to copy files between safekeepers
async fn timeline_files_handler(request: Request<Body>) -> Result<Response<Body>, ApiError> {
let ttid = TenantTimelineId::new(
parse_request_param(&request, "tenant_id")?,
parse_request_param(&request, "timeline_id")?,
);
check_permission(&request, Some(ttid.tenant_id))?;
let filename: String = parse_request_param(&request, "filename")?;
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
let filepath = tli.timeline_dir.join(filename);
let mut file = File::open(&filepath)
.await
.map_err(|e| ApiError::InternalServerError(e.into()))?;
let mut content = Vec::new();
// TODO: don't store files in memory
file.read_to_end(&mut content)
.await
.map_err(|e| ApiError::InternalServerError(e.into()))?;
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/octet-stream")
.body(Body::from(content))
.map_err(|e| ApiError::InternalServerError(e.into()))
}
/// Deactivates the timeline and removes its data directory. /// Deactivates the timeline and removes its data directory.
async fn timeline_delete_force_handler( async fn timeline_delete_force_handler(
mut request: Request<Body>, mut request: Request<Body>,
@@ -185,13 +231,11 @@ async fn timeline_delete_force_handler(
); );
check_permission(&request, Some(ttid.tenant_id))?; check_permission(&request, Some(ttid.tenant_id))?;
ensure_no_body(&mut request).await?; ensure_no_body(&mut request).await?;
let resp = tokio::task::spawn_blocking(move || { // FIXME: `delete_force` can fail from both internal errors and bad requests. Add better
// FIXME: `delete_force` can fail from both internal errors and bad requests. Add better // error handling here when we're able to.
// error handling here when we're able to. let resp = GlobalTimelines::delete_force(&ttid)
GlobalTimelines::delete_force(&ttid).map_err(ApiError::InternalServerError) .await
}) .map_err(ApiError::InternalServerError)?;
.await
.map_err(|e: JoinError| ApiError::InternalServerError(e.into()))??;
json_response(StatusCode::OK, resp) json_response(StatusCode::OK, resp)
} }
@@ -203,14 +247,11 @@ async fn tenant_delete_force_handler(
let tenant_id = parse_request_param(&request, "tenant_id")?; let tenant_id = parse_request_param(&request, "tenant_id")?;
check_permission(&request, Some(tenant_id))?; check_permission(&request, Some(tenant_id))?;
ensure_no_body(&mut request).await?; ensure_no_body(&mut request).await?;
let delete_info = tokio::task::spawn_blocking(move || { // FIXME: `delete_force_all_for_tenant` can return an error for multiple different reasons;
// FIXME: `delete_force_all_for_tenant` can return an error for multiple different reasons; // Using an `InternalServerError` should be fixed when the types support it
// Using an `InternalServerError` should be fixed when the types support it let delete_info = GlobalTimelines::delete_force_all_for_tenant(&tenant_id)
GlobalTimelines::delete_force_all_for_tenant(&tenant_id) .await
.map_err(ApiError::InternalServerError) .map_err(ApiError::InternalServerError)?;
})
.await
.map_err(|e: JoinError| ApiError::InternalServerError(e.into()))??;
json_response( json_response(
StatusCode::OK, StatusCode::OK,
delete_info delete_info
@@ -246,7 +287,7 @@ async fn record_safekeeper_info(mut request: Request<Body>) -> Result<Response<B
}; };
let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?; let tli = GlobalTimelines::get(ttid).map_err(ApiError::from)?;
tli.record_safekeeper_info(&proto_sk_info) tli.record_safekeeper_info(proto_sk_info)
.await .await
.map_err(ApiError::InternalServerError)?; .map_err(ApiError::InternalServerError)?;
@@ -306,11 +347,9 @@ async fn dump_debug_handler(mut request: Request<Body>) -> Result<Response<Body>
timeline_id, timeline_id,
}; };
let resp = tokio::task::spawn_blocking(move || { let resp = debug_dump::build(args)
debug_dump::build(args).map_err(ApiError::InternalServerError) .await
}) .map_err(ApiError::InternalServerError)?;
.await
.map_err(|e: JoinError| ApiError::InternalServerError(e.into()))??;
// TODO: use streaming response // TODO: use streaming response
json_response(StatusCode::OK, resp) json_response(StatusCode::OK, resp)
@@ -318,6 +357,8 @@ async fn dump_debug_handler(mut request: Request<Body>) -> Result<Response<Body>
/// Safekeeper http router. /// Safekeeper http router.
pub fn make_router(conf: SafeKeeperConf) -> RouterBuilder<hyper::Body, ApiError> { pub fn make_router(conf: SafeKeeperConf) -> RouterBuilder<hyper::Body, ApiError> {
use utils::http::endpoint::RequestSpan;
let mut router = endpoint::make_router(); let mut router = endpoint::make_router();
if conf.auth.is_some() { if conf.auth.is_some() {
router = router.middleware(auth_middleware(|request| { router = router.middleware(auth_middleware(|request| {
@@ -339,24 +380,34 @@ pub fn make_router(conf: SafeKeeperConf) -> RouterBuilder<hyper::Body, ApiError>
router router
.data(Arc::new(conf)) .data(Arc::new(conf))
.data(auth) .data(auth)
.get("/v1/status", status_handler) .get("/v1/status", |r| RequestSpan(status_handler).handle(r))
// Will be used in the future instead of implicit timeline creation // Will be used in the future instead of implicit timeline creation
.post("/v1/tenant/timeline", timeline_create_handler) .post("/v1/tenant/timeline", |r| {
RequestSpan(timeline_create_handler).handle(r)
})
.get("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
RequestSpan(timeline_status_handler).handle(r)
})
.delete("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
RequestSpan(timeline_delete_force_handler).handle(r)
})
.delete("/v1/tenant/:tenant_id", |r| {
RequestSpan(tenant_delete_force_handler).handle(r)
})
.post("/v1/pull_timeline", |r| {
RequestSpan(timeline_pull_handler).handle(r)
})
.get( .get(
"/v1/tenant/:tenant_id/timeline/:timeline_id", "/v1/tenant/:tenant_id/timeline/:timeline_id/file/:filename",
timeline_status_handler, |r| RequestSpan(timeline_files_handler).handle(r),
) )
.delete(
"/v1/tenant/:tenant_id/timeline/:timeline_id",
timeline_delete_force_handler,
)
.delete("/v1/tenant/:tenant_id", tenant_delete_force_handler)
// for tests // for tests
.post( .post("/v1/record_safekeeper_info/:tenant_id/:timeline_id", |r| {
"/v1/record_safekeeper_info/:tenant_id/:timeline_id", RequestSpan(record_safekeeper_info).handle(r)
record_safekeeper_info, })
) .get("/v1/debug_dump", |r| {
.get("/v1/debug_dump", dump_debug_handler) RequestSpan(dump_debug_handler).handle(r)
})
} }
#[cfg(test)] #[cfg(test)]
+11 -11
View File
@@ -50,7 +50,7 @@ pub struct AppendLogicalMessage {
pub pg_version: u32, pub pg_version: u32,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize)]
struct AppendResult { struct AppendResult {
// safekeeper state after append // safekeeper state after append
state: SafeKeeperState, state: SafeKeeperState,
@@ -73,12 +73,12 @@ pub async fn handle_json_ctrl<IO: AsyncRead + AsyncWrite + Unpin>(
// if send_proposer_elected is true, we need to update local history // if send_proposer_elected is true, we need to update local history
if append_request.send_proposer_elected { if append_request.send_proposer_elected {
send_proposer_elected(&tli, append_request.term, append_request.epoch_start_lsn)?; send_proposer_elected(&tli, append_request.term, append_request.epoch_start_lsn).await?;
} }
let inserted_wal = append_logical_message(&tli, append_request)?; let inserted_wal = append_logical_message(&tli, append_request).await?;
let response = AppendResult { let response = AppendResult {
state: tli.get_state().1, state: tli.get_state().await.1,
inserted_wal, inserted_wal,
}; };
let response_data = serde_json::to_vec(&response) let response_data = serde_json::to_vec(&response)
@@ -114,9 +114,9 @@ async fn prepare_safekeeper(
.await .await
} }
fn send_proposer_elected(tli: &Arc<Timeline>, term: Term, lsn: Lsn) -> anyhow::Result<()> { async fn send_proposer_elected(tli: &Arc<Timeline>, term: Term, lsn: Lsn) -> anyhow::Result<()> {
// add new term to existing history // add new term to existing history
let history = tli.get_state().1.acceptor_state.term_history; let history = tli.get_state().await.1.acceptor_state.term_history;
let history = history.up_to(lsn.checked_sub(1u64).unwrap()); let history = history.up_to(lsn.checked_sub(1u64).unwrap());
let mut history_entries = history.0; let mut history_entries = history.0;
history_entries.push(TermSwitchEntry { term, lsn }); history_entries.push(TermSwitchEntry { term, lsn });
@@ -129,11 +129,11 @@ fn send_proposer_elected(tli: &Arc<Timeline>, term: Term, lsn: Lsn) -> anyhow::R
timeline_start_lsn: lsn, timeline_start_lsn: lsn,
}); });
tli.process_msg(&proposer_elected_request)?; tli.process_msg(&proposer_elected_request).await?;
Ok(()) Ok(())
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize)]
pub struct InsertedWAL { pub struct InsertedWAL {
begin_lsn: Lsn, begin_lsn: Lsn,
pub end_lsn: Lsn, pub end_lsn: Lsn,
@@ -142,12 +142,12 @@ pub struct InsertedWAL {
/// Extend local WAL with new LogicalMessage record. To do that, /// Extend local WAL with new LogicalMessage record. To do that,
/// create AppendRequest with new WAL and pass it to safekeeper. /// create AppendRequest with new WAL and pass it to safekeeper.
pub fn append_logical_message( pub async fn append_logical_message(
tli: &Arc<Timeline>, tli: &Arc<Timeline>,
msg: &AppendLogicalMessage, msg: &AppendLogicalMessage,
) -> anyhow::Result<InsertedWAL> { ) -> anyhow::Result<InsertedWAL> {
let wal_data = encode_logical_message(&msg.lm_prefix, &msg.lm_message); let wal_data = encode_logical_message(&msg.lm_prefix, &msg.lm_message);
let sk_state = tli.get_state().1; let sk_state = tli.get_state().await.1;
let begin_lsn = msg.begin_lsn; let begin_lsn = msg.begin_lsn;
let end_lsn = begin_lsn + wal_data.len() as u64; let end_lsn = begin_lsn + wal_data.len() as u64;
@@ -171,7 +171,7 @@ pub fn append_logical_message(
wal_data: Bytes::from(wal_data), wal_data: Bytes::from(wal_data),
}); });
let response = tli.process_msg(&append_request)?; let response = tli.process_msg(&append_request).await?;
let append_response = match response { let append_response = match response {
Some(AcceptorProposerMessage::AppendResponse(resp)) => resp, Some(AcceptorProposerMessage::AppendResponse(resp)) => resp,
+1 -3
View File
@@ -15,6 +15,7 @@ pub mod handler;
pub mod http; pub mod http;
pub mod json_ctrl; pub mod json_ctrl;
pub mod metrics; pub mod metrics;
pub mod pull_timeline;
pub mod receive_wal; pub mod receive_wal;
pub mod remove_wal; pub mod remove_wal;
pub mod safekeeper; pub mod safekeeper;
@@ -35,7 +36,6 @@ pub mod defaults {
DEFAULT_PG_LISTEN_PORT, DEFAULT_PG_LISTEN_PORT,
}; };
pub const DEFAULT_WAL_BACKUP_RUNTIME_THREADS: usize = 8;
pub const DEFAULT_HEARTBEAT_TIMEOUT: &str = "5000ms"; pub const DEFAULT_HEARTBEAT_TIMEOUT: &str = "5000ms";
pub const DEFAULT_MAX_OFFLOADER_LAG_BYTES: u64 = 128 * (1 << 20); pub const DEFAULT_MAX_OFFLOADER_LAG_BYTES: u64 = 128 * (1 << 20);
} }
@@ -59,7 +59,6 @@ pub struct SafeKeeperConf {
pub heartbeat_timeout: Duration, pub heartbeat_timeout: Duration,
pub remote_storage: Option<RemoteStorageConfig>, pub remote_storage: Option<RemoteStorageConfig>,
pub max_offloader_lag_bytes: u64, pub max_offloader_lag_bytes: u64,
pub backup_runtime_threads: Option<usize>,
pub wal_backup_enabled: bool, pub wal_backup_enabled: bool,
pub auth: Option<Arc<JwtAuth>>, pub auth: Option<Arc<JwtAuth>>,
} }
@@ -90,7 +89,6 @@ impl SafeKeeperConf {
.parse() .parse()
.expect("failed to parse default broker endpoint"), .expect("failed to parse default broker endpoint"),
broker_keepalive_interval: Duration::from_secs(5), broker_keepalive_interval: Duration::from_secs(5),
backup_runtime_threads: None,
wal_backup_enabled: true, wal_backup_enabled: true,
auth: None, auth: None,
heartbeat_timeout: Duration::new(5, 0), heartbeat_timeout: Duration::new(5, 0),
+38 -28
View File
@@ -2,11 +2,12 @@
use std::{ use std::{
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::{Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
use ::metrics::{register_histogram, GaugeVec, Histogram, IntGauge, DISK_WRITE_SECONDS_BUCKETS}; use ::metrics::{register_histogram, GaugeVec, Histogram, IntGauge, DISK_WRITE_SECONDS_BUCKETS};
use anyhow::Result; use anyhow::Result;
use futures::Future;
use metrics::{ use metrics::{
core::{AtomicU64, Collector, Desc, GenericCounter, GenericGaugeVec, Opts}, core::{AtomicU64, Collector, Desc, GenericCounter, GenericGaugeVec, Opts},
proto::MetricFamily, proto::MetricFamily,
@@ -15,11 +16,12 @@ use metrics::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use postgres_ffi::XLogSegNo; use postgres_ffi::XLogSegNo;
use tokio::time::interval;
use utils::pageserver_feedback::PageserverFeedback;
use utils::{id::TenantTimelineId, lsn::Lsn}; use utils::{id::TenantTimelineId, lsn::Lsn};
use crate::{ use crate::{
safekeeper::{SafeKeeperState, SafekeeperMemState}, safekeeper::{SafeKeeperState, SafekeeperMemState},
timeline::ReplicaState,
GlobalTimelines, GlobalTimelines,
}; };
@@ -221,17 +223,20 @@ impl WalStorageMetrics {
} }
} }
/// Accepts a closure that returns a result, and returns the duration of the closure. /// Accepts async function that returns empty anyhow result, and returns the duration of its execution.
pub fn time_io_closure(closure: impl FnOnce() -> Result<()>) -> Result<f64> { pub async fn time_io_closure<E: Into<anyhow::Error>>(
closure: impl Future<Output = Result<(), E>>,
) -> Result<f64> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
closure()?; closure.await.map_err(|e| e.into())?;
Ok(start.elapsed().as_secs_f64()) Ok(start.elapsed().as_secs_f64())
} }
/// Metrics for a single timeline. /// Metrics for a single timeline.
#[derive(Clone)]
pub struct FullTimelineInfo { pub struct FullTimelineInfo {
pub ttid: TenantTimelineId, pub ttid: TenantTimelineId,
pub replicas: Vec<ReplicaState>, pub ps_feedback: PageserverFeedback,
pub wal_backup_active: bool, pub wal_backup_active: bool,
pub timeline_is_active: bool, pub timeline_is_active: bool,
pub num_computes: u32, pub num_computes: u32,
@@ -242,6 +247,7 @@ pub struct FullTimelineInfo {
pub persisted_state: SafeKeeperState, pub persisted_state: SafeKeeperState,
pub flush_lsn: Lsn, pub flush_lsn: Lsn,
pub remote_consistent_lsn: Lsn,
pub wal_storage: WalStorageMetrics, pub wal_storage: WalStorageMetrics,
} }
@@ -514,19 +520,6 @@ impl Collector for TimelineCollector {
let timeline_id = tli.ttid.timeline_id.to_string(); let timeline_id = tli.ttid.timeline_id.to_string();
let labels = &[tenant_id.as_str(), timeline_id.as_str()]; let labels = &[tenant_id.as_str(), timeline_id.as_str()];
let mut most_advanced: Option<pq_proto::PageserverFeedback> = None;
for replica in tli.replicas.iter() {
if let Some(replica_feedback) = replica.pageserver_feedback {
if let Some(current) = most_advanced {
if current.last_received_lsn < replica_feedback.last_received_lsn {
most_advanced = Some(replica_feedback);
}
} else {
most_advanced = Some(replica_feedback);
}
}
}
self.commit_lsn self.commit_lsn
.with_label_values(labels) .with_label_values(labels)
.set(tli.mem_state.commit_lsn.into()); .set(tli.mem_state.commit_lsn.into());
@@ -544,7 +537,7 @@ impl Collector for TimelineCollector {
.set(tli.mem_state.peer_horizon_lsn.into()); .set(tli.mem_state.peer_horizon_lsn.into());
self.remote_consistent_lsn self.remote_consistent_lsn
.with_label_values(labels) .with_label_values(labels)
.set(tli.mem_state.remote_consistent_lsn.into()); .set(tli.remote_consistent_lsn.into());
self.timeline_active self.timeline_active
.with_label_values(labels) .with_label_values(labels)
.set(tli.timeline_is_active as u64); .set(tli.timeline_is_active as u64);
@@ -567,15 +560,17 @@ impl Collector for TimelineCollector {
.with_label_values(labels) .with_label_values(labels)
.set(tli.wal_storage.flush_wal_seconds); .set(tli.wal_storage.flush_wal_seconds);
if let Some(feedback) = most_advanced { self.ps_last_received_lsn
self.ps_last_received_lsn .with_label_values(labels)
.set(tli.ps_feedback.last_received_lsn.0);
if let Ok(unix_time) = tli
.ps_feedback
.replytime
.duration_since(SystemTime::UNIX_EPOCH)
{
self.feedback_last_time_seconds
.with_label_values(labels) .with_label_values(labels)
.set(feedback.last_received_lsn); .set(unix_time.as_secs());
if let Ok(unix_time) = feedback.replytime.duration_since(SystemTime::UNIX_EPOCH) {
self.feedback_last_time_seconds
.with_label_values(labels)
.set(unix_time.as_secs());
}
} }
if tli.last_removed_segno != 0 { if tli.last_removed_segno != 0 {
@@ -621,3 +616,18 @@ impl Collector for TimelineCollector {
mfs mfs
} }
} }
/// Prometheus crate Collector interface is sync, and all safekeeper code is
/// async. To bridge the gap, this function wakes once in scrape interval and
/// copies metrics from under async lock to sync where collection can take it.
pub async fn metrics_shifter() -> anyhow::Result<()> {
let scrape_interval = Duration::from_secs(30);
let mut interval = interval(scrape_interval);
loop {
interval.tick().await;
let timelines = GlobalTimelines::get_all();
for tli in timelines {
tli.set_info_for_metrics().await;
}
}
}
+240
View File
@@ -0,0 +1,240 @@
use serde::{Deserialize, Serialize};
use anyhow::{bail, Context, Result};
use tokio::io::AsyncWriteExt;
use tracing::info;
use utils::id::{TenantId, TenantTimelineId, TimelineId};
use serde_with::{serde_as, DisplayFromStr};
use crate::{
control_file, debug_dump,
http::routes::TimelineStatus,
wal_storage::{self, Storage},
GlobalTimelines,
};
/// Info about timeline on safekeeper ready for reporting.
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
pub struct Request {
#[serde_as(as = "DisplayFromStr")]
pub tenant_id: TenantId,
#[serde_as(as = "DisplayFromStr")]
pub timeline_id: TimelineId,
pub http_hosts: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct Response {
// Donor safekeeper host
pub safekeeper_host: String,
// TODO: add more fields?
}
/// Find the most advanced safekeeper and pull timeline from it.
pub async fn handle_request(request: Request) -> Result<Response> {
let existing_tli = GlobalTimelines::get(TenantTimelineId::new(
request.tenant_id,
request.timeline_id,
));
if existing_tli.is_ok() {
bail!("Timeline {} already exists", request.timeline_id);
}
let client = reqwest::Client::new();
let http_hosts = request.http_hosts.clone();
// Send request to /v1/tenant/:tenant_id/timeline/:timeline_id
let responses = futures::future::join_all(http_hosts.iter().map(|url| {
let url = format!(
"{}/v1/tenant/{}/timeline/{}",
url, request.tenant_id, request.timeline_id
);
client.get(url).send()
}))
.await;
let mut statuses = Vec::new();
for (i, response) in responses.into_iter().enumerate() {
let response = response.context(format!("Failed to get status from {}", http_hosts[i]))?;
let status: crate::http::routes::TimelineStatus = response.json().await?;
statuses.push((status, i));
}
// Find the most advanced safekeeper
// TODO: current logic may be wrong, fix it later
let (status, i) = statuses
.into_iter()
.max_by_key(|(status, _)| {
(
status.acceptor_state.epoch,
status.flush_lsn,
status.commit_lsn,
)
})
.unwrap();
let safekeeper_host = http_hosts[i].clone();
assert!(status.tenant_id == request.tenant_id);
assert!(status.timeline_id == request.timeline_id);
pull_timeline(status, safekeeper_host).await
}
async fn pull_timeline(status: TimelineStatus, host: String) -> Result<Response> {
let ttid = TenantTimelineId::new(status.tenant_id, status.timeline_id);
info!(
"Pulling timeline {} from safekeeper {}, commit_lsn={}, flush_lsn={}, term={}, epoch={}",
ttid,
host,
status.commit_lsn,
status.flush_lsn,
status.acceptor_state.term,
status.acceptor_state.epoch
);
let conf = &GlobalTimelines::get_global_config();
let client = reqwest::Client::new();
// TODO: don't use debug dump, it should be used only in tests.
// This is a proof of concept, we should figure out a way
// to use scp without implementing it manually.
// Implementing our own scp over HTTP.
// At first, we need to fetch list of files from safekeeper.
let dump: debug_dump::Response = client
.get(format!(
"{}/v1/debug_dump?dump_all=true&tenant_id={}&timeline_id={}",
host, status.tenant_id, status.timeline_id
))
.send()
.await?
.json()
.await?;
if dump.timelines.len() != 1 {
bail!(
"Expected to fetch single timeline, got {} timelines",
dump.timelines.len()
);
}
let timeline = dump.timelines.into_iter().next().unwrap();
let disk_content = timeline.disk_content.ok_or(anyhow::anyhow!(
"Timeline {} doesn't have disk content",
ttid
))?;
let mut filenames = disk_content
.files
.iter()
.map(|file| file.name.clone())
.collect::<Vec<_>>();
// Sort filenames to make sure we pull files in correct order
// After sorting, we should have:
// - 000000010000000000000001
// - ...
// - 000000010000000000000002.partial
// - safekeeper.control
filenames.sort();
// safekeeper.control should be the first file, so we need to move it to the beginning
let control_file_index = filenames
.iter()
.position(|name| name == "safekeeper.control")
.ok_or(anyhow::anyhow!("safekeeper.control not found"))?;
filenames.remove(control_file_index);
filenames.insert(0, "safekeeper.control".to_string());
info!(
"Downloading {} files from safekeeper {}",
filenames.len(),
host
);
// Creating temp directory for a new timeline. It needs to be
// located on the same filesystem as the rest of the timelines.
// conf.workdir is usually /storage/safekeeper/data
// will try to transform it into /storage/safekeeper/tmp
let temp_base = conf
.workdir
.parent()
.ok_or(anyhow::anyhow!("workdir has no parent"))?
.join("tmp");
tokio::fs::create_dir_all(&temp_base).await?;
let tli_dir = tempfile::Builder::new()
.suffix("_temptli")
.prefix(&format!("{}_{}_", ttid.tenant_id, ttid.timeline_id))
.tempdir_in(temp_base)?;
let tli_dir_path = tli_dir.path().to_owned();
// Note: some time happens between fetching list of files and fetching files themselves.
// It's possible that some files will be removed from safekeeper and we will fail to fetch them.
// This function will fail in this case, should be retried by the caller.
for filename in filenames {
let file_path = tli_dir_path.join(&filename);
// /v1/tenant/:tenant_id/timeline/:timeline_id/file/:filename
let http_url = format!(
"{}/v1/tenant/{}/timeline/{}/file/{}",
host, status.tenant_id, status.timeline_id, filename
);
let mut file = tokio::fs::File::create(&file_path).await?;
let mut response = client.get(&http_url).send().await?;
while let Some(chunk) = response.chunk().await? {
file.write_all(&chunk).await?;
}
}
// TODO: fsync?
// Let's create timeline from temp directory and verify that it's correct
let control_path = tli_dir_path.join("safekeeper.control");
let control_store = control_file::FileStorage::load_control_file(control_path)?;
if control_store.server.wal_seg_size == 0 {
bail!("wal_seg_size is not set");
}
let wal_store =
wal_storage::PhysicalStorage::new(&ttid, tli_dir_path.clone(), conf, &control_store)?;
let commit_lsn = status.commit_lsn;
let flush_lsn = wal_store.flush_lsn();
info!(
"Finished downloading timeline {}, commit_lsn={}, flush_lsn={}",
ttid, commit_lsn, flush_lsn
);
assert!(status.commit_lsn <= status.flush_lsn);
// Move timeline dir to the correct location
let timeline_path = conf.timeline_dir(&ttid);
info!(
"Moving timeline {} from {} to {}",
ttid,
tli_dir_path.display(),
timeline_path.display()
);
tokio::fs::create_dir_all(conf.tenant_dir(&ttid.tenant_id)).await?;
tokio::fs::rename(tli_dir_path, &timeline_path).await?;
let tli = GlobalTimelines::load_timeline(ttid).context("Failed to load timeline after copy")?;
info!(
"Loaded timeline {}, flush_lsn={}",
ttid,
tli.get_flush_lsn().await
);
Ok(Response {
safekeeper_host: host,
})
}
+57 -45
View File
@@ -18,15 +18,16 @@ use postgres_backend::QueryError;
use pq_proto::BeMessage; use pq_proto::BeMessage;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::thread;
use std::thread::JoinHandle;
use tokio::io::AsyncRead; use tokio::io::AsyncRead;
use tokio::io::AsyncWrite; use tokio::io::AsyncWrite;
use tokio::sync::mpsc::channel; use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::task::spawn_blocking; use tokio::task;
use tokio::task::JoinHandle;
use tokio::time::Duration;
use tokio::time::Instant;
use tracing::*; use tracing::*;
use utils::id::TenantTimelineId; use utils::id::TenantTimelineId;
use utils::lsn::Lsn; use utils::lsn::Lsn;
@@ -95,7 +96,7 @@ impl SafekeeperPostgresHandler {
Err(res.expect_err("no error with WalAcceptor not spawn")) Err(res.expect_err("no error with WalAcceptor not spawn"))
} }
Some(handle) => { Some(handle) => {
let wal_acceptor_res = handle.join(); let wal_acceptor_res = handle.await;
// If there was any network error, return it. // If there was any network error, return it.
res?; res?;
@@ -105,7 +106,7 @@ impl SafekeeperPostgresHandler {
Ok(Ok(_)) => Ok(()), // can't happen currently; would be if we add graceful termination Ok(Ok(_)) => Ok(()), // can't happen currently; would be if we add graceful termination
Ok(Err(e)) => Err(CopyStreamHandlerEnd::Other(e.context("WAL acceptor"))), Ok(Err(e)) => Err(CopyStreamHandlerEnd::Other(e.context("WAL acceptor"))),
Err(_) => Err(CopyStreamHandlerEnd::Other(anyhow!( Err(_) => Err(CopyStreamHandlerEnd::Other(anyhow!(
"WalAcceptor thread panicked", "WalAcceptor task panicked",
))), ))),
} }
} }
@@ -152,10 +153,12 @@ impl<'a, IO: AsyncRead + AsyncWrite + Unpin> NetworkReader<'a, IO> {
} }
}; };
*self.acceptor_handle = Some( *self.acceptor_handle = Some(WalAcceptor::spawn(
WalAcceptor::spawn(tli.clone(), msg_rx, reply_tx, self.conn_id) tli.clone(),
.context("spawn WalAcceptor thread")?, msg_rx,
); reply_tx,
self.conn_id,
));
// Forward all messages to WalAcceptor // Forward all messages to WalAcceptor
read_network_loop(self.pgb_reader, msg_tx, next_msg).await read_network_loop(self.pgb_reader, msg_tx, next_msg).await
@@ -206,6 +209,10 @@ async fn network_write<IO: AsyncRead + AsyncWrite + Unpin>(
} }
} }
// Send keepalive messages to walproposer, to make sure it receives updates
// even when it writes a steady stream of messages.
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
/// Takes messages from msg_rx, processes and pushes replies to reply_tx. /// Takes messages from msg_rx, processes and pushes replies to reply_tx.
struct WalAcceptor { struct WalAcceptor {
tli: Arc<Timeline>, tli: Arc<Timeline>,
@@ -220,28 +227,19 @@ impl WalAcceptor {
msg_rx: Receiver<ProposerAcceptorMessage>, msg_rx: Receiver<ProposerAcceptorMessage>,
reply_tx: Sender<AcceptorProposerMessage>, reply_tx: Sender<AcceptorProposerMessage>,
conn_id: ConnectionId, conn_id: ConnectionId,
) -> anyhow::Result<JoinHandle<anyhow::Result<()>>> { ) -> JoinHandle<anyhow::Result<()>> {
let thread_name = format!("WAL acceptor {}", tli.ttid); task::spawn(async move {
thread::Builder::new() let mut wa = WalAcceptor {
.name(thread_name) tli,
.spawn(move || -> anyhow::Result<()> { msg_rx,
let mut wa = WalAcceptor { reply_tx,
tli, };
msg_rx,
reply_tx,
};
let runtime = tokio::runtime::Builder::new_current_thread() let span_ttid = wa.tli.ttid; // satisfy borrow checker
.enable_all() wa.run()
.build()?; .instrument(info_span!("WAL acceptor", cid = %conn_id, ttid = %span_ttid))
.await
let span_ttid = wa.tli.ttid; // satisfy borrow checker })
runtime.block_on(
wa.run()
.instrument(info_span!("WAL acceptor", cid = %conn_id, ttid = %span_ttid)),
)
})
.map_err(anyhow::Error::from)
} }
/// The main loop. Returns Ok(()) if either msg_rx or reply_tx got closed; /// The main loop. Returns Ok(()) if either msg_rx or reply_tx got closed;
@@ -253,27 +251,39 @@ impl WalAcceptor {
timeline: Arc::clone(&self.tli), timeline: Arc::clone(&self.tli),
}; };
let mut next_msg: ProposerAcceptorMessage; // After this timestamp we will stop processing AppendRequests and send a response
// to the walproposer. walproposer sends at least one AppendRequest per second,
// we will send keepalives by replying to these requests once per second.
let mut next_keepalive = Instant::now();
loop { loop {
let opt_msg = self.msg_rx.recv().await; let opt_msg = self.msg_rx.recv().await;
if opt_msg.is_none() { if opt_msg.is_none() {
return Ok(()); // chan closed, streaming terminated return Ok(()); // chan closed, streaming terminated
} }
next_msg = opt_msg.unwrap(); let mut next_msg = opt_msg.unwrap();
if matches!(next_msg, ProposerAcceptorMessage::AppendRequest(_)) { let reply_msg = if matches!(next_msg, ProposerAcceptorMessage::AppendRequest(_)) {
// loop through AppendRequest's while it's readily available to // loop through AppendRequest's while it's readily available to
// write as many WAL as possible without fsyncing // write as many WAL as possible without fsyncing
//
// Note: this will need to be rewritten if we want to read non-AppendRequest messages here.
// Otherwise, we might end up in a situation where we read a message, but don't
// process it.
while let ProposerAcceptorMessage::AppendRequest(append_request) = next_msg { while let ProposerAcceptorMessage::AppendRequest(append_request) = next_msg {
let noflush_msg = ProposerAcceptorMessage::NoFlushAppendRequest(append_request); let noflush_msg = ProposerAcceptorMessage::NoFlushAppendRequest(append_request);
if let Some(reply) = self.tli.process_msg(&noflush_msg)? { if let Some(reply) = self.tli.process_msg(&noflush_msg).await? {
if self.reply_tx.send(reply).await.is_err() { if self.reply_tx.send(reply).await.is_err() {
return Ok(()); // chan closed, streaming terminated return Ok(()); // chan closed, streaming terminated
} }
} }
// get out of this loop if keepalive time is reached
if Instant::now() >= next_keepalive {
break;
}
match self.msg_rx.try_recv() { match self.msg_rx.try_recv() {
Ok(msg) => next_msg = msg, Ok(msg) => next_msg = msg,
Err(TryRecvError::Empty) => break, Err(TryRecvError::Empty) => break,
@@ -282,18 +292,20 @@ impl WalAcceptor {
} }
// flush all written WAL to the disk // flush all written WAL to the disk
if let Some(reply) = self.tli.process_msg(&ProposerAcceptorMessage::FlushWAL)? { self.tli
if self.reply_tx.send(reply).await.is_err() { .process_msg(&ProposerAcceptorMessage::FlushWAL)
return Ok(()); // chan closed, streaming terminated .await?
}
}
} else { } else {
// process message other than AppendRequest // process message other than AppendRequest
if let Some(reply) = self.tli.process_msg(&next_msg)? { self.tli.process_msg(&next_msg).await?
if self.reply_tx.send(reply).await.is_err() { };
return Ok(()); // chan closed, streaming terminated
} if let Some(reply) = reply_msg {
if self.reply_tx.send(reply).await.is_err() {
return Ok(()); // chan closed, streaming terminated
} }
// reset keepalive time
next_keepalive = Instant::now() + KEEPALIVE_INTERVAL;
} }
} }
} }
@@ -308,8 +320,8 @@ impl Drop for ComputeConnectionGuard {
let tli = self.timeline.clone(); let tli = self.timeline.clone();
// tokio forbids to call blocking_send inside the runtime, and see // tokio forbids to call blocking_send inside the runtime, and see
// comments in on_compute_disconnect why we call blocking_send. // comments in on_compute_disconnect why we call blocking_send.
spawn_blocking(move || { tokio::spawn(async move {
if let Err(e) = tli.on_compute_disconnect() { if let Err(e) = tli.on_compute_disconnect().await {
error!("failed to unregister compute connection: {}", e); error!("failed to unregister compute connection: {}", e);
} }
}); });
+11 -8
View File
@@ -1,26 +1,29 @@
//! Thread removing old WAL. //! Thread removing old WAL.
use std::{thread, time::Duration}; use std::time::Duration;
use tokio::time::sleep;
use tracing::*; use tracing::*;
use crate::{GlobalTimelines, SafeKeeperConf}; use crate::{GlobalTimelines, SafeKeeperConf};
pub fn thread_main(conf: SafeKeeperConf) { pub async fn task_main(conf: SafeKeeperConf) -> anyhow::Result<()> {
let wal_removal_interval = Duration::from_millis(5000); let wal_removal_interval = Duration::from_millis(5000);
loop { loop {
let tlis = GlobalTimelines::get_all(); let tlis = GlobalTimelines::get_all();
for tli in &tlis { for tli in &tlis {
if !tli.is_active() { if !tli.is_active().await {
continue; continue;
} }
let ttid = tli.ttid; let ttid = tli.ttid;
let _enter = if let Err(e) = tli
info_span!("", tenant = %ttid.tenant_id, timeline = %ttid.timeline_id).entered(); .remove_old_wal(conf.wal_backup_enabled)
if let Err(e) = tli.remove_old_wal(conf.wal_backup_enabled) { .instrument(info_span!("", tenant = %ttid.tenant_id, timeline = %ttid.timeline_id))
warn!("failed to remove WAL: {}", e); .await
{
error!("failed to remove WAL: {}", e);
} }
} }
thread::sleep(wal_removal_interval) sleep(wal_removal_interval).await;
} }
} }
+75 -63
View File
@@ -18,7 +18,8 @@ use crate::control_file;
use crate::send_wal::HotStandbyFeedback; use crate::send_wal::HotStandbyFeedback;
use crate::wal_storage; use crate::wal_storage;
use pq_proto::{PageserverFeedback, SystemId}; use pq_proto::SystemId;
use utils::pageserver_feedback::PageserverFeedback;
use utils::{ use utils::{
bin_ser::LeSer, bin_ser::LeSer,
id::{NodeId, TenantId, TenantTimelineId, TimelineId}, id::{NodeId, TenantId, TenantTimelineId, TimelineId},
@@ -205,14 +206,13 @@ pub struct SafeKeeperState {
pub peers: PersistedPeers, pub peers: PersistedPeers,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
// In memory safekeeper state. Fields mirror ones in `SafeKeeperState`; values // In memory safekeeper state. Fields mirror ones in `SafeKeeperState`; values
// are not flushed yet. // are not flushed yet.
pub struct SafekeeperMemState { pub struct SafekeeperMemState {
pub commit_lsn: Lsn, pub commit_lsn: Lsn,
pub backup_lsn: Lsn, pub backup_lsn: Lsn,
pub peer_horizon_lsn: Lsn, pub peer_horizon_lsn: Lsn,
pub remote_consistent_lsn: Lsn,
#[serde(with = "hex")] #[serde(with = "hex")]
pub proposer_uuid: PgUuid, pub proposer_uuid: PgUuid,
} }
@@ -347,7 +347,7 @@ pub struct AppendRequestHeader {
} }
/// Report safekeeper state to proposer /// Report safekeeper state to proposer
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize)]
pub struct AppendResponse { pub struct AppendResponse {
// Current term of the safekeeper; if it is higher than proposer's, the // Current term of the safekeeper; if it is higher than proposer's, the
// compute is out of date. // compute is out of date.
@@ -540,7 +540,6 @@ where
commit_lsn: state.commit_lsn, commit_lsn: state.commit_lsn,
backup_lsn: state.backup_lsn, backup_lsn: state.backup_lsn,
peer_horizon_lsn: state.peer_horizon_lsn, peer_horizon_lsn: state.peer_horizon_lsn,
remote_consistent_lsn: state.remote_consistent_lsn,
proposer_uuid: state.proposer_uuid, proposer_uuid: state.proposer_uuid,
}, },
state, state,
@@ -568,25 +567,27 @@ where
/// Process message from proposer and possibly form reply. Concurrent /// Process message from proposer and possibly form reply. Concurrent
/// callers must exclude each other. /// callers must exclude each other.
pub fn process_msg( pub async fn process_msg(
&mut self, &mut self,
msg: &ProposerAcceptorMessage, msg: &ProposerAcceptorMessage,
) -> Result<Option<AcceptorProposerMessage>> { ) -> Result<Option<AcceptorProposerMessage>> {
match msg { match msg {
ProposerAcceptorMessage::Greeting(msg) => self.handle_greeting(msg), ProposerAcceptorMessage::Greeting(msg) => self.handle_greeting(msg).await,
ProposerAcceptorMessage::VoteRequest(msg) => self.handle_vote_request(msg), ProposerAcceptorMessage::VoteRequest(msg) => self.handle_vote_request(msg).await,
ProposerAcceptorMessage::Elected(msg) => self.handle_elected(msg), ProposerAcceptorMessage::Elected(msg) => self.handle_elected(msg).await,
ProposerAcceptorMessage::AppendRequest(msg) => self.handle_append_request(msg, true), ProposerAcceptorMessage::AppendRequest(msg) => {
ProposerAcceptorMessage::NoFlushAppendRequest(msg) => { self.handle_append_request(msg, true).await
self.handle_append_request(msg, false)
} }
ProposerAcceptorMessage::FlushWAL => self.handle_flush(), ProposerAcceptorMessage::NoFlushAppendRequest(msg) => {
self.handle_append_request(msg, false).await
}
ProposerAcceptorMessage::FlushWAL => self.handle_flush().await,
} }
} }
/// Handle initial message from proposer: check its sanity and send my /// Handle initial message from proposer: check its sanity and send my
/// current term. /// current term.
fn handle_greeting( async fn handle_greeting(
&mut self, &mut self,
msg: &ProposerGreeting, msg: &ProposerGreeting,
) -> Result<Option<AcceptorProposerMessage>> { ) -> Result<Option<AcceptorProposerMessage>> {
@@ -648,7 +649,7 @@ where
if msg.pg_version != UNKNOWN_SERVER_VERSION { if msg.pg_version != UNKNOWN_SERVER_VERSION {
state.server.pg_version = msg.pg_version; state.server.pg_version = msg.pg_version;
} }
self.state.persist(&state)?; self.state.persist(&state).await?;
} }
info!( info!(
@@ -663,7 +664,7 @@ where
} }
/// Give vote for the given term, if we haven't done that previously. /// Give vote for the given term, if we haven't done that previously.
fn handle_vote_request( async fn handle_vote_request(
&mut self, &mut self,
msg: &VoteRequest, msg: &VoteRequest,
) -> Result<Option<AcceptorProposerMessage>> { ) -> Result<Option<AcceptorProposerMessage>> {
@@ -677,7 +678,7 @@ where
// handle_elected instead. Currently not a big deal, as proposer is the // handle_elected instead. Currently not a big deal, as proposer is the
// only source of WAL; with peer2peer recovery it would be more // only source of WAL; with peer2peer recovery it would be more
// important. // important.
self.wal_store.flush_wal()?; self.wal_store.flush_wal().await?;
// initialize with refusal // initialize with refusal
let mut resp = VoteResponse { let mut resp = VoteResponse {
term: self.state.acceptor_state.term, term: self.state.acceptor_state.term,
@@ -691,7 +692,7 @@ where
let mut state = self.state.clone(); let mut state = self.state.clone();
state.acceptor_state.term = msg.term; state.acceptor_state.term = msg.term;
// persist vote before sending it out // persist vote before sending it out
self.state.persist(&state)?; self.state.persist(&state).await?;
resp.term = self.state.acceptor_state.term; resp.term = self.state.acceptor_state.term;
resp.vote_given = true as u64; resp.vote_given = true as u64;
@@ -714,12 +715,15 @@ where
ar ar
} }
fn handle_elected(&mut self, msg: &ProposerElected) -> Result<Option<AcceptorProposerMessage>> { async fn handle_elected(
&mut self,
msg: &ProposerElected,
) -> Result<Option<AcceptorProposerMessage>> {
info!("received ProposerElected {:?}", msg); info!("received ProposerElected {:?}", msg);
if self.state.acceptor_state.term < msg.term { if self.state.acceptor_state.term < msg.term {
let mut state = self.state.clone(); let mut state = self.state.clone();
state.acceptor_state.term = msg.term; state.acceptor_state.term = msg.term;
self.state.persist(&state)?; self.state.persist(&state).await?;
} }
// If our term is higher, ignore the message (next feedback will inform the compute) // If our term is higher, ignore the message (next feedback will inform the compute)
@@ -749,7 +753,7 @@ where
// intersection of our history and history from msg // intersection of our history and history from msg
// truncate wal, update the LSNs // truncate wal, update the LSNs
self.wal_store.truncate_wal(msg.start_streaming_at)?; self.wal_store.truncate_wal(msg.start_streaming_at).await?;
// and now adopt term history from proposer // and now adopt term history from proposer
{ {
@@ -781,13 +785,9 @@ where
// Initializing backup_lsn is useful to avoid making backup think it should upload 0 segment. // Initializing backup_lsn is useful to avoid making backup think it should upload 0 segment.
self.inmem.backup_lsn = max(self.inmem.backup_lsn, state.timeline_start_lsn); self.inmem.backup_lsn = max(self.inmem.backup_lsn, state.timeline_start_lsn);
// Initializing remote_consistent_lsn sets that we have nothing to
// stream to pageserver(s) immediately after creation.
self.inmem.remote_consistent_lsn =
max(self.inmem.remote_consistent_lsn, state.timeline_start_lsn);
state.acceptor_state.term_history = msg.term_history.clone(); state.acceptor_state.term_history = msg.term_history.clone();
self.persist_control_file(state)?; self.persist_control_file(state).await?;
} }
info!("start receiving WAL since {:?}", msg.start_streaming_at); info!("start receiving WAL since {:?}", msg.start_streaming_at);
@@ -799,7 +799,7 @@ where
/// ///
/// Note: it is assumed that 'WAL we have is from the right term' check has /// Note: it is assumed that 'WAL we have is from the right term' check has
/// already been done outside. /// already been done outside.
fn update_commit_lsn(&mut self, mut candidate: Lsn) -> Result<()> { async fn update_commit_lsn(&mut self, mut candidate: Lsn) -> Result<()> {
// Both peers and walproposer communicate this value, we might already // Both peers and walproposer communicate this value, we might already
// have a fresher (higher) version. // have a fresher (higher) version.
candidate = max(candidate, self.inmem.commit_lsn); candidate = max(candidate, self.inmem.commit_lsn);
@@ -821,30 +821,29 @@ where
// that we receive new epoch_start_lsn, and we still need to sync // that we receive new epoch_start_lsn, and we still need to sync
// control file in this case. // control file in this case.
if commit_lsn == self.epoch_start_lsn && self.state.commit_lsn != commit_lsn { if commit_lsn == self.epoch_start_lsn && self.state.commit_lsn != commit_lsn {
self.persist_control_file(self.state.clone())?; self.persist_control_file(self.state.clone()).await?;
} }
Ok(()) Ok(())
} }
/// Persist control file to disk, called only after timeline creation (bootstrap). /// Persist control file to disk, called only after timeline creation (bootstrap).
pub fn persist(&mut self) -> Result<()> { pub async fn persist(&mut self) -> Result<()> {
self.persist_control_file(self.state.clone()) self.persist_control_file(self.state.clone()).await
} }
/// Persist in-memory state to the disk, taking other data from state. /// Persist in-memory state to the disk, taking other data from state.
fn persist_control_file(&mut self, mut state: SafeKeeperState) -> Result<()> { async fn persist_control_file(&mut self, mut state: SafeKeeperState) -> Result<()> {
state.commit_lsn = self.inmem.commit_lsn; state.commit_lsn = self.inmem.commit_lsn;
state.backup_lsn = self.inmem.backup_lsn; state.backup_lsn = self.inmem.backup_lsn;
state.peer_horizon_lsn = self.inmem.peer_horizon_lsn; state.peer_horizon_lsn = self.inmem.peer_horizon_lsn;
state.remote_consistent_lsn = self.inmem.remote_consistent_lsn;
state.proposer_uuid = self.inmem.proposer_uuid; state.proposer_uuid = self.inmem.proposer_uuid;
self.state.persist(&state) self.state.persist(&state).await
} }
/// Handle request to append WAL. /// Handle request to append WAL.
#[allow(clippy::comparison_chain)] #[allow(clippy::comparison_chain)]
fn handle_append_request( async fn handle_append_request(
&mut self, &mut self,
msg: &AppendRequest, msg: &AppendRequest,
require_flush: bool, require_flush: bool,
@@ -867,17 +866,19 @@ where
// do the job // do the job
if !msg.wal_data.is_empty() { if !msg.wal_data.is_empty() {
self.wal_store.write_wal(msg.h.begin_lsn, &msg.wal_data)?; self.wal_store
.write_wal(msg.h.begin_lsn, &msg.wal_data)
.await?;
} }
// flush wal to the disk, if required // flush wal to the disk, if required
if require_flush { if require_flush {
self.wal_store.flush_wal()?; self.wal_store.flush_wal().await?;
} }
// Update commit_lsn. // Update commit_lsn.
if msg.h.commit_lsn != Lsn(0) { if msg.h.commit_lsn != Lsn(0) {
self.update_commit_lsn(msg.h.commit_lsn)?; self.update_commit_lsn(msg.h.commit_lsn).await?;
} }
// Value calculated by walproposer can always lag: // Value calculated by walproposer can always lag:
// - safekeepers can forget inmem value and send to proposer lower // - safekeepers can forget inmem value and send to proposer lower
@@ -893,7 +894,7 @@ where
if self.state.peer_horizon_lsn + (self.state.server.wal_seg_size as u64) if self.state.peer_horizon_lsn + (self.state.server.wal_seg_size as u64)
< self.inmem.peer_horizon_lsn < self.inmem.peer_horizon_lsn
{ {
self.persist_control_file(self.state.clone())?; self.persist_control_file(self.state.clone()).await?;
} }
trace!( trace!(
@@ -915,15 +916,15 @@ where
} }
/// Flush WAL to disk. Return AppendResponse with latest LSNs. /// Flush WAL to disk. Return AppendResponse with latest LSNs.
fn handle_flush(&mut self) -> Result<Option<AcceptorProposerMessage>> { async fn handle_flush(&mut self) -> Result<Option<AcceptorProposerMessage>> {
self.wal_store.flush_wal()?; self.wal_store.flush_wal().await?;
Ok(Some(AcceptorProposerMessage::AppendResponse( Ok(Some(AcceptorProposerMessage::AppendResponse(
self.append_response(), self.append_response(),
))) )))
} }
/// Update timeline state with peer safekeeper data. /// Update timeline state with peer safekeeper data.
pub fn record_safekeeper_info(&mut self, sk_info: &SafekeeperTimelineInfo) -> Result<()> { pub async fn record_safekeeper_info(&mut self, sk_info: &SafekeeperTimelineInfo) -> Result<()> {
let mut sync_control_file = false; let mut sync_control_file = false;
if (Lsn(sk_info.commit_lsn) != Lsn::INVALID) && (sk_info.last_log_term != INVALID_TERM) { if (Lsn(sk_info.commit_lsn) != Lsn::INVALID) && (sk_info.last_log_term != INVALID_TERM) {
@@ -931,7 +932,7 @@ where
// commit_lsn if our history matches (is part of) history of advanced // commit_lsn if our history matches (is part of) history of advanced
// commit_lsn provider. // commit_lsn provider.
if sk_info.last_log_term == self.get_epoch() { if sk_info.last_log_term == self.get_epoch() {
self.update_commit_lsn(Lsn(sk_info.commit_lsn))?; self.update_commit_lsn(Lsn(sk_info.commit_lsn)).await?;
} }
} }
@@ -940,14 +941,12 @@ where
self.state.backup_lsn + (self.state.server.wal_seg_size as u64) < new_backup_lsn; self.state.backup_lsn + (self.state.server.wal_seg_size as u64) < new_backup_lsn;
self.inmem.backup_lsn = new_backup_lsn; self.inmem.backup_lsn = new_backup_lsn;
let new_remote_consistent_lsn = max( // value in sk_info should be maximized over our local in memory value.
Lsn(sk_info.remote_consistent_lsn), let new_remote_consistent_lsn = Lsn(sk_info.remote_consistent_lsn);
self.inmem.remote_consistent_lsn, assert!(self.state.remote_consistent_lsn <= new_remote_consistent_lsn);
);
sync_control_file |= self.state.remote_consistent_lsn sync_control_file |= self.state.remote_consistent_lsn
+ (self.state.server.wal_seg_size as u64) + (self.state.server.wal_seg_size as u64)
< new_remote_consistent_lsn; < new_remote_consistent_lsn;
self.inmem.remote_consistent_lsn = new_remote_consistent_lsn;
let new_peer_horizon_lsn = max(Lsn(sk_info.peer_horizon_lsn), self.inmem.peer_horizon_lsn); let new_peer_horizon_lsn = max(Lsn(sk_info.peer_horizon_lsn), self.inmem.peer_horizon_lsn);
sync_control_file |= self.state.peer_horizon_lsn + (self.state.server.wal_seg_size as u64) sync_control_file |= self.state.peer_horizon_lsn + (self.state.server.wal_seg_size as u64)
@@ -955,7 +954,12 @@ where
self.inmem.peer_horizon_lsn = new_peer_horizon_lsn; self.inmem.peer_horizon_lsn = new_peer_horizon_lsn;
if sync_control_file { if sync_control_file {
self.persist_control_file(self.state.clone())?; let mut state = self.state.clone();
// Note: we do not persist remote_consistent_lsn in other paths of
// persisting cf -- that is not much needed currently. We could do
// that by storing Arc to walsenders in Safekeeper.
state.remote_consistent_lsn = new_remote_consistent_lsn;
self.persist_control_file(state).await?;
} }
Ok(()) Ok(())
} }
@@ -979,6 +983,7 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use futures::future::BoxFuture;
use postgres_ffi::WAL_SEGMENT_SIZE; use postgres_ffi::WAL_SEGMENT_SIZE;
use super::*; use super::*;
@@ -990,8 +995,9 @@ mod tests {
persisted_state: SafeKeeperState, persisted_state: SafeKeeperState,
} }
#[async_trait::async_trait]
impl control_file::Storage for InMemoryState { impl control_file::Storage for InMemoryState {
fn persist(&mut self, s: &SafeKeeperState) -> Result<()> { async fn persist(&mut self, s: &SafeKeeperState) -> Result<()> {
self.persisted_state = s.clone(); self.persisted_state = s.clone();
Ok(()) Ok(())
} }
@@ -1017,27 +1023,28 @@ mod tests {
lsn: Lsn, lsn: Lsn,
} }
#[async_trait::async_trait]
impl wal_storage::Storage for DummyWalStore { impl wal_storage::Storage for DummyWalStore {
fn flush_lsn(&self) -> Lsn { fn flush_lsn(&self) -> Lsn {
self.lsn self.lsn
} }
fn write_wal(&mut self, startpos: Lsn, buf: &[u8]) -> Result<()> { async fn write_wal(&mut self, startpos: Lsn, buf: &[u8]) -> Result<()> {
self.lsn = startpos + buf.len() as u64; self.lsn = startpos + buf.len() as u64;
Ok(()) Ok(())
} }
fn truncate_wal(&mut self, end_pos: Lsn) -> Result<()> { async fn truncate_wal(&mut self, end_pos: Lsn) -> Result<()> {
self.lsn = end_pos; self.lsn = end_pos;
Ok(()) Ok(())
} }
fn flush_wal(&mut self) -> Result<()> { async fn flush_wal(&mut self) -> Result<()> {
Ok(()) Ok(())
} }
fn remove_up_to(&self) -> Box<dyn Fn(XLogSegNo) -> Result<()>> { fn remove_up_to(&self, _segno_up_to: XLogSegNo) -> BoxFuture<'static, anyhow::Result<()>> {
Box::new(move |_segno_up_to: XLogSegNo| Ok(())) Box::pin(async { Ok(()) })
} }
fn get_metrics(&self) -> crate::metrics::WalStorageMetrics { fn get_metrics(&self) -> crate::metrics::WalStorageMetrics {
@@ -1045,8 +1052,8 @@ mod tests {
} }
} }
#[test] #[tokio::test]
fn test_voting() { async fn test_voting() {
let storage = InMemoryState { let storage = InMemoryState {
persisted_state: test_sk_state(), persisted_state: test_sk_state(),
}; };
@@ -1055,7 +1062,7 @@ mod tests {
// check voting for 1 is ok // check voting for 1 is ok
let vote_request = ProposerAcceptorMessage::VoteRequest(VoteRequest { term: 1 }); let vote_request = ProposerAcceptorMessage::VoteRequest(VoteRequest { term: 1 });
let mut vote_resp = sk.process_msg(&vote_request); let mut vote_resp = sk.process_msg(&vote_request).await;
match vote_resp.unwrap() { match vote_resp.unwrap() {
Some(AcceptorProposerMessage::VoteResponse(resp)) => assert!(resp.vote_given != 0), Some(AcceptorProposerMessage::VoteResponse(resp)) => assert!(resp.vote_given != 0),
r => panic!("unexpected response: {:?}", r), r => panic!("unexpected response: {:?}", r),
@@ -1070,15 +1077,15 @@ mod tests {
sk = SafeKeeper::new(storage, sk.wal_store, NodeId(0)).unwrap(); sk = SafeKeeper::new(storage, sk.wal_store, NodeId(0)).unwrap();
// and ensure voting second time for 1 is not ok // and ensure voting second time for 1 is not ok
vote_resp = sk.process_msg(&vote_request); vote_resp = sk.process_msg(&vote_request).await;
match vote_resp.unwrap() { match vote_resp.unwrap() {
Some(AcceptorProposerMessage::VoteResponse(resp)) => assert!(resp.vote_given == 0), Some(AcceptorProposerMessage::VoteResponse(resp)) => assert!(resp.vote_given == 0),
r => panic!("unexpected response: {:?}", r), r => panic!("unexpected response: {:?}", r),
} }
} }
#[test] #[tokio::test]
fn test_epoch_switch() { async fn test_epoch_switch() {
let storage = InMemoryState { let storage = InMemoryState {
persisted_state: test_sk_state(), persisted_state: test_sk_state(),
}; };
@@ -1110,10 +1117,13 @@ mod tests {
timeline_start_lsn: Lsn(0), timeline_start_lsn: Lsn(0),
}; };
sk.process_msg(&ProposerAcceptorMessage::Elected(pem)) sk.process_msg(&ProposerAcceptorMessage::Elected(pem))
.await
.unwrap(); .unwrap();
// check that AppendRequest before epochStartLsn doesn't switch epoch // check that AppendRequest before epochStartLsn doesn't switch epoch
let resp = sk.process_msg(&ProposerAcceptorMessage::AppendRequest(append_request)); let resp = sk
.process_msg(&ProposerAcceptorMessage::AppendRequest(append_request))
.await;
assert!(resp.is_ok()); assert!(resp.is_ok());
assert_eq!(sk.get_epoch(), 0); assert_eq!(sk.get_epoch(), 0);
@@ -1124,9 +1134,11 @@ mod tests {
h: ar_hdr, h: ar_hdr,
wal_data: Bytes::from_static(b"b"), wal_data: Bytes::from_static(b"b"),
}; };
let resp = sk.process_msg(&ProposerAcceptorMessage::AppendRequest(append_request)); let resp = sk
.process_msg(&ProposerAcceptorMessage::AppendRequest(append_request))
.await;
assert!(resp.is_ok()); assert!(resp.is_ok());
sk.wal_store.truncate_wal(Lsn(3)).unwrap(); // imitate the complete record at 3 %) sk.wal_store.truncate_wal(Lsn(3)).await.unwrap(); // imitate the complete record at 3 %)
assert_eq!(sk.get_epoch(), 1); assert_eq!(sk.get_epoch(), 1);
} }
} }
+422 -58
View File
@@ -1,21 +1,28 @@
//! This module implements the streaming side of replication protocol, starting //! This module implements the streaming side of replication protocol, starting
//! with the "START_REPLICATION" message. //! with the "START_REPLICATION" message, and registry of walsenders.
use crate::handler::SafekeeperPostgresHandler; use crate::handler::SafekeeperPostgresHandler;
use crate::timeline::{ReplicaState, Timeline}; use crate::timeline::Timeline;
use crate::wal_service::ConnectionId;
use crate::wal_storage::WalReader; use crate::wal_storage::WalReader;
use crate::GlobalTimelines; use crate::GlobalTimelines;
use anyhow::Context as AnyhowContext; use anyhow::Context as AnyhowContext;
use bytes::Bytes; use bytes::Bytes;
use parking_lot::Mutex;
use postgres_backend::PostgresBackend; use postgres_backend::PostgresBackend;
use postgres_backend::{CopyStreamHandlerEnd, PostgresBackendReader, QueryError}; use postgres_backend::{CopyStreamHandlerEnd, PostgresBackendReader, QueryError};
use postgres_ffi::get_current_timestamp; use postgres_ffi::get_current_timestamp;
use postgres_ffi::{TimestampTz, MAX_SEND_SIZE}; use postgres_ffi::{TimestampTz, MAX_SEND_SIZE};
use pq_proto::{BeMessage, PageserverFeedback, WalSndKeepAlive, XLogDataBody}; use pq_proto::{BeMessage, WalSndKeepAlive, XLogDataBody};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use utils::id::TenantTimelineId;
use utils::lsn::AtomicLsn;
use utils::pageserver_feedback::PageserverFeedback;
use std::cmp::min; use std::cmp::{max, min};
use std::net::SocketAddr;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -40,6 +47,8 @@ pub struct HotStandbyFeedback {
pub catalog_xmin: FullTransactionId, pub catalog_xmin: FullTransactionId,
} }
const INVALID_FULL_TRANSACTION_ID: FullTransactionId = 0;
impl HotStandbyFeedback { impl HotStandbyFeedback {
pub fn empty() -> HotStandbyFeedback { pub fn empty() -> HotStandbyFeedback {
HotStandbyFeedback { HotStandbyFeedback {
@@ -51,24 +60,294 @@ impl HotStandbyFeedback {
} }
/// Standby status update /// Standby status update
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct StandbyReply { pub struct StandbyReply {
pub write_lsn: Lsn, // last lsn received by pageserver pub write_lsn: Lsn, // The location of the last WAL byte + 1 received and written to disk in the standby.
pub flush_lsn: Lsn, // pageserver's disk consistent lSN pub flush_lsn: Lsn, // The location of the last WAL byte + 1 flushed to disk in the standby.
pub apply_lsn: Lsn, // pageserver's remote consistent lSN pub apply_lsn: Lsn, // The location of the last WAL byte + 1 applied in the standby.
pub reply_ts: TimestampTz, pub reply_ts: TimestampTz, // The client's system clock at the time of transmission, as microseconds since midnight on 2000-01-01.
pub reply_requested: bool, pub reply_requested: bool,
} }
/// Scope guard to unregister replication connection from timeline impl StandbyReply {
struct ReplicationConnGuard { fn empty() -> Self {
replica: usize, // replica internal ID assigned by timeline StandbyReply {
timeline: Arc<Timeline>, write_lsn: Lsn::INVALID,
flush_lsn: Lsn::INVALID,
apply_lsn: Lsn::INVALID,
reply_ts: 0,
reply_requested: false,
}
}
} }
impl Drop for ReplicationConnGuard { #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct StandbyFeedback {
reply: StandbyReply,
hs_feedback: HotStandbyFeedback,
}
/// WalSenders registry. Timeline holds it (wrapped in Arc).
pub struct WalSenders {
/// Lsn maximized over all walsenders *and* peer data, so might be higher
/// than what we receive from replicas.
remote_consistent_lsn: AtomicLsn,
mutex: Mutex<WalSendersShared>,
}
impl WalSenders {
pub fn new(remote_consistent_lsn: Lsn) -> Arc<WalSenders> {
Arc::new(WalSenders {
remote_consistent_lsn: AtomicLsn::from(remote_consistent_lsn),
mutex: Mutex::new(WalSendersShared::new()),
})
}
/// Register new walsender. Returned guard provides access to the slot and
/// automatically deregisters in Drop.
fn register(
self: &Arc<WalSenders>,
ttid: TenantTimelineId,
addr: SocketAddr,
conn_id: ConnectionId,
appname: Option<String>,
) -> WalSenderGuard {
let slots = &mut self.mutex.lock().slots;
let walsender_state = WalSenderState {
ttid,
addr,
conn_id,
appname,
feedback: ReplicationFeedback::Pageserver(PageserverFeedback::empty()),
};
// find empty slot or create new one
let pos = if let Some(pos) = slots.iter().position(|s| s.is_none()) {
slots[pos] = Some(walsender_state);
pos
} else {
let pos = slots.len();
slots.push(Some(walsender_state));
pos
};
WalSenderGuard {
id: pos,
walsenders: self.clone(),
}
}
/// Get state of all walsenders.
pub fn get_all(self: &Arc<WalSenders>) -> Vec<WalSenderState> {
self.mutex.lock().slots.iter().flatten().cloned().collect()
}
/// Get aggregated pageserver feedback.
pub fn get_ps_feedback(self: &Arc<WalSenders>) -> PageserverFeedback {
self.mutex.lock().agg_ps_feedback
}
/// Get aggregated pageserver and hot standby feedback (we send them to compute).
pub fn get_feedbacks(self: &Arc<WalSenders>) -> (PageserverFeedback, HotStandbyFeedback) {
let shared = self.mutex.lock();
(shared.agg_ps_feedback, shared.agg_hs_feedback)
}
/// Record new pageserver feedback, update aggregated values.
fn record_ps_feedback(self: &Arc<WalSenders>, id: WalSenderId, feedback: &PageserverFeedback) {
let mut shared = self.mutex.lock();
shared.get_slot_mut(id).feedback = ReplicationFeedback::Pageserver(*feedback);
shared.update_ps_feedback();
self.update_remote_consistent_lsn(shared.agg_ps_feedback.remote_consistent_lsn);
}
/// Record standby reply.
fn record_standby_reply(self: &Arc<WalSenders>, id: WalSenderId, reply: &StandbyReply) {
let mut shared = self.mutex.lock();
let slot = shared.get_slot_mut(id);
match &mut slot.feedback {
ReplicationFeedback::Standby(sf) => sf.reply = *reply,
ReplicationFeedback::Pageserver(_) => {
slot.feedback = ReplicationFeedback::Standby(StandbyFeedback {
reply: *reply,
hs_feedback: HotStandbyFeedback::empty(),
})
}
}
}
/// Record hot standby feedback, update aggregated value.
fn record_hs_feedback(self: &Arc<WalSenders>, id: WalSenderId, feedback: &HotStandbyFeedback) {
let mut shared = self.mutex.lock();
let slot = shared.get_slot_mut(id);
match &mut slot.feedback {
ReplicationFeedback::Standby(sf) => sf.hs_feedback = *feedback,
ReplicationFeedback::Pageserver(_) => {
slot.feedback = ReplicationFeedback::Standby(StandbyFeedback {
reply: StandbyReply::empty(),
hs_feedback: *feedback,
})
}
}
shared.update_hs_feedback();
}
/// Get remote_consistent_lsn reported by the pageserver. Returns None if
/// client is not pageserver.
fn get_ws_remote_consistent_lsn(self: &Arc<WalSenders>, id: WalSenderId) -> Option<Lsn> {
let shared = self.mutex.lock();
let slot = shared.get_slot(id);
match slot.feedback {
ReplicationFeedback::Pageserver(feedback) => Some(feedback.remote_consistent_lsn),
_ => None,
}
}
/// Get remote_consistent_lsn maximized across all walsenders and peers.
pub fn get_remote_consistent_lsn(self: &Arc<WalSenders>) -> Lsn {
self.remote_consistent_lsn.load()
}
/// Update maximized remote_consistent_lsn, return new (potentially) value.
pub fn update_remote_consistent_lsn(self: &Arc<WalSenders>, candidate: Lsn) -> Lsn {
self.remote_consistent_lsn
.fetch_max(candidate)
.max(candidate)
}
/// Unregister walsender.
fn unregister(self: &Arc<WalSenders>, id: WalSenderId) {
let mut shared = self.mutex.lock();
shared.slots[id] = None;
shared.update_hs_feedback();
}
}
struct WalSendersShared {
// aggregated over all walsenders value
agg_hs_feedback: HotStandbyFeedback,
// aggregated over all walsenders value
agg_ps_feedback: PageserverFeedback,
slots: Vec<Option<WalSenderState>>,
}
impl WalSendersShared {
fn new() -> Self {
WalSendersShared {
agg_hs_feedback: HotStandbyFeedback::empty(),
agg_ps_feedback: PageserverFeedback::empty(),
slots: Vec::new(),
}
}
/// Get content of provided id slot, it must exist.
fn get_slot(&self, id: WalSenderId) -> &WalSenderState {
self.slots[id].as_ref().expect("walsender doesn't exist")
}
/// Get mut content of provided id slot, it must exist.
fn get_slot_mut(&mut self, id: WalSenderId) -> &mut WalSenderState {
self.slots[id].as_mut().expect("walsender doesn't exist")
}
/// Update aggregated hot standy feedback. We just take min of valid xmins
/// and ts.
fn update_hs_feedback(&mut self) {
let mut agg = HotStandbyFeedback::empty();
for ws_state in self.slots.iter().flatten() {
if let ReplicationFeedback::Standby(standby_feedback) = ws_state.feedback {
let hs_feedback = standby_feedback.hs_feedback;
// doing Option math like op1.iter().chain(op2.iter()).min()
// would be nicer, but we serialize/deserialize this struct
// directly, so leave as is for now
if hs_feedback.xmin != INVALID_FULL_TRANSACTION_ID {
if agg.xmin != INVALID_FULL_TRANSACTION_ID {
agg.xmin = min(agg.xmin, hs_feedback.xmin);
} else {
agg.xmin = hs_feedback.xmin;
}
agg.ts = min(agg.ts, hs_feedback.ts);
}
if hs_feedback.catalog_xmin != INVALID_FULL_TRANSACTION_ID {
if agg.catalog_xmin != INVALID_FULL_TRANSACTION_ID {
agg.catalog_xmin = min(agg.catalog_xmin, hs_feedback.catalog_xmin);
} else {
agg.catalog_xmin = hs_feedback.catalog_xmin;
}
agg.ts = min(agg.ts, hs_feedback.ts);
}
}
}
self.agg_hs_feedback = agg;
}
/// Update aggregated pageserver feedback. LSNs (last_received,
/// disk_consistent, remote_consistent) and reply timestamp are just
/// maximized; timeline_size if taken from feedback with highest
/// last_received lsn. This is generally reasonable, but we might want to
/// implement other policies once multiple pageservers start to be actively
/// used.
fn update_ps_feedback(&mut self) {
let init = PageserverFeedback::empty();
let acc =
self.slots
.iter()
.flatten()
.fold(init, |mut acc, ws_state| match ws_state.feedback {
ReplicationFeedback::Pageserver(feedback) => {
if feedback.last_received_lsn > acc.last_received_lsn {
acc.current_timeline_size = feedback.current_timeline_size;
}
acc.last_received_lsn =
max(feedback.last_received_lsn, acc.last_received_lsn);
acc.disk_consistent_lsn =
max(feedback.disk_consistent_lsn, acc.disk_consistent_lsn);
acc.remote_consistent_lsn =
max(feedback.remote_consistent_lsn, acc.remote_consistent_lsn);
acc.replytime = max(feedback.replytime, acc.replytime);
acc
}
ReplicationFeedback::Standby(_) => acc,
});
self.agg_ps_feedback = acc;
}
}
// Serialized is used only for pretty printing in json.
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalSenderState {
#[serde_as(as = "DisplayFromStr")]
ttid: TenantTimelineId,
addr: SocketAddr,
conn_id: ConnectionId,
// postgres application_name
appname: Option<String>,
feedback: ReplicationFeedback,
}
// Receiver is either pageserver or regular standby, which have different
// feedbacks.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
enum ReplicationFeedback {
Pageserver(PageserverFeedback),
Standby(StandbyFeedback),
}
// id of the occupied slot in WalSenders to access it (and save in the
// WalSenderGuard). We could give Arc directly to the slot, but there is not
// much sense in that as values aggregation which is performed on each feedback
// receival iterates over all walsenders.
pub type WalSenderId = usize;
/// Scope guard to access slot in WalSenders registry and unregister from it in
/// Drop.
pub struct WalSenderGuard {
id: WalSenderId,
walsenders: Arc<WalSenders>,
}
impl Drop for WalSenderGuard {
fn drop(&mut self) { fn drop(&mut self) {
self.timeline.remove_replica(self.replica); self.walsenders.unregister(self.id);
} }
} }
@@ -97,16 +376,13 @@ impl SafekeeperPostgresHandler {
let tli = let tli =
GlobalTimelines::get(self.ttid).map_err(|e| CopyStreamHandlerEnd::Other(e.into()))?; GlobalTimelines::get(self.ttid).map_err(|e| CopyStreamHandlerEnd::Other(e.into()))?;
let state = ReplicaState::new(); // Use a guard object to remove our entry from the timeline when we are done.
// This replica_id is used below to check if it's time to stop replication. let ws_guard = Arc::new(tli.get_walsenders().register(
let replica_id = tli.add_replica(state); self.ttid,
*pgb.get_peer_addr(),
// Use a guard object to remove our entry from the timeline, when the background self.conn_id,
// thread and us have both finished using it. self.appname.clone(),
let _guard = Arc::new(ReplicationConnGuard { ));
replica: replica_id,
timeline: tli.clone(),
});
// Walproposer gets special handling: safekeeper must give proposer all // Walproposer gets special handling: safekeeper must give proposer all
// local WAL till the end, whether committed or not (walproposer will // local WAL till the end, whether committed or not (walproposer will
@@ -118,7 +394,7 @@ impl SafekeeperPostgresHandler {
// on this safekeeper itself. That's ok as (old) proposer will never be // on this safekeeper itself. That's ok as (old) proposer will never be
// able to commit such WAL. // able to commit such WAL.
let stop_pos: Option<Lsn> = if self.is_walproposer_recovery() { let stop_pos: Option<Lsn> = if self.is_walproposer_recovery() {
let wal_end = tli.get_flush_lsn(); let wal_end = tli.get_flush_lsn().await;
Some(wal_end) Some(wal_end)
} else { } else {
None None
@@ -133,7 +409,7 @@ impl SafekeeperPostgresHandler {
// switch to copy // switch to copy
pgb.write_message(&BeMessage::CopyBothResponse).await?; pgb.write_message(&BeMessage::CopyBothResponse).await?;
let (_, persisted_state) = tli.get_state(); let (_, persisted_state) = tli.get_state().await;
let wal_reader = WalReader::new( let wal_reader = WalReader::new(
self.conf.workdir.clone(), self.conf.workdir.clone(),
self.conf.timeline_dir(&tli.ttid), self.conf.timeline_dir(&tli.ttid),
@@ -154,16 +430,11 @@ impl SafekeeperPostgresHandler {
end_pos, end_pos,
stop_pos, stop_pos,
commit_lsn_watch_rx: tli.get_commit_lsn_watch_rx(), commit_lsn_watch_rx: tli.get_commit_lsn_watch_rx(),
replica_id, ws_guard: ws_guard.clone(),
wal_reader, wal_reader,
send_buf: [0; MAX_SEND_SIZE], send_buf: [0; MAX_SEND_SIZE],
}; };
let mut reply_reader = ReplyReader { let mut reply_reader = ReplyReader { reader, ws_guard };
reader,
tli,
replica_id,
feedback: ReplicaState::new(),
};
let res = tokio::select! { let res = tokio::select! {
// todo: add read|write .context to these errors // todo: add read|write .context to these errors
@@ -190,7 +461,7 @@ struct WalSender<'a, IO> {
// in recovery. // in recovery.
stop_pos: Option<Lsn>, stop_pos: Option<Lsn>,
commit_lsn_watch_rx: Receiver<Lsn>, commit_lsn_watch_rx: Receiver<Lsn>,
replica_id: usize, ws_guard: Arc<WalSenderGuard>,
wal_reader: WalReader, wal_reader: WalReader,
// buffer for readling WAL into to send it // buffer for readling WAL into to send it
send_buf: [u8; MAX_SEND_SIZE], send_buf: [u8; MAX_SEND_SIZE],
@@ -264,14 +535,20 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> WalSender<'_, IO> {
return Ok(()); return Ok(());
} }
// Timed out waiting for WAL, check for termination and send KA // Timed out waiting for WAL, check for termination and send KA
if self.tli.should_walsender_stop(self.replica_id) { if let Some(remote_consistent_lsn) = self
// Terminate if there is nothing more to send. .ws_guard
// TODO close the stream properly .walsenders
return Err(CopyStreamHandlerEnd::ServerInitiated(format!( .get_ws_remote_consistent_lsn(self.ws_guard.id)
"ending streaming to {:?} at {}, receiver is caughtup and there is no computes", {
self.appname, self.start_pos, if self.tli.should_walsender_stop(remote_consistent_lsn).await {
))); // Terminate if there is nothing more to send.
return Err(CopyStreamHandlerEnd::ServerInitiated(format!(
"ending streaming to {:?} at {}, receiver is caughtup and there is no computes",
self.appname, self.start_pos,
)));
}
} }
self.pgb self.pgb
.write_message(&BeMessage::KeepAlive(WalSndKeepAlive { .write_message(&BeMessage::KeepAlive(WalSndKeepAlive {
sent_ptr: self.end_pos.0, sent_ptr: self.end_pos.0,
@@ -286,9 +563,7 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> WalSender<'_, IO> {
/// A half driving receiving replies. /// A half driving receiving replies.
struct ReplyReader<IO> { struct ReplyReader<IO> {
reader: PostgresBackendReader<IO>, reader: PostgresBackendReader<IO>,
tli: Arc<Timeline>, ws_guard: Arc<WalSenderGuard>,
replica_id: usize,
feedback: ReplicaState,
} }
impl<IO: AsyncRead + AsyncWrite + Unpin> ReplyReader<IO> { impl<IO: AsyncRead + AsyncWrite + Unpin> ReplyReader<IO> {
@@ -303,29 +578,32 @@ impl<IO: AsyncRead + AsyncWrite + Unpin> ReplyReader<IO> {
match msg.first().cloned() { match msg.first().cloned() {
Some(HOT_STANDBY_FEEDBACK_TAG_BYTE) => { Some(HOT_STANDBY_FEEDBACK_TAG_BYTE) => {
// Note: deserializing is on m[1..] because we skip the tag byte. // Note: deserializing is on m[1..] because we skip the tag byte.
self.feedback.hs_feedback = HotStandbyFeedback::des(&msg[1..]) let hs_feedback = HotStandbyFeedback::des(&msg[1..])
.context("failed to deserialize HotStandbyFeedback")?; .context("failed to deserialize HotStandbyFeedback")?;
self.tli self.ws_guard
.update_replica_state(self.replica_id, self.feedback); .walsenders
.record_hs_feedback(self.ws_guard.id, &hs_feedback);
} }
Some(STANDBY_STATUS_UPDATE_TAG_BYTE) => { Some(STANDBY_STATUS_UPDATE_TAG_BYTE) => {
let _reply = let reply =
StandbyReply::des(&msg[1..]).context("failed to deserialize StandbyReply")?; StandbyReply::des(&msg[1..]).context("failed to deserialize StandbyReply")?;
// This must be a regular postgres replica, self.ws_guard
// because pageserver doesn't send this type of messages to safekeeper. .walsenders
// Currently we just ignore this, tracking progress for them is not supported. .record_standby_reply(self.ws_guard.id, &reply);
} }
Some(NEON_STATUS_UPDATE_TAG_BYTE) => { Some(NEON_STATUS_UPDATE_TAG_BYTE) => {
// pageserver sends this. // pageserver sends this.
// Note: deserializing is on m[9..] because we skip the tag byte and len bytes. // Note: deserializing is on m[9..] because we skip the tag byte and len bytes.
let buf = Bytes::copy_from_slice(&msg[9..]); let buf = Bytes::copy_from_slice(&msg[9..]);
let reply = PageserverFeedback::parse(buf); let ps_feedback = PageserverFeedback::parse(buf);
trace!("PageserverFeedback is {:?}", reply); trace!("PageserverFeedback is {:?}", ps_feedback);
self.feedback.pageserver_feedback = Some(reply); self.ws_guard
.walsenders
self.tli .record_ps_feedback(self.ws_guard.id, &ps_feedback);
.update_replica_state(self.replica_id, self.feedback); // in principle new remote_consistent_lsn could allow to
// deactivate the timeline, but we check that regularly through
// broker updated, not need to do it here
} }
_ => warn!("unexpected message {:?}", msg), _ => warn!("unexpected message {:?}", msg),
} }
@@ -368,3 +646,89 @@ async fn wait_for_lsn(rx: &mut Receiver<Lsn>, lsn: Lsn) -> anyhow::Result<Option
Err(_) => Ok(None), Err(_) => Ok(None),
} }
} }
#[cfg(test)]
mod tests {
use postgres_protocol::PG_EPOCH;
use utils::id::{TenantId, TimelineId};
use super::*;
fn mock_ttid() -> TenantTimelineId {
TenantTimelineId {
tenant_id: TenantId::from_slice(&[0x00; 16]).unwrap(),
timeline_id: TimelineId::from_slice(&[0x00; 16]).unwrap(),
}
}
fn mock_addr() -> SocketAddr {
"127.0.0.1:8080".parse().unwrap()
}
// add to wss specified feedback setting other fields to dummy values
fn push_feedback(wss: &mut WalSendersShared, feedback: ReplicationFeedback) {
let walsender_state = WalSenderState {
ttid: mock_ttid(),
addr: mock_addr(),
conn_id: 1,
appname: None,
feedback,
};
wss.slots.push(Some(walsender_state))
}
// form standby feedback with given hot standby feedback ts/xmin and the
// rest set to dummy values.
fn hs_feedback(ts: TimestampTz, xmin: FullTransactionId) -> ReplicationFeedback {
ReplicationFeedback::Standby(StandbyFeedback {
reply: StandbyReply::empty(),
hs_feedback: HotStandbyFeedback {
ts,
xmin,
catalog_xmin: 0,
},
})
}
// test that hs aggregation works as expected
#[test]
fn test_hs_feedback_no_valid() {
let mut wss = WalSendersShared::new();
push_feedback(&mut wss, hs_feedback(1, INVALID_FULL_TRANSACTION_ID));
wss.update_hs_feedback();
assert_eq!(wss.agg_hs_feedback.xmin, INVALID_FULL_TRANSACTION_ID);
}
#[test]
fn test_hs_feedback() {
let mut wss = WalSendersShared::new();
push_feedback(&mut wss, hs_feedback(1, INVALID_FULL_TRANSACTION_ID));
push_feedback(&mut wss, hs_feedback(1, 42));
push_feedback(&mut wss, hs_feedback(1, 64));
wss.update_hs_feedback();
assert_eq!(wss.agg_hs_feedback.xmin, 42);
}
// form pageserver feedback with given last_record_lsn / tli size and the
// rest set to dummy values.
fn ps_feedback(current_timeline_size: u64, last_received_lsn: Lsn) -> ReplicationFeedback {
ReplicationFeedback::Pageserver(PageserverFeedback {
current_timeline_size,
last_received_lsn,
disk_consistent_lsn: Lsn::INVALID,
remote_consistent_lsn: Lsn::INVALID,
replytime: *PG_EPOCH,
})
}
// test that ps aggregation works as expected
#[test]
fn test_ps_feedback() {
let mut wss = WalSendersShared::new();
push_feedback(&mut wss, ps_feedback(8, Lsn(42)));
push_feedback(&mut wss, ps_feedback(4, Lsn(84)));
wss.update_ps_feedback();
assert_eq!(wss.agg_ps_feedback.current_timeline_size, 4);
assert_eq!(wss.agg_ps_feedback.last_received_lsn, Lsn(84));
}
}

Some files were not shown because too many files have changed in this diff Show More