diff --git a/docs/configuration/pgdog.toml/plugins.md b/docs/configuration/pgdog.toml/plugins.md index cdd03b5..a388c7b 100644 --- a/docs/configuration/pgdog.toml/plugins.md +++ b/docs/configuration/pgdog.toml/plugins.md @@ -1,11 +1,6 @@ # Plugin settings -!!! warning - Plugins are not currently supported anymore. We will pick them up again if there is more interest - from the community. - -[Plugins](../../features/plugins/index.md) are dynamically loaded at pooler startup. These settings control which plugins are loaded. In the future, more -options will be available to configure plugin behavior. +[Plugins](../../features/plugins/index.md) are dynamically loaded at PgDog startup. These settings control which plugins are loaded. Plugins are a TOML list, so for each plugin you want to enable, add a `[[plugins]]` entry to `pgdog.toml`. For example: @@ -24,4 +19,12 @@ name = "alice_router" ### **`name`** Name of the plugin to load. This is used by PgDog to look up the shared library object in [`LD_LIBRARY_PATH`](https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html). 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. +name is `router`, PgDog will look for `librouter.so` on Linux, `librouter.dll` on Windows, and `librouter.dylib` on Mac OS. + +Additionally, you can pass the relative or absolute path to the shared library itself: + +```toml +name = "/opt/plugins/librouter.so" +``` + +Make sure the user running PgDog has read & execute permissions on the library. diff --git a/docs/features/plugins/.pages b/docs/features/plugins/.pages index 178cc16..2ad88ea 100644 --- a/docs/features/plugins/.pages +++ b/docs/features/plugins/.pages @@ -1,4 +1,2 @@ nav: - - 'index.md' - - 'rust.md' - '...' diff --git a/docs/features/plugins/c.md b/docs/features/plugins/c.md deleted file mode 100644 index 9b6681a..0000000 --- a/docs/features/plugins/c.md +++ /dev/null @@ -1,52 +0,0 @@ -# Plugins in C - -Writing PgDog plugins in C is pretty straight forward if you're comfortable in the language. The plugin API -is written in C (for compatibility), so if you're comfortable in C, you should be right at home. - -## Getting started - -### Includes - -The plugin headers are located in `pgdog-plugin/include`. Include `pgdog.h` for everything you need to get started: - -```c -#include "pgdog.h" -``` - -### Linking - -Your plugin will use `pgdog-plugin` internals, so you need to link to it at build time. To do so, first compile -`pgdog-plugin` by running this command in the root directory of the project: - -```bash -cargo build -``` - -This ensures all libraries and bindings are compiled before you get started. - -!!! note - If you're writing plugins for release (`-02`), build the crate using the release profile by passing `--release` flag to Cargo. - -The shared library will be placed in `target/(debug|release)` and you can link to it like so: - -```bash -export LIBRARY_PATH=target/debug -gcc plugin.c -lpgdog_routing -lshared -o plugin.so -``` - -### Memory safety - -All structures passed to plugins are owned by PgDog runtime, so make sure not to `free` any pointers. All structures passed back to PgDog will be freed automatically by PgDog, so you don't need to worry about leaks. - -If you allocate any memory during routine execution, make sure to free it before you return from the plugin API call. - -### Globals - -Access to `pgdog_route_query` is _not_ synchronized, so if you use any globals, make sure they are static or -protected by a mutex. You can initialize any globals in `pgdog_init` and clean them up in `pgdog_fini`. - -## Learn more - -- [routing-plugin-c](https://github.com/levkk/pgdog/tree/main/examples/routing-plugin-c) example plugin - -See [Rust](rust.md) documentation for how to implement plugins. diff --git a/docs/features/plugins/index.md b/docs/features/plugins/index.md index a145fd6..0002876 100644 --- a/docs/features/plugins/index.md +++ b/docs/features/plugins/index.md @@ -1,94 +1,178 @@ # Plugins overview -!!! warning - Plugins are currently disabled. The C FFI interface is too awkward to pass all the required - context to the query router. +PgDog comes with a powerful plugin system that allows you to customize the query routing behavior. Plugins are written in Rust, compiled into shared libraries, and loaded at runtime. -One of features that make PgDog particularly powerful is its plugin system. Users of PgDog can write plugins -in any language and inject them inside the query router to direct query traffic, to rewrite queries, or to block -them entirely and return custom results. -## API +## Getting started -PgDog plugins are shared libraries loaded at application startup. They can be written in any programming language, as long -as that language can be compiled to a shared library, and can expose a predefined set of C ABI-compatible functions. +PgDog plugins are Rust libraries. To create a plugin, first create a project with Cargo: -### Functions +``` +cargo init --lib my_plugin +``` -#### `pgdog_init` +#### Dependencies -This function is executed once when PgDog loads the plugin, at application startup. It allows to initialize any -kind of internal plugin state. Execution of this function is synchronized, so it's safe to execute any thread-unsafe +Inside your project's `Cargo.toml`, add the following settings and dependencies: + +```toml +[lib] +crate-type = ["rlib", "cdylib"] + +[dependencies] +pgdog-plugin = "0.1.6" +``` + +This turns the crate into a shared library, exposing its functions using the C ABI, which PgDog will call at runtime. + +!!! note + The `pgdog-plugin` crate is published on [crates.io](https://crates.io/crates/pgdog-plugin) and is fully documented. You can find our [Rust docs here](https://docsrs.pgdog.dev), including all dependencies like [`pg_query`](https://docsrs.pgdog.dev/pg_query/index.html). + +### Writing plugins + +When writing plugins, it's helpful to import most commonly used macros, functions and types. You can do so with just one line of code: + +```rust +use pgdog_plugin::prelude::*; +``` + +PgDog plugins have a list of required methods they need to expose. They are called by PgDog at plugin startup and validate that it +was correctly written. + +You don't need to implement them yourself. Add the following to your plugin's `src/lib.rs` file: + +```rust +macros::plugin!(); +``` + +These ensure the following requirements are followed: + +1. The plugin is compiled with the same version of the Rust compiler as PgDog itself +2. They are using the same version of `pg_query` + +See [Safety](#safety) section for more info. + + +## Functions + +### `init` + +This function is executed once at startup, when PgDog loads the plugin. It allows to initialize any +kind of internal plugin state. Execution of this function is synchronized, so it's safe to include any thread-unsafe functions or initialize synchronization primitives, like mutexes. This function has the following signature: -=== "Rust" - ```rust - pub extern "C" fn pgdog_init() {} - ``` -=== "C/C++" - ```c - void pgdog_init(); - ``` +```rust +#[init] +fn init() { + // Perform any initialization routines here. +} +``` -#### `pgdog_route_query` +### `route` -This function is called every time the query router sees a new query and needs to figure out -where this query should be sent. The query text and parameters will be provided and the router -expects the plugin to parse the query and provide a route. +This function is called every time the query router processes a query and needs to figure out +where this query should be sent. This function has the following signature: -=== "Rust" - ```rust - use pgdog_plugin::*; +```rust +#[route] +fn route(context: Context) -> Route { + Route::unknown() +} +``` + +#### Inputs + +The [`Context`](https://docsrs.pgdog.dev/pgdog_plugin/context/struct.Context.html) struct provides the following information: + +- Number of shards in the database cluster +- Does the cluster have replicas +- The Abstract Syntax Tree (AST) of the statement, parsed by `pg_query` - pub extern "C" fn pgdog_route_query(Input query) -> Output { - Route::unknown() - } - ``` -=== "C/C++" - ```c - Output pgdog_route_query(Input query); - ``` +#### Outputs -##### Data structures +The plugin is expected to return a [`Route`](https://docsrs.pgdog.dev/pgdog_plugin/context/struct.Route.html). It can pass the following information back to PgDog: -This function expects an input of type `Input` and must return a struct of type `Output`. The input contains -the query PgDog received and the current database configuration, e.g. number of shards, replicas, and if there -is a primary database that can serve writes. +- Which shard(s) to send the query to +- Is the query a read or a write, sending it to a replica or the primary, respectively -The output structure contains the routing decision (e.g. query should go to a replica) and any additional information that the plugin wants to communicate, which depends on the routing decision. For example, -if the plugin wants PgDog to intercept this query and return a custom result, rows of that result will be -included in the output. +Both of these are optional. If you don't return either one, the plugin doesn't influence the routing decision at all and can be used for logging queries, or some other purpose. -#### `pgdog_fini` -This function is called before the pooler is shut down. This allows plugins to perform any tasks, like saving +### `fini` + +This function is called before PgDog is shut down. It allows plugins to perform any cleanup tasks, like saving some internal state to a durable medium. This function has the following signature: -=== "Rust" - ```rust - pub extern "C" fn pgdog_fini() {} - ``` -=== "C/C++" - ```c - void pgdog_fini(); - ``` +```rust +#[fini] +fn fini() { + // Any cleanup routines go here. +} +``` + +## Loading plugins + +Plugins need to be compiled and placed into a folder on your machine where PgDog can find them. This can be achieved using several approaches: + +1. Place the shared library into a standard operating system folder, e.g.: `/usr/lib` or `/lib` +2. Export the plugin's parent directory into the `LD_LIBRARY_PATH` environment variable, provided to PgDog at runtime +3. Pass the absolute (or relative) path to the plugin in [`pgdog.toml`](../../configuration/pgdog.toml/plugins.md) + +!!! note + Make sure to compile plugins in release mode for good performance: `cargo build --release`. The plugin's shared library will be in `target/release` folder of your Cargo project, e.g., `target/release/libmy_plugin.so`. + +You then need to specify which plugins you'd like PgDog to load at runtime: + +```toml +[[plugins]] +name = "my_plugin" +``` + +This can be the name of the library (without the `lib` prefix or the `.so`/`.dylib` extension) or relative/absolute path to the shared library, for example: + +```toml +[[plugins]] +name = "/usr/lib/libmy_plugin.so" +``` ## Examples -Example plugins written in Rust and C are -included in [GitHub](https://github.com/levkk/pgdog/tree/main/examples). +Example plugins written in Rust are in [GitHub](https://github.com/pgdogdev/pgdog/tree/main/plugins). + +## Safety + +Rust plugins can do anything. There is no virtualization layer or checks on their behavior. With great power comes great responsibility, so make sure the plugins you use are trusted (and tested). + +This is intentional. We don't want to limit what you can do inside plugins nor are we there to tell you what you shouldn't be doing. It's your data stack, and you're the owner. + +An additional benefit of using Rust is: plugins are very fast! If written correctly, they will have minimal to no latency impact of your database. + +### Rust/C ABI + +Unlike C, the Rust language doesn't have a stable ABI. Therefore, additional care needs to be taken when loading and executing routines from shared libraries. This is enforced automatically by `pgdog-plugin`, but you should still be aware of them. + +#### Rust compiler version + +Whatever Rust compiler version is used to build PgDog itself needs to be used to build the plugins. This is checked at runtime and plugins that don't follow this requirement are **not loaded**. + +#### `pg_query` version + +Since we're passing the AST itself down to the plugins, we need to make sure that the versions of the `pg_query` library used by PgDog and the plugin are the same. This is done automatically if you're using the primitives exported by the `pgdog-plugin` crate: -## Learn more +```rust +// Manually use the exported primitives. +use pgdog_plugin::pg_query; -- [Plugins in Rust](rust.md) -- [Plugins in C](c.md) +// Automatically import them. +use pgdog_plugin::prelude::*; +``` diff --git a/docs/features/plugins/rust.md b/docs/features/plugins/rust.md deleted file mode 100644 index 1033aa8..0000000 --- a/docs/features/plugins/rust.md +++ /dev/null @@ -1,191 +0,0 @@ -# Plugins in Rust - -Writing PgDog plugins in Rust has first class support built into the [`pgdog-plugin`](https://github.com/levkk/pgdog/tree/main/pgdog-plugin) crate. The crate acts -as a bridge between plugins and PgDog internals, and provides safe methods for constructing C-compatible structs. - -## How it works - -For plugins to be truly dynamic, they have to be compiled into shared libraries (`.so` on Linux, `.dylib` on Mac). This way you can load arbitrary plugins into PgDog at runtime without having to recompile it. Since Rust doesn't have a stable [ABI](https://en.wikipedia.org/wiki/Application_binary_interface), we have to use the only stable ABI available to all programming languages: C. - -### C ABI - -Rust has great bindings for using (and exposing) C-compatible functions. You can learn more about this by reading the [`std::ffi`](https://doc.rust-lang.org/stable/std/ffi/index.html) documentation and other great sources like The Embedded Rust Book[^1]. - -The [`pgdog-plugin`](https://github.com/levkk/pgdog/tree/main/pgdog-plugin) crate contains C [headers](https://github.com/levkk/pgdog/tree/main/pgdog-plugin/include) that define -types and functions PgDog expects its plugins to use, with Rust bindings generated with [bindgen](https://docs.rs/bindgen/latest/bindgen/). - -[^1]: [https://docs.rust-embedded.org/book/interoperability/rust-with-c.html](https://docs.rust-embedded.org/book/interoperability/rust-with-c.html) - - -## Getting started - -Create a new library crate with Cargo, like so: - -```bash -cargo new --lib my_pgdog_plugin -``` - -Since plugins have to be C ABI compatible, you'll need to change the crate type to `cdylib` (C dynamic library). -Edit your `Cargo.toml` and add the following: - -```toml -[lib] -crate-type = ["rlib", "cdylib"] -``` - -### Add `pgdog-plugin` - -To make building plugins easier, PgDog provides a crate that defines and implements the structs used by -plugin functions. - -Before proceeding, add this crate to your dependencies: - -```bash -cargo add pgdog-plugin -``` - -### Implement the API - -The [plugin API](../plugins/index.md) is pretty simple. For this tutorial, we'll implement the query routing function `pgdog_route_query`, which is called for the first query in every transaction PgDog receives. - - -This function has the following signature: - -```rust -use pgdog_plugin::*; - -pub extern "C" fn pgdog_route_query(input: Input) -> Output { - todo!() -} -``` - -The [`Input`](https://docs.rs/pgdog-plugin/latest/pgdog_plugin/input/index.html) structure contains the query PgDog received and the current state of the pooler configuration, like -the number of shards, the number of replicas and their addresses, and other information which the plugin can use -to determine where the query should go. - -The plugin is expected to return an [`Output`](https://docs.rs/pgdog-plugin/latest/pgdog_plugin/output/index.html) structure which contains its routing decision and any additional data -the plugin wants PgDog to use, like an error it wants PgDog to return to the client instead, for example. - -Both structures have Rust implementations which can help us avoid having to write C-like initialization code. - -### Parse the input - -You can get the query PgDog received from the input structure like so: - -```rust -if let Some(query) = input.query() { - // Parse the query. -} -``` - -The query is a Rust string, so your routing algorithm can be as simple as: - -```rust -let route = if query.starts_with("SELECT") { - // Route this to any replica. - Route::read_any() -} else { - // Send the query to a primary. - Route::write_any() -} -``` - -Both `read_any` and `write_any` are typically used in a single shard configuration and tell PgDog -that the shard number is not important. PgDog will send the query to the first shard in the configuration. - -### Return the output - -The `Output` structure contains the routing decision and any additional metadata. Since our plugin parsed the query and decided to forward this query to a database without modifications, the return value for `Output` should be: - -```rust -return Output::forward(route) -``` - -Not all plugins have to make a routing decision. For example, if your plugin just wants to count how many queries of a certain type your database receives but doesn't care about routing, you can tell PgDog to skip your plugin's routing decision: - -```rust -return Output::skip() -``` - -PgDog will ignore this output and pass the query to the next plugin in the chain. - -### Parsing query parameters - -PostgreSQL protocol has two ways to send queries to the database: using the simple query method with the parameters -included in the query text, and the extended protocol which sends parameters separately to prevent SQL injection attacks and allow for query re-use (prepared statements). - -The extended protocol is widely used, so queries your plugins will see will typically look like this: - -```postgresql -SELECT * FROM users WHERE id = $1 -``` - -If your plugin is sharding requests based on a hash (or some other function) of the `"users"."id"` column, you need -to see the value of `$1` before your plugin can make a decision. - -PgDog supports parsing the extended protocol and provides the full query text and parameters to its plugins. You can access a specific parameter by calling `Query::parameter`: - -```rust -if let Some(id) = query.parameter(0) { - // Parse the parameter. -} -``` - -!!! note - PostgreSQL uses a lot of 1-based indexing, e.g. parameters and arrays - start at 1. PgDog is more "rusty" and uses 0-based indexing. To access the first - parameter in a query, index it by `0`, not `1`. - -Parameters are encoded using PostgreSQL wire protocol, so they can be either UTF-8 text or binary. If they are text, -which is often the case, you can access it like so: - -```rust -if let Some(id) = id.as_str() { - let id = id.parse::(); -} -``` - -In the case of binary encoding, `as_str()` will return `None` and you can parse the binary encoding instead: - -```rust -if let Ok(id) = id.as_bytes().try_into() { - let id = i64::from_be_bytes(id); -} -``` - -While this may seem tedious at first, this provides the highest flexibility for parsing parameters. A plugin -can use any kind of field for routing, e.g. cosine similarity of a vector column (to another), which requires -parsing vector-encoded fields. - -!!! note - As the project evolves, I expect we'll add - more helpers to the `pgdog-plugin` crate to help parse - parameters automatically. - - -## SQL parsers - -Parsing SQL manually can be error-prone, and there are multiple great SQL parsers you can pick off the shelf. The [pgdog-routing](https://github.com/levkk/pgdog/tree/main/plugins/pgdog-routing) plugin which ships with PgDog uses `pg_query.rs`, which in turn uses the internal PostgreSQL query -parser. This ensures all valid PostgreSQL queries are recognized and parsed correctly. - -Other SQL parsers in the Rust community include [sqlparser](https://docs.rs/sqlparser/latest/sqlparser/) which -can parse many dialects, including other databases like MySQL, if you wanted to rewrite MySQL queries to PostgreSQL queries transparently for example. - -## Handling errors - -Since plugins use the C ABI, PgDog is not able to catch panics inside plugins. Therefore, if a plugin panics, this will cause an abort and shutdown the pooler. - -The vast majority of the Rust standard library and crates avoid panicking and return errors instead. Plugin code must check for error conditions and handle them internally. Notably, don't use `unwrap()` on `Option` or `Result` types and handle each case instead. - -!!! note - Better error handling is on the roadmap, e.g. by using macros - that wrap plugin code into a panic handler. That being said, since - plugins do expose `extern "C"` functions, this limitation should be - explicitly stated to plugin authors. - -## Learn more - -PgDog plugins are in their infancy and many more features will be added over time. For now, the API -is pretty bare bones but can already do useful things. Our bundled plugin we use for routing is called -[pgdog-routing](https://github.com/levkk/pgdog/tree/main/plugins/pgdog-routing) and it can be used -as the basis for your plugin development.