-
Notifications
You must be signed in to change notification settings - Fork 463
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add /installed_extensions endpoint to collect statistics about extens…
…ion usage. (#8917) Add /installed_extensions endpoint to collect statistics about extension usage. It returns a list of installed extensions in the format: ```json { "extensions": [ { "extname": "extension_name", "versions": ["1.0", "1.1"], "n_databases": 5, } ] } ``` --------- Co-authored-by: Heikki Linnakangas <[email protected]>
- Loading branch information
1 parent
a181392
commit 63e7fab
Showing
8 changed files
with
266 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
use compute_api::responses::{InstalledExtension, InstalledExtensions}; | ||
use std::collections::HashMap; | ||
use std::collections::HashSet; | ||
use url::Url; | ||
|
||
use anyhow::Result; | ||
use postgres::{Client, NoTls}; | ||
use tokio::task; | ||
|
||
/// We don't reuse get_existing_dbs() just for code clarity | ||
/// and to make database listing query here more explicit. | ||
/// | ||
/// Limit the number of databases to 500 to avoid excessive load. | ||
fn list_dbs(client: &mut Client) -> Result<Vec<String>> { | ||
// `pg_database.datconnlimit = -2` means that the database is in the | ||
// invalid state | ||
let databases = client | ||
.query( | ||
"SELECT datname FROM pg_catalog.pg_database | ||
WHERE datallowconn | ||
AND datconnlimit <> - 2 | ||
LIMIT 500", | ||
&[], | ||
)? | ||
.iter() | ||
.map(|row| { | ||
let db: String = row.get("datname"); | ||
db | ||
}) | ||
.collect(); | ||
|
||
Ok(databases) | ||
} | ||
|
||
/// Connect to every database (see list_dbs above) and get the list of installed extensions. | ||
/// Same extension can be installed in multiple databases with different versions, | ||
/// we only keep the highest and lowest version across all databases. | ||
pub async fn get_installed_extensions(connstr: Url) -> Result<InstalledExtensions> { | ||
let mut connstr = connstr.clone(); | ||
|
||
task::spawn_blocking(move || { | ||
let mut client = Client::connect(connstr.as_str(), NoTls)?; | ||
let databases: Vec<String> = list_dbs(&mut client)?; | ||
|
||
let mut extensions_map: HashMap<String, InstalledExtension> = HashMap::new(); | ||
for db in databases.iter() { | ||
connstr.set_path(db); | ||
let mut db_client = Client::connect(connstr.as_str(), NoTls)?; | ||
let extensions: Vec<(String, String)> = db_client | ||
.query( | ||
"SELECT extname, extversion FROM pg_catalog.pg_extension;", | ||
&[], | ||
)? | ||
.iter() | ||
.map(|row| (row.get("extname"), row.get("extversion"))) | ||
.collect(); | ||
|
||
for (extname, v) in extensions.iter() { | ||
let version = v.to_string(); | ||
extensions_map | ||
.entry(extname.to_string()) | ||
.and_modify(|e| { | ||
e.versions.insert(version.clone()); | ||
// count the number of databases where the extension is installed | ||
e.n_databases += 1; | ||
}) | ||
.or_insert(InstalledExtension { | ||
extname: extname.to_string(), | ||
versions: HashSet::from([version.clone()]), | ||
n_databases: 1, | ||
}); | ||
} | ||
} | ||
|
||
Ok(InstalledExtensions { | ||
extensions: extensions_map.values().cloned().collect(), | ||
}) | ||
}) | ||
.await? | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
from logging import info | ||
|
||
from fixtures.neon_fixtures import NeonEnv | ||
|
||
|
||
def test_installed_extensions(neon_simple_env: NeonEnv): | ||
"""basic test for the endpoint that returns the list of installed extensions""" | ||
|
||
env = neon_simple_env | ||
|
||
env.create_branch("test_installed_extensions") | ||
|
||
endpoint = env.endpoints.create_start("test_installed_extensions") | ||
|
||
endpoint.safe_psql("CREATE DATABASE test_installed_extensions") | ||
endpoint.safe_psql("CREATE DATABASE test_installed_extensions_2") | ||
|
||
client = endpoint.http_client() | ||
res = client.installed_extensions() | ||
|
||
info("Extensions list: %s", res) | ||
info("Extensions: %s", res["extensions"]) | ||
# 'plpgsql' is a default extension that is always installed. | ||
assert any( | ||
ext["extname"] == "plpgsql" and ext["versions"] == ["1.0"] for ext in res["extensions"] | ||
), "The 'plpgsql' extension is missing" | ||
|
||
# check that the neon_test_utils extension is not installed | ||
assert not any( | ||
ext["extname"] == "neon_test_utils" for ext in res["extensions"] | ||
), "The 'neon_test_utils' extension is installed" | ||
|
||
pg_conn = endpoint.connect(dbname="test_installed_extensions") | ||
with pg_conn.cursor() as cur: | ||
cur.execute("CREATE EXTENSION neon_test_utils") | ||
cur.execute( | ||
"SELECT default_version FROM pg_available_extensions WHERE name = 'neon_test_utils'" | ||
) | ||
res = cur.fetchone() | ||
neon_test_utils_version = res[0] | ||
|
||
with pg_conn.cursor() as cur: | ||
cur.execute("CREATE EXTENSION neon version '1.1'") | ||
|
||
pg_conn_2 = endpoint.connect(dbname="test_installed_extensions_2") | ||
with pg_conn_2.cursor() as cur: | ||
cur.execute("CREATE EXTENSION neon version '1.2'") | ||
|
||
res = client.installed_extensions() | ||
|
||
info("Extensions list: %s", res) | ||
info("Extensions: %s", res["extensions"]) | ||
|
||
# check that the neon_test_utils extension is installed only in 1 database | ||
# and has the expected version | ||
assert any( | ||
ext["extname"] == "neon_test_utils" | ||
and ext["versions"] == [neon_test_utils_version] | ||
and ext["n_databases"] == 1 | ||
for ext in res["extensions"] | ||
) | ||
|
||
# check that the plpgsql extension is installed in all databases | ||
# this is a default extension that is always installed | ||
assert any(ext["extname"] == "plpgsql" and ext["n_databases"] == 4 for ext in res["extensions"]) | ||
|
||
# check that the neon extension is installed and has expected versions | ||
for ext in res["extensions"]: | ||
if ext["extname"] == "neon": | ||
assert ext["n_databases"] == 2 | ||
ext["versions"].sort() | ||
assert ext["versions"] == ["1.1", "1.2"] | ||
|
||
with pg_conn.cursor() as cur: | ||
cur.execute("ALTER EXTENSION neon UPDATE TO '1.3'") | ||
|
||
res = client.installed_extensions() | ||
|
||
info("Extensions list: %s", res) | ||
info("Extensions: %s", res["extensions"]) | ||
|
||
# check that the neon_test_utils extension is updated | ||
for ext in res["extensions"]: | ||
if ext["extname"] == "neon": | ||
assert ext["n_databases"] == 2 | ||
ext["versions"].sort() | ||
assert ext["versions"] == ["1.2", "1.3"] |
63e7fab
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5182 tests run: 4962 passed, 3 failed, 217 skipped (full report)
Failures on Postgres 16
test_pgbench[neon-github-actions-selfhosted-45-10]
: release-x86-64test_pgbench[vanilla-github-actions-selfhosted-45-10]
: release-x86-64test_basebackup_with_high_slru_count[github-actions-selfhosted-10-13-30]
: release-x86-64Flaky tests (2)
Postgres 17
test_subscriber_synchronous_commit
: release-arm64Postgres 14
test_subscriber_restart
: release-x86-64Code coverage* (full report)
functions
:31.3% (7503 of 23965 functions)
lines
:49.4% (60257 of 122034 lines)
* collected from Rust tests only
63e7fab at 2024-10-09T14:32:54.471Z :recycle: