/*
 * 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 anyhow::Error;
use changesets::ChangesetInsert;
use changesets::Changesets;
use changesets_impl::SqlChangesets;
use changesets_impl::SqlChangesetsBuilder;
use fbinit::FacebookInit;
use mononoke_types_mocks::changesetid::ONES_CSID;
use mononoke_types_mocks::changesetid::TWOS_CSID;
use mononoke_types_mocks::hash::ONES;
use mononoke_types_mocks::hash::TWOS;
use mononoke_types_mocks::repo::REPO_ZERO;
use rendezvous::RendezVousOptions;

use super::*;

async fn setup_changesets(ctx: &CoreContext) -> Result<SqlChangesets, Error> {
    let changesets = SqlChangesetsBuilder::with_sqlite_in_memory()?
        .build(RendezVousOptions::for_test(), REPO_ZERO);

    let ones = ChangesetInsert {
        cs_id: ONES_CSID,
        parents: vec![],
    };
    let twos = ChangesetInsert {
        cs_id: TWOS_CSID,
        parents: vec![ONES_CSID],
    };

    changesets.add(ctx, ones).await?;
    changesets.add(ctx, twos).await?;
    Ok(changesets)
}

#[fbinit::test]
async fn test_simple(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let store = SqlMutableRenamesStore::with_sqlite_in_memory()?;
    let mutable_renames = MutableRenames::new_test(RepositoryId::new(0), store);
    let changesets = setup_changesets(&ctx).await?;

    let dst_path = Some(MPath::new("dstpath")?);
    let src_path = Some(MPath::new("srcpath")?);
    let entry = MutableRenameEntry::new(
        TWOS_CSID,
        dst_path.clone(),
        ONES_CSID,
        src_path,
        Entry::Leaf(FileUnodeId::new(ONES)),
    )?;

    mutable_renames
        .add_or_overwrite_renames(&ctx, &changesets, vec![entry.clone()])
        .await?;
    let res = mutable_renames
        .get_rename(&ctx, TWOS_CSID, dst_path)
        .await?;

    assert_eq!(Some(entry), res);
    Ok(())
}

#[fbinit::test]
async fn test_insert_multiple(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let store = SqlMutableRenamesStore::with_sqlite_in_memory()?;
    let mutable_renames = MutableRenames::new_test(RepositoryId::new(0), store);
    let changesets = setup_changesets(&ctx).await?;

    let first_dst_path = Some(MPath::new("first_dstpath")?);
    let first_src_path = Some(MPath::new("second_srcpath")?);
    let first_entry = MutableRenameEntry::new(
        TWOS_CSID,
        first_dst_path.clone(),
        ONES_CSID,
        first_src_path,
        Entry::Leaf(FileUnodeId::new(ONES)),
    )?;

    let second_dst_path = Some(MPath::new("second_dstpath")?);
    let second_src_path = Some(MPath::new("second_srcpath")?);
    let second_entry = MutableRenameEntry::new(
        TWOS_CSID,
        second_dst_path.clone(),
        ONES_CSID,
        second_src_path,
        Entry::Leaf(FileUnodeId::new(TWOS)),
    )?;

    mutable_renames
        .add_or_overwrite_renames(
            &ctx,
            &changesets,
            vec![first_entry.clone(), second_entry.clone()],
        )
        .await?;
    let res = mutable_renames
        .get_rename(&ctx, TWOS_CSID, second_dst_path)
        .await?;

    assert_eq!(Some(second_entry), res);

    let res = mutable_renames
        .get_rename(&ctx, TWOS_CSID, first_dst_path)
        .await?;

    assert_eq!(Some(first_entry), res);
    Ok(())
}

#[fbinit::test]
async fn test_overwrite(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let store = SqlMutableRenamesStore::with_sqlite_in_memory()?;
    let mutable_renames = MutableRenames::new_test(RepositoryId::new(0), store);
    let changesets = setup_changesets(&ctx).await?;

    let dst_path = Some(MPath::new("first_dstpath")?);
    let first_src_path = Some(MPath::new("first_srcpath")?);
    let first_entry = MutableRenameEntry::new(
        TWOS_CSID,
        dst_path.clone(),
        ONES_CSID,
        first_src_path.clone(),
        Entry::Leaf(FileUnodeId::new(ONES)),
    )?;

    mutable_renames
        .add_or_overwrite_renames(&ctx, &changesets, vec![first_entry.clone()])
        .await?;

    let second_src_path = Some(MPath::new("second_srcpath")?);
    let second_entry = MutableRenameEntry::new(
        TWOS_CSID,
        dst_path.clone(),
        ONES_CSID,
        second_src_path,
        Entry::Leaf(FileUnodeId::new(TWOS)),
    )?;

    assert_ne!(first_entry, second_entry);
    mutable_renames
        .add_or_overwrite_renames(&ctx, &changesets, vec![second_entry.clone()])
        .await?;

    let res = mutable_renames
        .get_rename(&ctx, TWOS_CSID, dst_path)
        .await?;

    assert_eq!(Some(second_entry), res);
    Ok(())
}


/*
 * 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::sync::Arc;

use caching_ext::CacheHandlerFactory;
use metaconfig_types::RemoteMetadataDatabaseConfig;
use metaconfig_types::ShardableRemoteDatabaseConfig;
use mononoke_types::RepositoryId;
use sql::Connection;
use sql_construct::SqlShardableConstructFromMetadataDatabaseConfig;
use sql_construct::SqlShardedConstruct;
use sql_ext::SqlShardedConnections;

use crate::local_cache::LocalCache;
use crate::reader::FilenodesReader;
use crate::remote_cache::RemoteCache;
use crate::writer::FilenodesWriter;
use crate::NewFilenodes;

pub const MYSQL_INSERT_CHUNK_SIZE: usize = 1000;
pub const SQLITE_INSERT_CHUNK_SIZE: usize = 100;

pub struct NewFilenodesBuilder {
    reader: FilenodesReader,
    writer: FilenodesWriter,
}

impl SqlShardedConstruct for NewFilenodesBuilder {
    const LABEL: &'static str = "shardedfilenodes";

    const CREATION_QUERY: &'static str = include_str!("../schemas/sqlite-filenodes.sql");

    fn from_sql_shard_connections(shard_connections: SqlShardedConnections) -> Self {
        let SqlShardedConnections {
            read_connections,
            read_master_connections,
            write_connections,
        } = shard_connections;
        let chunk_size = match read_connections.get(0) {
            Some(Connection::Mysql(_)) => MYSQL_INSERT_CHUNK_SIZE,
            _ => SQLITE_INSERT_CHUNK_SIZE,
        };

        let reader = FilenodesReader::new(read_connections.clone(), read_master_connections);
        let writer = FilenodesWriter::new(chunk_size, write_connections, read_connections);

        Self { reader, writer }
    }
}

impl SqlShardableConstructFromMetadataDatabaseConfig for NewFilenodesBuilder {
    fn remote_database_config(
        remote: &RemoteMetadataDatabaseConfig,
    ) -> Option<&ShardableRemoteDatabaseConfig> {
        Some(&remote.filenodes)
    }
}

impl NewFilenodesBuilder {
    pub fn build(self, repo_id: RepositoryId) -> NewFilenodes {
        NewFilenodes {
            reader: Arc::new(self.reader),
            writer: Arc::new(self.writer),
            repo_id,
        }
    }

    pub fn enable_caching(
        &mut self,
        cache_handler_factory: CacheHandlerFactory,
        history_cache_handler_factory: CacheHandlerFactory,
        backing_store_name: &str,
        backing_store_params: &str,
    ) {
        // We require two cache builders for the two cache pools.
        self.reader.local_cache =
            LocalCache::new(&cache_handler_factory, &history_cache_handler_factory);

        // However, memcache doesn't have cache pools, so we can just use
        // either of the cache builders to construct the remote cache.
        self.reader.remote_cache = RemoteCache::new(
            &cache_handler_factory,
            backing_store_name,
            backing_store_params,
        );
    }
}


/*
 * 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 path_hash::PathHashBytes;
use path_hash::PathWithHash;
use sql::Connection;
use stats::prelude::*;
use vec1::Vec1;

define_stats! {
    prefix = "mononoke.filenodes";
    filenodes_conn_checkout: timeseries(Rate, Sum),
    history_conn_checkout: timeseries(Rate, Sum),
    paths_conn_checkout: timeseries(Rate, Sum),
}

#[derive(Copy, Clone)]
pub enum AcquireReason {
    Filenodes,
    History,
    Paths,
}

#[derive(Hash, Eq, PartialEq)]
pub struct ShardId {
    id: usize,
}

pub struct Connections {
    connections: Vec1<Connection>,
}

impl Connections {
    pub fn new(connections: Vec1<Connection>) -> Self {
        Self { connections }
    }
}

impl Connections {
    pub fn shard_id(&self, ph: &PathHashBytes) -> ShardId {
        ShardId {
            id: PathWithHash::shard_number_by_hash(ph, self.connections.len()),
        }
    }

    pub fn checkout<'a>(&'a self, pwh: &PathWithHash<'_>, reason: AcquireReason) -> &'a Connection {
        let shard_id = self.shard_id(&pwh.hash);
        self.checkout_by_shard_id(shard_id, reason)
    }

    pub fn checkout_by_shard_id<'a>(
        &'a self,
        shard_id: ShardId,
        reason: AcquireReason,
    ) -> &'a Connection {
        match reason {
            AcquireReason::Filenodes => STATS::filenodes_conn_checkout.add_value(1),
            AcquireReason::History => STATS::history_conn_checkout.add_value(1),
            AcquireReason::Paths => STATS::paths_conn_checkout.add_value(1),
        };

        &self.connections[shard_id.id] as _
    }
}


/*
 * 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 abomonation_derive::Abomonation;
use anyhow::Error;
use filenodes::FilenodeInfo;
use filenodes::FilenodeInfoCached;

#[derive(Abomonation, Clone)]
pub struct FilenodeHistoryCached {
    // TODO: We could store this more efficiently by deduplicating filenode IDs.
    history: Vec<FilenodeInfoCached>,
}

impl FilenodeHistoryCached {
    pub fn into_filenode_info(self) -> Result<Vec<FilenodeInfo>, Error> {
        self.history.into_iter().map(|c| c.try_into()).collect()
    }

    pub fn from_filenodes(filenodes: Vec<FilenodeInfo>) -> Self {
        let history = filenodes.into_iter().map(|f| f.into()).collect::<Vec<_>>();
        Self { history }
    }
}


/*
 * 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.
 */

