/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashSet;
use std::ffi::OsString;
use std::num::NonZeroU32;

use anyhow::Error;
use anyhow::Result;
use blobstore_factory::PutBehaviour;
use blobstore_factory::ScrubAction;
use blobstore_factory::SrubWriteOnly;
use blobstore_factory::DEFAULT_PUT_BEHAVIOUR;
use clap_old::App;
use clap_old::Arg;
use clap_old::ArgGroup;
use fbinit::FacebookInit;
use once_cell::sync::OnceCell;
use repo_factory::ReadOnlyStorage;
use slog::Record;
use sql_ext::facebook::SharedConnectionPool;
use strum::VariantNames;
use tokio::runtime::Runtime;

use super::cache::add_cachelib_args;
use super::cache::CachelibSettings;
use super::matches::MononokeMatches;

pub const CONFIG_PATH: &str = "mononoke-config-path";
pub const REPO_ID: &str = "repo-id";
pub const REPO_NAME: &str = "repo-name";
pub const SHARDED_SERVICE_NAME: &str = "sharded-service-name";
pub const SOURCE_REPO_GROUP: &str = "source-repo";
pub const SOURCE_REPO_ID: &str = "source-repo-id";
pub const SOURCE_REPO_NAME: &str = "source-repo-name";
pub const TARGET_REPO_GROUP: &str = "target-repo";
pub const TARGET_REPO_ID: &str = "target-repo-id";
pub const TARGET_REPO_NAME: &str = "target-repo-name";
pub const ENABLE_MCROUTER: &str = "enable-mcrouter";
pub const MYSQL_MASTER_ONLY: &str = "mysql-master-only";
pub const MYSQL_POOL_LIMIT: &str = "mysql-pool-limit";
pub const MYSQL_POOL_PER_KEY_LIMIT: &str = "mysql-pool-per-key-limit";
pub const MYSQL_POOL_THREADS_NUM: &str = "mysql-pool-threads-num";
pub const MYSQL_POOL_AGE_TIMEOUT: &str = "mysql-pool-age-timeout";
pub const MYSQL_POOL_IDLE_TIMEOUT: &str = "mysql-pool-idle-timeout";
pub const MYSQL_CONN_OPEN_TIMEOUT: &str = "mysql-conn-open-timeout";
pub const MYSQL_MAX_QUERY_TIME: &str = "mysql-query-time-limit";
pub const MYSQL_SQLBLOB_POOL_LIMIT: &str = "mysql-sqblob-pool-limit";
pub const MYSQL_SQLBLOB_POOL_PER_KEY_LIMIT: &str = "mysql-sqblob-pool-per-key-limit";
pub const MYSQL_SQLBLOB_POOL_THREADS_NUM: &str = "mysql-sqblob-pool-threads-num";
pub const MYSQL_SQLBLOB_POOL_AGE_TIMEOUT: &str = "mysql-sqblob-pool-age-timeout";
pub const MYSQL_SQLBLOB_POOL_IDLE_TIMEOUT: &str = "mysql-sqblob-pool-idle-timeout";
pub const RUNTIME_THREADS: &str = "runtime-threads";
pub const TUNABLES_CONFIG: &str = "tunables-config";
pub const TUNABLES_LOCAL_PATH: &str = "tunables-local-path";
pub const DISABLE_TUNABLES: &str = "disable-tunables";
pub const SCRIBE_LOGGING_DIRECTORY: &str = "scribe-logging-directory";
pub const RENDEZVOUS_FREE_CONNECTIONS: &str = "rendezvous-free-connections";

pub const READ_QPS_ARG: &str = "blobstore-read-qps";
pub const WRITE_QPS_ARG: &str = "blobstore-write-qps";
pub const READ_BYTES_ARG: &str = "blobstore-read-bytes-s";
pub const WRITE_BYTES_ARG: &str = "blobstore-write-bytes-s";
pub const READ_BURST_BYTES_ARG: &str = "blobstore-read-burst-bytes-s";
pub const WRITE_BURST_BYTES_ARG: &str = "blobstore-write-burst-bytes-s";
pub const BLOBSTORE_BYTES_MIN_THROTTLE_ARG: &str = "blobstore-bytes-min-throttle";
pub const READ_CHAOS_ARG: &str = "blobstore-read-chaos-rate";
pub const WRITE_CHAOS_ARG: &str = "blobstore-write-chaos-rate";
pub const WRITE_ZSTD_ARG: &str = "blobstore-write-zstd";
pub const WRITE_ZSTD_LEVEL_ARG: &str = "blobstore-write-zstd-level";
pub const CACHELIB_ATTEMPT_ZSTD_ARG: &str = "blobstore-cachelib-attempt-zstd";
pub const BLOBSTORE_PUT_BEHAVIOUR_ARG: &str = "blobstore-put-behaviour";
pub const BLOBSTORE_SCRUB_ACTION_ARG: &str = "blobstore-scrub-action";
pub const BLOBSTORE_SCRUB_GRACE_ARG: &str = "blobstore-scrub-grace";
pub const BLOBSTORE_SCRUB_WRITE_ONLY_MISSING_ARG: &str = "blobstore-scrub-write-only-missing";
pub const BLOBSTORE_SCRUB_QUEUE_PEEK_BOUND_ARG: &str = "blobstore-scrub-queue-peek";
pub const PUT_MEAN_DELAY_SECS_ARG: &str = "blobstore-put-mean-delay-secs";
pub const PUT_STDDEV_DELAY_SECS_ARG: &str = "blobstore-put-stddev-delay-secs";
pub const GET_MEAN_DELAY_SECS_ARG: &str = "blobstore-get-mean-delay-secs";
pub const GET_STDDEV_DELAY_SECS_ARG: &str = "blobstore-get-stddev-delay-secs";

pub const WITH_READONLY_STORAGE_ARG: &str = "with-readonly-storage";

pub const LOG_INCLUDE_TAG: &str = "log-include-tag";
pub const LOG_EXCLUDE_TAG: &str = "log-exclude-tag";
pub const LOGVIEW_CATEGORY: &str = "logview-category";
pub const LOGVIEW_ADDITIONAL_LEVEL_FILTER: &str = "logview-additional-level-filter";
pub const SCUBA_DATASET_ARG: &str = "scuba-dataset";
pub const SCUBA_LOG_FILE_ARG: &str = "scuba-log-file";
pub const NO_DEFAULT_SCUBA_DATASET_ARG: &str = "no-default-scuba-dataset";
pub const WARM_BOOKMARK_CACHE_SCUBA_DATASET_ARG: &str = "warm-bookmark-cache-scuba-dataset";

// Argument, responsible for instantiation of `ObservabilityContext::Dynamic`
pub const WITH_DYNAMIC_OBSERVABILITY: &str = "with-dynamic-observability";

pub const LOCAL_CONFIGERATOR_PATH_ARG: &str = "local-configerator-path";
pub const WITH_TEST_MEGAREPO_CONFIGS_CLIENT: &str = "with-test-megarepo-configs-client";
pub const CRYPTO_PATH_REGEX_ARG: &str = "crypto-path-regex";
pub const DERIVE_REMOTELY: &str = "derive-remotely";
pub const DERIVE_REMOTELY_TIER: &str = "derive-remotely-tier";

pub const ACL_FILE: &str = "acl-file";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArgType {
    /// Options related to mononoke config
    Config,
    /// Options related to stderr logging,
    Logging,
    /// Adds options related to mysql database connections
    Mysql,
    /// Adds options related to blobstore access
    Blobstore,
    /// Adds options related to cachelib and its use from blobstore
    Cachelib,
    /// Adds options related to tokio runtime
    Runtime,
    /// Adds options related to mononoke tunables
    Tunables,
    /// Adds options to select a repo. If not present then all repos.
    Repo,
    /// Adds options for scrubbing blobstores
    Scrub,
    /// Adds --source-repo-id/repo-name and --target-repo-id/repo-name options.
    /// Necessary for crossrepo operations
    /// Only visible if Repo group is visible.
    SourceAndTargetRepos,
    /// Adds just --source-repo-id/repo-name, for blobimport into a megarepo
    /// Only visible if Repo group is visible.
    SourceRepo,
    /// Adds --shutdown-grace-period and --shutdown-timeout for graceful shutdown.
    ShutdownTimeouts,
    /// Adds --scuba-dataset for scuba logging.
    ScubaLogging,
    /// Adds --disabled-hooks for disabling hooks.
    DisableHooks,
    /// Adds --fb303-thrift-port for stats and profiling
    Fb303,
    /// Adds --enable-mcrouter to use McRouter to talk to Memcache for places that support it,
    /// which can boot faster in dev binaries.
    McRouter,
    /// Adds arguments related to Scribe logging.
    Scribe,
    /// Adds options related to rendezvous
    RendezVous,
    /// Adds options related to derivation
    Derivation,
    /// Adds options related to acls
    Acls,
}

// Arguments that are enabled by default for MononokeAppBuilder
const DEFAULT_ARG_TYPES: &[ArgType] = &[
    ArgType::Blobstore,
    ArgType::Cachelib,
    ArgType::Config,
    ArgType::Logging,
    ArgType::Mysql,
    ArgType::Repo,
    ArgType::Runtime,
    ArgType::Tunables,
    ArgType::RendezVous,
    ArgType::Derivation,
    ArgType::Acls,
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepoRequirement {
    // The command will execute for exactly one repo at a time.
    ExactlyOne,
    // The command requires atleast one repo.
    AtLeastOne,
}

/// Build clap App with appropriate default settings.
pub struct MononokeAppBuilder {
    /// The app name.
    name: String,

    /// Whether to hide advanced Manifold configuration from help. Note that the arguments will
    /// still be available, just not displayed in help.
    hide_advanced_args: bool,

    /// Flag determinig if the command supports getting repos dynamically at runtime in
    /// addition to repos being provided through CLI before execution.
    dynamic_repos: bool,

    /// Whether to require the user select a repo if the option is present.
    repo_required: Option<RepoRequirement>,

    /// Which groups of arguments are enabled for this app
    arg_types: HashSet<ArgType>,

    /// This app is special admin tool, needs to run with specific PutBehaviour
    special_put_behaviour: Option<PutBehaviour>,

    /// Cachelib default settings, as shown in usage
    cachelib_settings: CachelibSettings,

    /// Whether to default to readonly storage or not
    readonly_storage_default: ReadOnlyStorage,

    /// Whether to default to attempting to compress to cachelib for large objects
    blobstore_cachelib_attempt_zstd_default: bool,

    /// Whether to default to limit blobstore read QPS
    blobstore_read_qps_default: Option<NonZeroU32>,

    /// The default Scuba dataset for this app, if any.
    default_scuba_dataset: Option<String>,

    // Whether to default to scrubbing when using a multiplexed blobstore
    scrub_action_default: Option<ScrubAction>,

    // Whether to allow a grace period before reporting a key missing in a store for recent keys
    scrub_grace_secs_default: Option<u64>,

    // Whether to report missing keys in write only blobstores as a scrub action when scrubbing
    scrub_action_on_missing_write_only_default: Option<SrubWriteOnly>,

    // Whether to set a default for how long to peek back at the multiplex queue when scrubbing
    scrub_queue_peek_bound_secs_default: Option<u64>,

    /// Additional filter for customising logging
    slog_filter_fn: Option<fn(&Record) -> bool>,
}

/// Things we want to live for the lifetime of the mononoke binary
#[derive(Default)]
pub struct MononokeAppData {
    pub cachelib_settings: CachelibSettings,
    pub repo_required: Option<RepoRequirement>,
    pub global_mysql_connection_pool: SharedConnectionPool,
    pub sqlblob_mysql_connection_pool: SharedConnectionPool,
    pub default_scuba_dataset: Option<String>,
    pub slog_filter_fn: Option<fn(&Record) -> bool>,
}

// Result of MononokeAppBuilder::build() which has clap plus the MononokeApp data
pub struct MononokeClapApp<'a, 'b> {
    clap: App<'a, 'b>,
    app_data: MononokeAppData,
    arg_types: HashSet<ArgType>,
}

impl<'a, 'b> MononokeClapApp<'a, 'b> {
    pub fn about<S: Into<&'b str>>(self, about: S) -> Self {
        Self {
            clap: self.clap.about(about),
            ..self
        }
    }

    pub fn subcommand(self, subcmd: App<'a, 'b>) -> Self {
        Self {
            clap: self.clap.subcommand(subcmd),
            ..self
        }
    }

    pub fn arg<A: Into<Arg<'a, 'b>>>(mut self, a: A) -> Self {
        self.clap.p.add_arg(a.into());
        self
    }

    pub fn args_from_usage(self, usage: &'a str) -> Self {
        Self {
            clap: self.clap.args_from_usage(usage),
            ..self
        }
    }

    pub fn group(self, group: ArgGroup<'a>) -> Self {
        Self {
            clap: self.clap.group(group),
            ..self
        }
    }

    pub fn get_matches(self, fb: FacebookInit) -> Result<(MononokeMatches<'a>, Runtime), Error> {
        MononokeMatches::new(fb, self.clap.get_matches(), self.app_data, self.arg_types)
    }

    pub fn get_matches_from<I, T>(
        self,
        fb: FacebookInit,
        itr: I,
    ) -> Result<(MononokeMatches<'a>, Runtime), Error>
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        MononokeMatches::new(
            fb,
            self.clap.get_matches_from(itr),
            self.app_data,
            self.arg_types,
        )
    }
}

