mirror of
https://github.com/neondatabase/neon.git
synced 2026-07-09 23:20:39 +00:00
Compare commits
10 Commits
readme_add
...
conrad-pla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4209e7a7 | ||
|
|
0e51790ebd | ||
|
|
28ba1cbb09 | ||
|
|
b2e2eb54ce | ||
|
|
55866e99d7 | ||
|
|
6014f15157 | ||
|
|
e675a21346 | ||
|
|
6b93230270 | ||
|
|
797aa4ffaa | ||
|
|
c45b56e0bb |
6
.github/workflows/build_and_test.yml
vendored
6
.github/workflows/build_and_test.yml
vendored
@@ -159,6 +159,10 @@ jobs:
|
||||
# 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
|
||||
#
|
||||
# The only difference between "clippy --debug" and "clippy --release" is that in --release mode,
|
||||
# #[cfg(debug_assertions)] blocks are not built. It's not worth building everything for second
|
||||
# time just for that, so skip "clippy --release".
|
||||
- run: |
|
||||
CLIPPY_COMMON_ARGS="$( source .neon_clippy_args; echo "$CLIPPY_COMMON_ARGS")"
|
||||
if [ "$CLIPPY_COMMON_ARGS" = "" ]; then
|
||||
@@ -168,8 +172,6 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Check documentation generation
|
||||
run: cargo doc --workspace --no-deps --document-private-items
|
||||
|
||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -3959,6 +3959,16 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-list"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe91484d5a948b56f858ff2b92fd5b20b97d21b11d2d41041db8e5ec12d56c5e"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"pinned-aliasable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.0"
|
||||
@@ -3991,6 +4001,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pinned-aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0f9ae89bf0ed03b69ac1f3f7ea2e6e09b4fa5448011df2e67d581c2b850b7b"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
@@ -6807,6 +6823,7 @@ dependencies = [
|
||||
"metrics",
|
||||
"nix 0.27.1",
|
||||
"once_cell",
|
||||
"pin-list",
|
||||
"pin-project-lite",
|
||||
"postgres_connection",
|
||||
"pq_proto",
|
||||
|
||||
10
README.md
10
README.md
@@ -52,11 +52,6 @@ Building Neon requires 3.15+ version of `protoc` (protobuf-compiler). If your di
|
||||
```
|
||||
# recommended approach from https://www.rust-lang.org/tools/install
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# bash-only: add $HOME/.cargo/bin to PATH. The script does this for zsh
|
||||
# echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
|
||||
|
||||
rustup show
|
||||
```
|
||||
|
||||
#### Installing dependencies on macOS (12.3.1)
|
||||
@@ -79,11 +74,6 @@ brew link --force m4
|
||||
```
|
||||
# recommended approach from https://www.rust-lang.org/tools/install
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# bash-only: add $HOME/.cargo/bin to PATH. The script does this for zsh
|
||||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
|
||||
|
||||
rustup show
|
||||
```
|
||||
|
||||
3. Install PostgreSQL Client
|
||||
|
||||
@@ -27,6 +27,7 @@ futures = { workspace = true}
|
||||
jsonwebtoken.workspace = true
|
||||
nix.workspace = true
|
||||
once_cell.workspace = true
|
||||
pin-list = "0.1"
|
||||
pin-project-lite.workspace = true
|
||||
regex.workspace = true
|
||||
routerify.workspace = true
|
||||
|
||||
@@ -82,7 +82,7 @@ impl ApiError {
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
ApiError::InternalServerError(err) => HttpErrorBody::response_from_msg_and_status(
|
||||
err.to_string(),
|
||||
format!("{err:#}"), // use alternative formatting so that we give the cause without backtrace
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -21,25 +21,49 @@
|
||||
//!
|
||||
//! Another explaination can be found here: <https://brandur.org/rate-limiting>
|
||||
|
||||
use std::{sync::Mutex, time::Duration};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::Mutex,
|
||||
task::{Poll, Waker},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::{sync::Notify, time::Instant};
|
||||
use pin_list::{Node, NodeData, PinList};
|
||||
use tokio::time::Instant;
|
||||
|
||||
pub struct LeakyBucketConfig {
|
||||
pub epoch: Instant,
|
||||
|
||||
/// This is the "time cost" of a single request unit.
|
||||
/// Should loosely represent how long it takes to handle a request unit in active resource time.
|
||||
/// Loosely speaking this is the inverse of the steady-rate requests-per-second
|
||||
pub cost: Duration,
|
||||
pub cost: Ns,
|
||||
|
||||
/// total size of the bucket
|
||||
pub bucket_width: Duration,
|
||||
pub bucket_width: Ns,
|
||||
}
|
||||
|
||||
impl LeakyBucketConfig {
|
||||
pub fn new(rps: f64, bucket_size: f64) -> Self {
|
||||
let cost = Duration::from_secs_f64(rps.recip());
|
||||
let bucket_width = cost.mul_f64(bucket_size);
|
||||
Self { cost, bucket_width }
|
||||
Self {
|
||||
epoch: Instant::now(),
|
||||
cost: Ns(cost.as_nanos() as u64),
|
||||
bucket_width: Ns(bucket_width.as_nanos() as u64),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_epoch(&self, t: Instant) -> InstantNs {
|
||||
InstantNs((t - self.epoch).into())
|
||||
}
|
||||
|
||||
pub fn from_epoch(&self, t: InstantNs) -> Instant {
|
||||
self.epoch + t
|
||||
}
|
||||
|
||||
pub fn now(&self) -> InstantNs {
|
||||
self.to_epoch(Instant::now())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +82,17 @@ pub struct LeakyBucketState {
|
||||
///
|
||||
/// This is inspired by the generic cell rate algorithm (GCRA) and works
|
||||
/// exactly the same as a leaky-bucket.
|
||||
pub empty_at: Instant,
|
||||
pub empty_at: InstantNs,
|
||||
}
|
||||
|
||||
impl LeakyBucketState {
|
||||
pub fn with_initial_tokens(config: &LeakyBucketConfig, initial_tokens: f64) -> Self {
|
||||
LeakyBucketState {
|
||||
empty_at: Instant::now() + config.cost.mul_f64(initial_tokens),
|
||||
empty_at: InstantNs(config.cost * initial_tokens),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bucket_is_empty(&self, now: Instant) -> bool {
|
||||
pub fn bucket_is_empty(&self, now: InstantNs) -> bool {
|
||||
// if self.end is after now, the bucket is not empty
|
||||
self.empty_at <= now
|
||||
}
|
||||
@@ -90,8 +114,22 @@ impl LeakyBucketState {
|
||||
started: Instant,
|
||||
n: f64,
|
||||
) -> Result<(), Instant> {
|
||||
let now = Instant::now();
|
||||
self.add_tokens_fast(
|
||||
config.bucket_width,
|
||||
config.to_epoch(started),
|
||||
config.to_epoch(Instant::now()),
|
||||
config.cost * n,
|
||||
)
|
||||
.map_err(|ready_at| config.from_epoch(ready_at))
|
||||
}
|
||||
|
||||
pub fn add_tokens_fast(
|
||||
&mut self,
|
||||
bucket_width: Ns,
|
||||
started: InstantNs,
|
||||
now: InstantNs,
|
||||
n: Ns,
|
||||
) -> Result<(), InstantNs> {
|
||||
// invariant: started <= now
|
||||
debug_assert!(started <= now);
|
||||
|
||||
@@ -103,9 +141,8 @@ impl LeakyBucketState {
|
||||
empty_at = started;
|
||||
}
|
||||
|
||||
let n = config.cost.mul_f64(n);
|
||||
let new_empty_at = empty_at + n;
|
||||
let allow_at = new_empty_at.checked_sub(config.bucket_width);
|
||||
let allow_at = new_empty_at.checked_sub(bucket_width);
|
||||
|
||||
// empty_at
|
||||
// allow_at | new_empty_at
|
||||
@@ -126,34 +163,242 @@ impl LeakyBucketState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RateLimiter {
|
||||
pub config: LeakyBucketConfig,
|
||||
pub state: Mutex<LeakyBucketState>,
|
||||
/// a queue to provide this fair ordering.
|
||||
pub queue: Notify,
|
||||
// u64 nanoseconds allows for 584 years
|
||||
#[derive(PartialEq, PartialOrd, Copy, Clone)]
|
||||
pub struct Ns(u64);
|
||||
|
||||
impl Debug for Ns {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Duration::from_nanos(self.0).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
struct Requeue<'a>(&'a Notify);
|
||||
impl Ns {
|
||||
fn as_secs_f64(self) -> f64 {
|
||||
(self.0 as f64) / 1_000_000_000.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Requeue<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.notify_one();
|
||||
impl From<Duration> for Ns {
|
||||
fn from(value: Duration) -> Self {
|
||||
Self(value.as_nanos() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f64> for Ns {
|
||||
type Output = Ns;
|
||||
fn mul(self, rhs: f64) -> Ns {
|
||||
Ns((self.0 as f64 * rhs) as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<u64> for Ns {
|
||||
type Output = Ns;
|
||||
fn mul(self, rhs: u64) -> Ns {
|
||||
Ns(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
// ns since some epoch
|
||||
#[derive(PartialEq, PartialOrd, Copy, Clone)]
|
||||
pub struct InstantNs(Ns);
|
||||
|
||||
impl std::ops::Add<Ns> for InstantNs {
|
||||
type Output = InstantNs;
|
||||
fn add(self, rhs: Ns) -> InstantNs {
|
||||
Self(Ns(self.0 .0 + rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for InstantNs {
|
||||
type Output = Ns;
|
||||
fn sub(self, rhs: InstantNs) -> Ns {
|
||||
Ns(self.0 .0 - rhs.0 .0)
|
||||
}
|
||||
}
|
||||
|
||||
impl InstantNs {
|
||||
fn checked_sub(self, rhs: Ns) -> Option<Self> {
|
||||
self.0 .0.checked_sub(rhs.0).map(Ns).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<InstantNs> for Instant {
|
||||
type Output = Instant;
|
||||
fn add(self, rhs: InstantNs) -> Instant {
|
||||
self + Duration::from_nanos(rhs.0 .0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RateLimiter {
|
||||
config: LeakyBucketConfig,
|
||||
queue: Mutex<Queue>,
|
||||
}
|
||||
|
||||
struct Queue {
|
||||
sleep_counter: u64,
|
||||
queue: PinList<RateLimitQueue>,
|
||||
state: Option<LeakyBucketState>,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
/// returns the sleep_counter start value on await.
|
||||
/// sleep_counter end value can be found within the enqueued.
|
||||
fn wait(&self, count: usize) -> Enqueued<'_> {
|
||||
Enqueued {
|
||||
entry: pin_list::Node::new(),
|
||||
limiter: self,
|
||||
sleep_counter: 0,
|
||||
state: None,
|
||||
count: self.config.cost * (count as u64),
|
||||
start: self.config.to_epoch(Instant::now()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RateLimitQueue = dyn pin_list::Types<
|
||||
Id = pin_list::id::Checked,
|
||||
// the waker that lets us wake the next in the queue
|
||||
// the instant is our acquire start time
|
||||
// the duration is our GCRA token cost
|
||||
Protected = (Waker, InstantNs, Ns),
|
||||
// the token that gives us access to the rate limit state, along with when it should be ready.
|
||||
// if None, then we were granted access already by the leader
|
||||
Removed = Option<(LeakyBucketState, InstantNs)>,
|
||||
// the sleep count at the start of the enqueue
|
||||
Unprotected = u64,
|
||||
>;
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
struct Enqueued<'a> {
|
||||
#[pin]
|
||||
entry: Node<RateLimitQueue>,
|
||||
state: Option<(LeakyBucketState, InstantNs)>,
|
||||
sleep_counter: u64,
|
||||
limiter: &'a RateLimiter,
|
||||
start: InstantNs,
|
||||
count: Ns,
|
||||
}
|
||||
|
||||
impl<'a> PinnedDrop for Enqueued<'a> {
|
||||
fn drop(this: Pin<&mut Self>) {
|
||||
let this = this.project();
|
||||
|
||||
#[allow(clippy::mut_mutex_lock, reason = "false positive")]
|
||||
let mut q = this.limiter.queue.lock().unwrap();
|
||||
|
||||
let mut state = if let Some(init) = this.entry.initialized_mut() {
|
||||
let (data, _start_count) = init.reset(&mut q.queue) ;
|
||||
match data {
|
||||
// we were in the queue and are not holding any resources.
|
||||
NodeData::Linked(_) | NodeData::Removed(None) => return,
|
||||
// we were the head of the queue and were about to be the current leader
|
||||
NodeData::Removed(Some((state, _ready_at))) => state
|
||||
}
|
||||
} else if let Some((state, _ready_at)) = this.state.take() {
|
||||
// we were holding the lock, and are now releasing it.
|
||||
q.sleep_counter = *this.sleep_counter;
|
||||
state
|
||||
} else {
|
||||
// we apparently didn't even get into the queue to begin with
|
||||
return;
|
||||
};
|
||||
|
||||
let mut cursor = q.queue.cursor_front_mut();
|
||||
let now = this.limiter.config.to_epoch(Instant::now());
|
||||
loop {
|
||||
match cursor.protected() {
|
||||
Some((_waker, start, n)) => {
|
||||
match state.add_tokens_fast(this.limiter.config.bucket_width, *start, now, *n) {
|
||||
Ok(()) => {
|
||||
let (waker, _, _) = cursor.remove_current(None)
|
||||
.map_err(|_| {}).expect("we have just checked that the current node is in the list");
|
||||
waker.wake();
|
||||
},
|
||||
// next in the queue has to sleep
|
||||
Err(ready_at) => {
|
||||
let (waker, _, _) = cursor.remove_current(Some((state, ready_at)))
|
||||
.map_err(|_| {}).expect("we have just checked that the current node is in the list");
|
||||
waker.wake();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
// no tasks left in the queue. unlocked
|
||||
None => {
|
||||
q.state = Some(state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::future::Future for Enqueued<'_> {
|
||||
type Output = u64;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let mut node = this.entry;
|
||||
|
||||
#[allow(clippy::mut_mutex_lock, reason = "false positive")]
|
||||
let mut q = this.limiter.queue.lock().unwrap();
|
||||
|
||||
if let Some(init) = node.as_mut().initialized_mut() {
|
||||
// we are registered in the queue
|
||||
|
||||
match init.take_removed(&q.queue) {
|
||||
// if we are removed from the queue, that means we are the new leader
|
||||
Ok((state, start_count)) => {
|
||||
*this.state = state;
|
||||
*this.sleep_counter = q.sleep_counter;
|
||||
Poll::Ready(start_count)
|
||||
}
|
||||
// if we are not removed from the queue, that means we are still waiting
|
||||
// and had a spurious wake up
|
||||
Err(init) => {
|
||||
init.protected_mut(&mut q.queue)
|
||||
.unwrap()
|
||||
.0
|
||||
.clone_from(cx.waker());
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we are not yet registered in the queue
|
||||
let start_count = q.sleep_counter;
|
||||
|
||||
if let Some(state) = q.state.take() {
|
||||
// we are the first in the queue and it is not yet acquired
|
||||
*this.state = Some((state, *this.start));
|
||||
*this.sleep_counter = q.sleep_counter;
|
||||
Poll::Ready(start_count)
|
||||
} else {
|
||||
// we push ourselves to the back of the queue
|
||||
q.queue.push_back(
|
||||
node,
|
||||
(cx.waker().clone(), *this.start, *this.count),
|
||||
start_count,
|
||||
);
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
pub fn with_initial_tokens(config: LeakyBucketConfig, initial_tokens: f64) -> Self {
|
||||
RateLimiter {
|
||||
state: Mutex::new(LeakyBucketState::with_initial_tokens(
|
||||
&config,
|
||||
initial_tokens,
|
||||
)),
|
||||
queue: Mutex::new(Queue {
|
||||
sleep_counter: 0,
|
||||
queue: PinList::new(pin_list::id::Checked::new()),
|
||||
state: Some(LeakyBucketState::with_initial_tokens(
|
||||
&config,
|
||||
initial_tokens,
|
||||
)),
|
||||
}),
|
||||
config,
|
||||
queue: {
|
||||
let queue = Notify::new();
|
||||
queue.notify_one();
|
||||
queue
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,32 +408,29 @@ impl RateLimiter {
|
||||
|
||||
/// returns true if we did throttle
|
||||
pub async fn acquire(&self, count: usize) -> bool {
|
||||
let mut throttled = false;
|
||||
let mut entry = std::pin::pin!(self.wait(count));
|
||||
let start_count = entry.as_mut().await;
|
||||
let entry = entry.project();
|
||||
|
||||
let start = tokio::time::Instant::now();
|
||||
|
||||
// wait until we are the first in the queue
|
||||
let mut notified = std::pin::pin!(self.queue.notified());
|
||||
if !notified.as_mut().enable() {
|
||||
throttled = true;
|
||||
notified.await;
|
||||
}
|
||||
|
||||
// notify the next waiter in the queue when we are done.
|
||||
let _guard = Requeue(&self.queue);
|
||||
let Some((state, ready_at)) = entry.state.as_mut() else {
|
||||
// we were woken up without the state,
|
||||
// thus the state leader must have allowed us to continue
|
||||
return start_count < *entry.sleep_counter;
|
||||
};
|
||||
|
||||
let mut now = self.config.to_epoch(Instant::now());
|
||||
loop {
|
||||
let res = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_tokens(&self.config, start, count as f64);
|
||||
match res {
|
||||
Ok(()) => return throttled,
|
||||
Err(ready_at) => {
|
||||
throttled = true;
|
||||
tokio::time::sleep_until(ready_at).await;
|
||||
}
|
||||
if *ready_at > now {
|
||||
*entry.sleep_counter += 1;
|
||||
tokio::time::sleep_until(self.config.from_epoch(*ready_at)).await;
|
||||
now = self.config.to_epoch(Instant::now());
|
||||
}
|
||||
|
||||
match state.add_tokens_fast(self.config.bucket_width, *entry.start, now, *entry.count) {
|
||||
Ok(()) => return start_count < *entry.sleep_counter,
|
||||
// we might hit this branch if we were the first in the queue
|
||||
// and the limit happened to be exhausted already
|
||||
Err(next_ready_at) => *ready_at = next_ready_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,16 +446,10 @@ mod tests {
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn check() {
|
||||
let config = LeakyBucketConfig {
|
||||
// average 100rps
|
||||
cost: Duration::from_millis(10),
|
||||
// burst up to 100 requests
|
||||
bucket_width: Duration::from_millis(1000),
|
||||
};
|
||||
|
||||
let mut state = LeakyBucketState {
|
||||
empty_at: Instant::now(),
|
||||
};
|
||||
// average 100rps
|
||||
// burst up to 100 requests
|
||||
let config = LeakyBucketConfig::new(100.0, 100.0);
|
||||
let mut state = LeakyBucketState::with_initial_tokens(&config, 0.0);
|
||||
|
||||
// supports burst
|
||||
{
|
||||
@@ -229,7 +465,7 @@ mod tests {
|
||||
{
|
||||
// after 1s we should have an empty bucket again.
|
||||
tokio::time::advance(Duration::from_secs(1)).await;
|
||||
assert!(state.bucket_is_empty(Instant::now()));
|
||||
assert!(state.bucket_is_empty(config.now()));
|
||||
|
||||
// after 1s more, we should not over count the tokens and allow more than 200 requests.
|
||||
tokio::time::advance(Duration::from_secs(1)).await;
|
||||
@@ -256,7 +492,7 @@ mod tests {
|
||||
{
|
||||
// start the bucket completely empty
|
||||
tokio::time::advance(Duration::from_secs(5)).await;
|
||||
assert!(state.bucket_is_empty(Instant::now()));
|
||||
assert!(state.bucket_is_empty(config.now()));
|
||||
|
||||
// requesting 200 tokens of space should take 200*cost = 2s
|
||||
// but we already have 1s available, so we wait 1s from start.
|
||||
|
||||
@@ -1177,10 +1177,10 @@ pub(crate) mod virtual_file_io_engine {
|
||||
}
|
||||
|
||||
struct GlobalAndPerTimelineHistogramTimer<'a, 'c> {
|
||||
global_metric: &'a Histogram,
|
||||
global_latency_histo: &'a Histogram,
|
||||
|
||||
// Optional because not all op types are tracked per-timeline
|
||||
timeline_metric: Option<&'a Histogram>,
|
||||
per_timeline_latency_histo: Option<&'a Histogram>,
|
||||
|
||||
ctx: &'c RequestContext,
|
||||
start: std::time::Instant,
|
||||
@@ -1212,9 +1212,10 @@ impl<'a, 'c> Drop for GlobalAndPerTimelineHistogramTimer<'a, 'c> {
|
||||
elapsed
|
||||
}
|
||||
};
|
||||
self.global_metric.observe(ex_throttled.as_secs_f64());
|
||||
if let Some(timeline_metric) = self.timeline_metric {
|
||||
timeline_metric.observe(ex_throttled.as_secs_f64());
|
||||
self.global_latency_histo
|
||||
.observe(ex_throttled.as_secs_f64());
|
||||
if let Some(per_timeline_getpage_histo) = self.per_timeline_latency_histo {
|
||||
per_timeline_getpage_histo.observe(ex_throttled.as_secs_f64());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1240,10 +1241,32 @@ pub enum SmgrQueryType {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SmgrQueryTimePerTimeline {
|
||||
global_metrics: [Histogram; SmgrQueryType::COUNT],
|
||||
per_timeline_getpage: Histogram,
|
||||
global_started: [IntCounter; SmgrQueryType::COUNT],
|
||||
global_latency: [Histogram; SmgrQueryType::COUNT],
|
||||
per_timeline_getpage_started: IntCounter,
|
||||
per_timeline_getpage_latency: Histogram,
|
||||
}
|
||||
|
||||
static SMGR_QUERY_STARTED_GLOBAL: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
register_int_counter_vec!(
|
||||
// it's a counter, but, name is prepared to extend it to a histogram of queue depth
|
||||
"pageserver_smgr_query_started_global_count",
|
||||
"Number of smgr queries started, aggregated by query type.",
|
||||
&["smgr_query_type"],
|
||||
)
|
||||
.expect("failed to define a metric")
|
||||
});
|
||||
|
||||
static SMGR_QUERY_STARTED_PER_TENANT_TIMELINE: Lazy<IntCounterVec> = Lazy::new(|| {
|
||||
register_int_counter_vec!(
|
||||
// it's a counter, but, name is prepared to extend it to a histogram of queue depth
|
||||
"pageserver_smgr_query_started_count",
|
||||
"Number of smgr queries started, aggregated by query type and tenant/timeline.",
|
||||
&["smgr_query_type", "tenant_id", "shard_id", "timeline_id"],
|
||||
)
|
||||
.expect("failed to define a metric")
|
||||
});
|
||||
|
||||
static SMGR_QUERY_TIME_PER_TENANT_TIMELINE: Lazy<HistogramVec> = Lazy::new(|| {
|
||||
register_histogram_vec!(
|
||||
"pageserver_smgr_query_seconds",
|
||||
@@ -1319,14 +1342,20 @@ impl SmgrQueryTimePerTimeline {
|
||||
let tenant_id = tenant_shard_id.tenant_id.to_string();
|
||||
let shard_slug = format!("{}", tenant_shard_id.shard_slug());
|
||||
let timeline_id = timeline_id.to_string();
|
||||
let global_metrics = std::array::from_fn(|i| {
|
||||
let global_started = std::array::from_fn(|i| {
|
||||
let op = SmgrQueryType::from_repr(i).unwrap();
|
||||
SMGR_QUERY_STARTED_GLOBAL
|
||||
.get_metric_with_label_values(&[op.into()])
|
||||
.unwrap()
|
||||
});
|
||||
let global_latency = std::array::from_fn(|i| {
|
||||
let op = SmgrQueryType::from_repr(i).unwrap();
|
||||
SMGR_QUERY_TIME_GLOBAL
|
||||
.get_metric_with_label_values(&[op.into()])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let per_timeline_getpage = SMGR_QUERY_TIME_PER_TENANT_TIMELINE
|
||||
let per_timeline_getpage_started = SMGR_QUERY_STARTED_PER_TENANT_TIMELINE
|
||||
.get_metric_with_label_values(&[
|
||||
SmgrQueryType::GetPageAtLsn.into(),
|
||||
&tenant_id,
|
||||
@@ -1334,9 +1363,20 @@ impl SmgrQueryTimePerTimeline {
|
||||
&timeline_id,
|
||||
])
|
||||
.unwrap();
|
||||
let per_timeline_getpage_latency = SMGR_QUERY_TIME_PER_TENANT_TIMELINE
|
||||
.get_metric_with_label_values(&[
|
||||
SmgrQueryType::GetPageAtLsn.into(),
|
||||
&tenant_id,
|
||||
&shard_slug,
|
||||
&timeline_id,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
global_metrics,
|
||||
per_timeline_getpage,
|
||||
global_started,
|
||||
global_latency,
|
||||
per_timeline_getpage_latency,
|
||||
per_timeline_getpage_started,
|
||||
}
|
||||
}
|
||||
pub(crate) fn start_timer<'c: 'a, 'a>(
|
||||
@@ -1344,8 +1384,11 @@ impl SmgrQueryTimePerTimeline {
|
||||
op: SmgrQueryType,
|
||||
ctx: &'c RequestContext,
|
||||
) -> Option<impl Drop + '_> {
|
||||
let global_metric = &self.global_metrics[op as usize];
|
||||
let start = Instant::now();
|
||||
|
||||
self.global_started[op as usize].inc();
|
||||
|
||||
// We subtract time spent throttled from the observed latency.
|
||||
match ctx.micros_spent_throttled.open() {
|
||||
Ok(()) => (),
|
||||
Err(error) => {
|
||||
@@ -1364,15 +1407,16 @@ impl SmgrQueryTimePerTimeline {
|
||||
}
|
||||
}
|
||||
|
||||
let timeline_metric = if matches!(op, SmgrQueryType::GetPageAtLsn) {
|
||||
Some(&self.per_timeline_getpage)
|
||||
let per_timeline_latency_histo = if matches!(op, SmgrQueryType::GetPageAtLsn) {
|
||||
self.per_timeline_getpage_started.inc();
|
||||
Some(&self.per_timeline_getpage_latency)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(GlobalAndPerTimelineHistogramTimer {
|
||||
global_metric,
|
||||
timeline_metric,
|
||||
global_latency_histo: &self.global_latency[op as usize],
|
||||
per_timeline_latency_histo,
|
||||
ctx,
|
||||
start,
|
||||
op,
|
||||
@@ -1423,9 +1467,12 @@ mod smgr_query_time_tests {
|
||||
let get_counts = || {
|
||||
let global: u64 = ops
|
||||
.iter()
|
||||
.map(|op| metrics.global_metrics[*op as usize].get_sample_count())
|
||||
.map(|op| metrics.global_latency[*op as usize].get_sample_count())
|
||||
.sum();
|
||||
(global, metrics.per_timeline_getpage.get_sample_count())
|
||||
(
|
||||
global,
|
||||
metrics.per_timeline_getpage_latency.get_sample_count(),
|
||||
)
|
||||
};
|
||||
|
||||
let (pre_global, pre_per_tenant_timeline) = get_counts();
|
||||
@@ -2576,6 +2623,12 @@ impl TimelineMetrics {
|
||||
let _ = STORAGE_IO_SIZE.remove_label_values(&[op, tenant_id, shard_id, timeline_id]);
|
||||
}
|
||||
|
||||
let _ = SMGR_QUERY_STARTED_PER_TENANT_TIMELINE.remove_label_values(&[
|
||||
SmgrQueryType::GetPageAtLsn.into(),
|
||||
tenant_id,
|
||||
shard_id,
|
||||
timeline_id,
|
||||
]);
|
||||
let _ = SMGR_QUERY_TIME_PER_TENANT_TIMELINE.remove_label_values(&[
|
||||
SmgrQueryType::GetPageAtLsn.into(),
|
||||
tenant_id,
|
||||
@@ -3227,11 +3280,14 @@ pub fn preinitialize_metrics() {
|
||||
}
|
||||
|
||||
// countervecs
|
||||
[&BACKGROUND_LOOP_PERIOD_OVERRUN_COUNT]
|
||||
.into_iter()
|
||||
.for_each(|c| {
|
||||
Lazy::force(c);
|
||||
});
|
||||
[
|
||||
&BACKGROUND_LOOP_PERIOD_OVERRUN_COUNT,
|
||||
&SMGR_QUERY_STARTED_GLOBAL,
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|c| {
|
||||
Lazy::force(c);
|
||||
});
|
||||
|
||||
// gauges
|
||||
WALRECEIVER_ACTIVE_MANAGERS.get();
|
||||
|
||||
@@ -439,11 +439,30 @@ impl Layer {
|
||||
|
||||
fn record_access(&self, ctx: &RequestContext) {
|
||||
if self.0.access_stats.record_access(ctx) {
|
||||
// Visibility was modified to Visible
|
||||
tracing::info!(
|
||||
"Layer {} became visible as a result of access",
|
||||
self.0.desc.key()
|
||||
);
|
||||
// Visibility was modified to Visible: maybe log about this
|
||||
match ctx.task_kind() {
|
||||
TaskKind::CalculateSyntheticSize
|
||||
| TaskKind::GarbageCollector
|
||||
| TaskKind::MgmtRequest => {
|
||||
// This situation is expected in code paths do binary searches of the LSN space to resolve
|
||||
// an LSN to a timestamp, which happens during GC, during GC cutoff calculations in synthetic size,
|
||||
// and on-demand for certain HTTP API requests.
|
||||
}
|
||||
_ => {
|
||||
// In all other contexts, it is unusual to do I/O involving layers which are not visible at
|
||||
// some branch tip, so we log the fact that we are accessing something that the visibility
|
||||
// calculation thought should not be visible.
|
||||
//
|
||||
// This case is legal in brief time windows: for example an in-flight getpage request can hold on to a layer object
|
||||
// which was covered by a concurrent compaction.
|
||||
tracing::info!(
|
||||
"Layer {} became visible as a result of access",
|
||||
self.0.desc.key()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the timeline's visible bytes count
|
||||
if let Some(tl) = self.0.timeline.upgrade() {
|
||||
tl.metrics
|
||||
.visible_physical_size_gauge
|
||||
|
||||
@@ -1025,6 +1025,15 @@ fn access_stats() {
|
||||
assert_eq!(access_stats.latest_activity(), lowres_time(atime));
|
||||
access_stats.set_visibility(LayerVisibilityHint::Visible);
|
||||
assert_eq!(access_stats.latest_activity(), lowres_time(atime));
|
||||
|
||||
// Recording access implicitly makes layer visible, if it wasn't already
|
||||
let atime = UNIX_EPOCH + Duration::from_secs(2200000000);
|
||||
access_stats.set_visibility(LayerVisibilityHint::Covered);
|
||||
assert_eq!(access_stats.visibility(), LayerVisibilityHint::Covered);
|
||||
assert!(access_stats.record_access_at(atime));
|
||||
access_stats.set_visibility(LayerVisibilityHint::Visible);
|
||||
assert!(!access_stats.record_access_at(atime));
|
||||
access_stats.set_visibility(LayerVisibilityHint::Visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4316,7 +4316,9 @@ impl Timeline {
|
||||
timer.stop_and_record();
|
||||
|
||||
// Creating image layers may have caused some previously visible layers to be covered
|
||||
self.update_layer_visibility().await?;
|
||||
if !image_layers.is_empty() {
|
||||
self.update_layer_visibility().await?;
|
||||
}
|
||||
|
||||
Ok(image_layers)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ use std::{
|
||||
use ahash::RandomState;
|
||||
use dashmap::DashMap;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::time::Instant;
|
||||
use tracing::info;
|
||||
use utils::leaky_bucket::LeakyBucketState;
|
||||
use utils::leaky_bucket::{InstantNs, LeakyBucketState};
|
||||
|
||||
use crate::intern::EndpointIdInt;
|
||||
|
||||
@@ -37,7 +36,7 @@ impl<K: Hash + Eq> LeakyBucketRateLimiter<K> {
|
||||
|
||||
/// Check that number of connections to the endpoint is below `max_rps` rps.
|
||||
pub(crate) fn check(&self, key: K, n: u32) -> bool {
|
||||
let now = Instant::now();
|
||||
let now = self.config.now();
|
||||
|
||||
if self.access_count.fetch_add(1, Ordering::AcqRel) % 2048 == 0 {
|
||||
self.do_gc(now);
|
||||
@@ -48,10 +47,17 @@ impl<K: Hash + Eq> LeakyBucketRateLimiter<K> {
|
||||
.entry(key)
|
||||
.or_insert_with(|| LeakyBucketState { empty_at: now });
|
||||
|
||||
entry.add_tokens(&self.config, now, n as f64).is_ok()
|
||||
entry
|
||||
.add_tokens_fast(
|
||||
self.config.bucket_width,
|
||||
now,
|
||||
now,
|
||||
self.config.cost * (n as u64),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn do_gc(&self, now: Instant) {
|
||||
fn do_gc(&self, now: InstantNs) {
|
||||
info!(
|
||||
"cleaning up bucket rate limiter, current size = {}",
|
||||
self.map.len()
|
||||
@@ -90,7 +96,7 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::time::Instant;
|
||||
use utils::leaky_bucket::LeakyBucketState;
|
||||
use utils::leaky_bucket::{LeakyBucketState, Ns};
|
||||
|
||||
use super::LeakyBucketConfig;
|
||||
|
||||
@@ -98,11 +104,11 @@ mod tests {
|
||||
async fn check() {
|
||||
let config: utils::leaky_bucket::LeakyBucketConfig =
|
||||
LeakyBucketConfig::new(500.0, 2000.0).into();
|
||||
assert_eq!(config.cost, Duration::from_millis(2));
|
||||
assert_eq!(config.bucket_width, Duration::from_secs(4));
|
||||
assert_eq!(config.cost, Ns::from(Duration::from_millis(2)));
|
||||
assert_eq!(config.bucket_width, Ns::from(Duration::from_secs(4)));
|
||||
|
||||
let mut bucket = LeakyBucketState {
|
||||
empty_at: Instant::now(),
|
||||
empty_at: config.now(),
|
||||
};
|
||||
|
||||
// should work for 2000 requests this second
|
||||
@@ -110,7 +116,7 @@ mod tests {
|
||||
bucket.add_tokens(&config, Instant::now(), 1.0).unwrap();
|
||||
}
|
||||
bucket.add_tokens(&config, Instant::now(), 1.0).unwrap_err();
|
||||
assert_eq!(bucket.empty_at - Instant::now(), config.bucket_width);
|
||||
assert_eq!(bucket.empty_at - config.now(), config.bucket_width);
|
||||
|
||||
// in 1ms we should drain 0.5 tokens.
|
||||
// make sure we don't lose any tokens
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
error::Error,
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
@@ -218,9 +219,16 @@ fn passthrough_api_error(node: &Node, e: mgmt_api::Error) -> ApiError {
|
||||
format!("{node} error receiving error body: {str}").into(),
|
||||
)
|
||||
}
|
||||
mgmt_api::Error::ReceiveBody(str) => {
|
||||
// Presume errors receiving body are connectivity/availability issues
|
||||
ApiError::ResourceUnavailable(format!("{node} error receiving body: {str}").into())
|
||||
mgmt_api::Error::ReceiveBody(err) if err.is_decode() => {
|
||||
// Return 500 for decoding errors.
|
||||
ApiError::InternalServerError(anyhow::Error::from(err).context("error decoding body"))
|
||||
}
|
||||
mgmt_api::Error::ReceiveBody(err) => {
|
||||
// Presume errors receiving body are connectivity/availability issues except for decoding errors
|
||||
let src_str = err.source().map(|e| e.to_string()).unwrap_or_default();
|
||||
ApiError::ResourceUnavailable(
|
||||
format!("{node} error receiving error body: {err} {}", src_str).into(),
|
||||
)
|
||||
}
|
||||
mgmt_api::Error::ApiError(StatusCode::NOT_FOUND, msg) => {
|
||||
ApiError::NotFound(anyhow::anyhow!(format!("{node}: {msg}")).into())
|
||||
|
||||
@@ -132,6 +132,7 @@ PAGESERVER_GLOBAL_METRICS: Tuple[str, ...] = (
|
||||
*histogram("pageserver_wait_lsn_seconds"),
|
||||
*histogram("pageserver_remote_operation_seconds"),
|
||||
*histogram("pageserver_io_operations_seconds"),
|
||||
"pageserver_smgr_query_started_global_count_total",
|
||||
"pageserver_tenant_states_count",
|
||||
"pageserver_circuit_breaker_broken_total",
|
||||
"pageserver_circuit_breaker_unbroken_total",
|
||||
@@ -146,6 +147,7 @@ PAGESERVER_PER_TENANT_METRICS: Tuple[str, ...] = (
|
||||
"pageserver_smgr_query_seconds_bucket",
|
||||
"pageserver_smgr_query_seconds_count",
|
||||
"pageserver_smgr_query_seconds_sum",
|
||||
"pageserver_smgr_query_started_count_total",
|
||||
"pageserver_archive_size",
|
||||
"pageserver_pitr_history_size",
|
||||
"pageserver_layer_bytes",
|
||||
|
||||
Reference in New Issue
Block a user