mod builder;
mod connections;
mod local_cache;
mod reader;
mod remote_cache;
mod shards;
mod sql_timeout_knobs;
mod writer;

#[cfg(test)]
mod test;

use std::sync::Arc;

use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
pub use builder::NewFilenodesBuilder;
use context::CoreContext;
use filenodes::FilenodeInfo;
use filenodes::FilenodeRange;
use filenodes::FilenodeResult;
use filenodes::Filenodes;
use filenodes::PreparedFilenode;
use mercurial_types::HgFileNodeId;
use mononoke_types::RepoPath;
use mononoke_types::RepositoryId;
pub use path_hash::PathHash;
use reader::FilenodesReader;
pub use sql_timeout_knobs::disable_sql_timeouts;
use thiserror::Error as DeriveError;
use writer::FilenodesWriter;

#[derive(Debug, DeriveError)]
pub enum ErrorKind {
    #[error("Internal error: failure while fetching file node {0} {1}")]
    FailFetchFilenode(HgFileNodeId, RepoPath),

    #[error("Internal error: failure while fetching file nodes for {0}")]
    FailFetchFilenodeRange(RepoPath),

    #[error("Internal error: failure while inserting filenodes")]
    FailAddFilenodes,
}

#[derive(Clone)]
pub struct NewFilenodes {
    reader: Arc<FilenodesReader>,
    writer: Arc<FilenodesWriter>,
    repo_id: RepositoryId,
}

#[async_trait]
impl Filenodes for NewFilenodes {
    async fn add_filenodes(
        &self,
        ctx: &CoreContext,
        info: Vec<PreparedFilenode>,
    ) -> Result<FilenodeResult<()>> {
        let ret = self
            .writer
            .insert_filenodes(ctx, self.repo_id, info, false /* replace */)
            .await
            .with_context(|| ErrorKind::FailAddFilenodes)?;
        Ok(ret)
    }

    async fn add_or_replace_filenodes(
        &self,
        ctx: &CoreContext,
        info: Vec<PreparedFilenode>,
    ) -> Result<FilenodeResult<()>> {
        let ret = self
            .writer
            .insert_filenodes(ctx, self.repo_id, info, true /* replace */)
            .await
            .with_context(|| ErrorKind::FailAddFilenodes)?;
        Ok(ret)
    }

    async fn get_filenode(
        &self,
        ctx: &CoreContext,
        path: &RepoPath,
        filenode_id: HgFileNodeId,
    ) -> Result<FilenodeResult<Option<FilenodeInfo>>> {
        let ret = self
            .reader
            .clone()
            .get_filenode(ctx, self.repo_id, path, filenode_id)
            .await
            .with_context(|| ErrorKind::FailFetchFilenode(filenode_id, path.clone()))?;
        Ok(ret)
    }

    async fn get_all_filenodes_maybe_stale(
        &self,
        ctx: &CoreContext,
        path: &RepoPath,
        limit: Option<u64>,
    ) -> Result<FilenodeResult<FilenodeRange>> {
        let ret = self
            .reader
            .clone()
            .get_all_filenodes_for_path(ctx, self.repo_id, path, limit)
            .await
            .with_context(|| ErrorKind::FailFetchFilenodeRange(path.clone()))?;
        Ok(ret)
    }

    fn prime_cache(&self, ctx: &CoreContext, filenodes: &[PreparedFilenode]) {
        self.reader.prime_cache(ctx, self.repo_id, filenodes);
    }
}


