mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 08:47:12 +03:00
3cd49f9d3c
Summary: Currently, Mononoke's configs are loaded at startup and only refreshed during restart. There are some exceptions to this, including throttling limits. Other Mononoke services (such as the LFS server) have their own implementations of hot reloadable configs, however there isn't a universally agreed upon method. Static configs makes it hard to roll out features gradually and safely. If a bad config option is enabled, it can't be rectified until the entire tier is restarted. However, Mononoke's code is structured with static configs in mind and doesn't support hot reloading. Changing this would require a lot of work (imagine trying to swap out blobstore configs during run time) and wouldn't necessarily provide much benefit. Instead, add a subset of hot reloadable configs called tunables. Tunables are accessible from anywhere in the code and are cheap to read as they only require reading an atomic value. This means that they can be used even in hot code paths. Currently tunables support reloading boolean values and i64s. In the future, I'd like to expand tunables to include more functionality, such as a rollout percentage. The `--tunables-config` flag points to a configerator spec that exports a Tunables thrift struct. This allows differents tiers and Mononoke services to have their own tunables. If this isn't provided, `MononokeTunables::default()` will be used. This diff adds a proc_macro that will generate the relevant `get` and `update` methods for the fields added to a struct which derives `Tunables`. This struct is then stored in a `once_cell` and can be accessed using `tunables::tunables()`. To add a new tunable, add a field to the `MononokeTunables` struct that is of type `AtomicBool` or `AtomicI64`. Update the relevant tunables configerator config to include your new field, with the exact same name. Removing a tunable from `MononokeTunables` is fine, as is removing a tunable from configerator. If the `--tunables-config` path isn't passed, then a default tunables config located at `scm/mononoke/tunables/default` will be loaded. There is also the `--disable-tunables` flag that won't load anything from configerator, it will instead use the `Tunable` struct's `default()` method to initialise it. This is useful in integration tests. Reviewed By: StanislavGlebik Differential Revision: D21177252 fbshipit-source-id: 02a93c1ceee99066019b23d81ea308e4c565d371
164 lines
4.6 KiB
Rust
164 lines
4.6 KiB
Rust
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This software may be used and distributed according to the terms of the
|
|
* GNU General Public License version 2.
|
|
*/
|
|
|
|
extern crate proc_macro;
|
|
|
|
use proc_macro2::TokenStream;
|
|
use quote::quote;
|
|
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};
|
|
|
|
const UNIMPLEMENTED_MSG: &str = "Only AtomicBool and AtomicI64 are supported";
|
|
const STRUCT_FIELD_MSG: &str = "Only implemented for named fields of a struct";
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
enum TunableType {
|
|
Bool,
|
|
I64,
|
|
}
|
|
|
|
#[proc_macro_derive(Tunables)]
|
|
// This proc macro accepts a struct and provides methods that get the atomic
|
|
// values stored inside of it. It does this by generating methods
|
|
// named get_<field>(). The macro also generates methods that update the
|
|
// atomic values inside of the struct, using a provided HashMap.
|
|
pub fn derive_tunables(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|
let parsed_input = parse_macro_input!(input as DeriveInput);
|
|
|
|
let struct_name = parsed_input.ident;
|
|
let names_and_types = parse_names_and_types(parsed_input.data).into_iter();
|
|
|
|
let getter_methods = generate_getter_methods(names_and_types.clone());
|
|
let updater_methods = generate_updater_methods(names_and_types);
|
|
|
|
let expanded = quote! {
|
|
impl #struct_name {
|
|
#updater_methods
|
|
#getter_methods
|
|
}
|
|
};
|
|
|
|
expanded.into()
|
|
}
|
|
|
|
impl TunableType {
|
|
fn external_type(&self) -> Ident {
|
|
match self {
|
|
Self::Bool => quote::format_ident!("{}", "bool"),
|
|
Self::I64 => quote::format_ident!("{}", "i64"),
|
|
}
|
|
}
|
|
|
|
fn generate_getter_method(&self, name: Ident) -> TokenStream {
|
|
let method = quote::format_ident!("get_{}", name);
|
|
let external_type = self.external_type();
|
|
|
|
quote! {
|
|
pub fn #method(&self) -> #external_type {
|
|
return self.#name.load(std::sync::atomic::Ordering::Relaxed)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn generate_getter_methods<I>(names_and_types: I) -> TokenStream
|
|
where
|
|
I: Iterator<Item = (Ident, TunableType)> + std::clone::Clone,
|
|
{
|
|
let mut methods = TokenStream::new();
|
|
|
|
for (name, ty) in names_and_types {
|
|
methods.extend(ty.generate_getter_method(name));
|
|
}
|
|
|
|
methods
|
|
}
|
|
|
|
fn generate_updater_methods<I>(names_and_types: I) -> TokenStream
|
|
where
|
|
I: Iterator<Item = (Ident, TunableType)> + std::clone::Clone,
|
|
{
|
|
let mut methods = TokenStream::new();
|
|
|
|
methods.extend(generate_updater_method(
|
|
names_and_types.clone(),
|
|
TunableType::Bool,
|
|
quote::format_ident!("update_bools"),
|
|
));
|
|
|
|
methods.extend(generate_updater_method(
|
|
names_and_types,
|
|
TunableType::I64,
|
|
quote::format_ident!("update_ints"),
|
|
));
|
|
|
|
methods
|
|
}
|
|
|
|
fn generate_updater_method<I>(
|
|
names_and_types: I,
|
|
ty: TunableType,
|
|
method_name: Ident,
|
|
) -> TokenStream
|
|
where
|
|
I: Iterator<Item = (Ident, TunableType)> + std::clone::Clone,
|
|
{
|
|
let names = names_and_types.filter(|(_, t)| *t == ty).map(|(n, _)| n);
|
|
|
|
let type_ident = ty.external_type();
|
|
let mut names = names.peekable();
|
|
let mut body = TokenStream::new();
|
|
|
|
if names.peek().is_some() {
|
|
body.extend(
|
|
quote! {
|
|
for (name, val) in tunables.iter() {
|
|
match name.as_ref() {
|
|
#(stringify!(#names) => self.#names.store(*val, std::sync::atomic::Ordering::Relaxed), )*
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
quote! {
|
|
fn #method_name(&self, tunables: &std::collections::HashMap<String, #type_ident>) {
|
|
#body
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_names_and_types(data: Data) -> Vec<(Ident, TunableType)> {
|
|
match data {
|
|
Data::Struct(data) => match data.fields {
|
|
Fields::Named(fields) => fields
|
|
.named
|
|
.into_iter()
|
|
.filter_map(|f| f.clone().ident.map(|i| (i, resolve_type(f.ty))))
|
|
.collect::<Vec<_>>(),
|
|
_ => unimplemented!("{}", STRUCT_FIELD_MSG),
|
|
},
|
|
_ => unimplemented!("{}", STRUCT_FIELD_MSG),
|
|
}
|
|
}
|
|
|
|
fn resolve_type(ty: Type) -> TunableType {
|
|
// TODO: Handle full paths to the types, such as
|
|
// std::sync::atomic::AtomicBool, rather than just the type name.
|
|
if let Type::Path(p) = ty {
|
|
if let Some(ident) = p.path.get_ident() {
|
|
match &ident.to_string()[..] {
|
|
"AtomicBool" => return TunableType::Bool,
|
|
"AtomicI64" => return TunableType::I64,
|
|
_ => unimplemented!("{}", UNIMPLEMENTED_MSG),
|
|
}
|
|
}
|
|
}
|
|
|
|
unimplemented!("{}", UNIMPLEMENTED_MSG);
|
|
}
|