impl MononokeAppBuilder {
    /// Start building a new Mononoke app.  This adds the standard Mononoke args.  Use the `build`
    /// method to get a `clap::App` that you can then customize further.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            hide_advanced_args: false,
            repo_required: None,
            arg_types: DEFAULT_ARG_TYPES.iter().cloned().collect(),
            special_put_behaviour: None,
            cachelib_settings: CachelibSettings::default(),
            readonly_storage_default: ReadOnlyStorage(false),
            blobstore_cachelib_attempt_zstd_default: true,
            blobstore_read_qps_default: None,
            default_scuba_dataset: None,
            scrub_action_default: None,
            scrub_grace_secs_default: None,
            scrub_action_on_missing_write_only_default: None,
            scrub_queue_peek_bound_secs_default: None,
            slog_filter_fn: None,
            dynamic_repos: false,
        }
    }

    /// Hide advanced args.
    pub fn with_advanced_args_hidden(mut self) -> Self {
        self.hide_advanced_args = true;
        self
    }

    /// This command operates on all configured repos, and removes the options for selecting a
    /// repo.  The default behaviour is for the arguments to specify the repo to be optional, which is
    /// probably not what you want, so you should call either this method or `with_repo_required`.
    pub fn with_all_repos(mut self) -> Self {
        self.arg_types.remove(&ArgType::Repo);
        self
    }

    /// This command operates on a specific repos, so this makes the options for selecting a
    /// repo required.  The default behaviour is for the arguments to specify the repo to be
    /// optional, which is probably not what you want, so you should call either this method or
    /// `with_all_repos`.
    pub fn with_repo_required(mut self, requirement: RepoRequirement) -> Self {
        self.arg_types.insert(ArgType::Repo);
        self.repo_required = Some(requirement);
        self
    }

    /// This command might operate on two repos in the same time. This is normally used
    /// for two repos where one repo is synced into another.
    pub fn with_source_and_target_repos(mut self) -> Self {
        self.arg_types.insert(ArgType::SourceAndTargetRepos);
        self
    }

    /// This command operates on one repo (--repo-id/name), but needs to be aware that commits
    /// are sourced from another repo.
    pub fn with_source_repos(mut self) -> Self {
        self.arg_types.insert(ArgType::SourceRepo);
        self
    }

    /// This command has arguments for graceful shutdown.
    pub fn with_shutdown_timeout_args(mut self) -> Self {
        self.arg_types.insert(ArgType::ShutdownTimeouts);
        self
    }

    /// This command has arguments for scuba logging.
    pub fn with_scuba_logging_args(mut self) -> Self {
        self.arg_types.insert(ArgType::ScubaLogging);
        self
    }

    /// This command has arguments for disabled hooks.
    pub fn with_disabled_hooks_args(mut self) -> Self {
        self.arg_types.insert(ArgType::DisableHooks);
        self
    }

    /// This command has arguments for fb303
    pub fn with_fb303_args(mut self) -> Self {
        self.arg_types.insert(ArgType::Fb303);
        self
    }

    /// This command can get repos through CLI OR
    /// at runtime dynamically through sharded execution.
    pub fn with_dynamic_repos(mut self) -> Self {
        self.dynamic_repos = true;
        self
    }

    /// This command has arguments for McRouter
    pub fn with_mcrouter_args(mut self) -> Self {
        self.arg_types.insert(ArgType::McRouter);
        self
    }

    pub fn with_scribe_args(mut self) -> Self {
        self.arg_types.insert(ArgType::Scribe);
        self
    }

    pub fn with_default_scuba_dataset(mut self, default: impl Into<String>) -> Self {
        self.default_scuba_dataset = Some(default.into());
        self
    }

    /// This command does expose these types of arguments
    pub fn with_arg_types<I>(mut self, types: I) -> Self
    where
        I: IntoIterator<Item = ArgType>,
    {
        for t in types {
            self.arg_types.insert(t);
        }
        self
    }

    /// This command does not expose these types of arguments
    pub fn without_arg_types<I>(mut self, types: I) -> Self
    where
        I: IntoIterator<Item = ArgType>,
    {
        for t in types {
            self.arg_types.remove(&t);
        }
        self
    }

    /// This command needs a special default put behaviour (e.g. its an admin tool)
    pub fn with_special_put_behaviour(mut self, put_behaviour: PutBehaviour) -> Self {
        self.special_put_behaviour = Some(put_behaviour);
        self
    }

    /// This command has a special default readonly storage setting
    pub fn with_readonly_storage_default(mut self, v: ReadOnlyStorage) -> Self {
        self.readonly_storage_default = v;
        self
    }

    /// This command has a special default blobstore_cachelib_attempt_zstd setting
    pub fn with_blobstore_cachelib_attempt_zstd_default(mut self, d: bool) -> Self {
        self.blobstore_cachelib_attempt_zstd_default = d;
        self
    }

    /// This command has a special default blobstore_read_qps default setting
    pub fn with_blobstore_read_qps_default(mut self, d: Option<NonZeroU32>) -> Self {
        self.blobstore_read_qps_default = d;
        self
    }

    /// This command has different cachelib defaults, show them in --help
    pub fn with_cachelib_settings(mut self, cachelib_settings: CachelibSettings) -> Self {
        self.cachelib_settings = cachelib_settings;
        self
    }

    /// This command has a special scrub_action default setting
    pub fn with_scrub_action_default(mut self, d: Option<ScrubAction>) -> Self {
        self.scrub_action_default = d;
        self
    }

    /// This command has a special grace period for recent keys when scrubbing
    pub fn with_scrub_grace_secs_default(mut self, d: Option<u64>) -> Self {
        self.scrub_grace_secs_default = d;
        self
    }

    /// This command has a special handling of write only stores when scrubbing
    pub fn with_scrub_action_on_missing_write_only_default(
        mut self,
        d: Option<SrubWriteOnly>,
    ) -> Self {
        self.scrub_action_on_missing_write_only_default = d;
        self
    }

    /// Enables custom logging filter
    pub fn with_slog_filter(mut self, slog_filter_fn: fn(&Record) -> bool) -> Self {
        self.slog_filter_fn = Some(slog_filter_fn);
        self
    }

    /// Build a MononokeClapApp around a `clap::App` for this Mononoke app, which can then be customized further.
    pub fn build<'a, 'b>(self) -> MononokeClapApp<'a, 'b> {
        let mut app = App::new(self.name.clone());

        if self.arg_types.contains(&ArgType::Config) {
            app = app.arg(
                Arg::with_name(CONFIG_PATH)
                    .long(CONFIG_PATH)
                    .value_name("MONONOKE_CONFIG_PATH")
                    .help("Path to the Mononoke configs"),
            )
            .arg(
                Arg::with_name(CRYPTO_PATH_REGEX_ARG)
                    .multiple(true)
                    .long(CRYPTO_PATH_REGEX_ARG)
                    .takes_value(true)
                    .help("Regex for a Configerator path that must be covered by Mononoke's crypto project")
            )
            .arg(
                Arg::with_name(LOCAL_CONFIGERATOR_PATH_ARG)
                    .long(LOCAL_CONFIGERATOR_PATH_ARG)
                    .takes_value(true)
                    .help("local path to fetch configerator configs from, instead of normal configerator"),
            );
        }

        if self.arg_types.contains(&ArgType::Repo) {
            let repo_conflicts: &[&str] = if self.arg_types.contains(&ArgType::SourceRepo) {
                &[TARGET_REPO_ID, TARGET_REPO_NAME]
            } else {
                &[
                    SOURCE_REPO_ID,
                    SOURCE_REPO_NAME,
                    TARGET_REPO_ID,
                    TARGET_REPO_NAME,
                ]
            };

            let mut repo_id_arg = Arg::with_name(REPO_ID)
                .long(REPO_ID)
                // This is an old form that some consumers use
                .alias("repo_id")
                .value_name("ID")
                .help("numeric ID of repository")
                .conflicts_with_all(repo_conflicts);

            let mut repo_name_arg = Arg::with_name(REPO_NAME)
                .long(REPO_NAME)
                .value_name("NAME")
                .help("Name of repository")
                .conflicts_with_all(repo_conflicts);

            let sharded_service_name_arg = Arg::with_name(SHARDED_SERVICE_NAME)
                .long(SHARDED_SERVICE_NAME)
                .value_name("NAME")
                .multiple(false)
                .help("The name of SM service to be used when the command needs to be executed in a sharded setting")
                .conflicts_with_all(repo_conflicts);

            let group_args = if self.dynamic_repos {
                vec![REPO_ID, REPO_NAME, SHARDED_SERVICE_NAME]
            } else {
                vec![REPO_ID, REPO_NAME]
            };
            let mut repo_group = ArgGroup::with_name("repo")
                .args(&group_args)
                .required(self.repo_required.is_some());

            if self.repo_required == Some(RepoRequirement::AtLeastOne) {
                repo_id_arg = repo_id_arg.multiple(true).number_of_values(1);
                repo_name_arg = repo_name_arg.multiple(true).number_of_values(1);
                repo_group = repo_group.multiple(true)
            }
            app = if self.dynamic_repos {
                app.arg(sharded_service_name_arg)
                    .arg(repo_id_arg)
                    .arg(repo_name_arg)
                    .group(repo_group)
            } else {
                app.arg(repo_id_arg).arg(repo_name_arg).group(repo_group)
            };

            if self.arg_types.contains(&ArgType::SourceRepo)
                || self.arg_types.contains(&ArgType::SourceAndTargetRepos)
            {
                app = app
                    .arg(
                        Arg::with_name(SOURCE_REPO_ID)
                        .long(SOURCE_REPO_ID)
                        .value_name("ID")
                        .help("numeric ID of source repository (used only for commands that operate on more than one repo)"),
                    )
                    .arg(
                        Arg::with_name(SOURCE_REPO_NAME)
                        .long(SOURCE_REPO_NAME)
                        .value_name("NAME")
                        .help("Name of source repository (used only for commands that operate on more than one repo)"),
                    )
                    .group(
                        ArgGroup::with_name(SOURCE_REPO_GROUP)
                            .args(&[SOURCE_REPO_ID, SOURCE_REPO_NAME])
                    )
            }

            if self.arg_types.contains(&ArgType::SourceAndTargetRepos) {
                app = app
                    .arg(
                        Arg::with_name(TARGET_REPO_ID)
                        .long(TARGET_REPO_ID)
                        .value_name("ID")
                        .help("numeric ID of target repository (used only for commands that operate on more than one repo)"),
                    )
                    .arg(
                        Arg::with_name(TARGET_REPO_NAME)
                        .long(TARGET_REPO_NAME)
                        .value_name("NAME")
                        .help("Name of target repository (used only for commands that operate on more than one repo)"),
                    )
                    .group(
                        ArgGroup::with_name(TARGET_REPO_GROUP)
                            .args(&[TARGET_REPO_ID, TARGET_REPO_NAME])
                    );
            }
        }

        if self.arg_types.contains(&ArgType::Logging) {
            app = add_logger_args(app);
        }
        if self.arg_types.contains(&ArgType::Mysql) {
            app = add_mysql_options_args(app);
        }
        if self.arg_types.contains(&ArgType::Blobstore) {
            app = self.add_blobstore_args(app);
        }
        if self.arg_types.contains(&ArgType::Cachelib) {
            app = add_cachelib_args(app, self.hide_advanced_args, self.cachelib_settings.clone());
        }
        if self.arg_types.contains(&ArgType::Runtime) {
            app = add_runtime_args(app);
        }
        if self.arg_types.contains(&ArgType::Tunables) {
            app = add_tunables_args(app);
        }
        if self.arg_types.contains(&ArgType::ShutdownTimeouts) {
            app = add_shutdown_timeout_args(app);
        }
        if self.arg_types.contains(&ArgType::ScubaLogging) {
            app = add_scuba_logging_args(app, self.default_scuba_dataset.is_some());
        }
        if self.arg_types.contains(&ArgType::DisableHooks) {
            app = add_disabled_hooks_args(app);
        }
        if self.arg_types.contains(&ArgType::Fb303) {
            app = add_fb303_args(app);
        }
        if self.arg_types.contains(&ArgType::McRouter) {
            app = add_mcrouter_args(app);
        }
        if self.arg_types.contains(&ArgType::Scribe) {
            app = add_scribe_logging_args(app);
        }
        if self.arg_types.contains(&ArgType::RendezVous) {
            app = add_rendezvous_args(app);
        }
        if self.arg_types.contains(&ArgType::Derivation) {
            app = add_derivation_args(app);
        }
        if self.arg_types.contains(&ArgType::Acls) {
            app = add_acls_args(app);
        }

        app = add_megarepo_svc_args(app);

        MononokeClapApp {
            clap: app,
            app_data: MononokeAppData {
                cachelib_settings: self.cachelib_settings,
                repo_required: self.repo_required,
                global_mysql_connection_pool: SharedConnectionPool::new(),
                sqlblob_mysql_connection_pool: SharedConnectionPool::new(),
                default_scuba_dataset: self.default_scuba_dataset,
                slog_filter_fn: self.slog_filter_fn,
            },
            arg_types: self.arg_types,
        }
    }

    fn add_blobstore_args<'a, 'b>(&self, app: App<'a, 'b>) -> App<'a, 'b> {
        let mut put_arg = Arg::with_name(BLOBSTORE_PUT_BEHAVIOUR_ARG)
            .long(BLOBSTORE_PUT_BEHAVIOUR_ARG)
            .takes_value(true)
            .required(false)
            .help("Desired blobstore behaviour when a put is made to an existing key.");

        if let Some(special_put_behaviour) = self.special_put_behaviour {
            put_arg = put_arg.default_value(special_put_behaviour.into());
        } else {
            // Add the default here so that it shows in --help
            put_arg = put_arg.default_value(DEFAULT_PUT_BEHAVIOUR.into());
        }

        let mut read_qps_arg = Arg::with_name(READ_QPS_ARG)
            .long(READ_QPS_ARG)
            .takes_value(true)
            .required(false)
            .help("Read QPS limit to ThrottledBlob");

        if let Some(default) = self.blobstore_read_qps_default {
            // Lazy static is nicer to LeakSanitizer than Box::leak
            static QPS_FORMATTED: OnceCell<String> = OnceCell::new();
            // clap needs &'static str
            read_qps_arg =
                read_qps_arg.default_value(QPS_FORMATTED.get_or_init(|| format!("{}", default)));
        }

        let app = app.arg(
           read_qps_arg
        )
        .arg(
            Arg::with_name(WRITE_QPS_ARG)
                .long(WRITE_QPS_ARG)
                .takes_value(true)
                .required(false)
                .help("Write QPS limit to ThrottledBlob"),
        )
        .arg(
            Arg::with_name(WRITE_BYTES_ARG)
                .long(WRITE_BYTES_ARG)
                .takes_value(true)
                .required(false)
                .help("Write Bytes/s limit to ThrottledBlob"),
        )
        .arg(
            Arg::with_name(READ_BYTES_ARG)
                .long(READ_BYTES_ARG)
                .takes_value(true)
                .required(false)
                .help("Read Bytes/s limit to ThrottledBlob"),
        )
        .arg(
            Arg::with_name(READ_BURST_BYTES_ARG)
                .long(READ_BURST_BYTES_ARG)
                .takes_value(true)
                .required(false)
                .help("Maximum burst bytes/s limit to ThrottledBlob.  Blobs larger than this will error rather than throttle due to consuming too much quota."),
        )
        .arg(
            Arg::with_name(WRITE_BURST_BYTES_ARG)
                .long(WRITE_BURST_BYTES_ARG)
                .takes_value(true)
                .required(false)
                .help("Maximum burst bytes/s limit to ThrottledBlob.  Blobs larger than this will error rather than throttle due to consuming too much quota."),
        )
        .arg(
            Arg::with_name(BLOBSTORE_BYTES_MIN_THROTTLE_ARG)
                .long(BLOBSTORE_BYTES_MIN_THROTTLE_ARG)
                .takes_value(true)
                .required(false)
                .help("Minimum number of bytes ThrottledBlob can count"),
        )
        .arg(
            Arg::with_name(READ_CHAOS_ARG)
                .long(READ_CHAOS_ARG)
                .takes_value(true)
                .required(false)
                .help("Rate of errors on reads. Pass N,  it will error randomly 1/N times. For multiplexed stores will only apply to the first store in the multiplex."),
        )
        .arg(
            Arg::with_name(WRITE_CHAOS_ARG)
                .long(WRITE_CHAOS_ARG)
                .takes_value(true)
                .required(false)
                .help("Rate of errors on writes. Pass N,  it will error randomly 1/N times. For multiplexed stores will only apply to the first store in the multiplex."),
        )
        .arg(
            Arg::with_name(WRITE_ZSTD_ARG)
                .long(WRITE_ZSTD_ARG)
                .takes_value(true)
                .required(false)
                .possible_values(BOOL_VALUES)
                .help("Allows one to override config to enable/disable zstd compression on write via packblob"),
        )
        .arg(
            Arg::with_name(WRITE_ZSTD_LEVEL_ARG)
                .long(WRITE_ZSTD_LEVEL_ARG)
                .takes_value(true)
                .required(false)
                .requires(WRITE_ZSTD_ARG)
                .help("Override the zstd compression leve used for writes via packblob."),
        )
        .arg(
            Arg::with_name(CACHELIB_ATTEMPT_ZSTD_ARG)
                .long(CACHELIB_ATTEMPT_ZSTD_ARG)
                .takes_value(true)
                .possible_values(BOOL_VALUES)
                .required(false)
                .default_value(bool_as_str(self.blobstore_cachelib_attempt_zstd_default))
                .help("Whether to attempt zstd compression when blobstore is putting things into cachelib over threshold size."),
        )
        .arg(
          put_arg
        )
        .arg(
            Arg::with_name(WITH_READONLY_STORAGE_ARG)
                .long(WITH_READONLY_STORAGE_ARG)
                .takes_value(true)
                .possible_values(BOOL_VALUES)
                .default_value(bool_as_str(self.readonly_storage_default.0))
                .help("Error on any attempts to write to storage if set to true"),
        )
        .arg(
            Arg::with_name(PUT_MEAN_DELAY_SECS_ARG)
                .long(PUT_MEAN_DELAY_SECS_ARG)
                .takes_value(true)
                .value_name(PUT_MEAN_DELAY_SECS_ARG)
                .requires(PUT_STDDEV_DELAY_SECS_ARG)
                .help("Mean value of additional delay for blobstore put calls"),
        )
        .arg(
            Arg::with_name(PUT_STDDEV_DELAY_SECS_ARG)
                .long(PUT_STDDEV_DELAY_SECS_ARG)
                .takes_value(true)
                .value_name(PUT_STDDEV_DELAY_SECS_ARG)
                .requires(PUT_MEAN_DELAY_SECS_ARG)
                .help("Stddev value of additional delay for blobstore put calls"),
        )
        .arg(
            Arg::with_name(GET_MEAN_DELAY_SECS_ARG)
                .long(GET_MEAN_DELAY_SECS_ARG)
                .takes_value(true)
                .value_name(GET_MEAN_DELAY_SECS_ARG)
                .requires(GET_STDDEV_DELAY_SECS_ARG)
                .help("Mean value of additional delay for blobstore get calls"),
        )
        .arg(
            Arg::with_name(GET_STDDEV_DELAY_SECS_ARG)
                .long(GET_STDDEV_DELAY_SECS_ARG)
                .takes_value(true)
                .value_name(GET_STDDEV_DELAY_SECS_ARG)
                .requires(GET_MEAN_DELAY_SECS_ARG)
                .help("Stddev value of additional delay for blobstore get calls"),
        );

        #[cfg(fbcode_build)]
        let app = blobstore_factory::ManifoldOptions::add_args(app);

        if self.arg_types.contains(&ArgType::Scrub) {
            let mut scrub_action_arg = Arg::with_name(BLOBSTORE_SCRUB_ACTION_ARG)
                .long(BLOBSTORE_SCRUB_ACTION_ARG)
                .takes_value(true)
                .required(false)
                .possible_values(ScrubAction::VARIANTS)
                .help("Enable ScrubBlobstore with the given action. Checks for keys missing from stores. In ReportOnly mode this logs only, otherwise it performs a copy to the missing stores.");
            if let Some(default) = self.scrub_action_default {
                scrub_action_arg = scrub_action_arg.default_value(default.into());
            }
            let mut scrub_grace_arg = Arg::with_name(BLOBSTORE_SCRUB_GRACE_ARG)
                .long(BLOBSTORE_SCRUB_GRACE_ARG)
                .takes_value(true)
                .required(false)
                .help("Number of seconds grace to give for key to arrive in multiple blobstores or the healer queue when scrubbing");
            if let Some(default) = self.scrub_grace_secs_default {
                static FORMATTED: OnceCell<String> = OnceCell::new(); // Lazy static is nicer to LeakSanitizer than Box::leak
                scrub_grace_arg =
                    scrub_grace_arg.default_value(FORMATTED.get_or_init(|| format!("{}", default)));
            }
            let mut scrub_queue_peek_bound_arg = Arg::with_name(
                BLOBSTORE_SCRUB_QUEUE_PEEK_BOUND_ARG,
            )
            .long(BLOBSTORE_SCRUB_QUEUE_PEEK_BOUND_ARG)
            .takes_value(true)
            .required(false)
            .requires(BLOBSTORE_SCRUB_ACTION_ARG)
            .help("Number of seconds within which we consider it worth checking the healer queue.");
            if let Some(default) = self.scrub_queue_peek_bound_secs_default {
                static FORMATTED: OnceCell<String> = OnceCell::new(); // Lazy static is nicer to LeakSanitizer than Box::leak
                scrub_queue_peek_bound_arg = scrub_queue_peek_bound_arg
                    .default_value(FORMATTED.get_or_init(|| format!("{}", default)));
            };
            let mut scrub_action_on_missing_write_only_arg =
                Arg::with_name(BLOBSTORE_SCRUB_WRITE_ONLY_MISSING_ARG)
                    .long(BLOBSTORE_SCRUB_WRITE_ONLY_MISSING_ARG)
                    .takes_value(true)
                    .required(false)
                    .possible_values(SrubWriteOnly::VARIANTS)
                    .help("Whether to allow missing values from write only stores when scrubbing");
            if let Some(default) = self.scrub_action_on_missing_write_only_default {
                scrub_action_on_missing_write_only_arg =
                    scrub_action_on_missing_write_only_arg.default_value(default.into());
            }
            app.arg(scrub_action_arg)
                .arg(scrub_grace_arg)
                .arg(scrub_action_on_missing_write_only_arg)
                .arg(scrub_queue_peek_bound_arg)
        } else {
            app
        }
    }
}

