Skip to content
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

Merged
merged 3 commits into from
Nov 23, 2023
Merged

Global config store #267

merged 3 commits into from
Nov 23, 2023

Conversation

jaytaph
Copy link
Member

@jaytaph jaytaph commented Nov 19, 2023

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:

   let exist = config::config_store().has("does.this.key.exist")

This setup makes it possible for any user agent to set their own configuration store:

fn main() {
  // Set the config through `config_store_write()` instead of `config_store()`
  config::config_store_write().set_storage(new SqliteStorage("config.db"));

  // do the rest
}

@jaytaph jaytaph marked this pull request as draft November 19, 2023 21:19
src/config.rs Outdated Show resolved Hide resolved
src/config.rs Outdated Show resolved Hide resolved
src/config.rs Outdated Show resolved Hide resolved
src/config.rs Outdated Show resolved Hide resolved
@jaytaph jaytaph changed the title tmp commit Initial attempt for "global store" Nov 20, 2023
src/config.rs Outdated Show resolved Hide resolved
src/config.rs Outdated Show resolved Hide resolved
@jaytaph jaytaph changed the title Initial attempt for "global store" Global config store Nov 22, 2023
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;
Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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 {
Copy link
Member Author

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! {
Copy link
Member Author

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());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// 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()
Copy link
Member Author

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)]
Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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))
Copy link
Member Author

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>>>,
Copy link
Member Author

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
Copy link
Member Author

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;
Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member Author

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) {
Copy link
Member Author

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>>,
Copy link
Member Author

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();
Copy link
Member Author

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),
Copy link
Member Author

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.

Copy link
Member

@Sharktheone Sharktheone left a 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;
Copy link
Member

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()
Copy link
Member

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

Copy link
Member Author

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

Copy link
Member

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

Copy link
Member

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.

Copy link
Member Author

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 :)

Copy link
Collaborator

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.)

Comment on lines +59 to +60
/// 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")
Copy link
Member

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.

Copy link
Member Author

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).

Copy link
Member

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.

Copy link
Member Author

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)]
Copy link
Member

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;
Copy link
Member

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
Comment on lines 233 to 235
if !self.has(key) {
panic!("key not found");
warn!("config: Setting {} is not known", key);
return;
Copy link
Member

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


match self {
Setting::Map(values) => values.clone(),
_ => Vec::new(),
Copy link
Member

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

Suggested change
_ => Vec::new(),
other => vec![other.to_string()],

Copy link
Member Author

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

@jaytaph jaytaph marked this pull request as ready for review November 22, 2023 17:39
@@ -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("");
Copy link
Member

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();
Copy link
Member

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,
Copy link
Member

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

Copy link
Member

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

Copy link
Member

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.

Copy link
Member

@Sharktheone Sharktheone left a 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 unwraps 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

Comment on lines +244 to +248
warn!(
"config: Setting {} is of different type than setting expects",
key
);
return;
Copy link
Member

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,
Copy link
Member

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.

@jaytaph jaytaph merged commit 27e9414 into main Nov 23, 2023
4 checks passed
@jaytaph jaytaph deleted the config-global branch November 23, 2023 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants