# Supporting custom user Extensions Created 2023-05-03 ## Motivation There are many extensions in the PostgreSQL ecosystem, and not all extensions are of a quality that we can confidently support them. Additionally, our current extension inclusion mechanism has several problems because we build all extensions into the primary Compute image: We build the extensions every time we build the compute image regardless of whether we actually need to rebuild the image, and the inclusion of these extensions in the image adds a hard dependency on all supported extensions - thus increasing the image size, and with it the time it takes to download that image - increasing first start latency. This RFC proposes a dynamic loading mechanism that solves most of these problems. ## Summary `compute_ctl` is made responsible for loading extensions on-demand into the container's file system for dynamically loaded extensions, and will also make sure that the extensions in `shared_preload_libraries` are downloaded before the compute node starts. ## Components compute_ctl, PostgreSQL, neon (extension), Compute Host Node, Extension Store ## Requirements Compute nodes with no extra extensions should not be negatively impacted by the existence of support for many extensions. Installing an extension into PostgreSQL should be easy. Non-preloaded extensions shouldn't impact startup latency. Uninstalled extensions shouldn't impact query latency. A small latency penalty for dynamically loaded extensions is acceptable in the first seconds of compute startup, but not in steady-state operations. ## Proposed implementation ### On-demand, JIT-loading of extensions TLDR; we download extensions as soon as we need them, or when we have spare time. That means, we first download the extensions required to start the PostMaster (`shared_preload_libraries` and their dependencies), then the libraries required before a backend can start processing user input (`preload_libraries` and dependencies), and then (with network limits applied) the remainder of the configured extensions, with prioritization for installed extensions. If PostgreSQL tries to load a library that is not yet fully on disk, it will ask `compute_ctl` first if the extension has been downloaded yet, and will wait for `compute_ctl` to finish downloading that extension. `compute_ctl` will prioritize downloading that extension over other extensions that were not yet requested. #### Workflow ```mermaid sequenceDiagram autonumber participant EX as External (control plane, ...) participant CTL as compute_ctl participant ST as extension store actor PG as PostgreSQL EX ->>+ CTL: Start compute with config X note over CTL: The configuration contains a list of all
extensions available to that compute node, etc. par Optionally parallel or concurrent loop Available extensions CTL ->>+ ST: Download control file of extension activate CTL ST ->>- CTL: Finish downloading control file CTL ->>- CTL: Put control file in extensions directory end loop For each extension in shared_preload_libraries CTL ->>+ ST: Download extension's data activate CTL ST ->>- CTL: Finish downloading CTL ->>- CTL: Put extension's files in the right place end end CTL ->>+ PG: Start PostgreSQL note over CTL: PostgreSQL can now start accepting
connections. However, users may still need to wait
for preload_libraries extensions to get downloaded. par Load preload_libraries loop For each extension in preload_libraries CTL ->>+ ST: Download extension's data activate CTL ST ->>- CTL: Finish downloading CTL ->>- CTL: Put extension's files in the right place end end note over CTL: After this, connections don't have any hard
waits for extension files left, except for those
connections that override preload_libraries
in their startup packet par PG's internal_load_library(library) alt Library is not yet loaded PG ->>+ CTL: Load library X CTL ->>+ ST: Download the extension that provides X ST ->>- CTL: Finish downloading CTL ->> CTL: Put extension's files in the right place CTL ->>- PG: Ready else Library is already loaded note over PG: No-op end and Download all remaining extensions loop Extension X CTL ->>+ ST: Download not-yet-downloaded extension X activate CTL ST ->>- CTL: Finish downloading CTL ->>- CTL: Put extension's files in the right place end end deactivate PG deactivate CTL ``` #### Summary Pros: - Startup is only as slow as it takes to load all (shared_)preload_libraries - Supports BYO Extension Cons: - O(sizeof(extensions)) IO requirement for loading all extensions. ### Alternative solutions 1. Allow users to add their extensions to the base image Pros: - Easy to deploy Cons: - Doesn't scale - first start size is dependent on image size; - All extensions are shared across all users: It doesn't allow users to bring their own restrictive-licensed extensions 2. Bring Your Own compute image Pros: - Still easy to deploy - User can bring own patched version of PostgreSQL Cons: - First start latency is O(sizeof(extensions image)) - Warm instance pool for skipping pod schedule latency is not feasible with O(n) custom images - Support channels are difficult to manage 3. Download all user extensions in bulk on compute start Pros: - Easy to deploy - No startup latency issues for "clean" users. - Warm instance pool for skipping pod schedule latency is possible Cons: - Downloading all extensions in advance takes a lot of time, thus startup latency issues 4. Store user's extensions in persistent storage Pros: - Easy to deploy - No startup latency issues - Warm instance pool for skipping pod schedule latency is possible Cons: - EC2 instances have only limited number of attachments shared between EBS volumes, direct-attached NVMe drives, and ENIs. - Compute instance migration isn't trivially solved for EBS mounts (e.g. the device is unavailable whilst moving the mount between instances). - EBS can only mount on one instance at a time (except the expensive IO2 device type). 5. Store user's extensions in network drive Pros: - Easy to deploy - Few startup latency issues - Warm instance pool for skipping pod schedule latency is possible Cons: - We'd need networked drives, and a lot of them, which would store many duplicate extensions. - **UNCHECKED:** Compute instance migration may not work nicely with networked IOs ### Idea extensions The extension store does not have to be S3 directly, but could be a Node-local caching service on top of S3. This would reduce the load on the network for popular extensions. ## Extension Store implementation Extension Store in our case is a private S3 bucket. Extensions are stored as tarballs in the bucket. The tarball contains the extension's control file and all the files that the extension needs to run. We may also store the control file separately from the tarball to speed up the extension loading. `s3:///extensions/ext-name/sha-256+1234abcd1234abcd1234abcd1234abcd/bundle.tar` where `ext-name` is an extension name and `sha-256+1234abcd1234abcd1234abcd1234abcd` is a hash of a specific extension version tarball. To ensure security, there is no direct access to the S3 bucket from compute node. Control plane forms a list of extensions available to the compute node and forms a short-lived [pre-signed URL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) for each extension that is available to the compute node. so, `compute_ctl` receives spec in the following format ``` "extensions": [{ "meta_format": 1, "extension_name": "postgis", "link": "https:///extensions/sha-256+1234abcd1234abcd1234abcd1234abcd/bundle.tar?AWSAccessKeyId=1234abcd1234abcd1234abcd1234abcd&Expires=1234567890&Signature=1234abcd1234abcd1234abcd1234abcd", ... }] ``` `compute_ctl` then downloads the extension from the link and unpacks it to the right place. ### How do we handle private extensions? Private and public extensions are treated equally from the Extension Store perspective. The only difference is that the private extensions are not listed in the user UI (managed by control plane). ### How to add new extension to the Extension Store? Since we need to verify that the extension is compatible with the compute node and doesn't contain any malicious code, we need to review the extension before adding it to the Extension Store. I do not expect that we will have a lot of extensions to review, so we can do it manually for now. Some admin UI may be added later to automate this process. The list of extensions available to a compute node is stored in the console database. ### How is the list of available extensions managed? We need to add new tables to the console database to store the list of available extensions, their versions and access rights. something like this: ``` CREATE TABLE extensions ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, version VARCHAR(255) NOT NULL, hash VARCHAR(255) NOT NULL, // this is the path to the extension in the Extension Store supported_postgres_versions integer[] NOT NULL, is_public BOOLEAN NOT NULL, // public extensions are available to all users is_shared_preload BOOLEAN NOT NULL, // these extensions require postgres restart is_preload BOOLEAN NOT NULL, license VARCHAR(255) NOT NULL, ); CREATE TABLE user_extensions ( user_id INTEGER NOT NULL, extension_id INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (extension_id) REFERENCES extensions (id) ); ``` When new extension is added to the Extension Store, we add a new record to the table and set permissions. In UI, user may select the extensions that they want to use with their compute node. NOTE: Extensions that require postgres restart will not be available until the next compute restart. Also, currently user cannot force postgres restart. We should add this feature later. For other extensions, we must communicate updates to `compute_ctl` and they will be downloaded in the background. ### How can user update the extension? User can update the extension by selecting the new version of the extension in the UI. ### Alternatives For extensions written on trusted languages we can also adopt `dbdev` PostgreSQL Package Manager based on `pg_tle` by Supabase. This will increase the amount supported extensions and decrease the amount of work required to support them.