fn add_tunables_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(TUNABLES_CONFIG)
            .long(TUNABLES_CONFIG)
            .takes_value(true)
            .help("Tunables dynamic config path in Configerator"),
    )
    .arg(
        Arg::with_name(TUNABLES_LOCAL_PATH)
            .long(TUNABLES_LOCAL_PATH)
            .conflicts_with(TUNABLES_CONFIG)
            .takes_value(true)
            .help("Tunables static config local path"),
    )
    .arg(
        Arg::with_name(DISABLE_TUNABLES)
            .long(DISABLE_TUNABLES)
            .conflicts_with_all(&[TUNABLES_CONFIG, TUNABLES_LOCAL_PATH])
            .help("Use the default values for all tunables (useful for tests)"),
    )
}
fn add_runtime_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(RUNTIME_THREADS)
            .long(RUNTIME_THREADS)
            .takes_value(true)
            .help("a number of threads to use in the tokio runtime"),
    )
}

fn add_logger_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name("panic-fate")
            .long("panic-fate")
            .value_name("PANIC_FATE")
            .possible_values(&["continue", "exit", "abort"])
            .default_value("abort")
            .help("fate of the process when a panic happens"),
    )
    .arg(
        Arg::with_name(LOGVIEW_CATEGORY)
            .long(LOGVIEW_CATEGORY)
            .takes_value(true)
            .help("logview category to log to. Logview is not used if not set"),
    )
    .arg(
        Arg::with_name(LOGVIEW_ADDITIONAL_LEVEL_FILTER)
            .long(LOGVIEW_ADDITIONAL_LEVEL_FILTER)
            .takes_value(true)
            .possible_values(&slog::LOG_LEVEL_NAMES)
            .requires(LOGVIEW_CATEGORY)
            .help("logview level to filter. If logview category is not set then this is ignored. \
             Note that this level is applied AFTER --log-level/--debug was applied, so it doesn't make sense to set this parameter to a lower level \
             than --log-level"),
    )
    .arg(
        Arg::with_name("debug")
            .short("d")
            .long("debug")
            .help("print debug output"),
    )
    .arg(
        Arg::with_name("log-level")
            .long("log-level")
            .help("log level to use (does not work with --debug)")
            .takes_value(true)
            .possible_values(&slog::LOG_LEVEL_NAMES)
            .conflicts_with("debug"),
    )
    .arg(
        Arg::with_name(LOG_INCLUDE_TAG)
            .long(LOG_INCLUDE_TAG)
            .short("l")
            .help("include only log messages with these slog::Record::tags()/log::Record::targets")
            .takes_value(true)
            .multiple(true)
            .number_of_values(1),
    )
    .arg(
        Arg::with_name(LOG_EXCLUDE_TAG)
            .long(LOG_EXCLUDE_TAG)
            .short("L")
            .help("exclude log messages with these slog::Record::tags()/log::Record::targets")
            .takes_value(true)
            .multiple(true)
            .number_of_values(1),
    )
    .arg(
        Arg::with_name(WITH_DYNAMIC_OBSERVABILITY)
            .long(WITH_DYNAMIC_OBSERVABILITY)
            .help(
                "whether to instantiate ObservabilityContext::Dynamic,\
                 which reads logging levels from configerator. Overwrites\
                 --log-level or --debug",
            )
            .takes_value(true)
            .possible_values(&["true", "false"])
            .default_value("false"),
    )
}

