This is an automated email from the ASF dual-hosted git repository.

viirya pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-gateway.git


The following commit(s) were added to refs/heads/main by this push:
     new 2d08fc0  [SPARK-59021] Add scg-store-memory and scg-pool-static crates
2d08fc0 is described below

commit 2d08fc0249a99f93cd2e36869f18f7188f064707
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Wed Aug 26 23:50:19 2026 -0700

    [SPARK-59021] Add scg-store-memory and scg-pool-static crates
    
    ### What changes were proposed in this pull request?
    
    Part of importing the Spark Connect Gateway reference implementation into
    `apache/spark-connect-gateway` as a series of small, bottom-up PRs (tracked 
in
    the do-not-merge reference PR #1).
    
    This adds two small crates that implement the `scg-routing` traits, grouped 
into
    one PR:
    
    - `scg-store-memory` — an in-process `AffinityStore` for single-replica
      deployments and tests (for multi-replica HA, `scg-store-redis`, added in a
      later PR, is used instead).
    - `scg-pool-static` — a backend `Pool` whose membership is fixed at startup 
(the
      dynamic K8s-Endpoints-watch variant `scg-pool-k8s` comes in a later PR).
    
    Both depend on the `scg-routing` crate whose traits they implement. They 
compile
    and test on their own (2 unit tests each) and are appended to the workspace
    `members` list. They are consumed later by the `proxy` crate (both are proxy
    dev-dependencies used by its tests).
    
    ### Why are the changes needed?
    
    Implements the reference gateway from the approved SPIP (SPARK-58455), 
imported
    one reviewable piece at a time.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D 
warnings`,
    `cargo build --workspace`, and `cargo test --workspace` all pass locally
    (store-memory 2 tests, pool-static 2 tests).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Yes, co-authored with Claude Code.
    
    Closes #11 from viirya/import-08-store-memory-pool-static.
    
    Authored-by: Liang-Chi Hsieh <[email protected]>
    Signed-off-by: Liang-Chi Hsieh <[email protected]>
---
 Cargo.lock                     | 18 ++++++++
 Cargo.toml                     |  2 +
 crates/pool-static/Cargo.toml  | 31 ++++++++++++++
 crates/pool-static/src/lib.rs  | 78 ++++++++++++++++++++++++++++++++++
 crates/store-memory/Cargo.toml | 35 +++++++++++++++
 crates/store-memory/src/lib.rs | 96 ++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 260 insertions(+)

diff --git a/Cargo.lock b/Cargo.lock
index 8194fef..c59f45e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2329,6 +2329,14 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "scg-pool-static"
+version = "0.1.0"
+dependencies = [
+ "scg-routing",
+ "thiserror 2.0.20",
+]
+
 [[package]]
 name = "scg-ratelimit"
 version = "0.1.0"
@@ -2352,6 +2360,16 @@ dependencies = [
  "tonic",
 ]
 
+[[package]]
+name = "scg-store-memory"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "parking_lot",
+ "scg-routing",
+ "tokio",
+]
+
 [[package]]
 name = "scg-tenant"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 77d7202..c527304 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,8 @@ members = [
     "crates/audit",
     "crates/ratelimit",
     "crates/tenant",
+    "crates/store-memory",
+    "crates/pool-static",
 ]
 
 [workspace.package]
diff --git a/crates/pool-static/Cargo.toml b/crates/pool-static/Cargo.toml
new file mode 100644
index 0000000..5c67017
--- /dev/null
+++ b/crates/pool-static/Cargo.toml
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[package]
+name = "scg-pool-static"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+repository.workspace = true
+readme.workspace = true
+keywords.workspace = true
+categories.workspace = true
+description = "Static round-robin backend pool for the Spark Connect Gateway."
+publish = false
+
+[dependencies]
+scg-routing = { path = "../routing" }
+thiserror = { workspace = true }
diff --git a/crates/pool-static/src/lib.rs b/crates/pool-static/src/lib.rs
new file mode 100644
index 0000000..8854f50
--- /dev/null
+++ b/crates/pool-static/src/lib.rs
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Backend pool whose membership is fixed at startup. Pairs with
+//! `scg-pool-k8s` for the dynamic K8s-Endpoints-watch variant; both
+//! implement the same `Pool` trait and can be swapped via config.
+
+use scg_routing::{BackendMember, Pool};
+
+#[derive(Debug, thiserror::Error)]
+pub enum StaticPoolError {
+    #[error("static pool: at least one backend address required")]
+    Empty,
+}
+
+pub struct StaticPool {
+    backends: Vec<String>,
+}
+
+impl StaticPool {
+    pub fn new<I, S>(addresses: I) -> Result<Self, StaticPoolError>
+    where
+        I: IntoIterator<Item = S>,
+        S: Into<String>,
+    {
+        let backends: Vec<String> = 
addresses.into_iter().map(Into::into).collect();
+        if backends.is_empty() {
+            return Err(StaticPoolError::Empty);
+        }
+        Ok(Self { backends })
+    }
+}
+
+impl Pool for StaticPool {
+    fn members(&self) -> Vec<BackendMember> {
+        // Static pools have no notion of health — every configured
+        // backend is presumed healthy. Wrap with `scg-healthcheck`'s
+        // `HealthAwarePool` to add active gRPC health probing; on a
+        // bare StaticPool a failed forward surfaces as an error to
+        // the client and the selection strategy moves onward for the
+        // next session. Labels/weights are defaults until the config
+        // grows per-address metadata.
+        self.backends.iter().map(BackendMember::new).collect()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn members_returns_configured_list_with_default_metadata() {
+        let p = StaticPool::new(["a", "b", "c"]).unwrap();
+        let m = p.members();
+        assert_eq!(
+            m.iter().map(|b| b.addr.as_str()).collect::<Vec<_>>(),
+            vec!["a", "b", "c"]
+        );
+        assert!(m.iter().all(|b| b.weight == 1 && b.labels.is_empty()));
+    }
+
+    #[test]
+    fn empty_rejected() {
+        assert!(StaticPool::new(Vec::<String>::new()).is_err());
+    }
+}
diff --git a/crates/store-memory/Cargo.toml b/crates/store-memory/Cargo.toml
new file mode 100644
index 0000000..6d1533a
--- /dev/null
+++ b/crates/store-memory/Cargo.toml
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[package]
+name = "scg-store-memory"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+repository.workspace = true
+readme.workspace = true
+keywords.workspace = true
+categories.workspace = true
+description = "In-process affinity store for single-replica Spark Connect 
Gateway deployments."
+publish = false
+
+[dependencies]
+scg-routing = { path = "../routing" }
+parking_lot = { workspace = true }
+async-trait = { workspace = true }
+
+[dev-dependencies]
+tokio = { workspace = true }
diff --git a/crates/store-memory/src/lib.rs b/crates/store-memory/src/lib.rs
new file mode 100644
index 0000000..9207196
--- /dev/null
+++ b/crates/store-memory/src/lib.rs
@@ -0,0 +1,96 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! In-process `AffinityStore` for single-replica deployments and
+//! tests. For multi-replica HA use `scg-store-redis` — the
+//! `AffinityStore` trait is kept narrow specifically so swapping
+//! the backing store does not ripple into the proxy layer.
+
+use parking_lot::RwLock;
+use scg_routing::{AffinityStore, SessionKey};
+use std::collections::HashMap;
+
+#[derive(Default)]
+pub struct MemoryStore {
+    sessions: RwLock<HashMap<SessionKey, String>>,
+    ops: RwLock<HashMap<String, String>>,
+}
+
+impl MemoryStore {
+    pub fn new() -> Self {
+        Self::default()
+    }
+}
+
+#[async_trait::async_trait]
+impl AffinityStore for MemoryStore {
+    async fn lookup_session(&self, key: &SessionKey) -> Option<String> {
+        self.sessions.read().get(key).cloned()
+    }
+
+    async fn bind_session_if_absent(&self, key: SessionKey, backend: String) 
-> String {
+        let mut g = self.sessions.write();
+        g.entry(key).or_insert(backend).clone()
+    }
+
+    async fn forget_session(&self, key: &SessionKey) {
+        self.sessions.write().remove(key);
+    }
+
+    async fn lookup_op(&self, op_id: &str) -> Option<String> {
+        self.ops.read().get(op_id).cloned()
+    }
+
+    async fn bind_op(&self, op_id: String, backend: String) {
+        self.ops.write().insert(op_id, backend);
+    }
+
+    async fn forget_op(&self, op_id: &str) {
+        self.ops.write().remove(op_id);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn session_stickiness_invariant() {
+        let s = MemoryStore::new();
+        let k = SessionKey::new("alice", "sess-1");
+        assert!(s.lookup_session(&k).await.is_none());
+        let first = s
+            .bind_session_if_absent(k.clone(), "be-a:15002".into())
+            .await;
+        assert_eq!(first, "be-a:15002");
+        // Re-binding must not move an existing session — stickiness invariant.
+        let second = s
+            .bind_session_if_absent(k.clone(), "be-b:15002".into())
+            .await;
+        assert_eq!(second, "be-a:15002");
+        s.forget_session(&k).await;
+        assert!(s.lookup_session(&k).await.is_none());
+    }
+
+    #[tokio::test]
+    async fn op_reverse_index() {
+        let s = MemoryStore::new();
+        assert!(s.lookup_op("op-1").await.is_none());
+        s.bind_op("op-1".into(), "be-a:15002".into()).await;
+        assert_eq!(s.lookup_op("op-1").await.as_deref(), Some("be-a:15002"));
+        s.forget_op("op-1").await;
+        assert!(s.lookup_op("op-1").await.is_none());
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to