1use std::{cmp::min, time::Duration};
3use anyhow::{anyhow, Context};
4use postgres::{types::ToSql, NoTls, Row};
7// DbLocker wraps a regular DbConn, only allowing access using the
8// `lock` method. This method will acquire the advisory lock before
9// allowing access to the database, and then release it afterwards.
11// We use advisory locks to avoid multiple Reshape instances working
12// on the same database as the same time. DbLocker is the only way to
13// get a DbConn which ensures that all database access is protected by
16// Postgres docs on advisory locks:
17// https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS
23 // Advisory lock keys in Postgres are 64-bit integers.
24 // The key we use was chosen randomly.
25 const LOCK_KEY: i64 = 4036779288569897133;
27 pub fn connect(config: &postgres::Config) -> anyhow::Result<Self> {
28 let mut pg = config.connect(NoTls)?;
30 // When running DDL queries that acquire locks, we risk causing a "lock queue".
31 // When attempting to acquire a lock, Postgres will wait for any long running queries to complete.
32 // At the same time, it will block other queries until the lock has been acquired and released.
33 // This has the bad effect of the long-running query blocking other queries because of us, forming
34 // a queue of other queries until we release our lock.
36 // We set the lock_timeout setting to avoid this. This puts an upper bound for how long Postgres will
37 // wait to acquire locks and also the maximum amount of time a long-running query can block other queries.
38 // We should also add automatic retries to handle these timeouts gracefully.
40 // Reference: https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680
42 // TODO: Make lock_timeout configurable
43 pg.simple_query("SET lock_timeout = '1s'")
44 .context("failed to set lock_timeout")?;
47 client: DbConn::new(pg),
53 f: impl FnOnce(&mut DbConn) -> anyhow::Result<()>,
54 ) -> anyhow::Result<()> {
56 let result = f(&mut self.client);
62 fn acquire_lock(&mut self) -> anyhow::Result<()> {
65 .query(&format!("SELECT pg_try_advisory_lock({})", Self::LOCK_KEY))?
67 .ok_or_else(|| anyhow!("unexpectedly failed when acquiring advisory lock"))
68 .map(|row| row.get::<'_, _, bool>(0))?;
73 Err(anyhow!("another instance of Reshape is already running"))
77 fn release_lock(&mut self) -> anyhow::Result<()> {
79 .query(&format!("SELECT pg_advisory_unlock({})", Self::LOCK_KEY))?
81 .ok_or_else(|| anyhow!("unexpectedly failed when releasing advisory lock"))?;
87 fn run(&mut self, query: &str) -> anyhow::Result<()>;
88 fn query(&mut self, query: &str) -> anyhow::Result<Vec<Row>>;
92 params: &[&(dyn ToSql + Sync)],
93 ) -> anyhow::Result<Vec<Row>>;
94 fn transaction(&mut self) -> anyhow::Result<Transaction>;
98 client: postgres::Client,
102 fn new(client: postgres::Client) -> Self {
107impl Conn for DbConn {
108 fn run(&mut self, query: &str) -> anyhow::Result<()> {
109 retry_automatically(|| self.client.batch_execute(query))?;
113 fn query(&mut self, query: &str) -> anyhow::Result<Vec<Row>> {
114 let rows = retry_automatically(|| self.client.query(query, &[]))?;
118 fn query_with_params(
121 params: &[&(dyn ToSql + Sync)],
122 ) -> anyhow::Result<Vec<Row>> {
123 let rows = retry_automatically(|| self.client.query(query, params))?;
127 fn transaction(&mut self) -> anyhow::Result<Transaction> {
128 let transaction = self.client.transaction()?;
129 Ok(Transaction { transaction })
133pub struct Transaction<'a> {
134 transaction: postgres::Transaction<'a>,
137impl Transaction<'_> {
138 pub fn commit(self) -> anyhow::Result<()> {
139 self.transaction.commit()?;
143 pub fn rollback(self) -> anyhow::Result<()> {
144 self.transaction.rollback()?;
149impl Conn for Transaction<'_> {
150 fn run(&mut self, query: &str) -> anyhow::Result<()> {
151 self.transaction.batch_execute(query)?;
155 fn query(&mut self, query: &str) -> anyhow::Result<Vec<Row>> {
156 let rows = self.transaction.query(query, &[])?;
160 fn query_with_params(
163 params: &[&(dyn ToSql + Sync)],
164 ) -> anyhow::Result<Vec<Row>> {
165 let rows = self.transaction.query(query, params)?;
169 fn transaction(&mut self) -> anyhow::Result<Transaction> {
170 let transaction = self.transaction.transaction()?;
171 Ok(Transaction { transaction })
175// Retry a database operation with exponential backoff and jitter
176fn retry_automatically<T>(
177 mut f: impl FnMut() -> Result<T, postgres::Error>,
178) -> Result<T, postgres::Error> {
179 const STARTING_WAIT_TIME: u64 = 100;
180 const MAX_WAIT_TIME: u64 = 3_200;
181 const MAX_ATTEMPTS: u32 = 10;
183 let mut rng = rand::thread_rng();
184 let mut attempts = 0;
188 let error = match result {
189 Ok(_) => return result,
193 // If we got a database error, we check if it's retryable.
194 // If we didn't get a database error, then it's most likely some kind of connection
195 // error which should also be retried.
196 if let Some(db_error) = error.as_db_error() {
197 if !error_retryable(db_error) {
203 if attempts >= MAX_ATTEMPTS {
207 // The wait time increases exponentially, starting at 100ms and doubling up to a max of 3.2s.
210 STARTING_WAIT_TIME * u64::pow(2, attempts - 1),
213 // The jitter is up to half the wait time
214 let jitter: u64 = rng.gen_range(0..wait_time / 2);
216 std::thread::sleep(Duration::from_millis(wait_time + jitter));
220// Check if a database error can be retried
221fn error_retryable(error: &postgres::error::DbError) -> bool {
222 // LOCK_NOT_AVAILABLE is caused by lock_timeout being exceeded
223 matches!(error.code(), &postgres::error::SqlState::LOCK_NOT_AVAILABLE)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list