fn add_mysql_options_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(MYSQL_MASTER_ONLY)
            .long(MYSQL_MASTER_ONLY)
            .help("Connect to MySQL master only")
            .takes_value(false),
    )
    // All the defaults for Mysql connection pool are derived from sql_ext::facebook::mysql
    // https://fburl.com/diffusion/n5isd68j
    // last synced on 17/12/2020
    .arg(
        Arg::with_name(MYSQL_POOL_LIMIT)
            .long(MYSQL_POOL_LIMIT)
            .help("Size of the connection pool")
            .takes_value(true)
            .default_value("10000"),
    )
    .arg(
        Arg::with_name(MYSQL_POOL_PER_KEY_LIMIT)
            .long(MYSQL_POOL_PER_KEY_LIMIT)
            .help("Mysql connection pool per key limit")
            .takes_value(true)
            .default_value("100"),
    )
    .arg(
        Arg::with_name(MYSQL_POOL_THREADS_NUM)
            .long(MYSQL_POOL_THREADS_NUM)
            .help("Number of threads in Mysql connection pool, i.e. number of real pools")
            .takes_value(true)
            .default_value("10"),
    )
    .arg(
        Arg::with_name(MYSQL_POOL_AGE_TIMEOUT)
            .long(MYSQL_POOL_AGE_TIMEOUT)
            .help("Mysql connection pool age timeout in millisecs")
            .takes_value(true)
            .default_value("60000"),
    )
    .arg(
        Arg::with_name(MYSQL_POOL_IDLE_TIMEOUT)
            .long(MYSQL_POOL_IDLE_TIMEOUT)
            .help("Mysql connection pool idle timeout in millisecs")
            .takes_value(true)
            .default_value("4000"),
    )
    // SQLBlob wants more aggressive timeouts, and does not benefit from sharing a pool with other users.
    .arg(
        Arg::with_name(MYSQL_SQLBLOB_POOL_LIMIT)
            .long(MYSQL_SQLBLOB_POOL_LIMIT)
            .help("Size of the connection pool")
            .takes_value(true)
            .default_value("10000"),
    )
    .arg(
        Arg::with_name(MYSQL_SQLBLOB_POOL_PER_KEY_LIMIT)
            .long(MYSQL_SQLBLOB_POOL_PER_KEY_LIMIT)
            .help("Mysql connection pool per key limit")
            .takes_value(true)
            .default_value("100"),
    )
    .arg(
        Arg::with_name(MYSQL_SQLBLOB_POOL_THREADS_NUM)
            .long(MYSQL_SQLBLOB_POOL_THREADS_NUM)
            .help("Number of threads in Mysql connection pool, i.e. number of real pools")
            .takes_value(true)
            .default_value("10"),
    )
    .arg(
        Arg::with_name(MYSQL_SQLBLOB_POOL_AGE_TIMEOUT)
            .long(MYSQL_SQLBLOB_POOL_AGE_TIMEOUT)
            .help("Mysql connection pool age timeout in millisecs")
            .takes_value(true)
            .default_value("60000"),
    )
    .arg(
        Arg::with_name(MYSQL_SQLBLOB_POOL_IDLE_TIMEOUT)
            .long(MYSQL_SQLBLOB_POOL_IDLE_TIMEOUT)
            .help("Mysql connection pool idle timeout in millisecs")
            .takes_value(true)
            .default_value("1000"),
    )
    .arg(
        Arg::with_name(MYSQL_CONN_OPEN_TIMEOUT)
            .long(MYSQL_CONN_OPEN_TIMEOUT)
            .help("Mysql connection open timeout in millisecs")
            .takes_value(true)
            .default_value("3000"),
    )
    .arg(
        Arg::with_name(MYSQL_MAX_QUERY_TIME)
            .long(MYSQL_MAX_QUERY_TIME)
            .help("Mysql query time limit in millisecs")
            .takes_value(true)
            .default_value("10000"),
    )
}

pub fn bool_as_str(v: bool) -> &'static str {
    if v { "true" } else { "false" }
}

pub(crate) const BOOL_VALUES: &[&str] = &["false", "true"];

fn add_mcrouter_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(ENABLE_MCROUTER)
            .long(ENABLE_MCROUTER)
            .help("Use local McRouter for rate limits")
            .takes_value(false),
    )
}

fn add_fb303_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.args_from_usage(r"--fb303-thrift-port=[PORT]    'port for fb303 service'")
}

fn add_disabled_hooks_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name("disabled-hooks")
            .long("disable-hook")
            .help("Disable a hook. Pass this argument multiple times to disable multiple hooks.")
            .multiple(true)
            .number_of_values(1)
            .takes_value(true),
    )
}

fn add_shutdown_timeout_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name("shutdown-grace-period")
            .long("shutdown-grace-period")
            .help(
                "Number of seconds to wait after receiving a shutdown signal before shutting down.",
            )
            .takes_value(true)
            .required(false)
            .default_value("0"),
    )
    .arg(
        Arg::with_name("shutdown-timeout")
            .long("shutdown-timeout")
            .help("Number of seconds to wait for requests to complete during shutdown.")
            .takes_value(true)
            .required(false)
            .default_value("10"),
    )
}

fn add_scuba_logging_args<'a, 'b>(app: App<'a, 'b>, has_default: bool) -> App<'a, 'b> {
    let mut app = app
        .arg(
            Arg::with_name(SCUBA_DATASET_ARG)
                .long(SCUBA_DATASET_ARG)
                .takes_value(true)
                .help("The name of the scuba dataset to log to"),
        )
        .arg(
            Arg::with_name(SCUBA_LOG_FILE_ARG)
                .long(SCUBA_LOG_FILE_ARG)
                .takes_value(true)
                .help("A log file to write JSON Scuba logs to (primarily useful in testing)"),
        )
        .arg(
            Arg::with_name(WARM_BOOKMARK_CACHE_SCUBA_DATASET_ARG)
                .long(WARM_BOOKMARK_CACHE_SCUBA_DATASET_ARG)
                .takes_value(true)
                .help(
                    "Special dataset to be used by warm bookmark cache. \
                If a binary doesn't use warm bookmark cache then this parameter is ignored",
                ),
        );

    if has_default {
        app = app.arg(
            Arg::with_name(NO_DEFAULT_SCUBA_DATASET_ARG)
                .long(NO_DEFAULT_SCUBA_DATASET_ARG)
                .takes_value(false)
                .help("Do not to the default scuba dataset for this app"),
        )
    }

    app
}

fn add_scribe_logging_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(SCRIBE_LOGGING_DIRECTORY)
            .long(SCRIBE_LOGGING_DIRECTORY)
            .takes_value(true)
            .help("Filesystem directory where to log all scribe writes"),
    )
}

fn add_rendezvous_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(RENDEZVOUS_FREE_CONNECTIONS)
            .long(RENDEZVOUS_FREE_CONNECTIONS)
            .takes_value(true)
            .default_value("5")
            .help("How many concurrent connections to allow before batching kicks in"),
    )
}

fn add_megarepo_svc_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(WITH_TEST_MEGAREPO_CONFIGS_CLIENT)
            .long(WITH_TEST_MEGAREPO_CONFIGS_CLIENT)
            .help(
                "whether to instantiate test-style MononokeMegarepoConfigs. \
                     Prod-style instance reads/writes from/to configerator and \
                     requires fb environment to work properly.",
            )
            .takes_value(true)
            .possible_values(&["true", "false"])
            .default_value("false"),
    )
}

fn add_derivation_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(DERIVE_REMOTELY)
            .long(DERIVE_REMOTELY)
            .help("Derive data remotely using default service"),
    )
    .arg(
        Arg::with_name(DERIVE_REMOTELY_TIER)
            .long(DERIVE_REMOTELY_TIER)
            .takes_value(true)
            .value_name("SMC")
            .help("Specify smc tier for derived data service"),
    )
}

fn add_acls_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
    app.arg(
        Arg::with_name(ACL_FILE)
            .long(ACL_FILE)
            .takes_value(true)
            .value_name("PATH")
            .help("Specify a file containing ACLs"),
    )
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::time::Duration;

use clap_old::App;
use clap_old::Arg;
use clap_old::ArgMatches;
pub use cmdlib_caching::CachelibSettings;
use environment::Caching;
pub use environment::LocalCacheConfig;
use fbinit::FacebookInit;
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;