/*
 * 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::marker::PhantomData;

use caching_ext::CacheHandlerFactory;
use caching_ext::CachelibHandler;
use filenodes::FilenodeInfo;
use filenodes::FilenodeRange;
use stats::prelude::*;

define_stats! {
    prefix = "mononoke.filenodes";
    fill_cache_fail: timeseries(Sum),
}

#[derive(Clone)]
pub struct CacheKey<V> {
    pub key: String,
    /// value is used to enforce that a CacheKey for a given type V can only be used to fetch
    /// values of type V.
    pub value: PhantomData<V>,
}

pub struct LocalCache {
    filenode_cache: CachelibHandler<FilenodeInfo>,
    history_cache: CachelibHandler<FilenodeRange>,
}

impl LocalCache {
    pub fn new(
        filenode_cache_handler_factory: &CacheHandlerFactory,
        history_cache_handler_factory: &CacheHandlerFactory,
    ) -> Self {
        LocalCache {
            filenode_cache: filenode_cache_handler_factory.cachelib(),
            history_cache: history_cache_handler_factory.cachelib(),
        }
    }

    pub fn new_noop() -> Self {
        Self::new(&CacheHandlerFactory::Noop, &CacheHandlerFactory::Noop)
    }

    #[cfg(test)]
    pub fn new_mock() -> Self {
        Self::new(&CacheHandlerFactory::Mocked, &CacheHandlerFactory::Mocked)
    }

    pub fn get_filenode(&self, key: &CacheKey<FilenodeInfo>) -> Option<FilenodeInfo> {
        match self.filenode_cache.get_cached(&key.key) {
            Ok(Some(r)) => Some(r),
            _ => None,
        }
    }

    pub fn fill_filenode(&self, key: &CacheKey<FilenodeInfo>, value: &FilenodeInfo) {
        let r = self.filenode_cache.set_cached(&key.key, value, None);
        if r.is_err() {
            STATS::fill_cache_fail.add_value(1);
        }
    }

    pub fn get_history(&self, key: &CacheKey<FilenodeRange>) -> Option<FilenodeRange> {
        match self.history_cache.get_cached(&key.key) {
            Ok(Some(r)) => Some(r),
            _ => None,
        }
    }

    pub fn fill_history(&self, key: &CacheKey<FilenodeRange>, value: &FilenodeRange) {
        let r = self.history_cache.set_cached(&key.key, value, None);
        if r.is_err() {
            STATS::fill_cache_fail.add_value(1);
        }
    }
}


/*
 * 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::Cow;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Error;
use context::CoreContext;
use context::PerfCounterType;
use faster_hex::hex_encode;
use filenodes::FilenodeInfo;
use filenodes::FilenodeRange;
use filenodes::FilenodeResult;
use filenodes::PreparedFilenode;
use futures::future;
use futures::future::Future;
use itertools::Itertools;
use mercurial_types::HgChangesetId;
use mercurial_types::HgFileNodeId;
use mononoke_types::RepoPath;
use mononoke_types::RepositoryId;
use path_hash::PathBytes;
use path_hash::PathHashBytes;
use path_hash::PathWithHash;
use rand::thread_rng;
use rand::Rng;
use sql::Connection;
use sql_ext::mononoke_queries;
use stats::prelude::*;
use thiserror::Error as DeriveError;
use tokio::time::timeout;
use tunables::tunables;
use vec1::Vec1;

use crate::connections::AcquireReason;
use crate::connections::Connections;
use crate::local_cache::CacheKey;
use crate::local_cache::LocalCache;
use crate::remote_cache::RemoteCache;
use crate::shards::Shards;
use crate::sql_timeout_knobs;

define_stats! {
    prefix = "mononoke.filenodes";
    gets: timeseries(Sum),
    gets_master: timeseries(Sum),
    gets_disabled: timeseries(Sum),
    range_gets: timeseries(Sum),
    range_gets_disabled: timeseries(Sum),
    path_gets: timeseries(Sum),
    get_local_cache_misses: timeseries(Sum),
    range_local_cache_misses: timeseries(Sum),
    remote_cache_timeouts: timeseries(Sum),
    sql_timeouts: timeseries(Sum),
    too_big_history: timeseries(Sum),
}

// Both of these are pretty convervative, and collected experimentally. They're here to ensure one
// bad query doesn't lock down an entire shard for an extended period of time.
const REMOTE_CACHE_TIMEOUT_MILLIS: u64 = 100;
const SQL_TIMEOUT_MILLIS: u64 = 5_000;

#[derive(Debug, DeriveError)]
pub enum ErrorKind {
    #[error("Internal error: path is not found: {0:?}")]
    PathNotFound(PathHashBytes),

    #[error("Internal error: invalid path: {0:?}")]
    InvalidPath(PathBytes, #[source] Error),

    #[error("Internal error: fixedcopyinfo is missing for filenode: {0:?}")]
    FixedCopyInfoMissing(HgFileNodeId),

    #[error("Internal error: SQL error")]
    SqlError(#[source] Error),

    #[error("Internal error: SQL timeout")]
    SqlTimeout,
}

struct PerfCounterRecorder<'a> {
    ctx: &'a CoreContext,
    counter: PerfCounterType,
}

impl<'a> PerfCounterRecorder<'a> {
    fn increment(&self) {
        self.ctx.perf_counters().increment_counter(self.counter);
    }
}

type FilenodeRow = (
    HgFileNodeId,
    HgChangesetId,
    Option<HgFileNodeId>,
    Option<HgFileNodeId>,
    i8,
    Option<PathHashBytes>,
    Option<HgFileNodeId>,
);

pub fn filenode_cache_key(
    repo_id: RepositoryId,
    pwh: &PathWithHash<'_>,
    filenode: &HgFileNodeId,
) -> CacheKey<FilenodeInfo> {
    let mut v = vec![0; pwh.hash.0.len() * 2];
    let is_tree = pwh.is_tree as u8;
    hex_encode(pwh.hash.0.as_ref(), &mut v).expect("failed to hex encode");
    let key = format!(
        "filenode.{}.{}.{}.{}",
        repo_id.id(),
        filenode,
        unsafe { String::from_utf8_unchecked(v) },
        is_tree
    );

    CacheKey {
        key,
        value: PhantomData,
    }
}

pub fn history_cache_key(
    repo_id: RepositoryId,
    pwh: &PathWithHash<'_>,
    limit: Option<u64>,
) -> CacheKey<FilenodeRange> {
    let mut v = vec![0; pwh.hash.0.len() * 2];
    let is_tree = pwh.is_tree as u8;
    hex_encode(pwh.hash.0.as_ref(), &mut v).expect("failed to hex encode");
    let key = match limit {
        Some(limit) => format!(
            "history.{}.limit.{}.{}.{}",
            repo_id.id(),
            limit,
            unsafe { String::from_utf8_unchecked(v) },
            is_tree
        ),
        None => format!(
            "history.{}.{}.{}",
            repo_id.id(),
            unsafe { String::from_utf8_unchecked(v) },
            is_tree
        ),
    };

    CacheKey {
        key,
        value: PhantomData,
    }
}
pub struct FilenodesReader {
    read_connections: Connections,
    read_master_connections: Connections,
    shards: Arc<Shards>,
    pub local_cache: LocalCache,
    pub remote_cache: RemoteCache,
}

impl FilenodesReader {
    pub fn new(
        read_connections: Vec1<Connection>,
        read_master_connections: Vec1<Connection>,
    ) -> Self {
        Self {
            shards: Arc::new(Shards::new(1000, 1000)),
            read_connections: Connections::new(read_connections),
            read_master_connections: Connections::new(read_master_connections),
            local_cache: LocalCache::new_noop(),
            remote_cache: RemoteCache::new_noop(),
        }
    }

    pub async fn get_filenode(
        self: Arc<Self>,
        ctx: &CoreContext,
        repo_id: RepositoryId,
        path: &RepoPath,
        filenode: HgFileNodeId,
    ) -> Result<FilenodeResult<Option<FilenodeInfo>>, Error> {
        STATS::gets.add_value(1);

        let pwh = PathWithHash::from_repo_path_cow(Cow::Owned(path.clone()));
        let key = filenode_cache_key(repo_id, &pwh, &filenode);

        if let Some(cached) = self.local_cache.get_filenode(&key) {
            return Ok(FilenodeResult::Present(Some(cached)));
        }

        let ctx = ctx.clone();
        self.shards
            .clone()
            .with_filenodes(path, filenode, move || {
                async move {
                    // Now that we acquired the permit, check our cache again, in case the previous permit
                    // owner just filed the cache with the filenode we're looking for.
                    if let Some(cached) = self.local_cache.get_filenode(&key) {
                        return Ok(FilenodeResult::Present(Some(cached)));
                    }

                    STATS::get_local_cache_misses.add_value(1);

                    if let Some(info) =
                        enforce_remote_cache_timeout(self.remote_cache.get_filenode(&key)).await
                    {
                        self.local_cache.fill_filenode(&key, &info);
                        return Ok(FilenodeResult::Present(Some(info)));
                    }

                    let cache_filler = FilenodeCacheFiller {
                        local_cache: &self.local_cache,
                        remote_cache: &self.remote_cache,
                        key: &key,
                    };

                    match select_filenode_from_sql(
                        cache_filler,
                        &self.read_connections,
                        repo_id,
                        &pwh,
                        filenode,
                        &PerfCounterRecorder {
                            ctx: &ctx,
                            counter: PerfCounterType::SqlReadsReplica,
                        },
                    )
                    .await
                    {
                        Ok(FilenodeResult::Disabled) => {
                            return Ok(FilenodeResult::Disabled);
                        }
                        Ok(FilenodeResult::Present(Some(res))) => {
                            return Ok(FilenodeResult::Present(Some(res)));
                        }
                        Ok(FilenodeResult::Present(None))
                        | Err(ErrorKind::FixedCopyInfoMissing(_))
                        | Err(ErrorKind::PathNotFound(_)) => {
                            // If the filenode wasn't found, or its copy info was missing, it might be present
                            // on the master.
                        }
                        Err(e) => {
                            return Err(e.into());
                        }
                    }

                    let ratio = tunables()
                        .filenodes_master_fallback_ratio()
                        .unwrap_or_default();
                    if ratio > 0 {
                        let mut rng = thread_rng();
                        let n = rng.gen_range(0..ratio);
                        if n > 0 {
                            return Ok(FilenodeResult::Disabled);
                        }
                    }

                    STATS::gets_master.add_value(1);

                    let res = select_filenode_from_sql(
                        cache_filler,
                        &self.read_master_connections,
                        repo_id,
                        &pwh,
                        filenode,
                        &PerfCounterRecorder {
                            ctx: &ctx,
                            counter: PerfCounterType::SqlReadsMaster,
                        },
                    )
                    .await?;

                    match res {
                        FilenodeResult::Present(res) => Ok(FilenodeResult::Present(res)),
                        FilenodeResult::Disabled => Ok(FilenodeResult::Disabled),
                    }
                }
            })
            .await?
    }

    pub async fn get_all_filenodes_for_path(
        self: Arc<Self>,
        ctx: &CoreContext,
        repo_id: RepositoryId,
        path: &RepoPath,
        limit: Option<u64>,
    ) -> Result<FilenodeResult<FilenodeRange>, Error> {
        STATS::range_gets.add_value(1);

        let pwh = PathWithHash::from_repo_path_cow(Cow::Owned(path.clone()));
        let key = history_cache_key(repo_id, &pwh, limit);

        if let Some(cached) = self.local_cache.get_history(&key) {
            return Ok(FilenodeResult::Present(cached));
        }
        let ctx = ctx.clone();
        self.shards
            .clone()
            .with_history(path, move || {
                async move {
                    // See above for rationale here.
                    if let Some(cached) = self.local_cache.get_history(&key) {
                        return Ok(FilenodeResult::Present(cached));
                    }

                    STATS::range_local_cache_misses.add_value(1);

                    if let Some(info) =
                        enforce_remote_cache_timeout(self.remote_cache.get_history(&key)).await
                    {
                        // TODO: We should compress if this is too big.
                        self.local_cache.fill_history(&key, &info);
                        return Ok(FilenodeResult::Present(info));
                    }

                    let cache_filler = HistoryCacheFiller {
                        local_cache: &self.local_cache,
                        remote_cache: &self.remote_cache,
                        key: &key,
                    };

                    select_history_from_sql(
                        &cache_filler,
                        &self.read_connections,
                        repo_id,
                        &pwh,
                        &PerfCounterRecorder {
                            ctx: &ctx,
                            counter: PerfCounterType::SqlReadsReplica,
                        },
                        limit,
                    )
                    .await
                }
            })
            .await?
    }

    pub fn prime_cache(
        &self,
        _ctx: &CoreContext,
        repo_id: RepositoryId,
        filenodes: &[PreparedFilenode],
    ) {
        for c in filenodes {
            let pwh = PathWithHash::from_repo_path(&c.path);
            let key = filenode_cache_key(repo_id, &pwh, &c.info.filenode);
            self.local_cache.fill_filenode(&key, &c.info)
        }
    }
}

#[derive(Copy, Clone)]
struct FilenodeCacheFiller<'a> {
    local_cache: &'a LocalCache,
    remote_cache: &'a RemoteCache,
    key: &'a CacheKey<FilenodeInfo>,
}

impl<'a> FilenodeCacheFiller<'a> {
    fn fill(&self, filenode: FilenodeInfo) {
        self.local_cache.fill_filenode(self.key, &filenode);
        self.remote_cache.fill_filenode(self.key, filenode);
    }
}

struct PartialFilenode {
    filenode: HgFileNodeId,
    p1: Option<HgFileNodeId>,
    p2: Option<HgFileNodeId>,
    copyfrom: Option<(PathHashBytes, HgFileNodeId)>,
    linknode: HgChangesetId,
}

async fn select_filenode_from_sql(
    filler: FilenodeCacheFiller<'_>,
    connections: &Connections,
    repo_id: RepositoryId,
    pwh: &PathWithHash<'_>,
    filenode: HgFileNodeId,
    recorder: &PerfCounterRecorder<'_>,
) -> Result<FilenodeResult<Option<FilenodeInfo>>, ErrorKind> {
    if tunables().filenodes_disabled().unwrap_or_default() {
        STATS::gets_disabled.add_value(1);
        return Ok(FilenodeResult::Disabled);
    }

    let partial = select_partial_filenode(connections, repo_id, pwh, filenode, recorder).await?;

    let partial = match partial {
        Some(partial) => partial,
        None => {
            return Ok(FilenodeResult::Present(None));
        }
    };

    let ret = match fill_paths(connections, pwh, repo_id, vec![partial], recorder)
        .await?
        .into_iter()
        .next()
    {
        Some(ret) => ret,
        None => {
            return Ok(FilenodeResult::Present(None));
        }
    };

    filler.fill(ret.clone());

    Ok(FilenodeResult::Present(Some(ret)))
}

async fn select_partial_filenode(
    connections: &Connections,
    repo_id: RepositoryId,
    pwh: &PathWithHash<'_>,
    filenode: HgFileNodeId,
    recorder: &PerfCounterRecorder<'_>,
) -> Result<Option<PartialFilenode>, ErrorKind> {
    let connection = connections.checkout(pwh, AcquireReason::Filenodes);

    recorder.increment();

    let rows = enforce_sql_timeout(SelectFilenode::query(
        connection,
        &repo_id,
        &pwh.hash,
        pwh.sql_is_tree(),
        &filenode,
    ))
    .await?;

    match rows.into_iter().next() {
        Some(row) => {
            let partial = convert_row_to_partial_filenode(row)?;
            Ok(Some(partial))
        }
        None => Ok(None),
    }
}

#[derive(Copy, Clone)]
struct HistoryCacheFiller<'a> {
    local_cache: &'a LocalCache,
    remote_cache: &'a RemoteCache,
    key: &'a CacheKey<FilenodeRange>,
}

impl<'a> HistoryCacheFiller<'a> {
    fn fill(&self, history: FilenodeRange) {
        self.local_cache.fill_history(self.key, &history);
        self.remote_cache.fill_history(self.key, history);
    }
}

async fn select_history_from_sql(
    filler: &HistoryCacheFiller<'_>,
    connections: &Connections,
    repo_id: RepositoryId,
    pwh: &PathWithHash<'_>,
    recorder: &PerfCounterRecorder<'_>,
    limit: Option<u64>,
) -> Result<FilenodeResult<FilenodeRange>, Error> {
    if tunables().filenodes_disabled().unwrap_or_default() {
        STATS::range_gets_disabled.add_value(1);
        return Ok(FilenodeResult::Disabled);
    }

    let maybe_partial = select_partial_history(connections, repo_id, pwh, recorder, limit).await?;
    if let Some(partial) = maybe_partial {
        let history = FilenodeRange::Filenodes(
            fill_paths(connections, pwh, repo_id, partial, recorder).await?,
        );
        filler.fill(history.clone());
        Ok(FilenodeResult::Present(history))
    } else {
        filler.fill(FilenodeRange::TooBig);
        Ok(FilenodeResult::Present(FilenodeRange::TooBig))
    }
}

async fn select_partial_history(
    connections: &Connections,
    repo_id: RepositoryId,
    pwh: &PathWithHash<'_>,
    recorder: &PerfCounterRecorder<'_>,
    limit: Option<u64>,
) -> Result<Option<Vec<PartialFilenode>>, ErrorKind> {
    let connection = connections.checkout(pwh, AcquireReason::History);

    recorder.increment();

    // Try to fetch one entry more - if we fetched limit + 1, then file
    // history is too big.
    let limit = limit.map(|l| l + 1);
    let rows = match limit {
        Some(limit) => {
            let rows = enforce_sql_timeout(SelectLimitedFilenodes::query(
                connection,
                &repo_id,
                &pwh.hash,
                pwh.sql_is_tree(),
                &limit,
            ))
            .await?;
            if rows.len() >= limit as usize {
                STATS::too_big_history.add_value(1);
                return Ok(None);
            }
            rows
        }
        None => {
            enforce_sql_timeout(SelectAllFilenodes::query(
                connection,
                &repo_id,
                &pwh.hash,
                pwh.sql_is_tree(),
            ))
            .await?
        }
    };

    let history = rows
        .into_iter()
        .map(convert_row_to_partial_filenode)
        .collect::<Result<Vec<PartialFilenode>, ErrorKind>>()?;

    // TODO: It'd be nice to have some eviction here.
    // TODO: It'd be nice to chain those.

    Ok(Some(history))
}

fn convert_row_to_partial_filenode(row: FilenodeRow) -> Result<PartialFilenode, ErrorKind> {
    let (filenode, linknode, p1, p2, has_copyinfo, from_path_hash, from_node) = row;

    let copyfrom = if has_copyinfo == 0 {
        None
    } else {
        let from_path_hash = from_path_hash.ok_or(ErrorKind::FixedCopyInfoMissing(filenode))?;

        let from_node = from_node.ok_or(ErrorKind::FixedCopyInfoMissing(filenode))?;

        Some((from_path_hash, from_node))
    };

    let ret = PartialFilenode {
        filenode,
        p1,
        p2,
        copyfrom,
        linknode,
    };

    Ok(ret)
}

async fn fill_paths(
    connections: &Connections,
    pwh: &PathWithHash<'_>,
    repo_id: RepositoryId,
    rows: Vec<PartialFilenode>,
    recorder: &PerfCounterRecorder<'_>,
) -> Result<Vec<FilenodeInfo>, ErrorKind> {
    let path_hashes_to_fetch = rows
        .iter()
        .filter_map(|r| r.copyfrom.as_ref().map(|c| c.0.clone()));

    let path_hashes_to_paths =
        select_paths(connections, repo_id, path_hashes_to_fetch, recorder).await?;

    let ret = rows
        .into_iter()
        .map(|partial| {
            let PartialFilenode {
                filenode,
                p1,
                p2,
                copyfrom,
                linknode,
            } = partial;

            let copyfrom = match copyfrom {
                Some((from_path_hash, from_node)) => {
                    let from_path = path_hashes_to_paths
                        .get(&from_path_hash)
                        .ok_or_else(|| ErrorKind::PathNotFound(from_path_hash.clone()))?;
                    let repo_path = if pwh.is_tree {
                        RepoPath::dir(&from_path.0[..])
                            .map_err(|e| ErrorKind::InvalidPath(from_path.clone(), e))?
                    } else {
                        RepoPath::file(&from_path.0[..])
                            .map_err(|e| ErrorKind::InvalidPath(from_path.clone(), e))?
                    };
                    Some((repo_path, from_node))
                }
                None => None,
            };

            let ret = FilenodeInfo {
                filenode,
                p1,
                p2,
                copyfrom,
                linknode,
            };

            Result::<_, ErrorKind>::Ok(ret)
        })
        .collect::<Result<Vec<_>, _>>()?;

    Ok(ret)
}

async fn select_paths<I: Iterator<Item = PathHashBytes>>(
    connections: &Connections,
    repo_id: RepositoryId,
    iter: I,
    recorder: &PerfCounterRecorder<'_>,
) -> Result<HashMap<PathHashBytes, PathBytes>, ErrorKind> {
    let futs = iter
        .group_by(|path_hash| connections.shard_id(path_hash))
        .into_iter()
        .map(|(shard_id, group)| {
            let group = group.collect::<Vec<_>>();

            STATS::path_gets.add_value(group.len() as i64);

            async move {
                recorder.increment();

                let connection = connections.checkout_by_shard_id(shard_id, AcquireReason::Paths);

                let output =
                    enforce_sql_timeout(SelectPaths::query(connection, &repo_id, &group[..]))
                        .await?
                        .into_iter()
                        .collect::<HashMap<_, _>>();

                Result::<_, ErrorKind>::Ok(output)
            }
        })
        .collect::<Vec<_>>();

    let groups = future::try_join_all(futs).await?;

    let mut ret = HashMap::new();
    for group in groups {
        ret.extend(group);
    }

    Ok(ret)
}

async fn enforce_remote_cache_timeout<T, Fut>(fut: Fut) -> Option<T>
where
    Fut: Future<Output = Option<T>>,
{
    match timeout(Duration::from_millis(REMOTE_CACHE_TIMEOUT_MILLIS), fut).await {
        Ok(r) => r,
        Err(_) => {
            STATS::remote_cache_timeouts.add_value(1);
            None
        }
    }
}

async fn enforce_sql_timeout<T, Fut>(fut: Fut) -> Result<T, ErrorKind>
where
    Fut: Future<Output = Result<T, Error>>,
{
    if !sql_timeout_knobs::should_enforce_sql_timeouts() {
        return fut.await.map_err(ErrorKind::SqlError);
    }

    match timeout(Duration::from_millis(SQL_TIMEOUT_MILLIS), fut).await {
        Ok(Ok(r)) => Ok(r),
        Ok(Err(e)) => Err(ErrorKind::SqlError(e)),
        Err(_) => {
            STATS::sql_timeouts.add_value(1);
            Err(ErrorKind::SqlTimeout)
        }
    }
}

mononoke_queries! {
    read SelectFilenode(
        repo_id: RepositoryId,
        path_hash: PathHashBytes,
        is_tree: i8,
        filenode: HgFileNodeId
    ) -> (
        HgFileNodeId,
        HgChangesetId,
        Option<HgFileNodeId>,
        Option<HgFileNodeId>,
        i8,
        Option<PathHashBytes>,
        Option<HgFileNodeId>,
    ) {
        "
        SELECT
            filenodes.filenode,
            filenodes.linknode,
            filenodes.p1,
            filenodes.p2,
            filenodes.has_copyinfo,
            fixedcopyinfo.frompath_hash,
            fixedcopyinfo.fromnode
        FROM filenodes
        LEFT JOIN fixedcopyinfo
           ON (
                   fixedcopyinfo.repo_id = filenodes.repo_id
               AND fixedcopyinfo.topath_hash = filenodes.path_hash
               AND fixedcopyinfo.tonode = filenodes.filenode
               AND fixedcopyinfo.is_tree = filenodes.is_tree
           )
        WHERE filenodes.repo_id = {repo_id}
          AND filenodes.path_hash = {path_hash}
          AND filenodes.is_tree = {is_tree}
          AND filenodes.filenode = {filenode}
        LIMIT 1
        "
    }

    read SelectAllFilenodes(
        repo_id: RepositoryId,
        path_hash: PathHashBytes,
        is_tree: i8,
    ) -> (
        HgFileNodeId,
        HgChangesetId,
        Option<HgFileNodeId>,
        Option<HgFileNodeId>,
        i8,
        Option<PathHashBytes>,
        Option<HgFileNodeId>,
    ) {
        "
        SELECT
            filenodes.filenode,
            filenodes.linknode,
            filenodes.p1,
            filenodes.p2,
            filenodes.has_copyinfo,
            fixedcopyinfo.frompath_hash,
            fixedcopyinfo.fromnode
        FROM filenodes
        LEFT JOIN fixedcopyinfo
           ON (
                   fixedcopyinfo.repo_id = filenodes.repo_id
               AND fixedcopyinfo.topath_hash = filenodes.path_hash
               AND fixedcopyinfo.tonode = filenodes.filenode
               AND fixedcopyinfo.is_tree = filenodes.is_tree
           )
        WHERE filenodes.repo_id = {repo_id}
          AND filenodes.path_hash = {path_hash}
          AND filenodes.is_tree = {is_tree}
        "
    }

    read SelectLimitedFilenodes(
        repo_id: RepositoryId,
        path_hash: PathHashBytes,
        is_tree: i8,
        limit: u64,
    ) -> (
        HgFileNodeId,
        HgChangesetId,
        Option<HgFileNodeId>,
        Option<HgFileNodeId>,
        i8,
        Option<PathHashBytes>,
        Option<HgFileNodeId>,
    ) {
        "
        SELECT
            filenodes.filenode,
            filenodes.linknode,
            filenodes.p1,
            filenodes.p2,
            filenodes.has_copyinfo,
            fixedcopyinfo.frompath_hash,
            fixedcopyinfo.fromnode
        FROM filenodes
        LEFT JOIN fixedcopyinfo
           ON (
                   fixedcopyinfo.repo_id = filenodes.repo_id
               AND fixedcopyinfo.topath_hash = filenodes.path_hash
               AND fixedcopyinfo.tonode = filenodes.filenode
               AND fixedcopyinfo.is_tree = filenodes.is_tree
           )
        WHERE filenodes.repo_id = {repo_id}
          AND filenodes.path_hash = {path_hash}
          AND filenodes.is_tree = {is_tree}
        LIMIT {limit}
        "
    }

    read SelectPaths(repo_id: RepositoryId, >list path_hashes: PathHashBytes) -> (PathHashBytes, PathBytes) {
        "SELECT path_hash, path
         FROM paths
         WHERE paths.repo_id = {repo_id}
           AND paths.path_hash in {path_hashes}"
    }
}


/*
 * 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::time::Duration;
use std::time::Instant;

use bytes::Bytes;
use caching_ext::CacheHandlerFactory;
use caching_ext::MemcacheHandler;
use fbthrift::compact_protocol;
use filenodes::thrift;
use filenodes::thrift::MC_CODEVER;
use filenodes::thrift::MC_SITEVER;
use filenodes::FilenodeInfo;
use filenodes::FilenodeRange;
use futures::future::try_join_all;
use memcache::KeyGen;
use memcache::MEMCACHE_VALUE_MAX_SIZE;
use rand::random;
use stats::prelude::*;
use time_ext::DurationExt;

use crate::local_cache::CacheKey;

define_stats! {
    prefix = "mononoke.filenodes";
    gaf_compact_bytes: histogram(
        "get_all_filenodes.thrift_compact.bytes";
        500, 0, 1_000_000, Average, Sum, Count; P 50; P 95; P 99
    ),
    point_filenode_hit: timeseries("point_filenode.memcache.hit"; Sum),
    point_filenode_miss: timeseries("point_filenode.memcache.miss"; Sum),
    point_filenode_internal_err: timeseries("point_filenode.memcache.internal_err"; Sum),
    point_filenode_deserialize_err: timeseries("point_filenode.memcache.deserialize_err"; Sum),
    point_filenode_pointers_err: timeseries("point_filenode.memcache.pointers_err"; Sum),
    gaf_hit: timeseries("get_all_filenodes.memcache.hit"; Sum),
    gaf_miss: timeseries("get_all_filenodes.memcache.miss"; Sum),
    gaf_pointers: timeseries("get_all_filenodes.memcache.pointers"; Sum),
    gaf_internal_err: timeseries("get_all_filenodes.memcache.internal_err"; Sum),
    gaf_deserialize_err: timeseries("get_all_filenodes.memcache.deserialize_err"; Sum),
    gaf_pointers_err: timeseries("get_all_filenodes.memcache.pointers_err"; Sum),
    get_latency: histogram("get.memcache.duration_us"; 100, 0, 10000, Average, Count; P 50; P 95; P 100),
    get_history: histogram("get_history.memcache.duration_us"; 100, 0, 10000, Average, Count; P 50; P 95; P 100),
}

const SITEVER_OVERRIDE_VAR: &str = "MONONOKE_OVERRIDE_FILENODES_MC_SITEVER";

const TTL_SEC: u64 = 8 * 60 * 60;

// Adding a random to TTL helps preventing eviction of all related keys at once
const TTL_SEC_RAND: u64 = 30 * 60; // 30min

pub struct RemoteCache {
    memcache: MemcacheHandler,
    keygen: KeyGen,
}

impl RemoteCache {
    pub fn new(
        cache_handler_factory: &CacheHandlerFactory,
        backing_store_name: &str,
        backing_store_params: &str,
    ) -> Self {
        Self {
            memcache: cache_handler_factory.memcache(),
            keygen: Self::create_key_gen(backing_store_name, backing_store_params),
        }
    }

    pub fn new_noop() -> Self {
        Self::new(&CacheHandlerFactory::Noop, "newfilenodes", "")
    }

    #[cfg(test)]
    pub fn new_mock() -> Self {
        Self::new(&CacheHandlerFactory::Mocked, "newfilenodes", "test")
    }

    fn create_key_gen(backing_store_name: &str, backing_store_params: &str) -> KeyGen {
        let key_prefix = format!(
            "scm.mononoke.filenodes.{}.{}",
            backing_store_name, backing_store_params,
        );

        let mc_sitever = match std::env::var(SITEVER_OVERRIDE_VAR) {
            Ok(v) => v.parse().unwrap_or(MC_SITEVER as u32),
            Err(_) => MC_SITEVER as u32,
        };

        KeyGen::new(key_prefix, MC_CODEVER as u32, mc_sitever)
    }

    // TODO: Can we optimize to reuse the existing PathWithHash we got?
    pub async fn get_filenode(&self, key: &CacheKey<FilenodeInfo>) -> Option<FilenodeInfo> {
        let now = Instant::now();

        let ret = get_single_filenode_from_memcache(&self.memcache, &self.keygen, key).await;

        let elapsed = now.elapsed().as_micros_unchecked() as i64;
        STATS::get_latency.add_value(elapsed);

        ret
    }

    // TODO: Need to use the same CacheKey here.
    pub fn fill_filenode(&self, key: &CacheKey<FilenodeInfo>, filenode: FilenodeInfo) {
        // Avoid wasting time spawning a fill operation if the memcache is a no-op
        if !self.memcache.is_noop() {
            schedule_fill_filenode(&self.memcache, &self.keygen, key, filenode);
        }
    }

    pub async fn get_history(&self, key: &CacheKey<FilenodeRange>) -> Option<FilenodeRange> {
        let now = Instant::now();

        let ret = get_history_from_memcache(&self.memcache, &self.keygen, key).await;

        let elapsed = now.elapsed().as_micros_unchecked() as i64;
        STATS::get_history.add_value(elapsed);

        ret
    }

    // TODO: Take ownership of key
    pub fn fill_history(&self, key: &CacheKey<FilenodeRange>, filenodes: FilenodeRange) {
        // Avoid wasting time spawning a fill operation if the memcache is a no-op
        if !self.memcache.is_noop() {
            schedule_fill_history(
                self.memcache.clone(),
                self.keygen.clone(),
                key.clone(),
                filenodes,
            );
        }
    }
}

type Pointer = i64;

fn get_mc_key_for_filenodes_list_chunk(
    keygen: &KeyGen,
    key: &CacheKey<FilenodeRange>,
    pointer: Pointer,
) -> String {
    keygen.key(format!("{}.{}", key.key, pointer))
}

async fn get_single_filenode_from_memcache(
    memcache: &MemcacheHandler,
    keygen: &KeyGen,
    key: &CacheKey<FilenodeInfo>,
) -> Option<FilenodeInfo> {
    let key = keygen.key(&key.key);

    let serialized = match memcache.get(key).await {
        Ok(Some(serialized)) => serialized,
        Ok(None) => {
            STATS::point_filenode_miss.add_value(1);
            return None;
        }
        Err(_) => {
            STATS::point_filenode_internal_err.add_value(1);
            return None;
        }
    };

    let thrift = match compact_protocol::deserialize(&serialized) {
        Ok(thrift) => thrift,
        Err(_) => {
            STATS::point_filenode_deserialize_err.add_value(1);
            return None;
        }
    };

    let info = match FilenodeInfo::from_thrift(thrift) {
        Ok(info) => info,
        Err(_) => {
            STATS::point_filenode_deserialize_err.add_value(1);
            return None;
        }
    };

    STATS::point_filenode_hit.add_value(1);

    Some(info)
}

async fn get_history_from_memcache(
    memcache: &MemcacheHandler,
    keygen: &KeyGen,
    key: &CacheKey<FilenodeRange>,
) -> Option<FilenodeRange> {
    // helper function for deserializing list of thrift FilenodeInfo into rust structure with proper
    // error returned
    fn deserialize_list(list: Vec<thrift::FilenodeInfo>) -> Option<Vec<FilenodeInfo>> {
        let res: Result<Vec<_>, _> = list.into_iter().map(FilenodeInfo::from_thrift).collect();
        if res.is_err() {
            STATS::gaf_deserialize_err.add_value(1);
        }
        res.ok()
    }

    let serialized = match memcache.get(keygen.key(&key.key)).await {
        Ok(Some(serialized)) => serialized,
        Ok(None) => {
            STATS::gaf_miss.add_value(1);
            return None;
        }
        Err(_) => {
            STATS::gaf_internal_err.add_value(1);
            return None;
        }
    };

    let thrift = match compact_protocol::deserialize(&serialized) {
        Ok(thrift) => thrift,
        Err(_) => {
            STATS::gaf_deserialize_err.add_value(1);
            return None;
        }
    };

    let res = match thrift {
        thrift::FilenodeInfoList::UnknownField(_) => {
            STATS::gaf_deserialize_err.add_value(1);
            return None;
        }
        thrift::FilenodeInfoList::Data(list) => {
            deserialize_list(list).map(FilenodeRange::Filenodes)
        }
        thrift::FilenodeInfoList::Pointers(list) => {
            STATS::gaf_pointers.add_value(1);

            let read_chunks_fut = list.into_iter().map(move |pointer| {
                let chunk_key = get_mc_key_for_filenodes_list_chunk(keygen, key, pointer);

                async move {
                    match memcache.get(chunk_key).await {
                        Ok(Some(chunk)) => Ok(chunk),
                        _ => Err(()),
                    }
                }
            });

            let blob = match try_join_all(read_chunks_fut).await {
                Ok(chunks) => chunks
                    .into_iter()
                    .flat_map(|b| b.into_iter())
                    .collect::<Vec<u8>>(),
                Err(_) => {
                    STATS::gaf_pointers_err.add_value(1);
                    return None;
                }
            };

            match compact_protocol::deserialize(&blob) {
                Ok(thrift::FilenodeInfoList::Data(list)) => {
                    deserialize_list(list).map(FilenodeRange::Filenodes)
                }
                Ok(thrift::FilenodeInfoList::TooBig(_)) => Some(FilenodeRange::TooBig),
                _ => {
                    STATS::gaf_pointers_err.add_value(1);
                    None
                }
            }
        }
        thrift::FilenodeInfoList::TooBig(_) => Some(FilenodeRange::TooBig),
    };

    if res.is_some() {
        STATS::gaf_hit.add_value(1);
    }

    res
}

fn schedule_fill_filenode(
    memcache: &MemcacheHandler,
    keygen: &KeyGen,
    key: &CacheKey<FilenodeInfo>,
    filenode: FilenodeInfo,
) {
    let serialized = compact_protocol::serialize(&filenode.into_thrift());

    // Quite unlikely that single filenode will be bigger than MEMCACHE_VALUE_MAX_SIZE
    // It's probably not even worth logging it
    if serialized.len() < MEMCACHE_VALUE_MAX_SIZE {
        let memcache = memcache.clone();
        let key = keygen.key(&key.key);
        let fut = async move {
            let _ = memcache.set(key, serialized).await;
        };

        tokio::spawn(fut);
    }
}

fn schedule_fill_history(
    memcache: MemcacheHandler,
    keygen: KeyGen,
    key: CacheKey<FilenodeRange>,
    filenodes: FilenodeRange,
) {
    let fut = async move {
        let _ = fill_history(&memcache, &keygen, &key, filenodes).await;
    };

    tokio::spawn(fut);
}

fn serialize_history(filenodes: FilenodeRange) -> Bytes {
    let filenodes = match filenodes {
        FilenodeRange::Filenodes(filenodes) => thrift::FilenodeInfoList::Data(
            filenodes
                .into_iter()
                .map(|filenode_info| filenode_info.into_thrift())
                .collect(),
        ),
        // Value in TooBig is ignored, so any value would work
        FilenodeRange::TooBig => thrift::FilenodeInfoList::TooBig(0),
    };
    compact_protocol::serialize(&filenodes)
}

async fn fill_history(
    memcache: &MemcacheHandler,
    keygen: &KeyGen,
    key: &CacheKey<FilenodeRange>,
    filenodes: FilenodeRange,
) -> Result<(), ()> {
    let serialized = serialize_history(filenodes);

    STATS::gaf_compact_bytes.add_value(serialized.len() as i64);

    let root = if serialized.len() < MEMCACHE_VALUE_MAX_SIZE {
        serialized
    } else {
        let write_chunks_fut = serialized
            .chunks(MEMCACHE_VALUE_MAX_SIZE)
            .map(Vec::from) // takes ownership
            .zip(PointersIter::new())
            .map({
                move |(chunk, pointer)| {
                    async move {
                        let chunk_key = get_mc_key_for_filenodes_list_chunk(keygen, key, pointer);

                        // give chunks non-random max TTL_SEC_RAND so that they always live
                        // longer than the pointer
                        let chunk_ttl = Duration::from_secs(TTL_SEC + TTL_SEC_RAND);

                        memcache
                            .set_with_ttl(chunk_key, chunk, chunk_ttl)
                            .await
                            .map_err(drop)?;

                        Ok(pointer)
                    }
                }
            })
            .collect::<Vec<_>>();

        let pointers = try_join_all(write_chunks_fut).await?;
        compact_protocol::serialize(&thrift::FilenodeInfoList::Pointers(pointers))
    };

    let root_key = keygen.key(&key.key);
    let root_ttl = Duration::from_secs(TTL_SEC + random::<u64>() % TTL_SEC_RAND);

    memcache
        .set_with_ttl(root_key, root, root_ttl)
        .await
        .map_err(drop)?;

    Ok(())
}

/// Infinite iterator over unique and random i64 values
struct PointersIter {
    seen: HashSet<Pointer>,
}

impl PointersIter {
    fn new() -> Self {
        Self {
            seen: HashSet::new(),
        }
    }
}

impl Iterator for PointersIter {
    type Item = Pointer;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let pointer = random();
            if self.seen.insert(pointer) {
                break Some(pointer);
            }
        }
    }
}

#[cfg(test)]
pub mod test {
    use std::time::Duration;

    use anyhow::Error;
    use fbinit::FacebookInit;
    use mercurial_types_mocks::nodehash::ONES_CSID;
    use mercurial_types_mocks::nodehash::ONES_FNID;
    use mononoke_types::RepoPath;
    use mononoke_types_mocks::repo::REPO_ZERO;
    use path_hash::PathWithHash;
    use tokio::time;

    use super::*;
    use crate::reader::filenode_cache_key;
    use crate::reader::history_cache_key;

    const TIMEOUT_MS: u64 = 100;
    const SLEEP_MS: u64 = 5;

    fn filenode() -> FilenodeInfo {
        FilenodeInfo {
            filenode: ONES_FNID,
            p1: None,
            p2: None,
            copyfrom: Some((RepoPath::file("copiedfrom").unwrap(), ONES_FNID)),
            linknode: ONES_CSID,
        }
    }

    pub async fn wait_for_filenode(
        cache: &RemoteCache,
        key: &CacheKey<FilenodeInfo>,
    ) -> Result<FilenodeInfo, Error> {
        let r = time::timeout(Duration::from_millis(TIMEOUT_MS), async {
            loop {
                match cache.get_filenode(key).await {
                    Some(f) => {
                        break f;
                    }
                    None => {}
                }
                time::sleep(Duration::from_millis(SLEEP_MS)).await;
            }
        })
        .await?;

        Ok(r)
    }

    pub async fn wait_for_history(
        cache: &RemoteCache,
        key: &CacheKey<FilenodeRange>,
    ) -> Result<FilenodeRange, Error> {
        let r = time::timeout(Duration::from_millis(TIMEOUT_MS), async {
            loop {
                match cache.get_history(key).await {
                    Some(f) => {
                        break f;
                    }
                    None => {}
                }
                time::sleep(Duration::from_millis(SLEEP_MS)).await;
            }
        })
        .await?;

        Ok(r)
    }

    #[fbinit::test]
    async fn test_store_filenode(_fb: FacebookInit) -> Result<(), Error> {
        let cache = RemoteCache::new_mock();
        let path = RepoPath::file("copiedto")?;
        let info = filenode();

        let key = filenode_cache_key(
            REPO_ZERO,
            &PathWithHash::from_repo_path(&path),
            &info.filenode,
        );

        cache.fill_filenode(&key, info.clone());
        let from_cache = wait_for_filenode(&cache, &key).await?;

        assert_eq!(from_cache, info);

        Ok(())
    }

    #[fbinit::test]
    async fn test_store_short_history(_fb: FacebookInit) -> Result<(), Error> {
        let cache = RemoteCache::new_mock();
        let path = RepoPath::file("copiedto")?;
        let info = filenode();
        let history = FilenodeRange::Filenodes(vec![info.clone(), info.clone(), info.clone()]);

        let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), None);

        cache.fill_history(&key, history.clone());
        let from_cache = wait_for_history(&cache, &key).await?;

        assert_eq!(from_cache, history);

        Ok(())
    }

    #[fbinit::test]
    async fn test_store_long_history(_fb: FacebookInit) -> Result<(), Error> {
        let cache = RemoteCache::new_mock();
        let path = RepoPath::file("copiedto")?;
        let info = filenode();

        let history =
            FilenodeRange::Filenodes((0..100_000).map(|_| info.clone()).collect::<Vec<_>>());
        assert!(serialize_history(history.clone()).len() >= MEMCACHE_VALUE_MAX_SIZE);

        let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), None);

        cache.fill_history(&key, history.clone());
        let from_cache = wait_for_history(&cache, &key).await?;

        assert_eq!(from_cache, history);

        Ok(())
    }

    #[fbinit::test]
    async fn test_store_too_long_history(_fb: FacebookInit) -> Result<(), Error> {
        let cache = RemoteCache::new_mock();
        let path = RepoPath::file("copiedto")?;

        let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), None);
        cache.fill_history(&key, FilenodeRange::TooBig);
        let from_cache = wait_for_history(&cache, &key).await?;

        assert_eq!(from_cache, FilenodeRange::TooBig);

        Ok(())
    }
}


/*
 * 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 core::future::Future;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;

use anyhow::Error;
use futures_stats::TimedTryFutureExt;
use mercurial_types::HgFileNodeId;
use mononoke_types::RepoPath;
use stats::prelude::*;
use time_ext::DurationExt;
use tokio::sync::Semaphore;

define_stats! {
    prefix = "mononoke.filenodes";
    filenodes_shard_checkout_ms: histogram(10, 0, 1_000, Average, Count; P 5; P 25; P 50; P 75; P 95; P 99; P 100),
    history_shard_checkout_ms: histogram(10, 0, 1_000, Average, Count; P 5; P 25; P 50; P 75; P 95; P 99; P 100),
}

#[derive(Debug)]
pub struct Shards {
    filenodes: Vec<Semaphore>,
    history: Vec<Semaphore>,
}

impl Shards {
    pub fn new(filenodes_concurrency: usize, history_concurrency: usize) -> Self {
        let filenodes = (0..filenodes_concurrency)
            .map(|_| Semaphore::new(1))
            .collect();

        let history = (0..history_concurrency)
            .map(|_| Semaphore::new(1))
            .collect();

        Self { filenodes, history }
    }
}
impl Shards {
    pub fn with_filenodes<F, T, Fut>(
        self: Arc<Self>,
        path: &RepoPath,
        filenode_id: HgFileNodeId,
        f: F,
    ) -> tokio::task::JoinHandle<Result<T, Error>>
    where
        F: FnOnce() -> Fut + Send + 'static,
        T: Send + Sync + 'static,
        Fut: Future<Output = Result<T, Error>> + Send,
    {
        let mut hasher = DefaultHasher::new();
        path.hash(&mut hasher);
        filenode_id.hash(&mut hasher);
        let index = (hasher.finish() % self.filenodes.len() as u64) as usize;
        // We must task::spawn() the code that runs while the semaphore is acquired
        // in order to reduce the risk of deadlocks. See T102183795 for details.
        tokio::spawn(async move {
            let (stats, _permit) = self.filenodes[index].acquire().try_timed().await?;
            STATS::filenodes_shard_checkout_ms
                .add_value(stats.completion_time.as_millis_unchecked() as i64);
            f().await
        })
    }

    pub fn with_history<F, T, Fut>(
        self: Arc<Self>,
        path: &RepoPath,
        f: F,
    ) -> tokio::task::JoinHandle<Result<T, Error>>
    where
        F: FnOnce() -> Fut + Send + 'static,
        T: Send + Sync + 'static,
        Fut: Future<Output = Result<T, Error>> + Send,
    {
        let mut hasher = DefaultHasher::new();
        path.hash(&mut hasher);
        let index = (hasher.finish() % self.history.len() as u64) as usize;
        // We must task::spawn() the code that runs while the semaphore is acquired
        // in order to reduce the risk of deadlocks. See T102183795 for details.
        tokio::spawn(async move {
            let (stats, _permit) = self.history[index].acquire().try_timed().await?;
            STATS::history_shard_checkout_ms
                .add_value(stats.completion_time.as_millis_unchecked() as i64);
            f().await
        })
    }
}


/*
 * 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::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;

static ENFORCE_SQL_TIMEOUTS: AtomicBool = AtomicBool::new(true);

pub fn should_enforce_sql_timeouts() -> bool {
    ENFORCE_SQL_TIMEOUTS.load(Ordering::Relaxed)
}

pub fn disable_sql_timeouts() {
    ENFORCE_SQL_TIMEOUTS.store(false, Ordering::Relaxed);
}


/*
 * 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 anyhow::Context;
use anyhow::Error;
use context::CoreContext;
use context::PerfCounterType;
use filenodes::FilenodeResult;
use filenodes::PreparedFilenode;
use futures::future;
use mercurial_types::HgChangesetId;
use mercurial_types::HgFileNodeId;
use mercurial_types::RepoPath;
use mononoke_types::RepositoryId;
use path_hash::PathBytes;
use path_hash::PathHash;
use path_hash::PathHashBytes;
use sql::Connection;
use sql_ext::mononoke_queries;
use stats::prelude::*;
use thiserror::Error as DeriveError;
use tunables::tunables;
use vec1::Vec1;

define_stats! {
    prefix = "mononoke.filenodes";
    adds: timeseries(Rate, Sum, Count),
    adds_disabled: timeseries(Rate, Sum, Count),
}

#[derive(Debug, Eq, DeriveError, PartialEq)]
pub enum ErrorKind {
    #[error("Invalid copy: {0:?} copied from {1:?}")]
    InvalidCopy(RepoPath, RepoPath),
}

pub struct FilenodesWriter {
    chunk_size: usize,
    write_connections: Vec1<Connection>,
    read_connections: Vec1<Connection>,
}

impl FilenodesWriter {
    pub fn new(
        chunk_size: usize,
        write_connections: Vec1<Connection>,
        read_connections: Vec1<Connection>,
    ) -> Self {
        Self {
            chunk_size,
            write_connections,
            read_connections,
        }
    }

    pub async fn insert_filenodes(
        &self,
        ctx: &CoreContext,
        repo_id: RepositoryId,
        filenodes: Vec<PreparedFilenode>,
        replace: bool,
    ) -> Result<FilenodeResult<()>, Error> {
        STATS::adds.add_value(filenodes.len() as i64);

        let shard_count = self.write_connections.len();

        let mut groups = Vec::with_capacity(shard_count);
        for _ in 0..shard_count {
            groups.push(Vec::new());
        }

        for filenode in filenodes {
            let pwh = PathHash::from_repo_path(&filenode.path);
            let shard_number = pwh.shard_number(shard_count);
            groups[shard_number].push((pwh, filenode));
        }

        let futs = groups
            .into_iter()
            .enumerate()
            .map(|(shard_number, group)| {
                self.insert_filenode_group(ctx, repo_id, shard_number, group, replace)
            })
            .collect::<Vec<_>>();

        let res = future::try_join_all(futs).await?;

        for r in res {
            if let FilenodeResult::Disabled = r {
                return Ok(FilenodeResult::Disabled);
            }
        }

        Ok(FilenodeResult::Present(()))
    }

    async fn insert_filenode_group(
        &self,
        ctx: &CoreContext,
        repo_id: RepositoryId,
        shard_number: usize,
        filenodes: Vec<(PathHash, PreparedFilenode)>,
        replace: bool,
    ) -> Result<FilenodeResult<()>, Error> {
        if tunables().filenodes_disabled().unwrap_or_default() {
            STATS::adds_disabled.add_value(1);
            return Ok(FilenodeResult::Disabled);
        }

        for chunk in filenodes.chunks(self.chunk_size) {
            let read_conn = &self.read_connections[shard_number];
            let write_conn = &self.write_connections[shard_number];
            ensure_paths_exists(ctx, read_conn, write_conn, repo_id, chunk)
                .await
                .context("Error ensuring filenode paths exist")?;
            insert_filenodes(ctx, write_conn, repo_id, chunk, replace)
                .await
                .context("Error inserting filenodes")?;
        }

        Ok(FilenodeResult::Present(()))
    }
}

async fn ensure_paths_exists(
    ctx: &CoreContext,
    read_conn: &Connection,
    write_conn: &Connection,
    repo_id: RepositoryId,
    filenodes: &[(PathHash, PreparedFilenode)],
) -> Result<(), Error> {
    let path_hashes = filenodes
        .iter()
        .map(|(pwh, _)| pwh.hash.clone())
        .collect::<Vec<_>>();

    ctx.perf_counters()
        .increment_counter(PerfCounterType::SqlReadsMaster);
    let mut paths_present = SelectAllPaths::query(read_conn, &repo_id, &path_hashes[..])
        .await?
        .into_iter()
        .map(|r| r.0)
        .collect::<HashSet<_>>();

    let mut paths_to_insert = filenodes
        .iter()
        .filter_map(|(pwh, _)| {
            if paths_present.insert(pwh.path_bytes.clone()) {
                Some((&repo_id, &pwh.path_bytes, &pwh.hash))
            } else {
                None
            }
        })
        .collect::<Vec<_>>();

    // If you have two concurrent INSERT OR IGNORE queries happening with the same rows, but in
    // different order, they will deadlock. Sorting the rows in each of our INSERT OR IGNORE
    // queries solves that. So we do it here.
    paths_to_insert.sort();

    ctx.perf_counters()
        .increment_counter(PerfCounterType::SqlWrites);
    InsertPaths::query(write_conn, &paths_to_insert[..])
        .await
        .with_context(|| format!("Error inserting {} filenode paths", paths_to_insert.len()))?;

    Ok(())
}

async fn insert_filenodes(
    ctx: &CoreContext,
    write_conn: &Connection,
    repo_id: RepositoryId,
    filenodes: &[(PathHash, PreparedFilenode)],
    replace: bool,
) -> Result<(), Error> {
    let mut filenode_rows = Vec::new();
    let mut copydata_rows = Vec::new();

    for (ph, filenode) in filenodes {
        filenode_rows.push((
            &repo_id,
            &ph.hash,
            ph.sql_is_tree(),
            &filenode.info.filenode,
            &filenode.info.linknode,
            &filenode.info.p1,
            &filenode.info.p2,
            if filenode.info.copyfrom.is_some() {
                &1i8
            } else {
                &0i8
            },
        ));

        if let Some(ref copyinfo) = filenode.info.copyfrom {
            let (ref frompath, ref fromnode) = copyinfo;
            let from_pwh = PathHash::from_repo_path(frompath);
            if from_pwh.is_tree != ph.is_tree {
                let e = ErrorKind::InvalidCopy(filenode.path.clone(), frompath.clone());
                return Err(e.into());
            }
            copydata_rows.push((
                &repo_id,
                &ph.hash,
                &filenode.info.filenode,
                ph.sql_is_tree(),
                from_pwh.hash,
                fromnode,
            ));
        }
    }

    let mut copydata_rows = copydata_rows
        .iter()
        .map(
            |&(repo_id, tohash, tonode, is_tree, ref fromhash, fromnode)| {
                (repo_id, tohash, tonode, is_tree, fromhash, fromnode)
            },
        )
        .collect::<Vec<_>>();

    // See above for why we sort.
    filenode_rows.sort();
    copydata_rows.sort();

    ctx.perf_counters()
        .increment_counter(PerfCounterType::SqlWrites);
    if replace {
        ReplaceFilenodes::query(write_conn, &filenode_rows[..]).await?;
    } else {
        InsertFilenodes::query(write_conn, &filenode_rows[..]).await?;
    }

    if !copydata_rows.is_empty() {
        ctx.perf_counters()
            .increment_counter(PerfCounterType::SqlWrites);
        InsertFixedcopyinfo::query(write_conn, &copydata_rows[..]).await?;
    }

    Ok(())
}

mononoke_queries! {
    write InsertPaths(values: (repo_id: RepositoryId, path: PathBytes, path_hash: PathHashBytes)) {
        insert_or_ignore,
        "{insert_or_ignore} INTO paths (repo_id, path, path_hash) VALUES {values}"
    }

    read SelectAllPaths(repo_id: RepositoryId, >list path_hashes: PathHashBytes) -> (PathBytes) {
        "SELECT path
         FROM paths
         WHERE paths.repo_id = {repo_id}
           AND paths.path_hash in {path_hashes}"
    }

    write InsertFilenodes(values: (
        repo_id: RepositoryId,
        path_hash: PathHashBytes,
        is_tree: i8,
        filenode: HgFileNodeId,
        linknode: HgChangesetId,
        p1: Option<HgFileNodeId>,
        p2: Option<HgFileNodeId>,
        has_copyinfo: i8,
    )) {
        insert_or_ignore,
        "{insert_or_ignore} INTO filenodes (
            repo_id
            , path_hash
            , is_tree
            , filenode
            , linknode
            , p1
            , p2
            , has_copyinfo
        ) VALUES {values}"
    }

    write ReplaceFilenodes(values: (
        repo_id: RepositoryId,
        path_hash: PathHashBytes,
        is_tree: i8,
        filenode: HgFileNodeId,
        linknode: HgChangesetId,
        p1: Option<HgFileNodeId>,
        p2: Option<HgFileNodeId>,
        has_copyinfo: i8,
    )) {
        none,
        "REPLACE INTO filenodes (
            repo_id
            , path_hash
            , is_tree
            , filenode
            , linknode
            , p1
            , p2
            , has_copyinfo
        ) VALUES {values}"
    }

    write InsertFixedcopyinfo(values: (
        repo_id: RepositoryId,
        topath_hash: PathHashBytes,
        tonode: HgFileNodeId,
        is_tree: i8,
        frompath_hash: PathHashBytes,
        fromnode: HgFileNodeId,
    )) {
        insert_or_ignore,
        "{insert_or_ignore} INTO fixedcopyinfo (
            repo_id
            , topath_hash
            , tonode
            , is_tree
            , frompath_hash
            , fromnode
        ) VALUES {values}"
    }
}


/*
 * 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.
 */

