/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


#include "pagespeed/system/system_caches.h"

#include <cstddef>
#include <cstdlib>
#include <utility>
#include <tuple>

#include "base/logging.h"
#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "pagespeed/system/apr_mem_cache.h"
#include "pagespeed/system/redis_cache.h"
#include "pagespeed/system/system_cache_path.h"
#include "pagespeed/system/system_rewrite_options.h"
#include "pagespeed/system/system_server_context.h"
#include "pagespeed/system/external_server_spec.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/abstract_shared_mem.h"
#include "pagespeed/kernel/base/md5_hasher.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/cache/async_cache.h"
#include "pagespeed/kernel/cache/cache_batcher.h"
#include "pagespeed/kernel/cache/cache_stats.h"
#include "pagespeed/kernel/cache/compressed_cache.h"
#include "pagespeed/kernel/cache/fallback_cache.h"
#include "pagespeed/kernel/cache/file_cache.h"
#include "pagespeed/kernel/cache/purge_context.h"
#include "pagespeed/kernel/cache/write_through_cache.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/thread/slow_worker.h"

namespace net_instaweb {

const char SystemCaches::kMemcachedAsync[] = "memcached_async";
const char SystemCaches::kMemcachedBlocking[] = "memcached_blocking";
const char SystemCaches::kRedisAsync[] = "redis_async";
const char SystemCaches::kRedisBlocking[] = "redis_blocking";
const char SystemCaches::kShmCache[] = "shm_cache";
const char SystemCaches::kDefaultSharedMemoryPath[] = "pagespeed_default_shm";

SystemCaches::SystemCaches(
    RewriteDriverFactory* factory, AbstractSharedMem* shm_runtime,
    int thread_limit)
    : factory_(factory),
      shared_mem_runtime_(shm_runtime),
      thread_limit_(thread_limit),
      is_root_process_(true),
      was_shut_down_(false),
      cache_hasher_(20),
      default_shm_metadata_cache_creation_failed_(false) {
}

SystemCaches::~SystemCaches() {
  DCHECK(was_shut_down_);
}

void SystemCaches::ShutDown(MessageHandler* message_handler) {
  DCHECK(!was_shut_down_);
  if (was_shut_down_) {
    return;
  }

  was_shut_down_ = true;

  // Shut down the cache cleaning thread so we no longer have to worry about
  // outstanding jobs in the slow_worker_ trying
  // to access FileCache and similar objects we're about to blow away.
  if (!is_root_process_) {
    slow_worker_->ShutDown();
  }

  // Take down any threads serving external caches, then wait for shut down to
  // complete and free memory. Note that this will block waiting for any
  // operations currently in progress to terminate. It's the reason we initiate
  // shutdown and wait for it instead of simple calling ShutDown().
  //
  // In case of memcached it can possibly require kill -9 to restart Apache if
  // memcached is permanently hung. In pracice, the patches made in
  // src/third_party/aprutil/apr_memcache2.c make that very unlikely.
  //
  // The alternative scenario of exiting with pending I/O will often
  // crash and always leak memory. Note that if memcached crashes, as
  // opposed to hanging, it will probably not appear wedged.
  if (memcached_pool_) {
    memcached_pool_->InitiateShutDown();
  }
  if (redis_pool_) {
    redis_pool_->InitiateShutDown();
  }
  if (memcached_pool_) {
    memcached_pool_->WaitForShutDownComplete();
    memcached_pool_.reset(nullptr);
  }
  if (redis_pool_) {
    redis_pool_->WaitForShutDownComplete();
    redis_pool_.reset(nullptr);
  }

  if (is_root_process_) {
    // Cleanup per-path shm resources.
    for (PathCacheMap::iterator p = path_cache_map_.begin(),
             e = path_cache_map_.end(); p != e; ++p) {
      p->second->GlobalCleanup(message_handler);
    }

    // And all the SHM caches.
    for (MetadataShmCacheMap::iterator p = metadata_shm_caches_.begin(),
             e = metadata_shm_caches_.end(); p != e; ++p) {
      if (p->second->cache_backend != NULL && p->second->initialized) {
        MetadataShmCache::GlobalCleanup(shared_mem_runtime_, p->second->segment,
                                        message_handler);
      }
    }
  }
}

SystemCachePath* SystemCaches::GetCache(SystemRewriteOptions* config) {
  GoogleString path = SystemCachePath::CachePath(config);
  SystemCachePath* system_cache_path = NULL;
  std::pair<PathCacheMap::iterator, bool> result = path_cache_map_.insert(
      PathCacheMap::value_type(path, system_cache_path));
  PathCacheMap::iterator iter = result.first;
  if (result.second) {
    iter->second = system_cache_path =
        new SystemCachePath(path, config, factory_, shared_mem_runtime_);
    factory_->TakeOwnership(system_cache_path);
  } else {
    system_cache_path = iter->second;
    system_cache_path->MergeConfig(config);
  }
  return system_cache_path;
}

SystemCaches::ExternalCacheInterfaces
SystemCaches::ConstructExternalCacheInterfacesFromBlocking(
    CacheInterface* backend,
    QueuedWorkerPool* pool, int batcher_max_parallel_lookups,
    const char* async_stats_name, const char* blocking_stats_name) {

  ExternalCacheInterfaces result;

  if (pool == NULL) {
    result.async = backend;
  } else {
    result.async = new AsyncCache(backend, pool);
    factory_->TakeOwnership(result.async);
  }

  // Put the batcher above the stats so that the stats sees the MultiGets
  // and can show us the histogram of how they are sized.
  result.async = new CacheStats(async_stats_name,
                                result.async,
                                factory_->timer(),
                                factory_->statistics());
  factory_->TakeOwnership(result.async);

  CacheBatcher::Options options;
  if (batcher_max_parallel_lookups != -1) {
    options.max_parallel_lookups = batcher_max_parallel_lookups;
  }
  CacheBatcher* batcher = new CacheBatcher(
      options,
      result.async,
      factory_->thread_system()->NewMutex(),
      factory_->statistics());
  factory_->TakeOwnership(batcher);
  result.async = batcher;

  // Populate the blocking interface, giving it its own
  // statistics wrapper.
  result.blocking = new CacheStats(blocking_stats_name, backend,
                                   factory_->timer(), factory_->statistics());
  factory_->TakeOwnership(result.blocking);
  return result;
}

SystemCaches::ExternalCacheInterfaces SystemCaches::NewMemcached(
    SystemRewriteOptions* config) {
  const ExternalClusterSpec& servers_specs = config->memcached_servers();
  AprMemCache* mem_cache =
      new AprMemCache(servers_specs, thread_limit_, &cache_hasher_,
                      factory_->statistics(), factory_->timer(),
                      factory_->message_handler());
  factory_->TakeOwnership(mem_cache);
  mem_cache->set_timeout_us(config->memcached_timeout_us());
  memcache_servers_.push_back(mem_cache);

  int num_threads = config->memcached_threads();
  if (num_threads != 0) {
    if (num_threads != 1) {
      factory_->message_handler()->Message(
          kWarning, "ModPagespeedMemcachedThreads support for >1 thread "
          "is not supported yet; changing to 1 thread (was %d)",
          num_threads);
      num_threads = 1;
    }

    if (memcached_pool_.get() == NULL) {
      // Note -- we will use the first value of ModPagespeedMemCacheThreads
      // that we see in a VirtualHost, ignoring later ones.
      memcached_pool_.reset(
          new QueuedWorkerPool(num_threads, "memcached",
                               factory_->thread_system()));
    }
    return ConstructExternalCacheInterfacesFromBlocking(
        mem_cache, memcached_pool_.get(), num_threads, kMemcachedAsync,
        kMemcachedBlocking);
  } else {
    return ConstructExternalCacheInterfacesFromBlocking(
        mem_cache,
        NULL,  // No worker pool.
        -1,    // Do not change batcher's max_parallel_lookups.
        kMemcachedAsync, kMemcachedBlocking);
  }
}

SystemCaches::ExternalCacheInterfaces SystemCaches::NewRedis(
    SystemRewriteOptions* config) {
  const ExternalServerSpec& server_spec = config->redis_server();
  // using database index -1 when property not specified in config
  const int redis_database_index =
      config->has_redis_database_index() ? config->redis_database_index() : -1;
  const int redis_ttl_sec =
      config->has_redis_ttl_sec() ? config->redis_ttl_sec() : -1;
  RedisCache* redis_server = new RedisCache(
      server_spec.host, server_spec.port, factory_->thread_system(),
      factory_->message_handler(), factory_->timer(),
      config->redis_reconnection_delay_ms(), config->redis_timeout_us(),
      factory_->statistics(), redis_database_index, redis_ttl_sec);
  factory_->TakeOwnership(redis_server);
  redis_servers_.push_back(redis_server);
  if (redis_pool_.get() == NULL) {
    // TODO(yeputons): consider using more than one thread and making the amount
    // configurable. For memcached using more than one thread was not boosting
    // performance and that opportunity was eventually disabled, but that could
    // be caused by some bug which is possibly already fixed.
    //
    // Looks like adding more threads won't be a win until RedisCache is async,
    // because all queries will still be queued as they require exclusive access
    // to RedisCache. Creating a separate single-threaded pool for different
    // RedisCaches could be a good idea, though.
    redis_pool_.reset(
        new QueuedWorkerPool(1, "redis", factory_->thread_system()));
  }
  return ConstructExternalCacheInterfacesFromBlocking(
      redis_server, redis_pool_.get(), 1, kRedisAsync, kRedisBlocking);
}

SystemCaches::ExternalCacheInterfaces SystemCaches::NewExternalCache(
    SystemRewriteOptions* config) {
  bool use_redis = !config->redis_server().empty();
  bool use_memcached = !config->memcached_servers().empty();

  if (use_redis && use_memcached) {
    factory_->message_handler()->Message(
        kWarning, "Redis and Memcached are enabled simultaneously, will use "
        "Redis and ignore Memcached");
    use_memcached = false;
  }

  // Some unique signature to distinguish server configurations.
  GoogleString spec_signature;
  if (use_redis) {
    spec_signature =
        StrCat("r;", config->redis_server().ToString(), ";",
               IntegerToString(config->redis_database_index()), ";",
               IntegerToString(config->redis_reconnection_delay_ms()), ";",
               IntegerToString(config->redis_timeout_us()), ";",
               IntegerToString(config->redis_ttl_sec()));
  } else if (use_memcached) {
    spec_signature = StrCat("m;", config->memcached_servers().ToString(), ";",
                            IntegerToString(config->memcached_threads()), ";",
                            IntegerToString(config->memcached_timeout_us()));
  } else {
    return ExternalCacheInterfaces();
  }

  ExternalCachesMap::iterator iterator;
  bool inserted;
  std::tie(iterator, inserted) = external_caches_map_.insert(
      ExternalCachesMap::value_type(spec_signature, ExternalCacheInterfaces()));
  if (inserted) {
    // Was not in the map, should construct new one and store it.
    if (use_redis) {
      iterator->second = NewRedis(config);
    } else if (use_memcached) {
      iterator->second = NewMemcached(config);
    }
  }

  // Some per-VirtualHost modifications follow, we do not want to store them in
  // map.
  ExternalCacheInterfaces result = iterator->second;
  if (use_memcached) {
    // Note that a distinct FallbackCache gets created for every VirtualHost
    // that employs memcached, even if the memcached and file-cache
    // specifications are identical.  This does no harm, because there
    // is no data in the cache object itself; just configuration.  Sharing
    // FallbackCache* objects would require making a map using the
    // memcache & file-cache specs as a key (and possibly duplicating memcache
    // objects as a result), so it's simpler to make a new small FallbackCache
    // object for each VirtualHost.

    CacheInterface* file_cache = GetCache(config)->file_cache();

    result.async = new FallbackCache(result.async, file_cache,
                                     AprMemCache::kValueSizeThreshold,
                                     factory_->message_handler());
    factory_->TakeOwnership(result.async);

    result.blocking = new FallbackCache(result.blocking, file_cache,
                                        AprMemCache::kValueSizeThreshold,
                                        factory_->message_handler());
    factory_->TakeOwnership(result.blocking);
  }
  return result;
}

bool SystemCaches::CreateShmMetadataCache(
    StringPiece name, int64 size_kb, GoogleString* error_msg) {
  MetadataShmCacheInfo* cache_info = NULL;
  std::pair<MetadataShmCacheMap::iterator, bool> result =
      metadata_shm_caches_.insert(
          MetadataShmCacheMap::value_type(name.as_string(), cache_info));
  if (result.second) {
    int entries, blocks;
    int64 size_cap;
    const int kSectors = 128;
    MetadataShmCache::ComputeDimensions(
        size_kb, 2 /* block/entry ratio, based empirically off load tests */,
        kSectors, &entries, &blocks, &size_cap);

    // Make sure the size cap is not unusably low. In particular, with 2K
    // inlining thresholds, something like 3K is needed. (As of time of writing,
    // that required about 4.3MiB).
    if (size_cap < 3 * 1024) {
      metadata_shm_caches_.erase(result.first);
      *error_msg = "Shared memory cache unusably small.";
      return false;
    } else {
      cache_info = new MetadataShmCacheInfo;
      factory_->TakeOwnership(cache_info);
      cache_info->segment = StrCat(name, "/metadata_cache");
      cache_info->cache_backend =
          new SharedMemCache<64>(
              shared_mem_runtime_,
              cache_info->segment,
              factory_->timer(),
              factory_->hasher(),
              kSectors,
              entries,  /* entries per sector */
              blocks /* blocks per sector*/,
              factory_->message_handler());
      factory_->TakeOwnership(cache_info->cache_backend);
      // We can't set cache_info->cache_to_use yet since statistics aren't ready
      // yet. It will happen in ::RootInit().
      result.first->second = cache_info;
      return true;
    }
  } else if (name == kDefaultSharedMemoryPath) {
    // If the default shared memory cache already exists, and we try to create
    // it again, that's not a problem.  This happens because when we check if
    // the cache exists yet we look at MetadataShmCacheInfo->cache_to_use which
    // isn't actually set until RootInit().
    return true;
  } else {
    *error_msg = StrCat("Cache named ", name, " already exists.");
    return false;
  }
}

NamedLockManager* SystemCaches::GetLockManager(SystemRewriteOptions* config) {
  return GetCache(config)->lock_manager();
}

SystemCaches::MetadataShmCacheInfo* SystemCaches::LookupShmMetadataCache(
    const GoogleString& name) {
  if (name.empty()) {
    return NULL;
  }
  MetadataShmCacheMap::iterator i = metadata_shm_caches_.find(name);
  if (i != metadata_shm_caches_.end()) {
    return i->second;
  }
  return NULL;
}

SystemCaches::MetadataShmCacheInfo* SystemCaches::GetShmMetadataCacheOrDefault(
    SystemRewriteOptions* config) {
  MetadataShmCacheInfo* shm_cache =
      LookupShmMetadataCache(config->file_cache_path());
  if (shm_cache != NULL) {
    return shm_cache;  // Explicitly configured.
  }
  if (shared_mem_runtime_->IsDummy()) {
    // We're on a system that doesn't actually support shared memory.
    return NULL;
  }
  if (config->default_shared_memory_cache_kb() == 0) {
    return NULL;  // User has disabled the default shm cache.
  }
  shm_cache = LookupShmMetadataCache(kDefaultSharedMemoryPath);
  if (shm_cache != NULL) {
    return shm_cache;  // Using the default shm cache, which already exists.
  }
  if (default_shm_metadata_cache_creation_failed_) {
    return NULL;  // Already tried to create the default shm cache and failed.
  }
  // This config is for the first server context to need the default cache;
  // create it.
  GoogleString error_msg;
  bool ok = CreateShmMetadataCache(kDefaultSharedMemoryPath,
                                   config->default_shared_memory_cache_kb(),
                                   &error_msg);
  if (!ok) {
    factory_->message_handler()->Message(
        kWarning, "Default shared memory cache: %s", error_msg.c_str());
    default_shm_metadata_cache_creation_failed_ = true;
    return NULL;
  }
  return LookupShmMetadataCache(kDefaultSharedMemoryPath);
}

void SystemCaches::SetupPcacheCohorts(ServerContext* server_context,
                                      bool enable_property_cache) {
  server_context->set_enable_property_cache(enable_property_cache);
  PropertyCache* pcache = server_context->page_property_cache();
  server_context->set_beacon_cohort(
      server_context->AddCohort(RewriteDriver::kBeaconCohort, pcache));
  server_context->set_dom_cohort(
      server_context->AddCohort(RewriteDriver::kDomCohort, pcache));
  server_context->set_dependencies_cohort(
      server_context->AddCohort(RewriteDriver::kDependenciesCohort, pcache));
}

void SystemCaches::SetupCaches(ServerContext* server_context,
                               bool enable_property_cache) {
  SystemRewriteOptions* config = dynamic_cast<SystemRewriteOptions*>(
      server_context->global_options());
  DCHECK(config != NULL);
  SystemCachePath* caches_for_path = GetCache(config);
  CacheInterface* lru_cache = caches_for_path->lru_cache();
  CacheInterface* file_cache = caches_for_path->file_cache();
  MetadataShmCacheInfo* shm_metadata_cache_info =
      GetShmMetadataCacheOrDefault(config);
  CacheInterface* shm_metadata_cache = (shm_metadata_cache_info != NULL) ?
      shm_metadata_cache_info->cache_to_use : NULL;
  CacheInterface* property_store_cache = NULL;
  CacheInterface* http_l2 = file_cache;
  Statistics* stats = server_context->statistics();

  ExternalCacheInterfaces external_cache = NewExternalCache(config);
  if (external_cache.async != nullptr) {
    CHECK(external_cache.blocking != nullptr);

    http_l2 = external_cache.async;

    // Use the blocking version of our external cache for the filesystem
    // metadata cache AND the property store cache.  Note that if there is
    // a shared-memory cache, then we will override this setting and use it
    // for the filesystem metadata cache below.
    server_context->set_filesystem_metadata_cache(external_cache.blocking);
    property_store_cache = external_cache.blocking;
  }

  // Figure out our L1/L2 hierarchy for http cache.
  // TODO(jmarantz): consider moving ownership of the LRU cache into the
  // factory, rather than having one per vhost.
  //
  // Note that a user can disable the LRU cache by setting its byte-count
  // to 0, and in fact this is the default setting.
  int64 max_content_length = config->max_cacheable_response_content_length();
  HTTPCache* http_cache = NULL;
  if (lru_cache == NULL) {
    // No L1, and so backend is just the L2.
    http_cache = new HTTPCache(http_l2, factory_->timer(),
                               factory_->hasher(), stats);
    http_cache->SetCompressionLevel(config->http_cache_compression_level());
  } else {
    // L1 is LRU, with the L2 as computed above.
    WriteThroughCache* write_through_http_cache = new WriteThroughCache(
        lru_cache, http_l2);
    server_context->DeleteCacheOnDestruction(write_through_http_cache);
    write_through_http_cache->set_cache1_limit(config->lru_cache_byte_limit());
    http_cache = new HTTPCache(write_through_http_cache, factory_->timer(),
                               factory_->hasher(), stats);
    http_cache->set_cache_levels(2);
    http_cache->SetCompressionLevel(config->http_cache_compression_level());
  }

  http_cache->set_max_cacheable_response_content_length(max_content_length);
  server_context->set_http_cache(http_cache);

  // And now the metadata cache. If we only have one level, it will be in
  // metadata_l2, with metadata_l1 set to NULL.
  CacheInterface* metadata_l1 = NULL;
  CacheInterface* metadata_l2 = NULL;
  size_t l1_size_limit = WriteThroughCache::kUnlimited;
  if (shm_metadata_cache != NULL) {
    if (external_cache.async != nullptr) {
      // If we have both a local SHM cache and a cache on an external server
      // we should go L1/L2 because there are likely to be other machines
      // that would like to use our metadata.
      metadata_l1 = shm_metadata_cache;
      metadata_l2 = external_cache.async;

      // Because external cache share the metadata cache across machines,
      // we need a filesystem metadata cache to validate LoadFromFile
      // entries.  We default to using external cache for that, even though
      // the LoadFromFile metadata is usually local to the machine,
      // unless the user specifies an NFS directory in LoadFromFile.
      // This is OK because it is keyed to the machine name.  But if
      // we have a shm cache, then use it instead for the metadata
      // cache.
      //
      // Note that we don't need to use a writethrough or fallback
      // strategy as the data is reasonably inexpensive to recompute
      // on a restart, unlike the metadata_cache which has
      // optimization results, and the payloads are all small (message
      // InputInfo in ../rewriter/cached_result.proto).
      server_context->set_filesystem_metadata_cache(shm_metadata_cache);

      // We checkpoint the shm cache when running with external caches, and
      // restore it (to local shm only) on restart.  Not checkpointing in this
      // case would also be defensible, but the implementation complexity would
      // be very high.  For example, you might have two vhosts that both use the
      // default-enabled shared memory metadata cache, but only one has an
      // external cache enabled.
    } else {
      // For persistence across restarts, we checkpoint the shm_metadata_cache
      // to disk every so often, and restore it on restart. This means we don't
      // need to write most objects through to the file cache, just ones too big
      // to store in the SHM cache.
      FallbackCache* metadata_fallback =
          new FallbackCache(
              shm_metadata_cache, file_cache,
              shm_metadata_cache_info->cache_backend->MaxValueSize(),
              factory_->message_handler());
      // SharedMemCache uses hash-produced fixed size keys internally, so its
      // value size limit isn't affected by key length changes.
      metadata_fallback->set_account_for_key_size(false);
      server_context->DeleteCacheOnDestruction(metadata_fallback);
      metadata_l2 = metadata_fallback;

      // TODO(jmarantz): do we really want to use the shm-cache as a
      // pcache?  The potential for inconsistent data across a
      // multi-server setup seems like it could give confusing results.
    }
  } else {
    l1_size_limit = config->lru_cache_byte_limit();
    metadata_l1 = lru_cache;  // may be NULL
    metadata_l2 = http_l2;  // external or file cache.
  }

  CacheInterface* metadata_cache;

  if (metadata_l1 != NULL) {
    WriteThroughCache* write_through_cache = new WriteThroughCache(
        metadata_l1, metadata_l2);
    server_context->DeleteCacheOnDestruction(write_through_cache);
    write_through_cache->set_cache1_limit(l1_size_limit);
    metadata_cache = write_through_cache;
  } else {
    metadata_cache = metadata_l2;
  }

  // TODO(jmarantz): We probably want to store HTTP cache compressed
  // even without this flag, but we should do it differently, storing
  // only the content compressed and putting in content-encoding:gzip
  // so that mod_gzip doesn't have to recompress on every request.
  if (property_store_cache == NULL) {
    property_store_cache = metadata_l2;
  }
  if (config->compress_metadata_cache()) {
    metadata_cache = new CompressedCache(metadata_cache, stats);
    server_context->DeleteCacheOnDestruction(metadata_cache);
    property_store_cache = new CompressedCache(property_store_cache, stats);
    server_context->DeleteCacheOnDestruction(property_store_cache);
  }
  DCHECK(property_store_cache->IsBlocking());
  server_context->MakePagePropertyCache(
      server_context->CreatePropertyStore(property_store_cache));
  server_context->set_metadata_cache(metadata_cache);
  SetupPcacheCohorts(server_context, enable_property_cache);
  SystemServerContext* system_server_context =
    dynamic_cast<SystemServerContext*>(server_context);
  system_server_context->SetCachePath(caches_for_path);
}

void SystemCaches::RegisterConfig(SystemRewriteOptions* config) {
  // Should fill in path_cache_map_.
  GetCache(config);
  // Should fill in external_caches_map_, memcache_servers_, and
  // redis_servers_.
  NewExternalCache(config);

  // GetShmMetadataCacheOrDefault will create a default cache if one is needed
  // and doesn't exist yet.
  GetShmMetadataCacheOrDefault(config);
}

void SystemCaches::RootInit() {
  const SystemRewriteOptions* global_options =
      SystemRewriteOptions::DynamicCast(factory_->default_options());
  for (MetadataShmCacheMap::iterator p = metadata_shm_caches_.begin(),
           e = metadata_shm_caches_.end(); p != e; ++p) {
    MetadataShmCacheInfo* cache_info = p->second;

    // If we're using the default shared memory cache and different vhosts have
    // set the FileCachePath differently, then where should we store the
    // snapshots?  There's no good answer here, because the FileCachePath is the
    // only path like this we require people to specify.  To handle this,
    // SetPathForSnapshots expects to be called multiple times and uses the file
    // cache matching its configured path, or if none match then whichever value
    // comes first alphabetically.
    //
    // We don't need separate directories here for explicit and default shm
    // caches, because if you set an explicit shm cache for a file cache path we
    // don't create a default one for vhosts using that path.
    //
    // Note: we couldn't set the FileCache when constructing the shm cache
    // because at the time we parsed the directive to construct the shm cache
    // we might not have seen the file cache directive yet.
    //
    // Tell the shm cache about file caches and let it pick one to use for
    // checkpointing.
    for (PathCacheMap::iterator q = path_cache_map_.begin(),
             f = path_cache_map_.end(); q != f; ++q) {
      FileCache* file_cache = q->second->file_cache_backend();
      // It's fine to call RegisterSnapshotFileCache multiple times: it
      // considers all the inputs and picks the best one.
      cache_info->cache_backend->RegisterSnapshotFileCache(
          file_cache,
          global_options->shm_metadata_cache_checkpoint_interval_sec());
    }

    if (cache_info->cache_backend->Initialize()) {
      cache_info->initialized = true;
      cache_info->cache_to_use =
          new CacheStats(kShmCache, cache_info->cache_backend,
                         factory_->timer(), factory_->statistics());
      factory_->TakeOwnership(cache_info->cache_to_use);
    } else {
      factory_->message_handler()->Message(
          kWarning, "Unable to initialize shared memory cache: %s.",
          p->first.c_str());
      cache_info->cache_backend = NULL;
      cache_info->cache_to_use = NULL;
    }
  }

  for (PathCacheMap::iterator p = path_cache_map_.begin(),
           e = path_cache_map_.end(); p != e; ++p) {
    SystemCachePath* cache = p->second;
    cache->RootInit();
  }
}

void SystemCaches::ChildInit() {
  is_root_process_ = false;

  slow_worker_.reset(
      new SlowWorker("slow_work_thread", factory_->thread_system()));
  for (MetadataShmCacheMap::iterator p = metadata_shm_caches_.begin(),
           e = metadata_shm_caches_.end(); p != e; ++p) {
    MetadataShmCacheInfo* cache_info = p->second;
    if ((cache_info->cache_backend != NULL) &&
        !cache_info->cache_backend->Attach()) {
      factory_->message_handler()->Message(
          kWarning, "Unable to attach to shared memory cache: %s.",
          p->first.c_str());
      delete cache_info->cache_backend;
      cache_info->cache_backend = NULL;
      cache_info->cache_to_use = NULL;
    }
  }

  for (PathCacheMap::iterator p = path_cache_map_.begin(),
           e = path_cache_map_.end(); p != e; ++p) {
    SystemCachePath* cache = p->second;
    cache->ChildInit(slow_worker_.get());
  }

  // TODO(yeputons): think about moving StartUp() to some base class of
  // RedisCache and AprMemCache and collapsing these two loops into one.
  for (int i = 0, n = memcache_servers_.size(); i < n; ++i) {
    AprMemCache* mem_cache = memcache_servers_[i];
    // TODO(yeputons): looks like this line does not really "connect", but just
    // loads list of servers into apr_memcached and connects later. Maybe the
    // name should be fixed.
    if (!mem_cache->Connect()) {
      factory_->message_handler()->MessageS(kError, "Memory cache failed");
      abort();  // TODO(jmarantz): is there a better way to exit?
    }
  }

  for (RedisCache* redis_cache : redis_servers_) {
    redis_cache->StartUp();
  }
}

void SystemCaches::StopCacheActivity() {
  if (is_root_process_) {
    // No caches used in root process, so nothing to shutdown.  We could run the
    // shutdown code anyway, except that starts a thread which is unsafe to do
    // in a forking server like Nginx.
    return;
  }

  // Iterate through the map of ExternalCacheInterface objects constructed and
  // try to stop pending operations on async caches. Note that these are not
  // typically AprMemCache* or RedisCache* objects, but instead are a hierarchy
  // of CacheStats*, CacheBatcher*, AsyncCache*, all of which must be stopped.
  for (auto item : external_caches_map_) {
    ExternalCacheInterfaces cache = item.second;
    cache.async->ShutDown();
  }

  // TODO(morlovich): Also shutdown shm caches
}

void SystemCaches::InitStats(Statistics* statistics) {
  AprMemCache::InitStats(statistics);
  FileCache::InitStats(statistics);
  CacheStats::InitStats(SystemCachePath::kFileCache, statistics);
  CacheStats::InitStats(SystemCachePath::kLruCache, statistics);
  CacheStats::InitStats(kShmCache, statistics);
  CacheStats::InitStats(kMemcachedAsync, statistics);
  CacheStats::InitStats(kMemcachedBlocking, statistics);
  CacheStats::InitStats(kRedisAsync, statistics);
  CacheStats::InitStats(kRedisBlocking, statistics);
  CompressedCache::InitStats(statistics);
  PurgeContext::InitStats(statistics);
  RedisCache::InitStats(statistics);
}

void SystemCaches::PrintCacheStats(StatFlags flags, GoogleString* out) {
  // We don't want to print this in per-vhost info since it would leak
  // all the declared caches.
  if (flags & kGlobalView) {
    for (MetadataShmCacheMap::iterator p = metadata_shm_caches_.begin(),
             e = metadata_shm_caches_.end(); p != e; ++p) {
      MetadataShmCacheInfo* cache_info = p->second;
      if (cache_info->cache_backend != NULL) {
        StrAppend(out, "\nShared memory metadata cache '", p->first,
                  "' statistics:\n");
        StringWriter writer(out);
        writer.Write(cache_info->cache_backend->DumpStats(),
                     factory_->message_handler());
      }
    }
  }

  if (flags & kIncludeMemcached) {
    for (int i = 0, n = memcache_servers_.size(); i < n; ++i) {
      AprMemCache* mem_cache = memcache_servers_[i];
      if (!mem_cache->GetStatus(out)) {
        StrAppend(out, "\nError getting memcached server status for ",
                  mem_cache->cluster_spec().ToString());
      }
    }
  }

  if (flags & kIncludeRedis) {
    for (RedisCache* redis : redis_servers_) {
      // We can have partial failures, where some cluster machines give errors
      // and others don't, so have GetStatus handle error reporting.
      redis->GetStatus(out);
    }
  }
}

}  // namespace net_instaweb