const CACHE_MODE: &str = "cache-mode";
const CACHE_SIZE_GB: &str = "cache-size-gb";
const USE_TUPPERWARE_SHRINKER: &str = "use-tupperware-shrinker";
const MAX_PROCESS_SIZE: &str = "max-process-size";
const MIN_PROCESS_SIZE: &str = "min-process-size";
const SKIP_CACHING: &str = "skip-caching";
const CACHELIB_DISABLE_CACHEADMIN: &str = "cachelib-disable-cacheadmin";
const CACHELIB_SHARDS: &str = "cachelib-shards";
const CACHELIB_REBALANCING_USE_LRU: &str = "cachelib-rebalancing-use-lru";
const CACHELIB_REBALANCING_INTERVAL: &str = "cachelib-rebalancing-interval-secs";

const PHASES_CACHE_SIZE: &str = "phases-cache-size";
const SEGMENTED_CHANGELOG_CACHE_SIZE: &str = "segmented-changelog-cache-size";
const GLOBALREVS_CACHE_SIZE: &str = "globalrevs-cache-size";
const SVNREVS_CACHE_SIZE: &str = "svnrevs-cache-size";
const BUCKETS_POWER: &str = "buckets-power";

const ONE_GIB: usize = 1073741824; // 2^30 aka 1GiB

const CACHE_ARGS: &[(&str, &str)] = &[
    ("blob-cache-size", "override size of the blob cache"),
    (
        "presence-cache-size",
        "override size of the blob presence cache",
    ),
    (
        "changesets-cache-size",
        "override size of the changesets cache",
    ),
    (
        "commit-graph-cache-size",
        "override size of the commit graph cache",
    ),
    (
        "filenodes-cache-size",
        "override size of the filenodes cache (individual filenodes)",
    ),
    (
        "filenodes-history-cache-size",
        "override size of the filenodes history cache (entire batches of history for a node)",
    ),
    (
        "idmapping-cache-size",
        "override size of the bonsai/hg mapping cache",
    ),
    (
        GLOBALREVS_CACHE_SIZE,
        "override size of the bonsai/globalrev mapping cache",
    ),
    (
        SVNREVS_CACHE_SIZE,
        "override size of the bonsai/svnrev mapping cache",
    ),
    (PHASES_CACHE_SIZE, "override size of the phases cache"),
    (
        BUCKETS_POWER,
        "override the bucket power for cachelib's hashtable",
    ),
    (
        SEGMENTED_CHANGELOG_CACHE_SIZE,
        "override the size of the segmented changelog cache",
    ),
];

pub(crate) fn add_cachelib_args<'a, 'b>(
    app: App<'a, 'b>,
    hide_advanced_args: bool,
    defaults: CachelibSettings,
) -> App<'a, 'b> {
    let cache_args: Vec<_> = CACHE_ARGS
        .iter()
        .map(|(flag, help)| {
            // XXX figure out a way to get default values in here -- note that .default_value
            // takes a &'a str, so we may need to have MononokeAppBuilder own it or similar.
            Arg::with_name(flag)
                .long(flag)
                .value_name("SIZE")
                .hidden(hide_advanced_args)
                .help(help)
        })
        .collect();

    // Computed default_value and help strings with lifetime 'b is problematic, so use once_cell
    static MIN_PROCESS_SIZE_HELP: Lazy<String> = Lazy::new(|| {
        format!(
            "process size at which cachelib will grow back to {} in GiB",
            CACHE_SIZE_GB
        )
    });

    static CACHE_SIZE_GB_DEFAULT: OnceCell<String> = OnceCell::new();

    app.arg(
        Arg::with_name(CACHE_SIZE_GB)
            .long(CACHE_SIZE_GB)
            .takes_value(true)
            .value_name("SIZE")
            .default_value(
                CACHE_SIZE_GB_DEFAULT.get_or_init(|| (defaults.cache_size / ONE_GIB).to_string()),
            )
            .help("size of the cachelib cache, in GiB"),
    )
    .arg(
        Arg::with_name(USE_TUPPERWARE_SHRINKER)
            .long(USE_TUPPERWARE_SHRINKER)
            .help("Use the Tupperware-aware cache shrinker to avoid OOM"),
    )
    .arg(
        Arg::with_name(MAX_PROCESS_SIZE)
            .long(MAX_PROCESS_SIZE)
            .takes_value(true)
            .value_name("SIZE")
            .help("process size at which cachelib will shrink, in GiB"),
    )
    .arg(
        Arg::with_name(MIN_PROCESS_SIZE)
            .long(MIN_PROCESS_SIZE)
            .takes_value(true)
            .value_name("SIZE")
            .help(&MIN_PROCESS_SIZE_HELP),
    )
    .arg(
        Arg::with_name(CACHELIB_DISABLE_CACHEADMIN)
            .long(CACHELIB_DISABLE_CACHEADMIN)
            .help("Disable cacheadmin for cachelib"),
    )
    .arg(
        Arg::with_name(CACHELIB_REBALANCING_USE_LRU)
            .long(CACHELIB_REBALANCING_USE_LRU)
            .help("Whether to ensure that objects of all size enjoy a similar LRU policy"),
    )
    .arg(
        Arg::with_name(CACHELIB_REBALANCING_INTERVAL)
            .long(CACHELIB_REBALANCING_INTERVAL)
            .takes_value(true)
            .help("How often to rebalance across allocation classes"),
    )
    .arg(
        Arg::with_name(CACHE_MODE)
            .long(CACHE_MODE)
            .possible_values(&["enabled", "local-only", "disabled"])
            .default_value("enabled")
            .help("Mode to initialize caching in"),
    )
    .arg(
        Arg::with_name(SKIP_CACHING)
            .long(SKIP_CACHING)
            .hidden(true)
            .help("do not init cachelib and disable caches (useful for tests)"),
    )
    .arg(
        Arg::with_name(CACHELIB_SHARDS)
            .long(CACHELIB_SHARDS)
            .takes_value(true)
            .help("number of shards to control concurrent access to a blobstore behind cachelib"),
    )
    .args(&cache_args)
}

pub(crate) fn parse_cachelib_shards(matches: &ArgMatches<'_>) -> LocalCacheConfig {
    let blobstore_cache_shards = match matches.value_of(CACHELIB_SHARDS) {
        Some(v) => v.parse().unwrap(),
        None => 0,
    };
    LocalCacheConfig {
        blobstore_cache_shards,
    }
}

fn parse_caching(matches: &ArgMatches<'_>) -> Caching {
    let cache_mode = matches.value_of(CACHE_MODE);
    if cache_mode == Some("disabled") {
        Caching::Disabled
    } else if cache_mode == Some("local-only") {
        Caching::LocalOnly(parse_cachelib_shards(matches))
    } else {
        Caching::Enabled(parse_cachelib_shards(matches))
    }
}

/// Provide a way for binaries to specify if they have different default cachelib settings
pub fn parse_and_init_cachelib(
    fb: FacebookInit,
    matches: &ArgMatches<'_>,
    mut settings: CachelibSettings,
) -> Caching {
    let caching = parse_caching(matches);

    match caching {
        Caching::Enabled(..) | Caching::LocalOnly(..) => {
            if let Some(cache_size) = matches.value_of(CACHE_SIZE_GB) {
                settings.cache_size =
                    (cache_size.parse::<f64>().unwrap() * ONE_GIB as f64) as usize;
            }
            if let Some(max_process_size) = matches.value_of(MAX_PROCESS_SIZE) {
                settings.max_process_size_gib = Some(max_process_size.parse().unwrap());
            }
            if let Some(min_process_size) = matches.value_of(MIN_PROCESS_SIZE) {
                settings.min_process_size_gib = Some(min_process_size.parse().unwrap());
            }
            settings.use_tupperware_shrinker = matches.is_present(USE_TUPPERWARE_SHRINKER);
            if let Some(presence_cache_size) = matches.value_of("presence-cache-size") {
                settings.presence_cache_size = Some(presence_cache_size.parse().unwrap());
            }
            if let Some(changesets_cache_size) = matches.value_of("changesets-cache-size") {
                settings.changesets_cache_size = Some(changesets_cache_size.parse().unwrap());
            }
            if let Some(commit_graph_cache_size) = matches.value_of("commit-graph-cache-size") {
                settings.commit_graph_cache_size = Some(commit_graph_cache_size.parse().unwrap());
            }
            if let Some(filenodes_cache_size) = matches.value_of("filenodes-cache-size") {
                settings.filenodes_cache_size = Some(filenodes_cache_size.parse().unwrap());
            }
            if let Some(filenodes_history_cache_size) =
                matches.value_of("filenodes-history-cache-size")
            {
                settings.filenodes_history_cache_size =
                    Some(filenodes_history_cache_size.parse().unwrap());
            }
            if let Some(idmapping_cache_size) = matches.value_of("idmapping-cache-size") {
                settings.idmapping_cache_size = Some(idmapping_cache_size.parse().unwrap());
            }
            if let Some(globalrev_cache_size) = matches.value_of(GLOBALREVS_CACHE_SIZE) {
                settings.globalrev_cache_size = Some(globalrev_cache_size.parse().unwrap());
            }
            if let Some(svnrev_cache_size) = matches.value_of(SVNREVS_CACHE_SIZE) {
                settings.svnrev_cache_size = Some(svnrev_cache_size.parse().unwrap());
            }
            if let Some(blob_cache_size) = matches.value_of("blob-cache-size") {
                settings.blob_cache_size = Some(blob_cache_size.parse().unwrap());
            }
            if let Some(phases_cache_size) = matches.value_of(PHASES_CACHE_SIZE) {
                settings.phases_cache_size = Some(phases_cache_size.parse().unwrap());
            }
            if let Some(segmented_changelog_cache_size) =
                matches.value_of(SEGMENTED_CHANGELOG_CACHE_SIZE)
            {
                settings.segmented_changelog_cache_size =
                    Some(segmented_changelog_cache_size.parse().unwrap());
            }
            if let Some(buckets_power) = matches.value_of(BUCKETS_POWER) {
                settings.buckets_power = Some(buckets_power.parse().unwrap());
            }
            settings.rebalancing_use_lru = matches.is_present(CACHELIB_REBALANCING_USE_LRU);
            if let Some(freq) = matches.value_of(CACHELIB_REBALANCING_INTERVAL) {
                settings.rebalancing_interval = Duration::from_secs(freq.parse().unwrap());
            }

            #[cfg(not(fbcode_build))]
            {
                let _ = fb;
                unimplemented!("Initialization of cachelib works only for fbcode builds")
            }
            #[cfg(fbcode_build)]
            {
                let enable_cacheadmin = !matches.is_present(CACHELIB_DISABLE_CACHEADMIN);
                cmdlib_caching::facebook::init_cachelib_from_settings(
                    fb,
                    settings,
                    enable_cacheadmin,
                )
                .unwrap();
            }
        }
        Caching::Disabled => {
            // No-op
        }
    };

    caching
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Borrow;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::hash::Hash;
use std::hash::Hasher;
use std::num::NonZeroU32;
use std::num::NonZeroUsize;
use std::panic::RefUnwindSafe;
use std::panic::UnwindSafe;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use anyhow::bail;
use anyhow::format_err;
use anyhow::Context;
use anyhow::Error;
use anyhow::Result;
use blobstore_factory::BlobstoreOptions;
use blobstore_factory::CachelibBlobstoreOptions;
use blobstore_factory::ChaosOptions;
use blobstore_factory::DelayOptions;
use blobstore_factory::PackOptions;
use blobstore_factory::PutBehaviour;
use blobstore_factory::ScrubAction;
use blobstore_factory::SrubWriteOnly;
use blobstore_factory::ThrottleOptions;
use cached_config::ConfigHandle;
use cached_config::ConfigStore;
use clap_old::ArgMatches;
use clap_old::OsValues;
use clap_old::Values;
use derived_data_remote::Address;
use derived_data_remote::RemoteDerivationOptions;
use environment::Caching;
use environment::MononokeEnvironment;
use fbinit::FacebookInit;
use maybe_owned::MaybeOwned;
use megarepo_config::MononokeMegarepoConfigsOptions;
use metaconfig_types::PackFormat;
use mononoke_app::args::parse_config_spec_to_path;
use observability::DynamicLevelDrain;
use observability::ObservabilityContext;
use panichandler::Fate;
use permission_checker::AclProvider;
use permission_checker::DefaultAclProvider;
use permission_checker::InternalAclProvider;
use rendezvous::RendezVousOptions;
use repo_factory::ReadOnlyStorage;
use scuba_ext::MononokeScubaSampleBuilder;
use slog::debug;
use slog::o;
use slog::Level;
use slog::Logger;
use slog::Never;
use slog::Record;
use slog::SendSyncRefUnwindSafeDrain;
use slog_ext::make_tag_filter_drain;
use slog_glog_fmt::kv_categorizer::FacebookCategorizer;
use slog_glog_fmt::kv_defaults::FacebookKV;
use slog_glog_fmt::GlogFormat;
use slog_term::TermDecorator;
use sql_ext::facebook::MysqlOptions;
use sql_ext::facebook::PoolConfig;
use sql_ext::facebook::ReadConnectionType;
use tokio::runtime::Handle;
use tokio::runtime::Runtime;
use tunables::init_tunables_worker;
use tunables::tunables;

pub type Normal = rand_distr::Normal<f64>;
use super::app::ArgType;
use super::app::MononokeAppData;
use super::app::ACL_FILE;
use super::app::BLOBSTORE_BYTES_MIN_THROTTLE_ARG;
use super::app::BLOBSTORE_PUT_BEHAVIOUR_ARG;
use super::app::BLOBSTORE_SCRUB_ACTION_ARG;
use super::app::BLOBSTORE_SCRUB_GRACE_ARG;
use super::app::BLOBSTORE_SCRUB_QUEUE_PEEK_BOUND_ARG;
use super::app::BLOBSTORE_SCRUB_WRITE_ONLY_MISSING_ARG;
use super::app::CACHELIB_ATTEMPT_ZSTD_ARG;
use super::app::CRYPTO_PATH_REGEX_ARG;
use super::app::DERIVE_REMOTELY;
use super::app::DERIVE_REMOTELY_TIER;
use super::app::DISABLE_TUNABLES;
use super::app::ENABLE_MCROUTER;
use super::app::GET_MEAN_DELAY_SECS_ARG;
use super::app::GET_STDDEV_DELAY_SECS_ARG;
use super::app::LOCAL_CONFIGERATOR_PATH_ARG;
use super::app::LOGVIEW_ADDITIONAL_LEVEL_FILTER;
use super::app::LOGVIEW_CATEGORY;
use super::app::LOG_EXCLUDE_TAG;
use super::app::LOG_INCLUDE_TAG;
use super::app::MYSQL_CONN_OPEN_TIMEOUT;
use super::app::MYSQL_MASTER_ONLY;
use super::app::MYSQL_MAX_QUERY_TIME;
use super::app::MYSQL_POOL_AGE_TIMEOUT;
use super::app::MYSQL_POOL_IDLE_TIMEOUT;
use super::app::MYSQL_POOL_LIMIT;
use super::app::MYSQL_POOL_PER_KEY_LIMIT;
use super::app::MYSQL_POOL_THREADS_NUM;
use super::app::MYSQL_SQLBLOB_POOL_AGE_TIMEOUT;
use super::app::MYSQL_SQLBLOB_POOL_IDLE_TIMEOUT;
use super::app::MYSQL_SQLBLOB_POOL_LIMIT;
use super::app::MYSQL_SQLBLOB_POOL_PER_KEY_LIMIT;
use super::app::MYSQL_SQLBLOB_POOL_THREADS_NUM;
use super::app::NO_DEFAULT_SCUBA_DATASET_ARG;
use super::app::PUT_MEAN_DELAY_SECS_ARG;
use super::app::PUT_STDDEV_DELAY_SECS_ARG;
use super::app::READ_BURST_BYTES_ARG;
use super::app::READ_BYTES_ARG;
use super::app::READ_CHAOS_ARG;
use super::app::READ_QPS_ARG;
use super::app::RENDEZVOUS_FREE_CONNECTIONS;
use super::app::RUNTIME_THREADS;
use super::app::SCUBA_DATASET_ARG;
use super::app::SCUBA_LOG_FILE_ARG;
use super::app::TUNABLES_CONFIG;
use super::app::TUNABLES_LOCAL_PATH;
use super::app::WARM_BOOKMARK_CACHE_SCUBA_DATASET_ARG;
use super::app::WITH_DYNAMIC_OBSERVABILITY;
use super::app::WITH_READONLY_STORAGE_ARG;
use super::app::WITH_TEST_MEGAREPO_CONFIGS_CLIENT;
use super::app::WRITE_BURST_BYTES_ARG;
use super::app::WRITE_BYTES_ARG;
use super::app::WRITE_CHAOS_ARG;
use super::app::WRITE_QPS_ARG;
use super::app::WRITE_ZSTD_ARG;
use super::app::WRITE_ZSTD_LEVEL_ARG;
use super::cache::parse_and_init_cachelib;
use crate::helpers::create_runtime;

trait Drain =
    slog::Drain<Ok = (), Err = Never> + Send + Sync + UnwindSafe + RefUnwindSafe + 'static;

const DEFAULT_TUNABLES_PATH: &str = "scm/mononoke/tunables/default";
const CRYPTO_PROJECT: &str = "SCM";

const CONFIGERATOR_POLL_INTERVAL: Duration = Duration::from_secs(1);
const CONFIGERATOR_REFRESH_TIMEOUT: Duration = Duration::from_secs(1);

pub struct MononokeMatches<'a> {
    matches: MaybeOwned<'a, ArgMatches<'a>>,
    app_data: MononokeAppData,
    environment: Arc<MononokeEnvironment>,
}

