Skip to content

Commit d4c1fc8

Browse files
authored
Reloadable config (#26)
* Reloadable config * readme * live config reload * test matrix
1 parent 4ca50b9 commit d4c1fc8

File tree

12 files changed

+250
-133
lines changed

12 files changed

+250
-133
lines changed

.circleci/run_tests.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ psql -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_select.sql > /
3434
# Replica/primary selection & more sharding tests
3535
psql -e -h 127.0.0.1 -p 6432 -f tests/sharding/query_routing_test_primary_replica.sql > /dev/null
3636

37+
# Test reload config
38+
kill -SIGHUP $(pgrep pgcat)
39+
3740
#
3841
# ActiveRecord tests!
3942
#

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ once_cell = "1"
2323
statsd = "0.15"
2424
sqlparser = "0.14"
2525
log = "0.4"
26+
arc-swap = "1"

README.md

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,18 @@ Meow. PgBouncer rewritten in Rust, with sharding, load balancing and failover su
99
**Alpha**: don't use in production just yet.
1010

1111
## Features
12-
13-
| **Feature** | **Status** | **Comments** |
14-
|--------------------------------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
15-
| Transaction pooling | :heavy_check_mark: | Identical to PgBouncer. |
16-
| Session pooling | :heavy_check_mark: | Identical to PgBouncer. |
17-
| `COPY` support | :heavy_check_mark: | Both `COPY TO` and `COPY FROM` are supported. |
18-
| Query cancellation | :heavy_check_mark: | Supported both in transaction and session pooling modes. |
19-
| Load balancing of read queries | :heavy_check_mark: | Using round-robin between replicas. Primary is included when `primary_reads_enabled` is enabled (default). |
20-
| Sharding | :heavy_check_mark: | Transactions are sharded using `SET SHARD TO` and `SET SHARDING KEY TO` syntax extensions; see examples below. |
21-
| Failover | :heavy_check_mark: | Replicas are tested with a health check. If a health check fails, remaining replicas are attempted; see below for algorithm description and examples. |
22-
| Statistics reporting | :heavy_check_mark: | Statistics similar to PgBouncers are reported via StatsD. |
23-
| Live configuration reloading | :x: :wrench: | On the roadmap; currently config changes require restart. |
24-
| Client authentication | :x: :wrench: | On the roadmap; currently all clients are allowed to connect and one user is used to connect to Postgres. |
12+
| **Feature** | **Status** | **Comments** |
13+
|--------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
14+
| Transaction pooling | :heavy_check_mark: | Identical to PgBouncer. |
15+
| Session pooling | :heavy_check_mark: | Identical to PgBouncer. |
16+
| `COPY` support | :heavy_check_mark: | Both `COPY TO` and `COPY FROM` are supported. |
17+
| Query cancellation | :heavy_check_mark: | Supported both in transaction and session pooling modes. |
18+
| Load balancing of read queries | :heavy_check_mark: | Using round-robin between replicas. Primary is included when `primary_reads_enabled` is enabled (default). |
19+
| Sharding | :heavy_check_mark: | Transactions are sharded using `SET SHARD TO` and `SET SHARDING KEY TO` syntax extensions; see examples below. |
20+
| Failover | :heavy_check_mark: | Replicas are tested with a health check. If a health check fails, remaining replicas are attempted; see below for algorithm description and examples. |
21+
| Statistics reporting | :heavy_check_mark: | Statistics similar to PgBouncers are reported via StatsD. |
22+
| Live configuration reloading | :construction_worker: | Reload config with a `SIGHUP` to the process, e.g. `kill -s SIGHUP $(pgrep pgcat)`. Not all settings can be reloaded without a restart. |
23+
| Client authentication | :x: :wrench: | On the roadmap; currently all clients are allowed to connect and one user is used to connect to Postgres. |
2524

2625
## Deployment
2726

@@ -48,17 +47,17 @@ pgbench -t 1000 -p 6432 -h 127.0.0.1 --protocol extended
4847

4948
See [sharding README](./tests/sharding/README.md) for sharding logic testing.
5049

51-
| **Feature** | **Tested in CI** | **Tested manually** | **Comments** |
52-
|----------------------|--------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------|
53-
| Transaction pooling | :heavy_check_mark: | :heavy_check_mark: | Used by default for all tests. |
54-
| Session pooling | :x: | :heavy_check_mark: | Easiest way to test is to enable it and run pgbench - results will be better than transaction pooling as expected. |
55-
| `COPY` | :heavy_check_mark: | :heavy_check_mark: | `pgbench -i` uses `COPY`. `COPY FROM` is tested as well. |
56-
| Query cancellation | :heavy_check_mark: | :heavy_check_mark: | `psql -c 'SELECT pg_sleep(1000);'` and press `Ctrl-C`. |
57-
| Load balancing | :x: | :heavy_check_mark: | We could test this by emitting statistics for each replica and compare them. |
58-
| Failover | :x: | :heavy_check_mark: | Misconfigure a replica in `pgcat.toml` and watch it forward queries to spares. CI testing could include using Toxiproxy. |
59-
| Sharding | :heavy_check_mark: | :heavy_check_mark: | See `tests/sharding` and `tests/ruby` for an Rails/ActiveRecord example. |
60-
| Statistics reporting | :x: | :heavy_check_mark: | Run `nc -l -u 8125` and watch the stats come in every 15 seconds. |
61-
50+
| **Feature** | **Tested in CI** | **Tested manually** | **Comments** |
51+
|-----------------------|--------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------|
52+
| Transaction pooling | :heavy_check_mark: | :heavy_check_mark: | Used by default for all tests. |
53+
| Session pooling | :x: | :heavy_check_mark: | Easiest way to test is to enable it and run pgbench - results will be better than transaction pooling as expected. |
54+
| `COPY` | :heavy_check_mark: | :heavy_check_mark: | `pgbench -i` uses `COPY`. `COPY FROM` is tested as well. |
55+
| Query cancellation | :heavy_check_mark: | :heavy_check_mark: | `psql -c 'SELECT pg_sleep(1000);'` and press `Ctrl-C`. |
56+
| Load balancing | :x: | :heavy_check_mark: | We could test this by emitting statistics for each replica and compare them. |
57+
| Failover | :x: | :heavy_check_mark: | Misconfigure a replica in `pgcat.toml` and watch it forward queries to spares. CI testing could include using Toxiproxy. |
58+
| Sharding | :heavy_check_mark: | :heavy_check_mark: | See `tests/sharding` and `tests/ruby` for an Rails/ActiveRecord example. |
59+
| Statistics reporting | :x: | :heavy_check_mark: | Run `nc -l -u 8125` and watch the stats come in every 15 seconds. |
60+
| Live config reloading | :heavy_check_mark: | :heavy_check_mark: | Run `kill -s SIGHUP $(pgrep pgcat)` and watch the config reload. |
6261

6362
## Usage
6463

@@ -173,6 +172,30 @@ SET SERVER ROLE TO 'auto'; -- let the query router figure out where the query sh
173172
SELECT * FROM users WHERE email = 'test@example.com'; -- shard setting lasts until set again; we are reading from the primary
174173
```
175174

175+
### Statistics reporting
176+
177+
Stats are reported using StatsD every 15 seconds. The address is configurable with `statsd_address`, the default is `127.0.0.1:8125`. The stats are very similar to what Pgbouncer reports and the names are kept to be comparable.
178+
179+
### Live configuration reloading
180+
181+
The config can be reloaded by sending a `kill -s SIGHUP` to the process. Not all settings are currently supported by live reload:
182+
183+
| **Config** | **Requires restart** |
184+
|-------------------------|----------------------|
185+
| `host` | yes |
186+
| `port` | yes |
187+
| `pool_mode` | no |
188+
| `connect_timeout` | yes |
189+
| `healthcheck_timeout` | no |
190+
| `ban_time` | no |
191+
| `statsd_address` | yes |
192+
| `user` | yes |
193+
| `shards` | yes |
194+
| `default_role` | no |
195+
| `primary_reads_enabled` | no |
196+
| `query_parser_enabled` | no |
197+
198+
176199
## Benchmarks
177200

178201
You can setup PgBench locally through PgCat:

pgcat.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ healthcheck_timeout = 1000
2929
# For how long to ban a server if it fails a health check (seconds).
3030
ban_time = 60 # Seconds
3131

32+
# Stats will be sent here
33+
statsd_address = "127.0.0.1:8125"
34+
3235
#
3336
# User to use for authentication against the server.
3437
[user]

src/client.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use tokio::net::{
1010

1111
use std::collections::HashMap;
1212

13+
use crate::config::get_config;
1314
use crate::constants::*;
1415
use crate::errors::Error;
1516
use crate::messages::*;
@@ -61,10 +62,12 @@ impl Client {
6162
pub async fn startup(
6263
mut stream: TcpStream,
6364
client_server_map: ClientServerMap,
64-
transaction_mode: bool,
6565
server_info: BytesMut,
6666
stats: Reporter,
6767
) -> Result<Client, Error> {
68+
let config = get_config();
69+
let transaction_mode = config.general.pool_mode.starts_with("t");
70+
drop(config);
6871
loop {
6972
// Could be StartupMessage or SSLRequest
7073
// which makes this variable length.
@@ -154,11 +157,7 @@ impl Client {
154157
}
155158

156159
/// Client loop. We handle all messages between the client and the database here.
157-
pub async fn handle(
158-
&mut self,
159-
mut pool: ConnectionPool,
160-
mut query_router: QueryRouter,
161-
) -> Result<(), Error> {
160+
pub async fn handle(&mut self, mut pool: ConnectionPool) -> Result<(), Error> {
162161
// The client wants to cancel a query it has issued previously.
163162
if self.cancel_mode {
164163
let (process_id, secret_key, address, port) = {
@@ -187,6 +186,8 @@ impl Client {
187186
return Ok(Server::cancel(&address, &port, process_id, secret_key).await?);
188187
}
189188

189+
let mut query_router = QueryRouter::new();
190+
190191
// Our custom protocol loop.
191192
// We expect the client to either start a transaction with regular queries
192193
// or issue commands for our sharding and server selection protocols.

src/config.rs

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
use arc_swap::{ArcSwap, Guard};
2+
use once_cell::sync::Lazy;
13
use serde_derive::Deserialize;
24
use tokio::fs::File;
35
use tokio::io::AsyncReadExt;
46
use toml;
57

68
use std::collections::{HashMap, HashSet};
9+
use std::sync::Arc;
710

811
use crate::errors::Error;
912

13+
static CONFIG: Lazy<ArcSwap<Config>> = Lazy::new(|| ArcSwap::from_pointee(Config::default()));
14+
1015
#[derive(Clone, PartialEq, Deserialize, Hash, std::cmp::Eq, Debug, Copy)]
1116
pub enum Role {
1217
Primary,
@@ -39,12 +44,32 @@ pub struct Address {
3944
pub role: Role,
4045
}
4146

47+
impl Default for Address {
48+
fn default() -> Address {
49+
Address {
50+
host: String::from("127.0.0.1"),
51+
port: String::from("5432"),
52+
shard: 0,
53+
role: Role::Replica,
54+
}
55+
}
56+
}
57+
4258
#[derive(Clone, PartialEq, Hash, std::cmp::Eq, Deserialize, Debug)]
4359
pub struct User {
4460
pub name: String,
4561
pub password: String,
4662
}
4763

64+
impl Default for User {
65+
fn default() -> User {
66+
User {
67+
name: String::from("postgres"),
68+
password: String::new(),
69+
}
70+
}
71+
}
72+
4873
#[derive(Deserialize, Debug, Clone)]
4974
pub struct General {
5075
pub host: String,
@@ -54,6 +79,22 @@ pub struct General {
5479
pub connect_timeout: u64,
5580
pub healthcheck_timeout: u64,
5681
pub ban_time: i64,
82+
pub statsd_address: String,
83+
}
84+
85+
impl Default for General {
86+
fn default() -> General {
87+
General {
88+
host: String::from("localhost"),
89+
port: 5432,
90+
pool_size: 15,
91+
pool_mode: String::from("transaction"),
92+
connect_timeout: 5000,
93+
healthcheck_timeout: 1000,
94+
ban_time: 60,
95+
statsd_address: String::from("127.0.0.1:8125"),
96+
}
97+
}
5798
}
5899

59100
#[derive(Deserialize, Debug, Clone)]
@@ -62,13 +103,32 @@ pub struct Shard {
62103
pub database: String,
63104
}
64105

106+
impl Default for Shard {
107+
fn default() -> Shard {
108+
Shard {
109+
servers: vec![(String::from("localhost"), 5432, String::from("primary"))],
110+
database: String::from("postgres"),
111+
}
112+
}
113+
}
114+
65115
#[derive(Deserialize, Debug, Clone)]
66116
pub struct QueryRouter {
67117
pub default_role: String,
68118
pub query_parser_enabled: bool,
69119
pub primary_reads_enabled: bool,
70120
}
71121

122+
impl Default for QueryRouter {
123+
fn default() -> QueryRouter {
124+
QueryRouter {
125+
default_role: String::from("any"),
126+
query_parser_enabled: false,
127+
primary_reads_enabled: true,
128+
}
129+
}
130+
}
131+
72132
#[derive(Deserialize, Debug, Clone)]
73133
pub struct Config {
74134
pub general: General,
@@ -77,8 +137,36 @@ pub struct Config {
77137
pub query_router: QueryRouter,
78138
}
79139

140+
impl Default for Config {
141+
fn default() -> Config {
142+
Config {
143+
general: General::default(),
144+
user: User::default(),
145+
shards: HashMap::from([(String::from("1"), Shard::default())]),
146+
query_router: QueryRouter::default(),
147+
}
148+
}
149+
}
150+
151+
impl Config {
152+
pub fn show(&self) {
153+
println!("> Pool size: {}", self.general.pool_size);
154+
println!("> Pool mode: {}", self.general.pool_mode);
155+
println!("> Ban time: {}s", self.general.ban_time);
156+
println!(
157+
"> Healthcheck timeout: {}ms",
158+
self.general.healthcheck_timeout
159+
);
160+
println!("> Connection timeout: {}ms", self.general.connect_timeout);
161+
}
162+
}
163+
164+
pub fn get_config() -> Guard<Arc<Config>> {
165+
CONFIG.load()
166+
}
167+
80168
/// Parse the config.
81-
pub async fn parse(path: &str) -> Result<Config, Error> {
169+
pub async fn parse(path: &str) -> Result<(), Error> {
82170
let mut contents = String::new();
83171
let mut file = match File::open(path).await {
84172
Ok(file) => file,
@@ -163,7 +251,9 @@ pub async fn parse(path: &str) -> Result<Config, Error> {
163251
}
164252
};
165253

166-
Ok(config)
254+
CONFIG.store(Arc::new(config.clone()));
255+
256+
Ok(())
167257
}
168258

169259
#[cfg(test)]
@@ -172,11 +262,11 @@ mod test {
172262

173263
#[tokio::test]
174264
async fn test_config() {
175-
let config = parse("pgcat.toml").await.unwrap();
176-
assert_eq!(config.general.pool_size, 15);
177-
assert_eq!(config.shards.len(), 3);
178-
assert_eq!(config.shards["1"].servers[0].0, "127.0.0.1");
179-
assert_eq!(config.shards["0"].servers[0].2, "primary");
180-
assert_eq!(config.query_router.default_role, "any");
265+
parse("pgcat.toml").await.unwrap();
266+
assert_eq!(get_config().general.pool_size, 15);
267+
assert_eq!(get_config().shards.len(), 3);
268+
assert_eq!(get_config().shards["1"].servers[0].0, "127.0.0.1");
269+
assert_eq!(get_config().shards["0"].servers[0].2, "primary");
270+
assert_eq!(get_config().query_router.default_role, "any");
181271
}
182272
}

0 commit comments

Comments
 (0)