diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca27f8..a8ed19f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,3 +43,11 @@ - `CODEOWNERS`, `.editorconfig`, and Dependabot config (cargo + GitHub Actions, weekly). - Expanded `.gitignore` with IDE and build directories. + +### Fixed + +- `get_tables` no longer issues DescribeTable calls serially. On AWS accounts + with hundreds of tables the serial loop took over a minute (~300ms per + table), exceeding the GUI's connection timeout and failing the initial + connection. Describes now run with bounded concurrency (16 in flight) and + results are re-sorted alphabetically to preserve ListTables ordering. diff --git a/Cargo.lock b/Cargo.lock index 77337d8..d0d8e7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -681,13 +681,14 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dynamodb-plugin" -version = "0.1.1" +version = "0.1.2" dependencies = [ "assert-json-diff", "aws-config", "aws-sdk-dynamodb", "aws-smithy-types", "base64 0.23.0", + "futures", "serde", "serde_json", "serde_yaml", @@ -750,6 +751,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.33" @@ -757,6 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -765,6 +782,34 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "futures-sink" version = "0.3.33" @@ -783,8 +828,13 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] diff --git a/Cargo.toml b/Cargo.toml index 4fd5a87..7d1dc6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ aws-config = { version = "1", features = ["behavior-version-latest"] } aws-sdk-dynamodb = { version = "1", features = ["behavior-version-latest"] } aws-smithy-types = "1" base64 = "0.23" +futures = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" diff --git a/src/handlers/metadata.rs b/src/handlers/metadata.rs index f56b32d..4bf1f1f 100644 --- a/src/handlers/metadata.rs +++ b/src/handlers/metadata.rs @@ -1,5 +1,6 @@ //! Schema metadata: tables, columns, indexes, foreign keys. +use futures::stream::{self, StreamExt}; use serde_json::{json, Value}; use crate::error::ErrorCode; @@ -8,6 +9,13 @@ use crate::handlers::models::ColumnResponse; use crate::rpc::{error_response, ok_response}; use crate::utils::extractor; +/// Max in-flight DescribeTable calls while building the table list. On +/// accounts with hundreds of tables a serial loop takes over a minute +/// (~300ms per describe), which trips the GUI's connection timeout. Bounding +/// concurrency keeps the full metadata (item count, size, status) without +/// overwhelming the account's DescribeTable rate limit. +const DESCRIBE_TABLE_CONCURRENCY: usize = 16; + /// Returns the list of tables in DynamoDB with metadata. pub async fn get_tables(id: Value, params: &Value) -> Value { let client = match connection::build_client(params).await { @@ -17,31 +25,38 @@ pub async fn get_tables(id: Value, params: &Value) -> Value { match client.list_tables().await { Ok(table_names) => { - let mut results = Vec::new(); - for name in table_names { - // Fetch full table metadata via describe_table - match client.describe_table(&name).await { - Ok(desc) => { - results.push(json!({ + let mut results: Vec = stream::iter(table_names.into_iter().map(|name| { + let client = client.clone(); + async move { + match client.describe_table(&name).await { + Ok(desc) => json!({ "name": name, "comment": null, "item_count": desc.item_count.unwrap_or(0), "table_size_bytes": desc.table_size_bytes.unwrap_or(0), "table_status": desc.table_status.unwrap_or_else(|| "ACTIVE".to_string()), - })); - } - Err(_) => { - // Fallback if describe_table fails - results.push(json!({ + }), + Err(_) => json!({ "name": name, "comment": null, "item_count": 0, "table_size_bytes": 0, "table_status": "UNKNOWN", - })); + }), } } - } + })) + .buffer_unordered(DESCRIBE_TABLE_CONCURRENCY) + .collect() + .await; + // buffer_unordered completes out of order; restore alphabetical + // ordering so the sidebar matches ListTables order. + results.sort_by(|a, b| { + a["name"] + .as_str() + .unwrap_or("") + .cmp(b["name"].as_str().unwrap_or("")) + }); ok_response(id, json!(results)) } Err(err) => error_response(id, ErrorCode::InternalError, &err.message),