impl<'a> MononokeMatches<'a> {
    /// Due to global log init this can be called only once per process and will error otherwise
    pub fn new(
        fb: FacebookInit,
        matches: ArgMatches<'a>,
        app_data: MononokeAppData,
        arg_types: HashSet<ArgType>,
    ) -> Result<(Self, Runtime), Error> {
        let log_level = get_log_level(&matches);

        let log_filter_fn: Option<fn(&Record) -> bool> = app_data.slog_filter_fn;
        let root_log_drain = create_root_log_drain(fb, &matches, log_level, log_filter_fn)
            .context("Failed to create root log drain")?;
        #[cfg(fbcode_build)]
        cmdlib_logging::glog::set_glog_log_level(fb, log_level)?;

        // TODO: FacebookKV for this one?
        let config_store =
            create_config_store(fb, Logger::root(root_log_drain.clone(), o![]), &matches)
                .context("Failed to create config store")?;

        let observability_context =
            create_observability_context(&matches, &config_store, log_level)
                .context("Faled to initialize observability context")?;

        let logger = create_logger(&matches, root_log_drain, observability_context.clone())
            .context("Failed to create logger")?;
        let scuba_sample_builder =
            create_scuba_sample_builder(fb, &matches, &app_data, &observability_context)
                .context("Failed to create scuba sample builder")?;

        let warm_bookmarks_cache_scuba_sample_builder =
            create_warm_bookmark_cache_scuba_sample_builder(fb, &matches)
                .context("Failed to create warm bookmark cache scuba sample builder")?;

        let caching = parse_and_init_cachelib(fb, &matches, app_data.cachelib_settings.clone());

        let runtime = init_runtime(&matches).context("Failed to create Tokio runtime")?;

        init_tunables(
            &matches,
            &config_store,
            logger.clone(),
            runtime.handle().clone(),
        )
        .context("Failed to initialize tunables")?;

        let mysql_options =
            parse_mysql_options(&matches, &app_data).context("Failed to parse MySQL options")?;
        let blobstore_options = parse_blobstore_options(&matches, &app_data, &arg_types)
            .context("Failed to parse blobstore options")?;
        let readonly_storage =
            parse_readonly_storage(&matches).context("Failed to parse readonly storage options")?;
        let rendezvous_options =
            parse_rendezvous_options(&matches).context("Failed to parse rendezvous options")?;
        let megarepo_configs_options = parse_mononoke_megarepo_configs_options(&matches)?;
        let remote_derivation_options = parse_remote_derivation_options(&matches)?;
        let acl_provider = create_acl_provider(fb, &matches)?;

        maybe_enable_mcrouter(fb, &matches, &arg_types);

        Ok((
            MononokeMatches {
                matches: MaybeOwned::from(matches),
                environment: Arc::new(MononokeEnvironment {
                    fb,
                    logger,
                    scuba_sample_builder,
                    warm_bookmarks_cache_scuba_sample_builder,
                    config_store,
                    caching,
                    observability_context,
                    runtime: runtime.handle().clone(),
                    mysql_options,
                    blobstore_options,
                    readonly_storage,
                    acl_provider,
                    rendezvous_options,
                    megarepo_configs_options,
                    remote_derivation_options,
                    disabled_hooks: HashMap::new(),
                    warm_bookmarks_cache_derived_data: None,
                    filter_repos: None,
                }),
                app_data,
            },
            runtime,
        ))
    }

    pub fn app_data(&self) -> &MononokeAppData {
        &self.app_data
    }

    pub fn environment(&self) -> &Arc<MononokeEnvironment> {
        &self.environment
    }

    pub fn caching(&self) -> Caching {
        self.environment.caching
    }

    pub fn config_store(&self) -> &ConfigStore {
        &self.environment.config_store
    }

    pub fn runtime(&self) -> &Handle {
        &self.environment.runtime
    }

    pub fn logger(&self) -> &Logger {
        &self.environment.logger
    }

    pub fn mysql_options(&self) -> &MysqlOptions {
        &self.environment.mysql_options
    }

    pub fn blobstore_options(&self) -> &BlobstoreOptions {
        &self.environment.blobstore_options
    }

    pub fn readonly_storage(&self) -> &ReadOnlyStorage {
        &self.environment.readonly_storage
    }

    pub fn acl_provider(&self) -> &Arc<dyn AclProvider> {
        &self.environment.acl_provider
    }

    pub fn scuba_sample_builder(&self) -> MononokeScubaSampleBuilder {
        self.environment.scuba_sample_builder.clone()
    }

    pub fn warm_bookmarks_cache_scuba_sample_builder(&self) -> MononokeScubaSampleBuilder {
        self.environment
            .warm_bookmarks_cache_scuba_sample_builder
            .clone()
    }

    // Delegate some common methods to save on .as_ref() calls
    pub fn is_present<S: AsRef<str>>(&self, name: S) -> bool {
        self.matches.is_present(name)
    }

    pub fn subcommand(&'a self) -> (&str, Option<&'a ArgMatches<'a>>) {
        self.matches.subcommand()
    }

    pub fn usage(&self) -> &str {
        self.matches.usage()
    }

    pub fn value_of<S: AsRef<str>>(&self, name: S) -> Option<&str> {
        self.matches.value_of(name)
    }

    pub fn value_of_os<S: AsRef<str>>(&self, name: S) -> Option<&OsStr> {
        self.matches.value_of_os(name)
    }

    pub fn values_of<S: AsRef<str>>(&'a self, name: S) -> Option<Values<'a>> {
        self.matches.values_of(name)
    }

    pub fn values_of_os<S: AsRef<str>>(&'a self, name: S) -> Option<OsValues<'a>> {
        self.matches.values_of_os(name)
    }
}

impl<'a> AsRef<ArgMatches<'a>> for MononokeMatches<'a> {
    fn as_ref(&self) -> &ArgMatches<'a> {
        &self.matches
    }
}

impl<'a> Borrow<ArgMatches<'a>> for MononokeMatches<'a> {
    fn borrow(&self) -> &ArgMatches<'a> {
        &self.matches
    }
}

/// Create a default root logger for Facebook services
fn glog_drain() -> impl Drain {
    let decorator = TermDecorator::new().build();
    // FacebookCategorizer is used for slog KV arguments.
    // At the time of writing this code FacebookCategorizer and FacebookKV
    // that was added below was mainly useful for logview logging and had no effect on GlogFormat
    let drain = GlogFormat::new(decorator, FacebookCategorizer).ignore_res();
    ::std::sync::Mutex::new(drain).ignore_res()
}

