-
Notifications
You must be signed in to change notification settings - Fork 1
Script worker #50
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?
Script worker #50
Conversation
9bbc770 to
3de270c
Compare
3de270c to
0e44c3d
Compare
Molter73
left a comment
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.
Sorry I left so many rust formatted blocks that should've been suggestions instead, the GH UI was making the diff show as only with the last line I selected, so I thought having actual formatting for the comments would be more readable.
Overall looks good, thanks for taking the time implementing this!
| let mut ast = vec![]; | ||
| let pairs = InstructionParser::parse(Rule::file, source)?; | ||
| for pair in pairs { | ||
| if let Rule::file = pair.as_rule() { | ||
| ast.push(build_ast_from_expr(pair.into_inner().next().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.
I think a functional approach might be a bit more readable here, something like this:
let ast = InstructionParser::parse(Rule::file, source)?
.filter_map(|p| {
if let Rule::file = p.as_rule() {
Some(build_ast_from_expr(p.into_inner().next()?))
} else {
None
}
})
.collect::<Result<Vec<_>, _>>()?;WDYT?
| let mut args: HashMap<String, String> = HashMap::new(); | ||
|
|
||
| for arg in args_pair { | ||
| let mut inner = arg.into_inner(); | ||
| let key = inner.next().unwrap(); | ||
| let value = inner.next().unwrap(); | ||
|
|
||
| assert_eq!(key.as_rule(), Rule::ident); | ||
| assert_eq!(value.as_rule(), Rule::value); | ||
|
|
||
| args.insert( | ||
| key.as_span().as_str().to_string(), | ||
| value.as_span().as_str().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.
Similar to the previous comment, this could be done in an iterator so we don't need a mutable hashmap
let args = args_pair
.map(|arg| {
let mut inner = arg.into_inner();
let key = inner.next().unwrap();
let value = inner.next().unwrap();
assert_eq!(key.as_rule(), Rule::ident);
assert_eq!(value.as_rule(), Rule::value);
let key = key.as_span().as_str().to_string();
let value = value.as_span().as_str().to_string();
(key, value)
})
.collect();| assert_eq!(key.as_rule(), Rule::ident); | ||
| assert_eq!(value.as_rule(), Rule::value); |
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 something to do in this PR, but we might want to take a closer look at how we are doing error handling across the project, I see a lot of unwraps and asserts.
| Ok(ast) | ||
| } | ||
|
|
||
| fn build_ast_from_expr(pair: pest::iterators::Pair<Rule>) -> Node { |
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.
Have you considered implementing the From or TryFrom traits instead of having build_* functions? I think this could result in more idiomatic code.
https://doc.rust-lang.org/std/convert/trait.From.html
https://doc.rust-lang.org/std/convert/trait.TryFrom.html
| fn build_ast_from_instr(pair: pest::iterators::Pair<Rule>) -> Vec<Instruction> { | ||
| let mut instr = vec![] as Vec<Instruction>; | ||
|
|
||
| for i in pair.into_inner() { | ||
| let mut instrs = i.into_inner().next().unwrap().into_inner(); | ||
| let name = instrs.next().unwrap().as_span().as_str().to_string(); | ||
| let args_pair = instrs.next().unwrap().into_inner(); | ||
| let args: Vec<Arg> = args_pair | ||
| .into_iter() | ||
| .map(|arg| { | ||
| let a = arg.into_inner().next().unwrap(); | ||
| match a.as_rule() { | ||
| Rule::constant => Arg::Const { | ||
| text: a | ||
| .into_inner() | ||
| .next() | ||
| .unwrap() | ||
| .as_span() | ||
| .as_str() | ||
| .to_string(), | ||
| }, | ||
| Rule::ident => Arg::Var { | ||
| name: a.as_span().as_str().to_string(), | ||
| }, | ||
| unknown => panic!("Unknown arg type {unknown:?}"), | ||
| } | ||
| }) | ||
| .collect(); | ||
|
|
||
| instr.push(Instruction::Task { name, args }); | ||
| } | ||
|
|
||
| instr | ||
| } |
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 function could be a single map operation, like this:
fn build_ast_from_instr(pair: pest::iterators::Pair<Rule>) -> Vec<Instruction> {
pair.into_inner()
.map(|i| {
let mut instrs = i.into_inner().next().unwrap().into_inner();
let name = instrs.next().unwrap().as_span().as_str().to_string();
let args_pair = instrs.next().unwrap().into_inner();
let args: Vec<Arg> = args_pair
.into_iter()
.map(|arg| {
let a = arg.into_inner().next().unwrap();
match a.as_rule() {
Rule::constant => Arg::Const {
text: a
.into_inner()
.next()
.unwrap()
.as_span()
.as_str()
.to_string(),
},
Rule::ident => Arg::Var {
name: a.as_span().as_str().to_string(),
},
unknown => panic!("Unknown arg type {unknown:?}"),
}
})
.collect();
Instruction::Task { name, args }
})
.collect()
}
src/main.rs
Outdated
| parse_instructions(&std::fs::read_to_string(script_path).unwrap()) | ||
| .unwrap(); | ||
|
|
||
| ast.iter().for_each(|node| { |
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 like iterators, but in this particular case a regular for loop might be better
| ast.iter().for_each(|node| { | |
| for node in ast { |
src/main.rs
Outdated
| fn run_script(script_path: String) -> Vec<Option<i32>> { | ||
| let mut handles = vec![]; | ||
|
|
||
| let ast: Vec<Node> = | ||
| parse_instructions(&std::fs::read_to_string(script_path).unwrap()) | ||
| .unwrap(); | ||
|
|
||
| ast.iter().for_each(|node| { | ||
| debug!("AST NODE: {:?}", node); | ||
|
|
||
| let Node::Work { | ||
| name: _, | ||
| args, | ||
| instructions: _, | ||
| dist: _, | ||
| } = node; | ||
|
|
||
| let workers: u32 = args | ||
| .get("workers") | ||
| .cloned() | ||
| .unwrap_or(String::from("0")) | ||
| .parse() | ||
| .unwrap(); | ||
| let h: Vec<_> = (0..workers) | ||
| .map(|_| { | ||
| let worker = new_script_worker(node.clone()); | ||
|
|
||
| match fork() { | ||
| Ok(Fork::Parent(child)) => { | ||
| info!("Child {}", child); | ||
| Some(child) | ||
| } | ||
| Ok(Fork::Child) => { | ||
| worker.run_payload().unwrap(); | ||
| None | ||
| } | ||
| Err(e) => { | ||
| warn!("Failed: {e:?}"); | ||
| None | ||
| } | ||
| } | ||
| }) | ||
| .collect(); | ||
|
|
||
| fn main() { | ||
| let args: Vec<String> = env::args().collect(); | ||
| let default_config = String::from("workload.toml"); | ||
| let config_path = &args.get(1).unwrap_or(&default_config); | ||
| let duration_timer = SystemTime::now(); | ||
| handles.extend(h); | ||
| }); | ||
|
|
||
| let config = Config::builder() | ||
| // Add in `./Settings.toml` | ||
| .add_source( | ||
| config::File::with_name("/etc/berserker/workload.toml") | ||
| .required(false), | ||
| ) | ||
| .add_source(config::File::with_name(config_path).required(false)) | ||
| // Add in settings from the environment (with a prefix of APP) | ||
| // Eg.. `BERSERKER__WORKLOAD__ARRIVAL_RATE=1` would set the `arrival_rate` key | ||
| .add_source( | ||
| config::Environment::with_prefix("BERSERKER") | ||
| .try_parsing(true) | ||
| .separator("__"), | ||
| ) | ||
| .build() | ||
| .unwrap() | ||
| .try_deserialize::<WorkloadConfig>() | ||
| .unwrap(); | ||
| handles | ||
| } |
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.
Ok, you might want to disregard the previous comment, because I know see this entire function could be one large iterator operation like this:
fn run_script(script_path: String) -> Vec<Option<i32>> {
parse_instructions(&std::fs::read_to_string(script_path).unwrap())
.unwrap()
.iter()
.map(|node| {
debug!("AST NODE: {:?}", node);
let Node::Work {
name: _,
args,
instructions: _,
dist: _,
} = node;
let workers: u32 = args
.get("workers")
.cloned()
.unwrap_or(String::from("0"))
.parse()
.unwrap();
(0..workers)
.map(|_| {
let worker = new_script_worker(node.clone());
match fork() {
Ok(Fork::Parent(child)) => {
info!("Child {}", child);
Some(child)
}
Ok(Fork::Child) => {
worker.run_payload().unwrap();
None
}
Err(e) => {
warn!("Failed: {e:?}");
None
}
}
})
.collect::<Vec<_>>()
})
.collect()
}Generally speaking, iterators should be used when you are doing something self contained that has no side effects and for loops for when you do want side effects. At least that is what I heard once and I like to stick to it.
src/main.rs
Outdated
| let Node::Work { | ||
| name: _, | ||
| args, | ||
| instructions: _, | ||
| dist: _, | ||
| } = node; |
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 should be more concise if you only want one field
let Node::Work { args, .. } = node;| .cloned() | ||
| .unwrap_or(String::from("0")) |
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.
Since we are just going to parse the string, I don't think we need to make a copy of it
| .cloned() | |
| .unwrap_or(String::from("0")) | |
| .unwrap_or("0") |
src/main.rs
Outdated
| let default_config = String::from("workload.toml"); | ||
| let duration_timer = SystemTime::now(); | ||
| let script_path = args.flag_f; | ||
| let config_path = args.flag_c.unwrap_or(default_config); | ||
|
|
||
| let config = Config::builder() | ||
| // Add in `./Settings.toml` | ||
| .add_source( | ||
| config::File::with_name("/etc/berserker/workload.toml") | ||
| .required(false), | ||
| ) | ||
| .add_source( | ||
| config::File::with_name(config_path.as_str()).required(false), |
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.
[nit] You don't actually need a String for default_config and config_path since you end up just calling as_str() on it anyways, you can do this instead:
let default_config = "workload.toml";
let duration_timer = SystemTime::now();
let script_path = args.flag_f;
let config_path = args.flag_c.as_ref().unwrap_or(default_config);
let config = Config::builder()
// Add in `./Settings.toml`
.add_source(
config::File::with_name("/etc/berserker/workload.toml")
.required(false),
)
.add_source(
config::File::with_name(config_path).required(false),Add networking instructions. Add functions with different set of arguments.
8b9e023 to
026c376
Compare
Add helpers to generate random values for workload. Use BTF to attach fentry/fexit to BPF programs (WIP). Add SIGTERM hook to finish workers processes.
8dc07d4 to
32ea55b
Compare
Script worker allows much more flexible definition of load structure. Few examples: