-
Notifications
You must be signed in to change notification settings - Fork 822
feat: Lazy ETCD initialization #5690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,10 +12,12 @@ use std::time::Duration; | |||||||||||||||||||
| use std::{collections::HashMap, path::PathBuf}; | ||||||||||||||||||||
| use std::{env, fmt}; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| use crate::CancellationToken; | ||||||||||||||||||||
| use crate::transports::etcd as etcd_transport; | ||||||||||||||||||||
| use crate::{CancellationToken, Runtime}; | ||||||||||||||||||||
| use async_trait::async_trait; | ||||||||||||||||||||
| use futures::StreamExt; | ||||||||||||||||||||
| use once_cell::sync::OnceCell; | ||||||||||||||||||||
| use oneshot; | ||||||||||||||||||||
| use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode}; | ||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -139,6 +141,47 @@ pub enum Selector { | |||||||||||||||||||
| // Nats not listed because likely we want to remove that impl. It is not currently used and not well tested. | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl Selector { | ||||||||||||||||||||
| fn build(self, runtime: Option<Runtime>) -> Result<KeyValueStoreEnum, StoreError> { | ||||||||||||||||||||
| match self { | ||||||||||||||||||||
| Selector::Etcd(opts) => { | ||||||||||||||||||||
| if let Some(runtime) = runtime { | ||||||||||||||||||||
| let (tx, rx) = oneshot::channel(); | ||||||||||||||||||||
| runtime.primary().spawn(async move { | ||||||||||||||||||||
| let etcd_client = etcd_transport::Client::new(*opts, runtime.clone()) | ||||||||||||||||||||
| .await | ||||||||||||||||||||
| .map_err(StoreError::from); | ||||||||||||||||||||
| tx.send(etcd_client).unwrap(); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // We block our async task a tiny bit here, but not a big deal since we only ever do this once. | ||||||||||||||||||||
| Ok(KeyValueStoreEnum::Etcd(EtcdStore::new( | ||||||||||||||||||||
| rx.recv() | ||||||||||||||||||||
| .map_err(|x| StoreError::from(anyhow::anyhow!(x)))??, | ||||||||||||||||||||
| ))) | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why create the transport in a different task? Doesn't this resolve to creating it inline?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see, it's the async. Thats' a pain. |
||||||||||||||||||||
| } else { | ||||||||||||||||||||
| Err(StoreError::BuildError(anyhow::anyhow!( | ||||||||||||||||||||
| "Runtime is required for Etcd selector" | ||||||||||||||||||||
| ))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Selector::File(path) => { | ||||||||||||||||||||
| if let Some(runtime) = runtime { | ||||||||||||||||||||
| Ok(KeyValueStoreEnum::File(FileStore::new( | ||||||||||||||||||||
| runtime.primary_token(), | ||||||||||||||||||||
| path, | ||||||||||||||||||||
| ))) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| Err(StoreError::BuildError(anyhow::anyhow!( | ||||||||||||||||||||
| "Runtime is required for File selector" | ||||||||||||||||||||
| ))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Selector::Memory => Ok(KeyValueStoreEnum::Memory(MemoryStore::new())), | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl fmt::Display for Selector { | ||||||||||||||||||||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||||||||||||||||
| match self { | ||||||||||||||||||||
|
|
@@ -247,30 +290,31 @@ impl KeyValueStoreEnum { | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Clone)] | ||||||||||||||||||||
| pub struct Manager(Arc<KeyValueStoreEnum>); | ||||||||||||||||||||
| pub struct Manager { | ||||||||||||||||||||
| selector: Selector, | ||||||||||||||||||||
| kv_store: Arc<OnceCell<KeyValueStoreEnum>>, | ||||||||||||||||||||
| runtime: Option<Runtime>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl Default for Manager { | ||||||||||||||||||||
| fn default() -> Self { | ||||||||||||||||||||
| Manager::memory() | ||||||||||||||||||||
| Manager::new(Selector::Memory, None) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl Manager { | ||||||||||||||||||||
| /// In-memory KeyValueStoreManager for testing | ||||||||||||||||||||
| pub fn memory() -> Self { | ||||||||||||||||||||
| Self::new(KeyValueStoreEnum::Memory(MemoryStore::new())) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn etcd(etcd_client: crate::transports::etcd::Client) -> Self { | ||||||||||||||||||||
| Self::new(KeyValueStoreEnum::Etcd(EtcdStore::new(etcd_client))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn file<P: Into<PathBuf>>(cancel_token: CancellationToken, root: P) -> Self { | ||||||||||||||||||||
| Self::new(KeyValueStoreEnum::File(FileStore::new(cancel_token, root))) | ||||||||||||||||||||
| pub fn new(s: Selector, runtime: Option<Runtime>) -> Manager { | ||||||||||||||||||||
| Manager { | ||||||||||||||||||||
| selector: s, | ||||||||||||||||||||
| kv_store: Arc::new(OnceCell::new()), | ||||||||||||||||||||
| runtime, | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn new(s: KeyValueStoreEnum) -> Manager { | ||||||||||||||||||||
| Manager(Arc::new(s)) | ||||||||||||||||||||
| fn get_kv_store(&self) -> Result<&KeyValueStoreEnum, StoreError> { | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||||||||||||||||||||
| let selector = self.selector.clone(); | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are cloning the selector every time, but only using it on the very first call.
|
||||||||||||||||||||
| self.kv_store | ||||||||||||||||||||
| .get_or_try_init(|| selector.build(self.runtime.clone())) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub async fn get_or_create_bucket( | ||||||||||||||||||||
|
|
@@ -279,26 +323,28 @@ impl Manager { | |||||||||||||||||||
| // auto-delete items older than this | ||||||||||||||||||||
| ttl: Option<Duration>, | ||||||||||||||||||||
| ) -> Result<Box<dyn Bucket>, StoreError> { | ||||||||||||||||||||
| self.0.get_or_create_bucket(bucket_name, ttl).await | ||||||||||||||||||||
| self.get_kv_store()? | ||||||||||||||||||||
| .get_or_create_bucket(bucket_name, ttl) | ||||||||||||||||||||
| .await | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub async fn get_bucket( | ||||||||||||||||||||
| &self, | ||||||||||||||||||||
| bucket_name: &str, | ||||||||||||||||||||
| ) -> Result<Option<Box<dyn Bucket>>, StoreError> { | ||||||||||||||||||||
| self.0.get_bucket(bucket_name).await | ||||||||||||||||||||
| self.get_kv_store()?.get_bucket(bucket_name).await | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub fn connection_id(&self) -> u64 { | ||||||||||||||||||||
| self.0.connection_id() | ||||||||||||||||||||
| self.get_kv_store().unwrap().connection_id() | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't have any |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
338
to
340
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential panic on misconfigured Manager.
Consider returning 💡 Suggested alternativesOption 1: Return Result - pub fn connection_id(&self) -> u64 {
- self.get_kv_store().unwrap().connection_id()
+ pub fn connection_id(&self) -> Result<u64, StoreError> {
+ Ok(self.get_kv_store()?.connection_id())
}Option 2: Add expect with context - self.get_kv_store().unwrap().connection_id()
+ self.get_kv_store()
+ .expect("KV store must be initialized before calling connection_id")
+ .connection_id()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| pub async fn load<T: for<'a> Deserialize<'a>>( | ||||||||||||||||||||
| &self, | ||||||||||||||||||||
| bucket: &str, | ||||||||||||||||||||
| key: &Key, | ||||||||||||||||||||
| ) -> Result<Option<T>, StoreError> { | ||||||||||||||||||||
| let Some(bucket) = self.0.get_bucket(bucket).await? else { | ||||||||||||||||||||
| let Some(bucket) = self.get_kv_store()?.get_bucket(bucket).await? else { | ||||||||||||||||||||
| // No bucket means no cards | ||||||||||||||||||||
| return Ok(None); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
@@ -328,7 +374,7 @@ impl Manager { | |||||||||||||||||||
| let watch_task = tokio::spawn(async move { | ||||||||||||||||||||
| // Start listening for changes but don't poll this yet | ||||||||||||||||||||
| let bucket = self | ||||||||||||||||||||
| .0 | ||||||||||||||||||||
| .get_kv_store()? | ||||||||||||||||||||
| .get_or_create_bucket(&bucket_name, bucket_ttl) | ||||||||||||||||||||
| .await?; | ||||||||||||||||||||
| let mut stream = bucket.watch().await?; | ||||||||||||||||||||
|
|
@@ -373,7 +419,10 @@ impl Manager { | |||||||||||||||||||
| obj: &mut T, | ||||||||||||||||||||
| ) -> anyhow::Result<StoreOutcome> { | ||||||||||||||||||||
| let obj_json = serde_json::to_vec(obj)?; | ||||||||||||||||||||
| let bucket = self.0.get_or_create_bucket(bucket_name, bucket_ttl).await?; | ||||||||||||||||||||
| let bucket = self | ||||||||||||||||||||
| .get_kv_store()? | ||||||||||||||||||||
| .get_or_create_bucket(bucket_name, bucket_ttl) | ||||||||||||||||||||
| .await?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let outcome = bucket.insert(key, obj_json.into(), obj.revision()).await?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -388,7 +437,7 @@ impl Manager { | |||||||||||||||||||
| /// Cleanup any temporary state. | ||||||||||||||||||||
| /// TODO: Should this be async? Take &mut self? | ||||||||||||||||||||
| pub fn shutdown(&self) { | ||||||||||||||||||||
| self.0.shutdown() | ||||||||||||||||||||
| self.get_kv_store().unwrap().shutdown() | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same for unwrap. A panic here would mask any shutdown errors. |
||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -471,6 +520,9 @@ pub enum StoreError { | |||||||||||||||||||
|
|
||||||||||||||||||||
| #[error("Race condition, retry the call")] | ||||||||||||||||||||
| Retry, | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[error("Error building key-value store: {0}")] | ||||||||||||||||||||
| BuildError(#[from] anyhow::Error), | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// A trait allowing to get/set a revision on an object. | ||||||||||||||||||||
|
|
||||||||||||||||||||
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.
I think you lost this part. That was a hard earned logging statement and comment.