sapling/eden/mononoke/tunables/tunables-derive/lib.rs
Harvey Hunt 3cd49f9d3c mononoke: Add tunables - a simple form of config hot reloading
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
2020-04-30 16:08:30 -07:00

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);
}