mod test_cache_fill;
mod test_reader;
mod test_writer;
mod util;


/*
 * 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::sync::Arc;

use anyhow::anyhow;
use anyhow::Error;
use context::CoreContext;
use fbinit::FacebookInit;
use filenodes::FilenodeInfo;
use filenodes::FilenodeRange;
use filenodes::PreparedFilenode;
use mercurial_types_mocks::nodehash::ONES_CSID;
use mercurial_types_mocks::nodehash::ONES_FNID;
use mercurial_types_mocks::nodehash::TWOS_CSID;
use mercurial_types_mocks::nodehash::TWOS_FNID;
use mononoke_types::RepoPath;
use mononoke_types_mocks::repo::REPO_ZERO;
use path_hash::PathWithHash;
use vec1::vec1;

use super::util::build_reader_writer;
use super::util::build_shard;
use crate::local_cache::LocalCache;
use crate::reader::filenode_cache_key;
use crate::reader::history_cache_key;
use crate::remote_cache::test::wait_for_filenode;
use crate::remote_cache::test::wait_for_history;
use crate::remote_cache::RemoteCache;

fn filenode() -> FilenodeInfo {
    FilenodeInfo {
        filenode: ONES_FNID,
        p1: None,
        p2: None,
        copyfrom: None,
        linknode: ONES_CSID,
    }
}

fn second_filenode() -> FilenodeInfo {
    FilenodeInfo {
        filenode: TWOS_FNID,
        p1: None,
        p2: None,
        copyfrom: None,
        linknode: TWOS_CSID,
    }
}

#[fbinit::test]
async fn test_filenode_fill(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mut reader, writer) = build_reader_writer(vec1![build_shard()?]);

    reader.local_cache = LocalCache::new_mock();
    reader.remote_cache = RemoteCache::new_mock();
    let mut reader = Arc::new(reader);

    let path = RepoPath::file("file")?;
    let info = filenode();

    writer
        .insert_filenodes(
            &ctx,
            REPO_ZERO,
            vec![PreparedFilenode {
                path: path.clone(),
                info: info.clone(),
            }],
            false,
        )
        .await?
        .do_not_handle_disabled_filenodes()?;

    let key = filenode_cache_key(
        REPO_ZERO,
        &PathWithHash::from_repo_path(&path),
        &info.filenode,
    );

    // A local miss should fill the remote cache:
    reader
        .clone()
        .get_filenode(&ctx, REPO_ZERO, &path, info.filenode)
        .await?
        .do_not_handle_disabled_filenodes()?;
    wait_for_filenode(&reader.remote_cache, &key).await?;

    // A local hit should not fill the remote cache:
    Arc::get_mut(&mut reader).unwrap().remote_cache = RemoteCache::new_mock();
    reader
        .clone()
        .get_filenode(&ctx, REPO_ZERO, &path, info.filenode)
        .await?
        .do_not_handle_disabled_filenodes()?;
    let r = wait_for_filenode(&reader.remote_cache, &key).await;
    assert!(r.is_err());

    Ok(())
}

#[fbinit::test]
async fn test_history_fill(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mut reader, writer) = build_reader_writer(vec1![build_shard()?]);

    reader.local_cache = LocalCache::new_mock();
    reader.remote_cache = RemoteCache::new_mock();
    let mut reader = Arc::new(reader);

    let path = RepoPath::file("file")?;
    let info = filenode();

    writer
        .insert_filenodes(
            &ctx,
            REPO_ZERO,
            vec![PreparedFilenode {
                path: path.clone(),
                info: info.clone(),
            }],
            false,
        )
        .await?
        .do_not_handle_disabled_filenodes()?;

    let limit = None;
    // A local miss should fill the remote cache:
    reader
        .clone()
        .get_all_filenodes_for_path(&ctx, REPO_ZERO, &path, limit)
        .await?
        .do_not_handle_disabled_filenodes()?;

    let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), limit);
    wait_for_history(&reader.remote_cache, &key).await?;

    // A local hit should not fill the remote cache:
    Arc::get_mut(&mut reader).unwrap().remote_cache = RemoteCache::new_mock();
    reader
        .clone()
        .get_all_filenodes_for_path(&ctx, REPO_ZERO, &path, limit)
        .await?
        .do_not_handle_disabled_filenodes()?;
    let r = wait_for_history(&reader.remote_cache, &key).await;
    assert!(r.is_err());

    Ok(())
}

#[fbinit::test]
async fn test_too_big_caching(fb: FacebookInit) -> Result<(), Error> {
    let ctx = CoreContext::test_mock(fb);
    let (mut reader, writer) = build_reader_writer(vec1![build_shard()?]);

    reader.local_cache = LocalCache::new_mock();
    reader.remote_cache = RemoteCache::new_mock();
    let reader = Arc::new(reader);

    let path = RepoPath::file("file")?;
    let info = filenode();
    let second_info = second_filenode();

    writer
        .insert_filenodes(
            &ctx,
            REPO_ZERO,
            vec![
                PreparedFilenode {
                    path: path.clone(),
                    info: info.clone(),
                },
                PreparedFilenode {
                    path: path.clone(),
                    info: second_info.clone(),
                },
            ],
            false,
        )
        .await?
        .do_not_handle_disabled_filenodes()?;

    let limit = Some(1);
    reader
        .clone()
        .get_all_filenodes_for_path(&ctx, REPO_ZERO, &path, limit)
        .await?
        .do_not_handle_disabled_filenodes()?;

    let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), limit);
    let res = reader
        .local_cache
        .get_history(&key)
        .ok_or_else(|| anyhow!("key not found"))?;

    assert_eq!(res, FilenodeRange::TooBig);

    // Make sure we get a cache miss if another limit parameter is used
    let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), None);
    assert!(reader.local_cache.get_history(&key).is_none());

    let key = history_cache_key(REPO_ZERO, &PathWithHash::from_repo_path(&path), Some(2));
    assert!(reader.local_cache.get_history(&key).is_none());

    Ok(())
}