fn get_log_level(matches: &ArgMatches<'_>) -> Level {
    if matches.is_present("debug") {
        Level::Debug
    } else {
        match matches.value_of("log-level") {
            Some(log_level_str) => Level::from_str(log_level_str)
                .unwrap_or_else(|_| panic!("Unknown log level: {}", log_level_str)),
            None => Level::Info,
        }
    }
}

fn create_root_log_drain(
    fb: FacebookInit,
    matches: &ArgMatches<'_>,
    log_level: Level,
    log_filter_fn: Option<fn(&Record) -> bool>,
) -> Result<impl Drain + Clone> {
    // Set the panic handler up here. Not really relevent to logger other than it emits output
    // when things go wrong. This writes directly to stderr as coredumper expects.
    let fate = match matches
        .value_of("panic-fate")
        .expect("no default on panic-fate")
    {
        "none" => None,
        "continue" => Some(Fate::Continue),
        "exit" => Some(Fate::Exit(101)),
        "abort" => Some(Fate::Abort),
        bad => bail!("bad panic-fate {}", bad),
    };
    if let Some(fate) = fate {
        panichandler::set_panichandler(fate);
    }

    let stdlog_env = "RUST_LOG";

    let glog_drain = make_tag_filter_drain(
        glog_drain(),
        matches
            .values_of(LOG_INCLUDE_TAG)
            .map(|v| v.map(|v| v.to_string()).collect())
            .unwrap_or_default(),
        matches
            .values_of(LOG_EXCLUDE_TAG)
            .map(|v| v.map(|v| v.to_string()).collect())
            .unwrap_or_default(),
        true, // Log messages which have no tags
    )?;

    let root_log_drain: Arc<dyn SendSyncRefUnwindSafeDrain<Ok = (), Err = Never>> =
        match matches.value_of(LOGVIEW_CATEGORY) {
            Some(category) => {
                #[cfg(fbcode_build)]
                {
                    // Sometimes scribe writes can fail due to backpressure - it's OK to drop these
                    // since logview is sampled anyway.
                    let mut logview_drain = ::slog_logview::LogViewDrain::new(fb, category);
                    logview_drain.with_thrift_serialization();
                    let logview_drain = logview_drain.ignore_res();
                    match matches.value_of(LOGVIEW_ADDITIONAL_LEVEL_FILTER) {
                        Some(log_level_str) => {
                            let logview_level = Level::from_str(log_level_str)
                                .map_err(|_| format_err!("Unknown log level: {}", log_level_str))?;

                            let drain = slog::Duplicate::new(
                                glog_drain,
                                logview_drain.filter_level(logview_level).ignore_res(),
                            );
                            Arc::new(drain.ignore_res())
                        }
                        None => {
                            let drain = slog::Duplicate::new(glog_drain, logview_drain);
                            Arc::new(drain.ignore_res())
                        }
                    }
                }
                #[cfg(not(fbcode_build))]
                {
                    let _unused = LOGVIEW_ADDITIONAL_LEVEL_FILTER;
                    let _ = (fb, category);
                    unimplemented!(
                        "Passed --{}, but it is supported only for fbcode builds",
                        LOGVIEW_CATEGORY
                    )
                }
            }
            None => Arc::new(glog_drain),
        };

    let root_log_drain = if let Some(filter_fn) = log_filter_fn {
        Arc::new(slog::IgnoreResult::new(slog::Filter::new(
            root_log_drain,
            filter_fn,
        )))
    } else {
        root_log_drain
    };

    // NOTE: We pass an unfiltered Logger to init_stdlog_once. That's because we do the filtering
    // at the stdlog level there.
    let stdlog_logger = Logger::root(root_log_drain.clone(), o![]);
    let stdlog_level = cmdlib_logging::log::init_stdlog_once(stdlog_logger, stdlog_env)?;

    // Note what level we enabled stdlog at, so that if someone is trying to debug they get
    // informed of potentially needing to set RUST_LOG.
    debug!(
        Logger::root(
            root_log_drain.clone().filter_level(log_level).ignore_res(),
            o![]
        ),
        "enabled stdlog with level: {:?} (set {} to configure)", stdlog_level, stdlog_env
    );

    Ok(root_log_drain)
}

fn create_logger(
    matches: &ArgMatches<'_>,
    root_log_drain: impl Drain + Clone,
    observability_context: ObservabilityContext,
) -> Result<Logger> {
    let root_log_drain = DynamicLevelDrain::new(root_log_drain, observability_context);

    let kv = FacebookKV::new().context("Failed to initialize FacebookKV")?;

    let logger = if matches.is_present("fb303-thrift-port") {
        Logger::root(slog_stats::StatsDrain::new(root_log_drain), o![kv])
    } else {
        Logger::root(root_log_drain, o![kv])
    };

    Ok(logger)
}

fn create_scuba_sample_builder(
    fb: FacebookInit,
    matches: &ArgMatches<'_>,
    app_data: &MononokeAppData,
    observability_context: &ObservabilityContext,
) -> Result<MononokeScubaSampleBuilder> {
    let mut scuba_logger = if let Some(scuba_dataset) = matches.value_of(SCUBA_DATASET_ARG) {
        MononokeScubaSampleBuilder::new(fb, scuba_dataset)?
    } else if let Some(default_scuba_dataset) = app_data.default_scuba_dataset.as_ref() {
        if matches.is_present(NO_DEFAULT_SCUBA_DATASET_ARG) {
            MononokeScubaSampleBuilder::with_discard()
        } else {
            MononokeScubaSampleBuilder::new(fb, default_scuba_dataset)?
        }
    } else {
        MononokeScubaSampleBuilder::with_discard()
    };
    if let Some(scuba_log_file) = matches.value_of(SCUBA_LOG_FILE_ARG) {
        scuba_logger = scuba_logger.with_log_file(scuba_log_file)?;
    }
    let mut scuba_logger = scuba_logger
        .with_observability_context(observability_context.clone())
        .with_seq("seq");

    scuba_logger.add_common_server_data();

    Ok(scuba_logger)
}

fn create_warm_bookmark_cache_scuba_sample_builder(
    fb: FacebookInit,
    matches: &ArgMatches<'_>,
) -> Result<MononokeScubaSampleBuilder, Error> {
    let maybe_scuba = match matches
        .value_of(WARM_BOOKMARK_CACHE_SCUBA_DATASET_ARG)
        .map(|s| s.to_string())
    {
        Some(scuba) => {
            let hostname = hostname::get_hostname()?;
            let sampling_pct = tunables()
                .warm_bookmark_cache_logging_sampling_pct()
                .unwrap_or_default() as u64;

            let mut hasher = DefaultHasher::new();
            hostname.hash(&mut hasher);

            if hasher.finish() % 100 < sampling_pct {
                Some(scuba)
            } else {
                None
            }
        }
        None => None,
    };

    MononokeScubaSampleBuilder::with_opt_table(fb, maybe_scuba)
}

fn parse_readonly_storage(matches: &ArgMatches<'_>) -> Result<ReadOnlyStorage, Error> {
    let ro = matches
        .value_of(WITH_READONLY_STORAGE_ARG)
        .map(|v| v.parse())
        .transpose()
        .with_context(|| format!("Provided {} is not bool", WITH_READONLY_STORAGE_ARG))?
        .unwrap_or(false);
    Ok(ReadOnlyStorage(ro))
}

