-
Notifications
You must be signed in to change notification settings - Fork 53
Rework configuration handling #205
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?
Conversation
Add --log-level switch Ignore missing ini file Add test cases
remove initial logging
add AMQP_URL to upstream setting
Are there any ci/test github actions that should be running? |
Yes but the workflow is missing the pull request event. amqproxy/.github/workflows/ci.yml Lines 1 to 11 in 4c9512d
It should be corrected in main branch, and after that you can rebase, and CI will run. |
so, 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.
How you handle Config
and Options
structs doesn't feel idiomatic crystal. You recreate the structs every time you call with
instead of having it as one Class where you just update the fields on that object.
Having one object that you initialize in the CLI, keeping the reference through out the applications makes it easier to follow and any object that has a reference to that object will be able to read any changes to the config.
The content of the options.cr
file looks like you repeat the same variables three times, this might be the way in C#. But in crystal you create one object and reuse that.
Breaking out the code form CLI.cr is nice, but keep it as one file, config and options can be merged.
The general idea was to have an immutable object. Which can only be initialized through the load_with_cli method. Also, I think that config should not be altered after initialization. I see what you mean about the remark about the Option.cr. Although I doubt Config and Option can be merged. |
@snichme I did some rework. Could you take a look at the pr? Also there are workflows failing, but I don't think it has anything to do with my changes. |
Any reason why it shouldn't be altered later on? If you have one config object and during runtime change that I think it's good that the change applies to all other objects that have a reference to the config object. |
If you instead do it like this in cli.cr:
and after than parse the options and set them directly on the Like so:
wdyt? |
From an OO perspective I believe that setting properties should be done from within the object. Other object should not need to know the correct precendence of loading the configuration. At the moment there are no validations on the configuration, but that should also be the responsibility of the If configuration is allowed to change in the lifecyle of the application, a method can be added for a specific action that results in configuration changes. e.g. a I also considered adding the args as parameter to the |
This can still happen if you create a method like so: |
@carlhoerberg The |
Here is another interesting idea on how to handle config values by using crystal macros, so you have one property and you can tag it with different annotations depending on if it's from file, env or as arg: cloudamqp/lavinmq#917 |
That's expected and can be ignored |
In that PR the cli arguments are parsed in the config class. If that is okay, then it can also solve the issue of the Options class. I think there's one issue with the solution in that PR. In the ini file in amqproxy there are two settings that handle the same property. Or is it possible to add multiple attributes of the same type to a property? when "bind", "address" then @listen_address = value |
@snichme I moved the OptionParser to the config class and thus removing the extra |
def self.load_with_cli(argv) | ||
new() | ||
.load_cli_options(argv.dup) # handle config file/help/version options | ||
.load_from_file | ||
.load_from_env | ||
.load_cli_options(argv) | ||
rescue ex | ||
abort ex.message | ||
end |
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.
def self.load_with_cli(argv) | |
new() | |
.load_cli_options(argv.dup) # handle config file/help/version options | |
.load_from_file | |
.load_from_env | |
.load_cli_options(argv) | |
rescue ex | |
abort ex.message | |
end | |
def initialize(argv) | |
load_cli_options(argv.dup) | |
load_from_file | |
load_from_env | |
load_cli_options(argv) | |
end |
This feels more crystal style IMO, also move to top since it's the constructor
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 it be possible to do a solution where we didn't need to parse the ARGV twice?
Like the first load_cli_options
returns something that we can later apply.
If we return a NamedTuple
from load_cli_options
we can also make use of that and pass the path to the config file to load_from_file
like load_from_file(opts[:config_file])
to make it clearer.
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 returning a NamedTuple we're sort of down to the Option record/class again. Which is fine by me, but is it a bad thing to process the cli options twice? Something that happens once at startup and seems cpu friendly.
else raise "Unsupported config #{name}/#{key}" | ||
end | ||
end | ||
else raise "Unsupported config section #{name}" |
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 know this exists today, but config options that isn't support shouldn't we just skip them?
How does it work for unsupported cli args, does that raise?
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 agree that this seems a little harsh.
end | ||
|
||
unless config = @config | ||
raise "Configuration has not been loaded" |
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 means that for some reason @config
isn't set we dont disconnect clients, we just stop the process. Isn't @config
always set here?
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.
Yes It is always set, but here my Crystal knowledge falls short. I just don't know how I can convince the compiler it is. When I use not_nil I get the suggestion not to use 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.
You need to make sure that it is set by being strict that it has a value, i.e. not letting it be nilable :)
@idle_connection_timeout : Int32 = 5 | ||
@term_timeout = -1 | ||
@term_client_close_timeout = 0 | ||
@config : AMQProxy::Config? = nil |
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.
Possible to make this not optional?
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.
How can I do that? creating the instance happens in the run method.
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.
Instead of a run()
method, you can use an initializer and let cli.new()
set @config
when initializing, that way I think you can avoid letting it be Nilable.
@snichme Any thoughts on my replies? |
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 be good to avoid having empty files in the repository, maybe this file can be created when needed in config_spec.cr and then cleaned up at the end of the spec.
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.
Why is it good to avoid empty files in the repo? If the file serves a purpose (used in specs? I haven't actually checked this 🙈), we should not be afraid to have it around. Creating it with code sounds both more complex and slower to me.
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.
My reasoning is mostly about cleanliness so the repository remains free of files that are solely for transient test purposes.
Sure complexion grows slightly, thats a good point. I don't know the performance impact of creating an empty file, but is it really notably expensive to create once?
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 we have files like this in the repo I think it should be clear in some way what they are used for so its less confusing.
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.
Agree it should be more made more clear. Could be made by having a "fixtures" or "config fixtures" directory.
@idle_connection_timeout : Int32 = 5 | ||
@term_timeout = -1 | ||
@term_client_close_timeout = 0 | ||
@config : AMQProxy::Config? = nil |
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.
Instead of a run()
method, you can use an initializer and let cli.new()
set @config
when initializing, that way I think you can avoid letting it be Nilable.
end | ||
|
||
unless config = @config | ||
raise "Configuration has not been loaded" |
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 make sure that it is set by being strict that it has a value, i.e. not letting it be nilable :)
previous_argv = ARGV.clone | ||
ARGV.clear | ||
|
||
ARGV.concat([ | ||
"--config=/tmp/non_existing_file.ini", | ||
]) | ||
|
||
config = AMQProxy::Config.load_with_cli(ARGV) |
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 don't need to do all the ARGV handling in each spec, you can just pass an array of strings (or an empty array where needed) to load_with_cli
instead.
previous_argv = ARGV.clone | |
ARGV.clear | |
ARGV.concat([ | |
"--config=/tmp/non_existing_file.ini", | |
]) | |
config = AMQProxy::Config.load_with_cli(ARGV) | |
config = AMQProxy::Config.load_with_cli(["--config=/tmp/non_existing_file.ini"]) |
@@ -1,3 +1,4 @@ | |||
require "./config" | |||
require "./version" |
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.
require "./version"
should probably be moved to config.cr
since that's where AMQProxy::VERSION
is used?
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's this?
COPY spec/config.ini /tmp/config.ini | ||
COPY spec/config_empty.ini /tmp/config_empty.ini |
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.
All files in spec/
is already copied on the line above?
Hi @carlhoerberg , To be honest, I'm a little hesitent to finish this PR. I don't agree with several of the comments I got on this PR. Although my background is c#, some of the principles are not bound to a language. Things like immutability can result into more predictable code. For instance the concept of a class to store the cli arguments in an Another thing is exposing the methods for reading config sections cli/env/file/defaults is imo not the responsibility of the code that is invoking the methods. Just call Configure or some method name should suffice. It is the responsibility of the Config object to know the sequence. Let me know what's your take on it. Regards, |
Rework the configuration, so the configuration is seperated from cli program.
Add missing configurable properties
ini file:
Add
http_port
as option to thelisten
section of the ini file.enviroment variables:
UPSTREAM
for consistent naming.AMPQ_URL
takes precendence overUPSTREAM
for backwards compatibility cliCli options:
--log-level
for the Log Levelresolves #204