Skip to content

Commit 7da3e49

Browse files
authored
feat: add primary only plugin (#826)
``` pgdog=# EXPLAIN SELECT * FROM sessions; QUERY PLAN ------------------------------------------------------------------------ Seq Scan on sessions (cost=0.00..32.60 rows=2260 width=8) PgDog Routing: Summary: shard=0 role=primary Shard 0: SELECT omnishard round robin Note: plugin pgdog_primary_only_tables adjusted routing role=primary (6 rows) ``` #823
1 parent 4445df6 commit 7da3e49

19 files changed

Lines changed: 426 additions & 34 deletions

File tree

.schema/pgdog.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,13 @@
12561256
"description": "Plugins are dynamically loaded at PgDog startup. These settings control which plugins are loaded.\n\nNote: Plugins can only be configured at PgDog startup. They cannot be changed after the process is running.\n\nhttps://docs.pgdog.dev/configuration/pgdog.toml/plugins/",
12571257
"type": "object",
12581258
"properties": {
1259+
"config": {
1260+
"description": "Path to the configuration file for the plugin, if any. Plugin-specific settings can be\nplaced there. It's completely plugin-specific and any fomrat is acceptable.",
1261+
"type": [
1262+
"string",
1263+
"null"
1264+
]
1265+
},
12591266
"name": {
12601267
"description": "Name of the plugin to load. This is used by PgDog to look up the shared library object in `LD_LIBRARY_PATH`. For example, if your plugin name is `router`, PgDog will look for `librouter.so` on Linux, `librouter.dll` on Windows, and `librouter.dylib` on Mac OS.\n\n**Note:** Make sure the user running PgDog has read & execute permissions on the library.\n\nhttps://docs.pgdog.dev/configuration/pgdog.toml/plugins/#name",
12611268
"type": "string"

Cargo.lock

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ members = [
1010
"pgdog-postgres-types",
1111
"pgdog-stats",
1212
"pgdog-vector",
13-
"plugins/pgdog-example-plugin",
13+
"plugins/pgdog-example-plugin", "plugins/pgdog-primary-only-tables",
1414
"scripts/*",
1515
]
1616

1717
[workspace.package]
1818
edition = "2024"
1919

2020
[workspace.dependencies]
21-
pgdog-plugin = { path = "./pgdog-plugin", version = "0.2.0" }
21+
pgdog-plugin = { path = "./pgdog-plugin", version = "0.3.0" }
2222
pgdog-config = { path = "./pgdog-config", version = "0.1.0" }
2323
schemars = { version = "1.2.1", features = ["uuid1"] }
2424
serde_json = "1.0"

Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ RUN source ~/.cargo/env && \
1616
export RUSTFLAGS="-Ctarget-feature=+lse"; \
1717
fi && \
1818
cd pgdog && \
19-
cargo build --release
19+
cargo build --release && \
20+
cd .. && \
21+
cargo build --release -p pgdog-primary-only-tables
2022

2123
FROM ubuntu:latest
2224
ENV RUST_LOG=info
@@ -34,6 +36,7 @@ RUN install -d /usr/share/postgresql-common/pgdg && \
3436
RUN apt update && apt install -y postgresql-client-${PSQL_VERSION}
3537

3638
COPY --from=builder /build/target/release/pgdog /usr/local/bin/pgdog
39+
COPY --from=builder /build/target/release/libpgdog_primary_only_tables.so /usr/lib/libpgdog_primary_only_tables.so
3740

3841
WORKDIR /pgdog
3942
STOPSIGNAL SIGINT

integration/load_balancer/pgdog.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pooler_mode = "transaction"
1616
load_balancing_strategy = "round_robin"
1717
auth_type = "trust"
1818
read_write_split = "exclude_primary"
19+
expanded_explain = true
1920
lsn_check_delay = 0
2021

2122
[rewrite]
@@ -50,6 +51,10 @@ role = "replica"
5051
port = 45002
5152

5253

54+
[[plugins]]
55+
name = "pgdog_primary_only_tables"
56+
config = "integration/load_balancer/plugin_config.toml"
57+
5358
[tcp]
5459
retries = 3
5560
time = 1000
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestPrimaryOnlyTables(t *testing.T) {
13+
pool := GetPool()
14+
defer pool.Close()
15+
16+
_, err := pool.Exec(context.Background(), `CREATE TABLE IF NOT EXISTS lb_plugin_primary_only (
17+
id BIGINT,
18+
data VARCHAR
19+
)`)
20+
assert.NoError(t, err)
21+
defer pool.Exec(context.Background(), "DROP TABLE IF EXISTS lb_plugin_primary_only")
22+
23+
time.Sleep(2 * time.Second)
24+
25+
ResetStats()
26+
27+
for i := range 50 {
28+
_, err = pool.Exec(context.Background(), "SELECT $1::bigint FROM lb_plugin_primary_only", int64(i))
29+
assert.NoError(t, err)
30+
}
31+
32+
primaryCalls := LoadStatsForPrimary("lb_plugin_primary_only")
33+
assert.Equal(t, int64(50), primaryCalls.Calls)
34+
}
35+
36+
func TestPrimaryOnlyTablesExplain(t *testing.T) {
37+
pool := GetPool()
38+
defer pool.Close()
39+
40+
_, err := pool.Exec(context.Background(), `CREATE TABLE IF NOT EXISTS lb_plugin_primary_only (
41+
id BIGINT,
42+
data VARCHAR
43+
)`)
44+
assert.NoError(t, err)
45+
defer pool.Exec(context.Background(), "DROP TABLE IF EXISTS lb_plugin_primary_only")
46+
47+
time.Sleep(2 * time.Second)
48+
49+
rows, err := pool.Query(context.Background(), "EXPLAIN SELECT * FROM lb_plugin_primary_only")
50+
assert.NoError(t, err)
51+
52+
var explainLines []string
53+
foundPluginAnnotation := false
54+
for rows.Next() {
55+
var line string
56+
err = rows.Scan(&line)
57+
assert.NoError(t, err)
58+
explainLines = append(explainLines, line)
59+
60+
if strings.Contains(line, "plugin pgdog_primary_only_tables adjusted routing role=primary") {
61+
foundPluginAnnotation = true
62+
}
63+
}
64+
rows.Close()
65+
66+
t.Logf("EXPLAIN output:\n%s", strings.Join(explainLines, "\n"))
67+
assert.True(t, foundPluginAnnotation, "EXPLAIN output should contain plugin routing annotation")
68+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[[tables]]
2+
name = "lb_plugin_primary_only"

integration/load_balancer/run.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ for p in 45000 45001 45002; do
2727
fi
2828
done
2929

30+
pushd ${SCRIPT_DIR}/../../plugins/pgdog-primary-only-tables
31+
cargo build --release
32+
popd
33+
34+
export LD_LIBRARY_PATH=${SCRIPT_DIR}/../../target/release:${LD_LIBRARY_PATH:-}
35+
export DYLD_LIBRARY_PATH=${LD_LIBRARY_PATH}
36+
3037
docker-compose up -d
3138

3239
echo "Waiting for Postgres to be ready"

pgdog-config/src/users.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use serde::{Deserialize, Serialize};
22
use std::env;
3+
use std::path::PathBuf;
34
use tracing::warn;
45

56
use super::core::Config;
@@ -21,6 +22,10 @@ pub struct Plugin {
2122
///
2223
/// https://docs.pgdog.dev/configuration/pgdog.toml/plugins/#name
2324
pub name: String,
25+
26+
/// Path to the configuration file for the plugin, if any. Plugin-specific settings can be
27+
/// placed there. It's completely plugin-specific and any fomrat is acceptable.
28+
pub config: Option<PathBuf>,
2429
}
2530

2631
/// This configuration controls which users are allowed to connect to PgDog. This is a TOML list so for each user, add a `[[users]]` section to `users.toml`.

pgdog-macros/src/lib.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ pub fn plugin(_input: TokenStream) -> TokenStream {
4040
*output = version;
4141
}
4242
}
43+
44+
#[unsafe(no_mangle)]
45+
pub extern "C" fn pgdog_logging_init(config: pgdog_plugin::PdConfig) {
46+
pgdog_plugin::logging::init(&config);
47+
}
4348
};
4449
TokenStream::from(expanded)
4550
}
@@ -63,6 +68,28 @@ pub fn init(_attr: TokenStream, item: TokenStream) -> TokenStream {
6368
TokenStream::from(expanded)
6469
}
6570

71+
/// Generate the `pgdog_config` method that's executed at plugin load time.
72+
#[proc_macro_attribute]
73+
pub fn config(_attr: TokenStream, item: TokenStream) -> TokenStream {
74+
let input_fn = parse_macro_input!(item as ItemFn);
75+
let fn_name = &input_fn.sig.ident;
76+
77+
let expanded = quote! {
78+
79+
#[unsafe(no_mangle)]
80+
pub extern "C" fn pgdog_config(
81+
pd_config: pgdog_plugin::PdConfig,
82+
result: *mut u8)
83+
{
84+
#input_fn
85+
86+
#fn_name(pd_config, result);
87+
}
88+
};
89+
90+
TokenStream::from(expanded)
91+
}
92+
6693
/// Generate the `pgdog_fini` method that runs at PgDog shutdown.
6794
#[proc_macro_attribute]
6895
pub fn fini(_attr: TokenStream, item: TokenStream) -> TokenStream {

0 commit comments

Comments
 (0)