fn parse_mysql_pool_options(matches: &ArgMatches<'_>) -> Result<PoolConfig, Error> {
    let size: usize = matches
        .value_of(MYSQL_POOL_LIMIT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-limit is not usize")?;
    let threads_num: i32 = matches
        .value_of(MYSQL_POOL_THREADS_NUM)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-threads-num is not i32")?;
    let per_key_limit: u64 = matches
        .value_of(MYSQL_POOL_PER_KEY_LIMIT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-per-key-limit is not u64")?;
    let conn_age_timeout: u64 = matches
        .value_of(MYSQL_POOL_AGE_TIMEOUT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-age-timeout is not u64")?;
    let conn_idle_timeout: u64 = matches
        .value_of(MYSQL_POOL_IDLE_TIMEOUT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-limit is not usize")?;
    let conn_open_timeout: u64 = matches
        .value_of(MYSQL_CONN_OPEN_TIMEOUT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-conn-open-timeout is not u64")?;
    let max_query_time: Duration = Duration::from_millis(
        matches
            .value_of(MYSQL_MAX_QUERY_TIME)
            .expect("A default is set, should never be None")
            .parse()
            .context("Provided mysql-query-time-limit is not u64")?,
    );

    Ok(PoolConfig::new(
        size,
        threads_num,
        per_key_limit,
        conn_age_timeout,
        conn_idle_timeout,
        conn_open_timeout,
        max_query_time,
    ))
}

fn parse_mysql_options(
    matches: &ArgMatches<'_>,
    app_data: &MononokeAppData,
) -> Result<MysqlOptions, Error> {
    let pool = app_data.global_mysql_connection_pool.clone();
    let pool_config =
        parse_mysql_pool_options(matches).context("Failed to parse MySQL pool options")?;
    let read_connection_type = if matches.is_present(MYSQL_MASTER_ONLY) {
        ReadConnectionType::Master
    } else {
        ReadConnectionType::ReplicaOnly
    };

    Ok(MysqlOptions {
        pool,
        pool_config,
        read_connection_type,
    })
}

fn parse_mysql_sqlblob_pool_options(matches: &ArgMatches<'_>) -> Result<PoolConfig, Error> {
    let size: usize = matches
        .value_of(MYSQL_SQLBLOB_POOL_LIMIT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-limit is not usize")?;
    let threads_num: i32 = matches
        .value_of(MYSQL_SQLBLOB_POOL_THREADS_NUM)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-threads-num is not i32")?;
    let per_key_limit: u64 = matches
        .value_of(MYSQL_SQLBLOB_POOL_PER_KEY_LIMIT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-per-key-limit is not u64")?;
    let conn_age_timeout: u64 = matches
        .value_of(MYSQL_SQLBLOB_POOL_AGE_TIMEOUT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-age-timeout is not u64")?;
    let conn_idle_timeout: u64 = matches
        .value_of(MYSQL_SQLBLOB_POOL_IDLE_TIMEOUT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-pool-limit is not usize")?;
    let conn_open_timeout: u64 = matches
        .value_of(MYSQL_CONN_OPEN_TIMEOUT)
        .expect("A default is set, should never be None")
        .parse()
        .context("Provided mysql-conn-open-timeout is not u64")?;
    let max_query_time: Duration = Duration::from_millis(
        matches
            .value_of(MYSQL_MAX_QUERY_TIME)
            .expect("A default is set, should never be None")
            .parse()
            .context("Provided mysql-query-time-limit is not u64")?,
    );

    Ok(PoolConfig::new(
        size,
        threads_num,
        per_key_limit,
        conn_age_timeout,
        conn_idle_timeout,
        conn_open_timeout,
        max_query_time,
    ))
}

fn parse_sqlblob_mysql_options(
    matches: &ArgMatches<'_>,
    app_data: &MononokeAppData,
) -> Result<MysqlOptions, Error> {
    let pool = app_data.sqlblob_mysql_connection_pool.clone();
    let pool_config = parse_mysql_sqlblob_pool_options(matches)?;
    let read_connection_type = if matches.is_present(MYSQL_MASTER_ONLY) {
        ReadConnectionType::Master
    } else {
        ReadConnectionType::ReplicaOnly
    };

    Ok(MysqlOptions {
        pool,
        pool_config,
        read_connection_type,
    })
}

fn parse_blobstore_options(
    matches: &ArgMatches<'_>,
    app_data: &MononokeAppData,
    arg_types: &HashSet<ArgType>,
) -> Result<BlobstoreOptions, Error> {
    let read_qps: Option<NonZeroU32> = matches
        .value_of(READ_QPS_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided qps is not u32")?;

    let write_qps: Option<NonZeroU32> = matches
        .value_of(WRITE_QPS_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided qps is not u32")?;

    let read_bytes: Option<NonZeroUsize> = matches
        .value_of(READ_BYTES_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided Bytes/s is not usize")?;

    let write_bytes: Option<NonZeroUsize> = matches
        .value_of(WRITE_BYTES_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided Bytes/s is not usize")?;

    let read_burst_bytes: Option<NonZeroUsize> = matches
        .value_of(READ_BURST_BYTES_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided Bytes/s is not usize")?;

    let write_burst_bytes: Option<NonZeroUsize> = matches
        .value_of(WRITE_BURST_BYTES_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided Bytes/s is not usize")?;

    let bytes_min_count: Option<NonZeroUsize> = matches
        .value_of(BLOBSTORE_BYTES_MIN_THROTTLE_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided Bytes/s is not usize")?;

    let read_chaos: Option<NonZeroU32> = matches
        .value_of(READ_CHAOS_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided chaos is not u32")?;

    let write_chaos: Option<NonZeroU32> = matches
        .value_of(WRITE_CHAOS_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided chaos is not u32")?;

    #[cfg(fbcode_build)]
    let manifold_options = blobstore_factory::ManifoldOptions::parse_args(matches)?;

    let write_zstd: Option<bool> = matches
        .value_of(WRITE_ZSTD_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided value is not bool")?;

    let write_zstd_level: Option<i32> = matches
        .value_of(WRITE_ZSTD_LEVEL_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided Zstd compression level is not i32")?;

    let put_format_override = match (write_zstd, write_zstd_level) {
        (Some(false), Some(level)) => bail!(
            "Doesn't make sense to pass --{}=false with --{}={}",
            WRITE_ZSTD_ARG,
            WRITE_ZSTD_LEVEL_ARG,
            level
        ),
        (Some(false), None) => Some(PackFormat::Raw),
        (Some(true), None) => bail!(
            "When enabling --{} must also pass --{}",
            WRITE_ZSTD_ARG,
            WRITE_ZSTD_LEVEL_ARG
        ),
        (Some(true), Some(v)) => Some(PackFormat::ZstdIndividual(v)),
        (None, Some(level)) => bail!(
            "--{}={} requires --{}",
            WRITE_ZSTD_LEVEL_ARG,
            level,
            WRITE_ZSTD_ARG,
        ),
        (None, None) => None,
    };

    let attempt_zstd: bool = matches
        .value_of(CACHELIB_ATTEMPT_ZSTD_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided blobstore-cachelib-attempt-zstd is not bool")?
        .ok_or_else(|| format_err!("A default is set, should never be None"))?;

    let blobstore_put_behaviour: Option<PutBehaviour> = matches
        .value_of(BLOBSTORE_PUT_BEHAVIOUR_ARG)
        .map(|v| v.parse())
        .transpose()
        .context("Provided blobstore-put-behaviour is not PutBehaviour")?;

    let get_delay =
        parse_norm_distribution(matches, GET_MEAN_DELAY_SECS_ARG, GET_STDDEV_DELAY_SECS_ARG)?;
    let put_delay =
        parse_norm_distribution(matches, PUT_MEAN_DELAY_SECS_ARG, PUT_STDDEV_DELAY_SECS_ARG)?;

    let blobstore_options = BlobstoreOptions::new(
        ChaosOptions::new(read_chaos, write_chaos),
        DelayOptions {
            get_dist: get_delay,
            put_dist: put_delay,
        },
        ThrottleOptions {
            read_qps,
            write_qps,
            read_bytes,
            write_bytes,
            read_burst_bytes,
            write_burst_bytes,
            bytes_min_count,
        },
        #[cfg(fbcode_build)]
        manifold_options,
        PackOptions::new(put_format_override),
        CachelibBlobstoreOptions::new_lazy(Some(attempt_zstd)),
        blobstore_put_behaviour,
        parse_sqlblob_mysql_options(matches, app_data)
            .context("Failed to parse sqlblob MySQL options")?,
    );

    let blobstore_options = if arg_types.contains(&ArgType::Scrub) {
        let scrub_action = matches
            .value_of(BLOBSTORE_SCRUB_ACTION_ARG)
            .map(ScrubAction::from_str)
            .transpose()?;
        let scrub_grace = matches
            .value_of(BLOBSTORE_SCRUB_GRACE_ARG)
            .map(u64::from_str)
            .transpose()?;

        let scrub_action_on_missing_write_only = matches
            .value_of(BLOBSTORE_SCRUB_WRITE_ONLY_MISSING_ARG)
            .map(SrubWriteOnly::from_str)
            .transpose()?;
        let mut blobstore_options = blobstore_options
            .with_scrub_action(scrub_action)
            .with_scrub_grace(scrub_grace);
        if let Some(v) = scrub_action_on_missing_write_only {
            blobstore_options = blobstore_options.with_scrub_action_on_missing_write_only(v)
        }
        let scrub_queue_peek_bound = matches
            .value_of(BLOBSTORE_SCRUB_QUEUE_PEEK_BOUND_ARG)
            .map(u64::from_str)
            .transpose()?;
        if let Some(v) = scrub_queue_peek_bound {
            blobstore_options = blobstore_options.with_scrub_queue_peek_bound(v)
        }
        blobstore_options
    } else {
        blobstore_options
    };

    Ok(blobstore_options)
}

fn parse_norm_distribution(
    matches: &ArgMatches,
    mean_key: &str,
    stddev_key: &str,
) -> Result<Option<Normal>, Error> {
    let put_mean = crate::args::get_and_parse_opt(matches, mean_key);
    let put_stddev = crate::args::get_and_parse_opt(matches, stddev_key);
    match (put_mean, put_stddev) {
        (Some(put_mean), Some(put_stddev)) => {
            let dist = Normal::new(put_mean, put_stddev)
                .map_err(|err| format_err!("can't create normal distribution {:?}", err))?;
            Ok(Some(dist))
        }
        _ => Ok(None),
    }
}

fn parse_rendezvous_options(matches: &ArgMatches<'_>) -> Result<RendezVousOptions, Error> {
    let free_connections = matches
        .value_of(RENDEZVOUS_FREE_CONNECTIONS)
        .expect("A default is set, should never be None")
        .parse()
        .with_context(|| format!("Provided {} is not an integer", RENDEZVOUS_FREE_CONNECTIONS))?;
    Ok(RendezVousOptions { free_connections })
}

fn parse_mononoke_megarepo_configs_options(
    matches: &ArgMatches<'_>,
) -> Result<MononokeMegarepoConfigsOptions, Error> {
    let use_test: bool = matches
        .value_of(WITH_TEST_MEGAREPO_CONFIGS_CLIENT)
        .expect("A default is set, should never be None")
        .parse()
        .with_context(|| {
            format!(
                "Provided {} is not a bool",
                WITH_TEST_MEGAREPO_CONFIGS_CLIENT
            )
        })?;

    if use_test {
        if let Some(path) = matches.value_of(LOCAL_CONFIGERATOR_PATH_ARG) {
            Ok(MononokeMegarepoConfigsOptions::IntegrationTest(path.into()))
        } else {
            Ok(MononokeMegarepoConfigsOptions::UnitTest)
        }
    } else {
        Ok(MononokeMegarepoConfigsOptions::Prod)
    }
}

fn init_tunables<'a>(
    matches: &'a ArgMatches<'a>,
    config_store: &'a ConfigStore,
    logger: Logger,
    handle: Handle,
) -> Result<()> {
    if matches.is_present(DISABLE_TUNABLES) {
        debug!(logger, "Tunables are disabled");
        return Ok(());
    }

    if let Some(tunables_local_path) = matches.value_of(TUNABLES_LOCAL_PATH) {
        let value = std::fs::read_to_string(tunables_local_path)
            .with_context(|| format!("failed to open tunables path {}", tunables_local_path))?;
        let config_handle = ConfigHandle::from_json(&value)
            .with_context(|| format!("failed to parse tunables at path {}", tunables_local_path))?;
        return tunables::init_tunables(&logger, &config_handle);
    }

    let tunables_spec = matches
        .value_of(TUNABLES_CONFIG)
        .unwrap_or(DEFAULT_TUNABLES_PATH);

    let config_handle =
        config_store.get_config_handle(parse_config_spec_to_path(tunables_spec)?)?;

    init_tunables_worker(logger, config_handle, handle)
}

/// Initialize a new `Runtime` with thread number parsed from the CLI
fn init_runtime(matches: &ArgMatches<'_>) -> Result<Runtime> {
    let core_threads = matches
        .value_of(RUNTIME_THREADS)
        .map(|v| v.parse())
        .transpose()
        .with_context(|| format!("Failed to parse {}", RUNTIME_THREADS))?;
    let rt = create_runtime(None, core_threads).context("Failed to create runtime")?;
    Ok(rt)
}

fn create_observability_context<'a>(
    matches: &'a ArgMatches<'a>,
    config_store: &'a ConfigStore,
    log_level: Level,
) -> Result<ObservabilityContext, Error> {
    match matches.value_of(WITH_DYNAMIC_OBSERVABILITY) {
        Some("true") => Ok(ObservabilityContext::new(config_store)?),
        Some("false") | None => Ok(ObservabilityContext::new_static(log_level)),
        Some(other) => panic!(
            "Unexpected --{} value: {}",
            WITH_DYNAMIC_OBSERVABILITY, other
        ),
    }
}

fn create_config_store<'a>(
    fb: FacebookInit,
    logger: Logger,
    matches: &'a ArgMatches<'a>,
) -> Result<ConfigStore, Error> {
    let local_configerator_path = matches.value_of(LOCAL_CONFIGERATOR_PATH_ARG);
    let crypto_regex = matches.values_of(CRYPTO_PATH_REGEX_ARG).map_or(
        vec![
            (
                "scm/mononoke/tunables/.*".to_string(),
                CRYPTO_PROJECT.to_string(),
            ),
            (
                "scm/mononoke/repos/.*".to_string(),
                CRYPTO_PROJECT.to_string(),
            ),
            (
                "scm/mononoke/redaction/.*".to_string(),
                CRYPTO_PROJECT.to_string(),
            ),
            (
                "scm/mononoke/lfs_server/.*".to_string(),
                CRYPTO_PROJECT.to_string(),
            ),
        ],
        |it| {
            it.map(|regex| (regex.to_string(), CRYPTO_PROJECT.to_string()))
                .collect()
        },
    );
    match local_configerator_path {
        // A local configerator path wins
        Some(path) => Ok(ConfigStore::file(
            logger,
            PathBuf::from(path),
            String::new(),
            CONFIGERATOR_POLL_INTERVAL,
        )),
        // Prod instances do have network configerator, with signature checks
        None => ConfigStore::regex_signed_configerator(
            fb,
            logger,
            crypto_regex,
            CONFIGERATOR_POLL_INTERVAL,
            CONFIGERATOR_REFRESH_TIMEOUT,
        ),
    }
}

fn maybe_enable_mcrouter(fb: FacebookInit, matches: &ArgMatches<'_>, arg_types: &HashSet<ArgType>) {
    if !arg_types.contains(&ArgType::McRouter) {
        return;
    }

    if !matches.is_present(ENABLE_MCROUTER) {
        return;
    }

    #[cfg(fbcode_build)]
    {
        ::ratelim::use_proxy_if_available(fb);
    }

    #[cfg(not(fbcode_build))]
    {
        let _ = fb;
        unimplemented!(
            "Passed --{}, but it is supported only for fbcode builds",
            ENABLE_MCROUTER
        );
    }
}

fn parse_remote_derivation_options(
    matches: &ArgMatches<'_>,
) -> Result<RemoteDerivationOptions, Error> {
    let derive_remotely = matches.is_present(DERIVE_REMOTELY);
    let address = match matches
        .value_of(DERIVE_REMOTELY_TIER)
        .map(|s| s.to_string())
    {
        Some(tier) => Address::SmcTier(tier),
        None => Address::Empty,
    };
    Ok(RemoteDerivationOptions {
        derive_remotely,
        address,
    })
}

fn create_acl_provider(
    fb: FacebookInit,
    matches: &ArgMatches<'_>,
) -> Result<Arc<dyn AclProvider>, Error> {
    match matches.value_of(ACL_FILE) {
        Some(file) => InternalAclProvider::from_file(file),
        None => Ok(DefaultAclProvider::new(fb)),
    }
}