-
Notifications
You must be signed in to change notification settings - Fork 0
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
Feature/config and clean shutdown #52
Conversation
Please excuse the formatting, I'll fix it and squash the commits. |
kairos-server/src/config.rs
Outdated
pub fn read_config() -> Config { | ||
let config_file = "kairos_config.toml"; | ||
if !std::path::Path::new(config_file).exists() { | ||
let default_config = Config { | ||
server: Server { | ||
address: "0.0.0.0".to_string(), | ||
port: 8080, | ||
}, | ||
log: Log { | ||
level: Level::TRACE, | ||
log_file: None, | ||
} | ||
}; | ||
let toml_string = toml::to_string_pretty(&default_config).unwrap(); | ||
std::fs::write(config_file, toml_string).expect("Failed to write config file, file permissions might be incorrect."); | ||
} | ||
let config: Config = toml::from_str( | ||
&std::fs::read_to_string(config_file).expect("Failed to read config file, file permissions might be incorrect.") | ||
).expect("Failed to parse config file"); | ||
|
||
config | ||
} |
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.
In general apps should not create state unnecessarily.
This can be an issue when running in readonly filesystems.
ENVs are used to avoid this problem, they also play better with docker and nix.
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.
Would env variable/dotenv overrides and not panicking if it can't write the config file be a good compromise?
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 servers should not be creating state.
I'd be fine with reading a config file, but we should never write one, unless told to by the user.
And if we are reading a config you have to ask where, and then we get into xdg which is more for apps anyway.
ENVs are the industry standard for a reason.
dotenvy
turns a set of ENVs into a config file in the local directory (just like kairos_config.toml).
Supporting logging to a file directly instead of piping stderr/stdout is definitely something we could support. But it should not be the default, and we should not duplicate standard functionality we get from tracing's env_logger support. Take a look at the options from https://docs.rs/env_logger/latest/env_logger/ cd ./kairos-server
RUST_LOG=trace cargo run > kairos-server.log |
It isn't the default, it's disabled by default. Also isn't the default functionality, it saves the file and line the tracing occured. I originally intended to use fern, its also a logging library like env_logger. Would that of interest? |
This we can fix. As for |
65d37ef
to
791f8be
Compare
Okay, it now doesn't generate a kairos_config.toml file nor does it have defaults.
I added a small README.md that details configuring the server. Added defaults to the .env file. I'm still using tracing-subscriber, adding another crate for configuring logging makes little sense, they all use the same backend anyway. Also tracing-subscriber has an ecosystem around it supported by tokio, has drop in libraries for providing OpenTelemetry/Prometheus metrics. |
791f8be
to
aa3d1e8
Compare
removed log file/stdout config, initialize logger in config added optional log file with more detailed logging added back dotenv support, hierarchical config loading updated Cargo.lock
aa3d1e8
to
da2ad95
Compare
I like the readme, the defaults, and the shutdown. I would strongly suggest just using In the current state I am not likely to approve this PR. Some general notes: |
Global configuration has become such a common practice that tokio has an in-house lazy_static! they added within the last two weeks, I'll swap to it because of the hierarchical loading of the config files, you can have a separate config for development and production, with production essentially just overriding the defaults provided by development .env cannot represent complex structures, depending on business requirements that might be of interest, toml supports more types. it will also start to look very messy the more that has to be added to it. dotenv doesnt have the same seperation of concern toml can provide. the custom log level functionality isn't hard to extend, tracing-subscriber supports everything env_logger does with it's filters. |
The use of The problem is global variables are not the right choice for config. In general it's best to avoid implicit logical dependencies. This is why globals are discouraged across languages.
The point of using The current toml does not allow anything the existing code does not. |
I think that a global config as an option (not default) can be useful. When deploying the casper stuff to cctl I would prefer to use a config file over .env. Having .env as the default and an optional config file seems comprehensive and allows for more flexibility. What I like about Rome's solution is that when using the casper client as a library to interact with the L1 I can access the correctly typed values without having to read from the environment. |
Environment variables should be parsed at server startup. This is common between both approaches. Parts of config that you access in a handler should be passed as a readonly part of axum state not in a global variable or read from environment. |
But you can construct "State" from .env or a config file, there is no reason not to have an optional config file. |
Just because axum provides their axum::extract::State pattern doesn't mean we have to use it. Global references are useful because we don't need to propagate configuration or connections down from method to method. There's more to this program than just axum's handlers. It seems silly to have to add 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.
Not sure how this improves setting up our server. I personally prefer configuring through CLI arguments, especially if it's only a port and an address.
TOML as a configuration file format is questionable too for me.
pub struct ServerConfig { | ||
pub port: u16, | ||
} | ||
fn serialize_level_filter<S>(level: &Level, serializer: S) -> Result<S::Ok, S::Error> |
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.
there is a crate called tracing-serde https://github.com/tokio-rs/tracing/tree/master/tracing-serde
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.
Thank you! Can't believe I missed 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.
Turns out this is for serializing logs to a machine readable format. Not useful for serializing/deserialzing the log level.
|
||
#[derive(Debug, Clone, Deserialize, Serialize)] | ||
pub struct Server { | ||
pub address: 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.
I think this should be a URL
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 yeah, do we have a crate with a Uri/Url type included? Are we planning on using the http 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.
What is the type that axum expects?
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.
SocketAddr, the Config struct has an impl that creates it
} | ||
|
||
#[allow(clippy::new_without_default)] | ||
impl 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.
Isn't there a crate that takes a type and just derives the toml serializer/ parser
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 allow clippy statement is because Avi told me not to construct state. There were default parameters in place before and the allow clippy wasn't needed. If I put the default function back, then the config crate will first construct the default, then load from toml, then dotenv, and then environment. ATM it will simply panic if there are parameters it can't gather from all those sources.
kairos-server/src/main.rs
Outdated
|
||
let config = ServerConfig::from_env() | ||
.unwrap_or_else(|e| panic!("Failed to parse server config from environment: {}", e)); | ||
CONFIG.initialize_logger(); |
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.
Semantically it is not the config that should initialize logging. It's the server that should initialize logging given a configuration
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 don't mind moving the logging configuration to main.rs or out of config.rs. Do you have a suggestion?
.expect("Failed to install Ctrl+C handler"); | ||
}; | ||
|
||
let terminate = async { |
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'm pretty sure this is what tokio already does for us when we send SIGTERM. There is no need to implement this ourselves, especially given that we don't need to handle any special conditions when we terminate
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.
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 crate I'm using for loading the config is super flexible, I could add CLI options as well if that's something you'd like to see. I would really like to leave the toml config in place however, because it's flexible. For example, if I or anyone else wants to provide a list of nodes for the deposit listener to connect to, or more likely a list of nodes we know are trustworthy or low latency, toml is pretty practical. I can go to the config file and simply type the following:
[[nodes]]
address = "192.168.1.1"
port = 11101
[[nodes]]
address = "12.168.1.1"
port = 11101
But dotenv out of the box doesn't support complex structures and while a cli could definitely do it, it's a little cumbersome.
No one is being forced to use the toml file, it's simply an option and only costs resources on startup not during runtime. Everyone can stick with dotenv and potentially CLI options if we want to go down that route.
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 issue is it's duplicate code that does not currently add functionality.
In the future it might make sense, but I don't anticipate having a complex set of args.
In this case nodes can be a CSV String address:port,*
.
If we have a super complex set of structured args in the future we can revisit using toml.
Here are my thoughts on each feature: 👍 Clean Shutdown
I am 100% sure we will soon need to handle the shutdown of multiple components, not only the database when the DA layer is built, but also the Casper event state synchronizer upon its implementation and integration. Currently, the main binary is 👍 Optional Logging FileThis feature is really useful for infrastructure operators. Instead of figuring out how to redirect stdout - which might vary depending on whether you run Kairos with systemd, nohup, Docker, etc. - it can simply be enabled in the config. 👍 TOML Config FileI understand the concerns of others that it's just another representation of the config, offering no immediate benefit with the added code. However, I can envision TOML's support for arrays becoming handy soon. Another reason to use TOML is the improved semantics - I dislike seeing .env files where everything is just a string. ❓ Globally Accessible Static ConfigThis sounds like an anti-pattern; do we really need it? @Rom3dius, except for the static global config, I have no other concerns 😉. For future reference, my recommendation is to avoid bundling multiple features in one PR; it will make it easier to avoid being blocked on individual parts of it. I am convinced that TOML support is something we want. However, if @Avi-D-coder and @marijanp insist on avoiding it, perhaps you could extract that work into a separate enhancement-labeled PR. It could remain pending for some time until we have a more complex config that becomes cumbersome to manage with plain .env. |
Can you explain why this special handling when the instance runs out of scope could not be part of the Drop trait, which normally gets exectured when the application shuts down.
A service like this would run in a systemd service and systemd comes with systemd-journald which keeps track of logs and allows for querying with journalctl From the manual:
Config files are awkward as I need to go ahead and create a file configuring things that actually should have good default values. And in this case I create a file to only configure an address and port. The config file assumes a specific location that I don't know without reading the code. It could be in the CWD or in the xdg-config directory. Or sometimes I want it somewhere else. How do I configure this? Thus both approaches environment variables or config files are not very explicit when I call the app. Instead what I like is a CLI with good defaults, because when I run |
It's tricky to implement a drop trait for futures, you need to call task::spawn and wrap data in an Arc before passing it in so it doesn't go out of scope, otherwise you can't spawn any async code.
A service like this could also be containerized, docker doesn't log to systemd by default. Depending on what tools an operator is using for insight, or how the operator has their infrastructure set up, they could collect that log file automatically and store it should anything go wrong. tracing-subscriber also allows for different logging outputs to be configured differently, so we can include more (or less) information in the log file and leave stdout for cleaner more human-readable logs. Potential use-case would be the following, log only warnings or worse to the log file, ingest using a loki ingester, on Grafana send webhooks to slack/discord/zulip warning the operator that their might be an issue with the application.
I agree having to manually create a config file and populate it is awkward, in the original pull request upon startup if the app didn't discover a config file it generated a default file with sensible defaults. A CLI for a service is a bit anti-pattern, this is reflected by the fact that all of the configuration crates I looked at on crates.io do not come with CLI parsing. What could also be useful is implementing the following: if a path is provided as an argument, create/load the config from there.
Therefore I think there should be a config file generated automatically if one is missing and that one can specified as a command line argument. I disagree that any assumptions have to be made when using the config file instead of a CLI, because the information pertaining to the usage of the config is already in the README, as is standard practice for such a service. |
Coordinating shutdown goes beyond what the
This assumption applies to our initial production infrastructure only. Consider other use cases where Nix with systemd is not utilized. We can make life easier for developers who might want to run it differently.
If you're expressing concerns about the nature of config files, it's worth noting that a |
Docker apps are also easier to configure with stateless envs or cli. Getting logs out of a file from docker or nix is not exactly fun, we should use a different networked tracing subscriber or stdout. https://docs.docker.com/reference/cli/docker/container/logs/ |
I don't understand the issue here a Future is a trait, as Drop is. If your structs takes care of cleaning up it's resources which it should do by implementing the drop trait. Drop will be called by rust. Also I still can't see any of the code you are referring to, that needs such a cleanup at this point
You can create docker containers directly from nixos system configurations.
Can you elaborate why a CLI is an antipattern? Just because everybody else is not providing a CLI in crates.io, it I'm not convinced. I'm ok with adding logging to a file through an option, as long as it's not the default
|
It's tricky to implement a drop trait for futures, you need to call task::spawn and wrap data in an Arc before passing it in so it doesn't go out of scope, otherwise you can't spawn any async code.
The axum API already uses this code to clean up. It stops accepting new requests and finishes serving existing ones instead of simply interrupting them.
The host running docker will not log to systemd without additional configuration.
I'm not completely opposed to CLI arguments, even if I don't think they have a place in a service.
It's still not the default.
Except it's already not just an address and port, its also log level and optional log file. Then later it will be another port for prometheus metrics, then there's the verifier contract address, the path of the secret key and whatever else I couldn't think of off the top of my head. This is how the config ended up looking in the bare-minimum prototype Jonas scraped together.
|
fixed kairos-config.toml in gitignore
To clarify here, the async Drop issue is not about if a future implements The contents owned by a future *will ( I do think shutdown code is useful. |
@Avi-D-coder Adding |
It's the writing of the log file in the container that I don't like, that's what stdout and The state I don't like is writing a default config if the config is not found. I consider these issues separate since we could add an option to log to a file using ENVs, CLI, or a Config file. We just would not use it in prod or testing. |
Clean shutdown was merged in #93 |
New Features
Deprecated
Loading the server port from .env could be brought back, but I do not see much of a need. The program will automatically generate a config file and run on port 8080 if no config exists, otherwise it uses existing config.
EDIT