-
-
Notifications
You must be signed in to change notification settings - Fork 120
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
Global config store #267
Global config store #267
Conversation
configuration store.
2203a55
to
fd2f4c5
Compare
use gosub_engine::config::{ConfigStore, Store}; | ||
use gosub_engine::config::storage::json::JsonStorageAdapter; | ||
use gosub_engine::config::storage::sqlite::SqliteStorageAdapter; | ||
use gosub_engine::config::StorageAdapter; |
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.
The name "store" was a bit ambiguous. They are actually storage adapters since we already have a config store.
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.
Maybe it would make sense to reexport the StorageAdapters in gosub_engine::config::storage
, so you can import them directly with
use gosub_engine::config::storage::*
Then you could also make your json
memory
and sqlite
module private
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.
@Sharktheone Can you guve an example on what you mean?
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 mean, doing this in storage.rs
mod json;
mod memory;
mod sqlite;
pub use json::*;
pub use memory::*;
pub use sqlite::*;
Then you can do this
use gosub_engine::config::storage::*
and still use JsonStorageAdapter
and SqliteStorageAdapter
. This makes things cleaner. Most of the time when you import one of the storage atapters, you most likely need all.
/// This can be used to storage settings in a database, json file, etc | ||
/// Note that we need to implement Send so we can send the storage adapter | ||
/// to other threads. | ||
pub trait StorageAdapter: Send + Sync { |
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.
Send + Sync are important here, otherwise we cannot share them with other threads. We must make sure the adapters are thread-aware.
fn all(&self) -> crate::types::Result<HashMap<String, Setting>>; | ||
} | ||
|
||
lazy_static! { |
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.
This is static reference, but through lazy_static!
we can actually initialize it just like regular variables.
lazy_static! { | ||
// Initial config store will have a memory storage adapter. It will save within the session, but not | ||
// persist this on disk. | ||
static ref CONFIG_STORE: RwLock<ConfigStore> = RwLock::new(ConfigStore::default()); |
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.
RwLock since we can have multiple reads (for instance, https://github.com/gosub-browser/gosub-engine/pull/267/files#diff-d55130faaf1118028faf388a306896363c166c769c0bab6bdff2b23051659575R101)
/// Returns a reference to the config store, which is locked by a mutex. | ||
/// Any callers of the config store can just do config::config_store().get("dns.local_resolver.enabled") | ||
pub fn config_store() -> std::sync::RwLockReadGuard<'static, ConfigStore> { | ||
CONFIG_STORE.read().unwrap() |
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.
this returns a regular read locked store
}; | ||
} | ||
|
||
#[allow(clippy::crate_in_macro_def)] |
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.
clippy wants the macro to use $crate
instead of crate
, but i think this is correct. We want THIS crate, not the crate where the macro is invoked from.
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 guess then you could use gosub_engine
instead of crate
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.
no, it won't work. It gives an unknown crate issue. I'll leave it for now
crate::config::config_store().set($key, Setting::SInt($val)) | ||
}; | ||
(map $key:expr, $val:expr) => { | ||
crate::config::config_store().set($key, Setting::Map($val)) |
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.
we can do "set" even though we have an immutable reference. This is due to internal mutability through the RefCell<>
/// The mutex allows to share between multiple threads, | ||
/// The refcell allows us to use mutable references in a non-mutable way (ie: settings can be | ||
/// stored while doing a immutable get()) | ||
settings: std::sync::Mutex<std::cell::RefCell<HashMap<String, Setting>>>, |
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.
A mutex to share the config store between threads
A RefCell to allow us to mutate from a immutable config store
for (key, value) in all_settings { | ||
store.settings.insert(key, value); | ||
self.settings |
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.
lock the mutex,
unwrap the mutex guard,
borrow the refcel as mutable reference
and insert the key/value into the hashmap
src/config.rs
Outdated
if !self.has(key) { | ||
panic!("Setting {} not found", key); | ||
return None; |
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.
This was a panic, but we just return None.. it's up to the caller to deal with it
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 this doesn't make sense at all. I mean we're asking self.settings
if we have the key and lower we get the key from self.settings
and if it doesn't has the setting we search it in self.storage
, but we can't get to that point. Am I missing something? I am a bit confused ride now.
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.
This might indeed be false. I'll check
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.
fixed this. This flow was not correct after some refactoring here and there.. surprised it still passed the tests
if let Some(setting) = self.storage.get_setting(key) { | ||
self.settings.insert(key.to_string(), setting.clone()); | ||
return setting.clone(); | ||
if let Some(setting) = self.storage.get(key) { |
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.
If the key is not found in the current settings, we load it from the storage, and save it in the settings for the next time.
|
||
pub struct JsonStorageAdapter { | ||
path: String, | ||
file_mutex: Arc<Mutex<File>>, | ||
elements: HashMap<String, Setting>, | ||
elements: Mutex<HashMap<String, Setting>>, |
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.
Mutex to guard against multiple threads. This needs to be fixed better when saving the json
self.elements.get(key).cloned() | ||
impl StorageAdapter for JsonStorageAdapter { | ||
fn get(&self, key: &str) -> Option<Setting> { | ||
let lock = self.elements.lock().unwrap(); |
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.
We read during a lock. This is fast enough. I don't even know if we really need it here.
@@ -22,14 +23,18 @@ impl TryFrom<&String> for SqliteStorageAdapter { | |||
)"; | |||
conn.execute(query)?; | |||
|
|||
Ok(SqliteStorageAdapter { connection: conn }) | |||
Ok(SqliteStorageAdapter { | |||
connection: Mutex::new(conn), |
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.
Connections from Sqlite cannot be shared between threads (the compiler wont let us). Instead, we guard it with a mutex and lock whenever we need to read/write.
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 would remove all the unwraps and at leased replace them with a expect
but better would be to just us a Result
for the function
use gosub_engine::config::{ConfigStore, Store}; | ||
use gosub_engine::config::storage::json::JsonStorageAdapter; | ||
use gosub_engine::config::storage::sqlite::SqliteStorageAdapter; | ||
use gosub_engine::config::StorageAdapter; |
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.
Maybe it would make sense to reexport the StorageAdapters in gosub_engine::config::storage
, so you can import them directly with
use gosub_engine::config::storage::*
Then you could also make your json
memory
and sqlite
module private
/// Returns a reference to the config store, which is locked by a mutex. | ||
/// Any callers of the config store can just do config::config_store().get("dns.local_resolver.enabled") | ||
pub fn config_store() -> std::sync::RwLockReadGuard<'static, ConfigStore> { | ||
CONFIG_STORE.read().unwrap() |
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.
Hmm, i don't like the unwrap here. I also don't really like them in the config-store.rs
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.
you need to unwrap the guard before you can use it. I'm open for suggestions on better ways
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.
Maybe return a Result
which you can just use the ?
on in functions that also return a Result
. Replacing it with a expect
is also already better
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.
You have a -
in the filename, I've seen this also in the other files in the bin directory now. Everywhere we have underscores and not dashes in the file names. I guess going for consistency is probably good. I'd generally avoid dashes/spaces and so on in file names, I just use a underscore for it.
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 personally like dashes in binaries and options :)
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.
Same here — I prefer dashes to underscores in binary executable names. E.g., in Linux, the executable name is google-chrome
. (For binary executables, even better is a short name with no dashes or underscores.)
/// Note that when you cannot find the key, it will return a default value. This is not always | ||
/// what you want, but you can test for existence of the key with config_store().has("key") |
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.
it might make sense to panic when a config value isn't found. I mean, the config keys are static anyway, and so if something isn't found, this most likely is an issue in the code. Finding out, that the issue is a typo in the name can be quite difficult, especially when the two characters look similar.
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.
If the config key is not found, we have a default value in the json configuration. There might be a chance that we will be using user defined keys later on (for instance for plugins).
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.
Ah, yeah... then it makes definitely sense to not do this.
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.
this flow was a bit off. This should be fixed now:
get:
- if key exists in store: return key
- if key exists in storage: save key in store and return key
- if key info exists: return default value from info
set:
- if key info exist: set in store and set in storage (persist)
Later on, we can register keys to the configuration by registering an settings_info struct. From that point on, it should work exactly like it works with our "global" configuration keys. I'm not sure if this works the way i envision it, but it allows plugins to actually register their own keys (only in the user.* or plugin.* namespace, for instance)
Maybe we can do dynamic keys as well.. but i'm not 100% sure how that would work
}; | ||
} | ||
|
||
#[allow(clippy::crate_in_macro_def)] |
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 guess then you could use gosub_engine
instead of crate
src/config.rs
Outdated
if !self.has(key) { | ||
panic!("Setting {} not found", key); | ||
return None; |
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 this doesn't make sense at all. I mean we're asking self.settings
if we have the key and lower we get the key from self.settings
and if it doesn't has the setting we search it in self.storage
, but we can't get to that point. Am I missing something? I am a bit confused ride now.
src/config.rs
Outdated
if !self.has(key) { | ||
panic!("key not found"); | ||
warn!("config: Setting {} is not known", key); | ||
return; |
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.
Here it's the same, i guess the self.has
method should look in self.storage
and not in self.settings
src/config/settings.rs
Outdated
|
||
match self { | ||
Setting::Map(values) => values.clone(), | ||
_ => Vec::new(), |
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.
Maybe it'd make sense to not just return a empty vec, but push the contents of the values
_ => Vec::new(), | |
other => vec![other.to_string()], |
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.
yeah, makes sense I guess
@@ -155,7 +155,7 @@ impl FromStr for Setting { | |||
|
|||
/// Converts a string to a setting or None when the string is invalid | |||
fn from_str(key: &str) -> Result<Setting> { | |||
let (key_type, key_value) = key.split_once(':').unwrap(); | |||
let (key_type, key_value) = key.split_once(':').expect(""); |
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.
something that says, what has failed in the expect would be nice, else it is more or less the same as unwrap
Ok(_) => {} | ||
Err(_) => {} | ||
} | ||
let _ = SimpleLogger::new().init(); |
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.
When i think about this, I'm not sure if we want to to this here. It maybe would make sense to use the SimpleLogger
instead of our own and invoke the logger init somewhere in the main fn. The other option I'd see, to upgrade our logger and use this instead. I guess it makes sense to only have one logger.
Setting::SInt(value) => *value, | ||
Setting::UInt(value) => *value as isize, | ||
Setting::Bool(value) => *value as isize, | ||
Setting::String(value) => is_bool_value(value) as isize, |
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.
For to_sint
and to_uint
, should the string convert to the actual integer representation or leave it as bool here?
If there was a setting s:123
stored as a string and we tried converting it to its integer representation then it would return 1 which may be unexpected.
I'm not sure what cases if/when that would ever come up, but something to consider
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.
Of course, we shouldn't store integer settings as strings, but.. :P
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.
Makes sense to me, to do that.
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.
LGTM. The only thing i noticed is the unwrap
s but i think they are okay for now. I also don't really have a better solution that is convenient. I might look into that at some point. I maybe add a todo on tasks project
warn!( | ||
"config: Setting {} is of different type than setting expects", | ||
key | ||
); | ||
return; |
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 guess we could try to convert the setting here to the expected type. (e.g int to string)
Setting::SInt(value) => *value, | ||
Setting::UInt(value) => *value as isize, | ||
Setting::Bool(value) => *value as isize, | ||
Setting::String(value) => is_bool_value(value) as isize, |
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.
Makes sense to me, to do that.
The config store is now usuable as a global instance through macros (in the same spirit as you can use warn! info! etc from the logger).
You can use
config!(string, "my.key")
to get the given key or it will return a default value when the key is not found. There is always the possibily to do checks on the actual store:This setup makes it possible for any user agent to set their own configuration store: