diff --git a/configs/volume.config.default b/configs/volume.config.default index c1aa03644a7..6eafd85aed8 100644 --- a/configs/volume.config.default +++ b/configs/volume.config.default @@ -29,6 +29,37 @@ # disk (assuming each disk has enough free space available). # # To create one volume of size 10% of the total cache space and -# another 1 Gig volume, +# another 1 Gig volume, # volume=1 scheme=http size=10% # volume=2 scheme=http size=1024 +# +# Additional optional parameters: +# +# ramcache=true/false +# Enable or disable RAM cache for this volume (default: true) +# +# ram_cache_size= +# Allocate a dedicated RAM cache pool for this volume (e.g., 512M, 2G) +# This amount is automatically subtracted from the global ram_cache.size +# setting, with the remainder shared among other volumes. +# +# ram_cache_cutoff= +# Override the global ram_cache_cutoff for this volume (e.g., 64K, 1M) +# Objects larger than this will not be stored in RAM cache. +# +# avg_obj_size= +# Override the global min_average_object_size for this volume +# +# fragment_size= +# Override the global target_fragment_size for this volume (max: 4MB) +# +# Advanced RAM cache configuration examples: +# +# Example 1: Volume with dedicated 2GB RAM cache +# volume=1 scheme=http size=40% ram_cache_size=2G +# +# Example 2: Small objects with custom cutoff and dedicated RAM +# volume=2 scheme=http size=20% ram_cache_size=512M ram_cache_cutoff=64K +# +# Example 3: Large media with higher cutoff (shares remaining RAM pool) +# volume=3 scheme=http size=40% ram_cache_cutoff=1M diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 2336a2de6ad..e3e6bc09bfc 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -2617,6 +2617,34 @@ Cache Control used in determining the number of :term:`directory buckets ` to allocate for the in-memory cache directory. +.. ts:cv:: CONFIG proxy.config.cache.default_volumes STRING "" + + Specifies a comma-separated list of cache volume numbers to use as the default + for cache stripe selection when no more specific volume configuration applies. + For example, ``"1,2"`` would use volumes 1 and 2 as the default. + + The volume selection priority order is: + + 1. ``@volume=`` directive in :file:`remap.config` (highest priority) + 2. Hostname matching in :file:`hosting.config` + 3. ``proxy.config.cache.default_volumes`` (if non-empty) + 4. All available cache volumes (lowest priority) + + An empty string (the default) disables this feature, causing |TS| to fall + back directly to using all available volumes when no other configuration + matches. + + This is useful for scenarios where you want to restrict default caching to + specific volumes without configuring hostname patterns in :file:`hosting.config`. + For example, you might want to reserve certain volumes for specific remap rules + while having a different set of default volumes for all other traffic. + +.. topic:: Example + + Assign volumes 1 and 2 as defaults for general traffic :: + + CONFIG proxy.config.cache.default_volumes STRING "1,2" + .. ts:cv:: CONFIG proxy.config.cache.permit.pinning INT 0 :reloadable: @@ -2741,6 +2769,11 @@ RAM Cache Alternatively, it can be set to a fixed value such as **20GB** (21474836480) + This global setting can be overridden on a per-volume basis using the + ``ram_cache_size`` parameter in :file:`volume.config`. Per-volume + allocations are subtracted from the total RAM cache size before + distributing the remainder among volumes without explicit settings. + .. ts:cv:: CONFIG proxy.config.cache.ram_cache_cutoff INT 4194304 Objects greater than this size will not be kept in the RAM cache. @@ -2748,6 +2781,11 @@ RAM Cache in memory in order to improve performance. **4MB** (4194304) + This global setting can be overridden on a per-volume basis using the + ``ram_cache_cutoff`` parameter in :file:`volume.config`. When set, + the per-volume cutoff takes precedence over this global setting for + that specific volume. + .. ts:cv:: CONFIG proxy.config.cache.ram_cache.algorithm INT 1 Two distinct RAM caches are supported, the default (1) being the simpler diff --git a/doc/admin-guide/files/remap.config.en.rst b/doc/admin-guide/files/remap.config.en.rst index 75787502169..73012823734 100644 --- a/doc/admin-guide/files/remap.config.en.rst +++ b/doc/admin-guide/files/remap.config.en.rst @@ -455,6 +455,54 @@ will pass "1" and "2" to plugin1.so and "3" to plugin2.so. This will pass "1" and "2" to plugin1.so and "3" to plugin2.so +.. _remap-config-cache-volume-selection: + +Cache Volume Selection +====================== + +The ``@volume`` directive allows you to override the default cache volume selection +for specific remap rules, bypassing the hostname-based volume selection configured in +:file:`hosting.config`. This provides fine-grained control over which cache volumes +are used for different URL patterns. + +Format +------ + +:: + + @volume= + +Where ```` can be either: + +- A single volume number: ``@volume=4`` +- Multiple comma-separated volume numbers: ``@volume=3,4,5`` + +Volume numbers must be between 1 and 255 (volume 0 is reserved and not usable). +All specified volumes must be defined in :file:`volume.config`. + +Examples +-------- + +:: + + # Single volume for API requests (backward compatibility) + map https://api.example.com/ https://api-origin.example.com/ @volume=4 + + # Multiple volumes for load distribution across SSD volumes + map https://cdn.example.com/ https://cdn-origin.example.com/ @volume=2,3,4 + + # Single high-performance volume for critical services + map https://checkout.example.com/ https://checkout-origin.example.com/ @volume=1 + + # Everything else gets the default volume allocations (hosting.config rules) + map https://www.example.com/ https://origin.example.com/ + +.. note:: + + When using ``@volume``, ensure that the target volumes have appropriate disk space and + performance characteristics for the expected traffic patterns. For multiple volumes, + consider the combined capacity and performance of all specified volumes. + .. _remap-config-named-filters: NextHop Selection Strategies diff --git a/doc/admin-guide/files/volume.config.en.rst b/doc/admin-guide/files/volume.config.en.rst index 70ec2725afd..a55370b739f 100644 --- a/doc/admin-guide/files/volume.config.en.rst +++ b/doc/admin-guide/files/volume.config.en.rst @@ -73,20 +73,65 @@ Optional directory entry sizing You can also add an option ``avg_obj_size=`` to the volume configuration line. This overrides the global :ts:cv:`proxy.config.cache.min_average_object_size` -configuration for this volume. This is useful if you have a volume that is dedicated -for say very small objects, and you need a lot of directory entries to store them. +configuration for this volume. The size supports multipliers (K, M, G, T) for +convenience (e.g., ``avg_obj_size=64K`` or ``avg_obj_size=1M``). This is useful +if you have a volume that is dedicated for say very small objects, and you need +a lot of directory entries to store them. Optional fragment size setting ------------------------------ You can also add an option ``fragment_size=`` to the volume configuration line. This overrides the global :ts:cv:`proxy.config.cache.target_fragment_size` -configuration for this volume. This allows for a smaller, or larger, fragment size -for a particular volume. This may be useful together with ``avg_obj_size`` as well, -since a larger fragment size could reduce the number of directory entries needed -for a large object. +configuration for this volume. The size supports multipliers (K, M, G, T) for +convenience (e.g., ``fragment_size=512K`` or ``fragment_size=2M``). This allows +for a smaller, or larger, fragment size for a particular volume. This may be +useful together with ``avg_obj_size`` as well, since a larger fragment size could +reduce the number of directory entries needed for a large object. -Note that this setting has a maximmum value of 4MB. +Note that this setting has a maximum value of 4MB. + +Optional RAM cache size allocation +----------------------------------- + +You can add an option ``ram_cache_size=`` to the volume configuration line +to allocate a dedicated RAM cache pool for this volume. The size supports +multipliers (K, M, G, T) for convenience (e.g., ``ram_cache_size=512M`` or +``ram_cache_size=2G``). Setting ``ram_cache_size=0`` disables the RAM cache +for this volume, which is equivalent to ``ramcache=false``. + +When ``ram_cache_size`` is specified for a volume, that amount is **automatically +subtracted** from the global :ts:cv:`proxy.config.cache.ram_cache.size` setting, +and the remainder is shared among volumes without private allocations. This ensures +total RAM cache usage never exceeds the configured global limit. + +For example, if the global RAM cache size is 4GB and you allocate 1GB to volume 1 +and 512MB to volume 2, the remaining 2.5GB will be distributed among other volumes +using the normal proportional allocation based on disk space. + +**Important notes:** + +* If the sum of all ``ram_cache_size`` allocations exceeds the global RAM cache size, + a warning is logged and the private allocations are disabled, falling back to + the standard shared allocation. +* This setting only takes effect when :ts:cv:`proxy.config.cache.ram_cache.size` + is set to a positive value (not ``-1`` for automatic sizing). + +Optional RAM cache cutoff override +----------------------------------- + +You can add an option ``ram_cache_cutoff=`` to the volume configuration line +to override the global :ts:cv:`proxy.config.cache.ram_cache_cutoff` setting for +this specific volume. The size supports multipliers (K, M, G, T) for convenience +(e.g., ``ram_cache_cutoff=64K`` or ``ram_cache_cutoff=1M``). + +This cutoff determines the maximum object size that will be stored in the RAM cache. +Objects larger than this size will only be stored on disk. Setting different cutoffs +per volume allows you to: + +* Use larger cutoffs for volumes serving frequently accessed large objects +* Use smaller cutoffs for volumes with many small objects to maximize RAM cache hits +* Disable RAM caching entirely for certain objects by setting a very low cutoff Exclusive spans and volume sizes ================================ @@ -126,5 +171,24 @@ ramcache has been disabled.:: volume=1 scheme=http size=20% volume=2 scheme=http size=20% volume=3 scheme=http size=20% - volume=4 scheme=http size=20% avg_obj_size=4096 - volume=5 scheme=http size=20% ramcache=false fragment_size=524288 + volume=4 scheme=http size=20% avg_obj_size=4K + volume=5 scheme=http size=20% ramcache=false fragment_size=512K + +The following example shows advanced RAM cache configuration with dedicated +allocations and custom cutoffs:: + + # Volume 1: General content with 2GB dedicated RAM cache + volume=1 scheme=http size=40% ram_cache_size=2G + + # Volume 2: Small API responses with custom cutoff and 512MB RAM cache + volume=2 scheme=http size=20% ram_cache_size=512M ram_cache_cutoff=64K + + # Volume 3: Large media with higher cutoff for thumbnails + volume=3 scheme=http size=40% ram_cache_cutoff=1M + +In this example, assuming a global ``proxy.config.cache.ram_cache.size`` of 4GB: + +* Volume 1 gets a dedicated 2GB RAM cache allocation +* Volume 2 gets a dedicated 512MB RAM cache allocation and only caches objects up to 64KB +* Volume 3 shares from the remaining 1.5GB pool (4GB - 2GB - 512MB) and caches objects up to 1MB +* The automatic subtraction ensures total RAM usage stays within the 4GB limit diff --git a/include/iocore/cache/Cache.h b/include/iocore/cache/Cache.h index 2e8369c9091..3a7da523b56 100644 --- a/include/iocore/cache/Cache.h +++ b/include/iocore/cache/Cache.h @@ -52,6 +52,7 @@ struct CacheDisk; class URL; class HTTPHdr; class HTTPInfo; +struct CacheHostRecord; using CacheHTTPHdr = HTTPHdr; using CacheURL = URL; @@ -83,9 +84,9 @@ struct CacheProcessor : public Processor { Action *scan(Continuation *cont, std::string_view hostname = std::string_view{}, int KB_per_second = SCAN_KB_PER_SECOND); Action *lookup(Continuation *cont, const HttpCacheKey *key, CacheFragType frag_type = CACHE_FRAG_TYPE_HTTP); Action *open_read(Continuation *cont, const HttpCacheKey *key, CacheHTTPHdr *request, const HttpConfigAccessor *params, - CacheFragType frag_type = CACHE_FRAG_TYPE_HTTP); + CacheFragType frag_type = CACHE_FRAG_TYPE_HTTP, const CacheHostRecord *volume_host_rec = nullptr); Action *open_write(Continuation *cont, const HttpCacheKey *key, CacheHTTPInfo *old_info, time_t pin_in_cache = 0, - CacheFragType frag_type = CACHE_FRAG_TYPE_HTTP); + CacheFragType frag_type = CACHE_FRAG_TYPE_HTTP, const CacheHostRecord *volume_host_rec = nullptr); Action *remove(Continuation *cont, const HttpCacheKey *key, CacheFragType frag_type = CACHE_FRAG_TYPE_HTTP); /** Mark physical disk/device/file as offline. diff --git a/include/proxy/ReverseProxy.h b/include/proxy/ReverseProxy.h index e7aca26ca04..6e13ba7b331 100644 --- a/include/proxy/ReverseProxy.h +++ b/include/proxy/ReverseProxy.h @@ -57,4 +57,5 @@ bool response_url_remap(HTTPHdr *response_header, UrlRewrite *table); bool reloadUrlRewrite(); bool urlRewriteVerify(); -int url_rewrite_CB(const char *name, RecDataT data_type, RecData data, void *cookie); +void init_remap_volume_host_records(); +int url_rewrite_CB(const char *name, RecDataT data_type, RecData data, void *cookie); diff --git a/include/proxy/http/HttpTransact.h b/include/proxy/http/HttpTransact.h index 64e13a991de..ac4e96df434 100644 --- a/include/proxy/http/HttpTransact.h +++ b/include/proxy/http/HttpTransact.h @@ -102,6 +102,7 @@ using ink_time_t = time_t; struct HttpConfigParams; class HttpSM; +struct CacheHostRecord; #include "iocore/net/ConnectionTracker.h" #include "tscore/InkErrno.h" @@ -493,6 +494,8 @@ class HttpTransact URL *parent_selection_url = nullptr; URL parent_selection_url_storage; + const CacheHostRecord *volume_host_rec = nullptr; + _CacheLookupInfo() {} }; @@ -705,8 +708,9 @@ class HttpTransact MgmtByte cache_open_write_fail_action = 0; - HttpConfigParams *http_config_param = nullptr; - CacheLookupInfo cache_info; + HttpConfigParams *http_config_param = nullptr; + CacheLookupInfo cache_info; + ResolveInfo dns_info; RedirectInfo redirect_info; ConnectionTracker::TxnState outbound_conn_track_state; diff --git a/include/proxy/http/remap/RemapConfig.h b/include/proxy/http/remap/RemapConfig.h index 8456dd846c3..559604e5602 100644 --- a/include/proxy/http/remap/RemapConfig.h +++ b/include/proxy/http/remap/RemapConfig.h @@ -40,6 +40,7 @@ class UrlRewrite; #define REMAP_OPTFLG_INTERNAL 0x0080u /* only allow internal requests to hit this remap */ #define REMAP_OPTFLG_IN_IP 0x0100u /* "in_ip=" option (used for ACL filtering)*/ #define REMAP_OPTFLG_STRATEGY 0x0200u /* "strategy=" the name of the nexthop selection strategy */ +#define REMAP_OPTFLG_VOLUME 0x0400u /* "volume=" cache volume override */ #define REMAP_OPTFLG_MAP_ID 0x0800u /* associate a map ID with this rule */ #define REMAP_OPTFLG_INVERT 0x80000000u /* "invert" the rule (for src_ip and src_ip_category at least) */ #define REMAP_OPTFLG_ALL_FILTERS \ diff --git a/include/proxy/http/remap/UrlMapping.h b/include/proxy/http/remap/UrlMapping.h index dabab071185..8888e1568ff 100644 --- a/include/proxy/http/remap/UrlMapping.h +++ b/include/proxy/http/remap/UrlMapping.h @@ -24,6 +24,8 @@ #pragma once +#include +#include #include #include "tscore/ink_config.h" @@ -36,6 +38,7 @@ #include "tscore/List.h" class NextHopSelectionStrategy; +struct CacheHostRecord; /** * Used to store http referrer strings (and/or regexp) @@ -112,20 +115,41 @@ class url_mapping bool ip_allow_check_enabled_p = false; acl_filter_rule *filter = nullptr; // acl filtering (linked list of rules) LINK(url_mapping, link); // For use with the main Queue linked list holding all the mapping - NextHopSelectionStrategy *strategy = nullptr; - std::string remapKey; - std::atomic _hitCount = 0; // counter can overflow + NextHopSelectionStrategy *strategy = nullptr; + std::string remapKey; + std::atomic _hitCount = 0; // counter can overflow + std::atomic volume_host_rec = nullptr; + + CacheHostRecord * + getVolumeHostRec() const + { + return volume_host_rec.load(std::memory_order_acquire); + } + + void + setVolume(const char *str) + { + if (str && *str) { + _volume_str = str; + } + } + + const std::string & + getVolume() const + { + return _volume_str; + } int getRank() const { return _rank; - }; + } void setRank(int rank) { _rank = rank; - }; + } void setRemapKey() @@ -145,9 +169,12 @@ class url_mapping _hitCount++; } + bool initVolumeHostRec(char *errbuf, size_t errbufsize); + private: std::vector _plugin_inst_list; int _rank = 0; + std::string _volume_str; }; /** diff --git a/include/proxy/http/remap/UrlMappingPathIndex.h b/include/proxy/http/remap/UrlMappingPathIndex.h index 63bc90cd6e6..15acb667835 100644 --- a/include/proxy/http/remap/UrlMappingPathIndex.h +++ b/include/proxy/http/remap/UrlMappingPathIndex.h @@ -40,6 +40,18 @@ class UrlMappingPathIndex void Print() const; std::string PrintUrlMappingPathIndex() const; + // Apply a function to each url_mapping in this index + template + void + foreach_mapping(Func &&f) const + { + for (const auto &trie_pair : m_tries) { + for (auto const &mapping : *trie_pair.second) { + f(const_cast(mapping)); + } + } + } + private: using UrlMappingTrie = Trie; diff --git a/src/iocore/cache/Cache.cc b/src/iocore/cache/Cache.cc index 295a4eadb8c..4cc6f78ff84 100644 --- a/src/iocore/cache/Cache.cc +++ b/src/iocore/cache/Cache.cc @@ -100,6 +100,7 @@ ClassAllocator evacuationBlockAllocator("evacuationBl ClassAllocator cacheRemoveContAllocator("cacheRemoveCont"); ClassAllocator evacuationKeyAllocator("evacuationKey"); std::unordered_set known_bad_disks; +CacheHostRecord *default_volumes_host_rec = nullptr; namespace { @@ -231,7 +232,7 @@ Cache::open_done() } ReplaceablePtr::ScopedReader hosttable(&this->hosttable); - if (hosttable->gen_host_rec.num_cachevols == 0) { + if (hosttable->getGenHostRecCacheVols() == 0) { ready = CacheInitState::FAILED; } else { ready = CacheInitState::INITIALIZED; @@ -242,6 +243,22 @@ Cache::open_done() Emergency("Failed to initialize cache host table"); } + // Initialize default_volumes_host_rec from proxy.config.cache.default_volumes + if (ready == CacheInitState::INITIALIZED && default_volumes_host_rec == nullptr) { + auto default_volumes_str = RecGetRecordStringAlloc("proxy.config.cache.default_volumes"); + + if (default_volumes_str && !default_volumes_str.value().empty()) { + char errbuf[256]; + + default_volumes_host_rec = createCacheHostRecord(default_volumes_str.value().c_str(), errbuf, sizeof(errbuf)); + if (default_volumes_host_rec != nullptr) { + Dbg(dbg_ctl_cache_init, "Initialized default_volumes from '%s'", default_volumes_str.value().c_str()); + } else { + Warning("Failed to parse proxy.config.cache.default_volumes '%s': %s", default_volumes_str.value().c_str(), errbuf); + } + } + } + cacheProcessor.cacheInitialized(); return 0; @@ -529,7 +546,7 @@ Cache::scan(Continuation *cont, std::string_view hostname, int KB_per_second) co Action * Cache::open_read(Continuation *cont, const CacheKey *key, CacheHTTPHdr *request, const HttpConfigAccessor *params, - CacheFragType type, std::string_view hostname) const + CacheFragType type, std::string_view hostname, const CacheHostRecord *volume_host_rec) const { if (!CacheProcessor::IsCacheReady(type)) { cont->handleEvent(CACHE_EVENT_OPEN_READ_FAILED, reinterpret_cast(-ECACHE_NOT_READY)); @@ -537,7 +554,7 @@ Cache::open_read(Continuation *cont, const CacheKey *key, CacheHTTPHdr *request, } ink_assert(caches[type] == this); - StripeSM *stripe = key_to_stripe(key, hostname); + StripeSM *stripe = key_to_stripe(key, hostname, volume_host_rec); Dir result, *last_collision = nullptr; ProxyMutex *mutex = cont->mutex.get(); OpenDirEntry *od = nullptr; @@ -603,8 +620,8 @@ Cache::open_read(Continuation *cont, const CacheKey *key, CacheHTTPHdr *request, // main entry point for writing of http documents Action * -Cache::open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *info, time_t apin_in_cache, CacheFragType type, - std::string_view hostname) const +Cache::open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *old_info, time_t pin_in_cache, CacheFragType type, + std::string_view hostname, const CacheHostRecord *volume_host_rec) const { if (!CacheProcessor::IsCacheReady(type)) { cont->handleEvent(CACHE_EVENT_OPEN_WRITE_FAILED, reinterpret_cast(-ECACHE_NOT_READY)); @@ -613,7 +630,7 @@ Cache::open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *info, ink_assert(caches[type] == this); intptr_t err = 0; - int if_writers = reinterpret_cast(info) == CACHE_ALLOW_MULTIPLE_WRITES; + int if_writers = reinterpret_cast(old_info) == CACHE_ALLOW_MULTIPLE_WRITES; CacheVC *c = new_CacheVC(cont); c->vio.op = VIO::WRITE; c->first_key = *key; @@ -629,10 +646,10 @@ Cache::open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *info, } while (DIR_MASK_TAG(c->key.slice32(2)) == DIR_MASK_TAG(c->first_key.slice32(2))); c->earliest_key = c->key; c->frag_type = CACHE_FRAG_TYPE_HTTP; - c->stripe = key_to_stripe(key, hostname); + c->stripe = key_to_stripe(key, hostname, volume_host_rec); StripeSM *stripe = c->stripe; - c->info = info; - if (c->info && reinterpret_cast(info) != CACHE_ALLOW_MULTIPLE_WRITES) { + c->info = old_info; + if (c->info && reinterpret_cast(old_info) != CACHE_ALLOW_MULTIPLE_WRITES) { /* Update has the following code paths : a) Update alternate header only : @@ -664,9 +681,9 @@ Cache::open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *info, c->f.update = 1; c->op_type = static_cast(CacheOpType::Update); DDbg(dbg_ctl_cache_update, "Update called"); - info->object_key_get(&c->update_key); + old_info->object_key_get(&c->update_key); ink_assert(!(c->update_key.is_zero())); - c->update_len = info->object_size_get(); + c->update_len = old_info->object_size_get(); } else { c->op_type = static_cast(CacheOpType::Write); } @@ -674,7 +691,7 @@ Cache::open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *info, ts::Metrics::Gauge::increment(cache_rsb.status[c->op_type].active); ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.status[c->op_type].active); // coverity[Y2K38_SAFETY:FALSE] - c->pin_in_cache = static_cast(apin_in_cache); + c->pin_in_cache = static_cast(pin_in_cache); { CACHE_TRY_LOCK(lock, c->stripe->mutex, cont->mutex->thread_holding); @@ -745,41 +762,61 @@ CacheVConnection::CacheVConnection() : VConnection(nullptr) {} // if generic_host_rec.stripes == nullptr, what do we do??? StripeSM * -Cache::key_to_stripe(const CacheKey *key, std::string_view hostname) const +Cache::key_to_stripe(const CacheKey *key, std::string_view hostname, const CacheHostRecord *volume_host_rec) const { ReplaceablePtr::ScopedReader hosttable(&this->hosttable); - uint32_t h = (key->slice32(2) >> DIR_TAG_WIDTH) % STRIPE_HASH_TABLE_SIZE; - unsigned short *hash_table = hosttable->gen_host_rec.vol_hash_table; - const CacheHostRecord *host_rec = &hosttable->gen_host_rec; + uint32_t h = (key->slice32(2) >> DIR_TAG_WIDTH) % STRIPE_HASH_TABLE_SIZE; + const CacheHostRecord *host_rec = hosttable->getGenHostRec(); + unsigned short *hash_table = host_rec->vol_hash_table; + StripeSM *selected_stripe = nullptr; + bool remap_selection = false; - if (hosttable->m_numEntries > 0 && !hostname.empty()) { + // Priority 1: @volume directive (highest priority) + if (volume_host_rec && volume_host_rec->vol_hash_table) { + selected_stripe = volume_host_rec->stripes[volume_host_rec->vol_hash_table[h]]; + remap_selection = true; + Dbg(dbg_ctl_cache_hosting, "@volume directive: using volume hash table for stripe selection"); + } + // Priority 2: Normal hostname-based volume selection (from hosting.config) + if (!selected_stripe && hosttable->getNumEntries() > 0 && !hostname.empty()) { CacheHostResult res; + hosttable->Match(hostname, &res); if (res.record) { unsigned short *host_hash_table = res.record->vol_hash_table; + if (host_hash_table) { - if (dbg_ctl_cache_hosting.on()) { - char format_str[50]; - snprintf(format_str, sizeof(format_str), "Volume: %%xd for host: %%.%ds", static_cast(hostname.length())); - Dbg(dbg_ctl_cache_hosting, format_str, res.record, hostname.data()); - } - return res.record->stripes[host_hash_table[h]]; + Dbg(dbg_ctl_cache_hosting, "Volume: %p for host: %.*s", res.record, static_cast(hostname.length()), hostname.data()); + selected_stripe = res.record->stripes[host_hash_table[h]]; } } } - if (hash_table) { - if (dbg_ctl_cache_hosting.on()) { - char format_str[50]; - snprintf(format_str, sizeof(format_str), "Generic volume: %%xd for host: %%.%ds", static_cast(hostname.length())); - Dbg(dbg_ctl_cache_hosting, format_str, host_rec, hostname.data()); + + // Priority 3: Global default volumes from proxy.config.cache.default_volumes + if (!selected_stripe && default_volumes_host_rec && default_volumes_host_rec->vol_hash_table) { + selected_stripe = default_volumes_host_rec->stripes[default_volumes_host_rec->vol_hash_table[h]]; + Dbg(dbg_ctl_cache_hosting, "Using default_volumes for stripe selection"); + } + + // Priority 4: Generic/default volume selection (fallback) + if (!selected_stripe) { + if (hash_table) { + selected_stripe = host_rec->stripes[hash_table[h]]; + } else { + selected_stripe = host_rec->stripes[0]; } - return host_rec->stripes[hash_table[h]]; - } else { - return host_rec->stripes[0]; } -} + if (dbg_ctl_cache_hosting.on() && selected_stripe && selected_stripe->cache_vol) { + Dbg(dbg_ctl_cache_hosting, "Cache volume selected: %d (%s) for key=%08x%08x hostname='%.*s' %s", + selected_stripe->cache_vol->vol_number, + selected_stripe->cache_vol->ramcache_enabled ? "ramcache_enabled" : "ramcache_disabled", key->slice32(0), key->slice32(1), + static_cast(hostname.length()), hostname.data(), remap_selection ? "(remap)" : "(calculated)"); + } + + return selected_stripe; +} int FragmentSizeUpdateCb(const char * /* name ATS_UNUSED */, RecDataT /* data_type ATS_UNUSED */, RecData data, void * /* cookie ATS_UNUSED */) diff --git a/src/iocore/cache/CacheHosting.cc b/src/iocore/cache/CacheHosting.cc index c8f7d5f6790..32d8c4292ee 100644 --- a/src/iocore/cache/CacheHosting.cc +++ b/src/iocore/cache/CacheHosting.cc @@ -38,6 +38,7 @@ namespace DbgCtl dbg_ctl_cache_hosting{"cache_hosting"}; DbgCtl dbg_ctl_matcher{"matcher"}; +constexpr static int MAX_VOLUME_IDX = 255; } // end anonymous namespace /************************************************************* @@ -652,6 +653,8 @@ ConfigVolumes::BuildListFromString(char *config_file_path, char *file_buf) bool ramcache_enabled = true; int avg_obj_size = -1; // Defaults int fragment_size = -1; + int64_t ram_cache_size = -1; // -1 means use shared allocation + int64_t ram_cache_cutoff = -1; // -1 means use global cutoff while (true) { // skip all blank spaces at beginning of line @@ -696,7 +699,7 @@ ConfigVolumes::BuildListFromString(char *config_file_path, char *file_buf) break; } - if (volume_number < 1 || volume_number > 255) { + if (volume_number < 1 || volume_number > MAX_VOLUME_IDX) { err = "Bad Volume Number"; break; } @@ -740,17 +743,33 @@ ConfigVolumes::BuildListFromString(char *config_file_path, char *file_buf) in_percent = 0; } } else if (strcasecmp(tmp, "avg_obj_size") == 0) { // match avg_obj_size - tmp += 13; - avg_obj_size = atoi(tmp); + tmp += 13; + if (!ParseRules::is_digit(*tmp)) { + err = "Invalid avg_obj_size value (must start with a number, e.g., 64K)"; + break; + } + avg_obj_size = static_cast(ink_atoi64(tmp)); - while (ParseRules::is_digit(*tmp)) { + if (avg_obj_size < 0) { + err = "Invalid avg_obj_size value (must be >= 0)"; + break; + } + while (*tmp && (ParseRules::is_digit(*tmp) || strchr("kmgtKMGT", *tmp))) { tmp++; } } else if (strcasecmp(tmp, "fragment_size") == 0) { // match fragment_size - tmp += 14; - fragment_size = atoi(tmp); + tmp += 14; + if (!ParseRules::is_digit(*tmp)) { + err = "Invalid fragment_size value (must start with a number, e.g., 1M)"; + break; + } + fragment_size = static_cast(ink_atoi64(tmp)); - while (ParseRules::is_digit(*tmp)) { + if (fragment_size < 0) { + err = "Invalid fragment_size value (must be >= 0)"; + break; + } + while (*tmp && (ParseRules::is_digit(*tmp) || strchr("kmgtKMGT", *tmp))) { tmp++; } } else if (strcasecmp(tmp, "ramcache") == 0) { // match ramcache @@ -765,11 +784,42 @@ ConfigVolumes::BuildListFromString(char *config_file_path, char *file_buf) err = "Unexpected end of line"; break; } + } else if (strcasecmp(tmp, "ram_cache_size") == 0) { // match ram_cache_size + tmp += 15; + if (!ParseRules::is_digit(*tmp)) { + err = "Invalid ram_cache_size value (must start with a number, e.g., 10G)"; + break; + } + ram_cache_size = ink_atoi64(tmp); + + if (ram_cache_size < 0) { + err = "Invalid ram_cache_size value (must be >= 0)"; + break; + } + // Note: ram_cache_size=0 disables RAM cache for this volume, same as ramcache=false + while (*tmp && (ParseRules::is_digit(*tmp) || strchr("kmgtKMGT", *tmp))) { + tmp++; + } + } else if (strcasecmp(tmp, "ram_cache_cutoff") == 0) { // match ram_cache_cutoff + tmp += 17; + if (!ParseRules::is_digit(*tmp)) { + err = "Invalid ram_cache_cutoff value (must start with a number, e.g., 5M)"; + break; + } + ram_cache_cutoff = ink_atoi64(tmp); + + if (ram_cache_cutoff < 0) { + err = "Invalid ram_cache_cutoff value (must be >= 0)"; + break; + } + while (*tmp && (ParseRules::is_digit(*tmp) || strchr("kmgtKMGT", *tmp))) { + tmp++; + } } // ends here if (end < line_end) { - tmp++; + tmp = line_end; } } @@ -791,6 +841,8 @@ ConfigVolumes::BuildListFromString(char *config_file_path, char *file_buf) configp->size = size; configp->avg_obj_size = avg_obj_size; configp->fragment_size = fragment_size; + configp->ram_cache_size = ram_cache_size; + configp->ram_cache_cutoff = ram_cache_cutoff; configp->cachep = nullptr; configp->ramcache_enabled = ramcache_enabled; cp_queue.enqueue(configp); @@ -809,3 +861,53 @@ ConfigVolumes::BuildListFromString(char *config_file_path, char *file_buf) return; } + +// Wrapper function for deleting CacheHostRecord from outside the cache module. +void +destroyCacheHostRecord(CacheHostRecord *rec) +{ + delete rec; +} + +// Build a CacheHostRecord for @volume= directive with comma-separated volumes +// This reuses CacheHostRecord::Init() by constructing a minimal matcher_line +CacheHostRecord * +createCacheHostRecord(const char *volume_str, char *errbuf, size_t errbufsize) +{ + if (!volume_str || !*volume_str) { + snprintf(errbuf, errbufsize, "Empty volume specification"); + return nullptr; + } + + CacheHostRecord *host_rec = new CacheHostRecord(); + + if (!host_rec) { + snprintf(errbuf, errbufsize, "Memory allocation failed"); + return nullptr; + } + + // Build a minimal matcher_line structure with just the volume= directive + matcher_line ml; + memset(&ml, 0, sizeof(ml)); + + ml.line[0][0] = const_cast("volume"); + ml.line[1][0] = const_cast(volume_str); + ml.num_el = 1; + ml.dest_entry = -1; + ml.line_num = 0; + ml.type = MATCH_NONE; + ml.next = nullptr; + + int result = host_rec->Init(&ml, CacheType::HTTP); + + if (result != 0) { + delete host_rec; + snprintf(errbuf, errbufsize, "Failed to initialize volume record (check volume.config)"); + return nullptr; + } + + Dbg(dbg_ctl_cache_hosting, "Created remap volume record with %d volumes, %d stripes", host_rec->num_cachevols, + host_rec->num_vols); + + return host_rec; +} diff --git a/src/iocore/cache/CacheProcessor.cc b/src/iocore/cache/CacheProcessor.cc index 5610ed7040b..7d2edc63435 100644 --- a/src/iocore/cache/CacheProcessor.cc +++ b/src/iocore/cache/CacheProcessor.cc @@ -411,16 +411,16 @@ CacheProcessor::lookup(Continuation *cont, const HttpCacheKey *key, CacheFragTyp Action * CacheProcessor::open_read(Continuation *cont, const HttpCacheKey *key, CacheHTTPHdr *request, const HttpConfigAccessor *params, - CacheFragType type) + CacheFragType frag_type, const CacheHostRecord *volume_host_rec) { - return caches[type]->open_read(cont, &key->hash, request, params, type, key->hostname); + return caches[frag_type]->open_read(cont, &key->hash, request, params, frag_type, key->hostname, volume_host_rec); } Action * CacheProcessor::open_write(Continuation *cont, const HttpCacheKey *key, CacheHTTPInfo *old_info, time_t pin_in_cache, - CacheFragType type) + CacheFragType frag_type, const CacheHostRecord *volume_host_rec) { - return caches[type]->open_write(cont, &key->hash, old_info, pin_in_cache, type, key->hostname); + return caches[frag_type]->open_write(cont, &key->hash, old_info, pin_in_cache, frag_type, key->hostname, volume_host_rec); } //---------------------------------------------------------------------------- @@ -483,7 +483,7 @@ CacheProcessor::mark_storage_offline(CacheDisk *d, ///< Target disk } else { // check cache types specifically if (theCache) { ReplaceablePtr::ScopedReader hosttable(&theCache->hosttable); - if (!hosttable->gen_host_rec.vol_hash_table) { + if (!hosttable->getGenHostRec()->vol_hash_table) { unsigned int caches_ready = 0; caches_ready = caches_ready | (1 << CACHE_FRAG_TYPE_HTTP); caches_ready = caches_ready | (1 << CACHE_FRAG_TYPE_NONE); @@ -506,8 +506,8 @@ void rebuild_host_table(Cache *cache) { ReplaceablePtr::ScopedWriter hosttable(&cache->hosttable); - build_vol_hash_table(&hosttable->gen_host_rec); - if (hosttable->m_numEntries != 0) { + build_vol_hash_table(const_cast(hosttable->getGenHostRec())); + if (hosttable->getNumEntries() != 0) { CacheHostMatcher *hm = hosttable->getHostMatcher(); CacheHostRecord *h_rec = hm->getDataArray(); int h_rec_len = hm->getNumElements(); @@ -1166,6 +1166,8 @@ cplist_update() cp->ramcache_enabled = config_vol->ramcache_enabled; cp->avg_obj_size = config_vol->avg_obj_size; cp->fragment_size = config_vol->fragment_size; + cp->ram_cache_size = config_vol->ram_cache_size; + cp->ram_cache_cutoff = config_vol->ram_cache_cutoff; config_vol->cachep = cp; } else { /* delete this volume from all the disks */ @@ -1438,9 +1440,27 @@ CacheProcessor::cacheInitialized() } } + // Calculate total private RAM allocations from per-volume configurations int64_t http_ram_cache_size = 0; + int64_t total_private_ram = 0; + + if (cache_config_ram_cache_size != AUTO_SIZE_RAM_CACHE) { + CacheVol *cp = cp_list.head; + + for (; cp; cp = cp->link.next) { + if (cp->ram_cache_size > 0) { + total_private_ram += cp->ram_cache_size; + Dbg(dbg_ctl_cache_init, "Volume %d has private RAM allocation: %" PRId64 " bytes (%" PRId64 " MB)", cp->vol_number, + cp->ram_cache_size, cp->ram_cache_size / (1024 * 1024)); + } + } + + if (total_private_ram > 0) { + Dbg(dbg_ctl_cache_init, "Total private RAM allocations: %" PRId64 " bytes (%" PRId64 " MB)", total_private_ram, + total_private_ram / (1024 * 1024)); + } + } - // let us calculate the Size if (cache_config_ram_cache_size == AUTO_SIZE_RAM_CACHE) { Dbg(dbg_ctl_cache_init, "cache_config_ram_cache_size == AUTO_SIZE_RAM_CACHE"); } else { @@ -1448,12 +1468,31 @@ CacheProcessor::cacheInitialized() // TODO, should we check the available system memories, or you will // OOM or swapout, that is not a good situation for the server Dbg(dbg_ctl_cache_init, "%" PRId64 " != AUTO_SIZE_RAM_CACHE", cache_config_ram_cache_size); - http_ram_cache_size = - static_cast((static_cast(theCache->cache_size) / total_size) * cache_config_ram_cache_size); + + // Calculate shared pool: global RAM cache size minus private allocations + int64_t shared_pool = cache_config_ram_cache_size - total_private_ram; + + if (shared_pool < 0) { + Warning("Total private RAM cache allocations (%" PRId64 " bytes) exceed global ram_cache.size (%" PRId64 " bytes). " + "Using global limit. Consider increasing proxy.config.cache.ram_cache.size.", + total_private_ram, cache_config_ram_cache_size); + shared_pool = cache_config_ram_cache_size; // Fall back to using the global pool for all + + CacheVol *cp = cp_list.head; + + for (; cp; cp = cp->link.next) { + cp->ram_cache_size = 0; + } + } else if (total_private_ram > 0) { + Dbg(dbg_ctl_cache_init, "Shared RAM cache pool (after private allocations): %" PRId64 " bytes (%" PRId64 " MB)", + shared_pool, shared_pool / (1024 * 1024)); + } + + http_ram_cache_size = static_cast((static_cast(theCache->cache_size) / total_size) * shared_pool); Dbg(dbg_ctl_cache_init, "http_ram_cache_size = %" PRId64 " = %" PRId64 "Mb", http_ram_cache_size, http_ram_cache_size / (1024 * 1024)); - int64_t stream_ram_cache_size = cache_config_ram_cache_size - http_ram_cache_size; + int64_t stream_ram_cache_size = shared_pool - http_ram_cache_size; Dbg(dbg_ctl_cache_init, "stream_ram_cache_size = %" PRId64 " = %" PRId64 "Mb", stream_ram_cache_size, stream_ram_cache_size / (1024 * 1024)); @@ -1466,23 +1505,55 @@ CacheProcessor::cacheInitialized() uint64_t total_cache_bytes = 0; // bytes that can used in total_size uint64_t total_direntries = 0; // all the direntries in the cache uint64_t used_direntries = 0; // and used - uint64_t total_ram_cache_bytes = 0; + uint64_t total_ram_cache_bytes = 0; // Total RAM cache size across all volumes + uint64_t shared_cache_size = 0; // Total cache size of volumes without explicit RAM allocations + + // Calculate total cache size of volumes without explicit RAM allocations + if (http_ram_cache_size > 0) { + for (int i = 0; i < gnstripes; i++) { + if (gstripes[i]->cache_vol->ram_cache_size <= 0) { + shared_cache_size += (gstripes[i]->len >> STORE_BLOCK_SHIFT); + } + } + Dbg(dbg_ctl_cache_init, "Shared cache size (for RAM pool distribution): %" PRId64 " blocks", shared_cache_size); + } for (int i = 0; i < gnstripes; i++) { StripeSM *stripe = gstripes[i]; int64_t ram_cache_bytes = 0; - if (stripe->cache_vol->ramcache_enabled) { - if (http_ram_cache_size == 0) { + // If RAM cache enabled, check if this volume has a private RAM cache allocation + if (stripe->cache_vol->ramcache_enabled && stripe->cache_vol->ram_cache_size != 0) { + if (stripe->cache_vol->ram_cache_size > 0) { + int64_t volume_stripe_count = 0; + + for (int j = 0; j < gnstripes; j++) { + if (gstripes[j]->cache_vol == stripe->cache_vol) { + volume_stripe_count++; + } + } + + if (volume_stripe_count > 0) { + ram_cache_bytes = stripe->cache_vol->ram_cache_size / volume_stripe_count; + Dbg(dbg_ctl_cache_init, "Volume %d stripe %d using private RAM allocation: %" PRId64 " bytes (%" PRId64 " MB)", + stripe->cache_vol->vol_number, i, ram_cache_bytes, ram_cache_bytes / (1024 * 1024)); + } + } else if (http_ram_cache_size == 0) { // AUTO_SIZE_RAM_CACHE ram_cache_bytes = stripe->dirlen() * DEFAULT_RAM_CACHE_MULTIPLIER; } else { - ink_assert(stripe->cache != nullptr); - - double factor = static_cast(static_cast(stripe->len >> STORE_BLOCK_SHIFT)) / theCache->cache_size; - Dbg(dbg_ctl_cache_init, "factor = %f", factor); - - ram_cache_bytes = static_cast(http_ram_cache_size * factor); + // Use shared pool allocation - distribute only among volumes without explicit allocations + if (shared_cache_size > 0) { + ink_assert(stripe->cache != nullptr); + double factor = static_cast(static_cast(stripe->len >> STORE_BLOCK_SHIFT)) / shared_cache_size; + + Dbg(dbg_ctl_cache_init, "factor = %f (divisor = %" PRId64 ")", factor, shared_cache_size); + ram_cache_bytes = static_cast(http_ram_cache_size * factor); + } else { + ram_cache_bytes = 0; + Dbg(dbg_ctl_cache_init, "Volume %d stripe has no explicit RAM allocation, but shared pool is empty", + stripe->cache_vol->vol_number); + } } stripe->ram_cache->init(ram_cache_bytes, stripe); @@ -1493,19 +1564,19 @@ CacheProcessor::cacheInitialized() ram_cache_bytes, ram_cache_bytes / (1024 * 1024)); } - uint64_t vol_total_cache_bytes = stripe->len - stripe->dirlen(); - total_cache_bytes += vol_total_cache_bytes; + uint64_t vol_total_cache_bytes = stripe->len - stripe->dirlen(); + uint64_t vol_total_direntries = stripe->directory.entries(); + uint64_t vol_used_direntries = stripe->directory.entries_used(); + + total_cache_bytes += vol_total_cache_bytes; ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.bytes_total, vol_total_cache_bytes); ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.stripes); Dbg(dbg_ctl_cache_init, "total_cache_bytes = %" PRId64 " = %" PRId64 "Mb", total_cache_bytes, total_cache_bytes / (1024 * 1024)); - uint64_t vol_total_direntries = stripe->directory.entries(); - total_direntries += vol_total_direntries; + total_direntries += vol_total_direntries; ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.direntries_total, vol_total_direntries); - - uint64_t vol_used_direntries = stripe->directory.entries_used(); ts::Metrics::Gauge::increment(stripe->cache_vol->vol_rsb.direntries_used, vol_used_direntries); used_direntries += vol_used_direntries; } diff --git a/src/iocore/cache/CacheVC.cc b/src/iocore/cache/CacheVC.cc index d230463e1b1..e21c7a70282 100644 --- a/src/iocore/cache/CacheVC.cc +++ b/src/iocore/cache/CacheVC.cc @@ -412,15 +412,17 @@ CacheVC::handleReadDone(int event, Event * /* e ATS_UNUSED */) // Put the request in the ram cache only if its a open_read or lookup if (vio.op == VIO::READ && okay) { bool cutoff_check; + // Determine effective cutoff: use per-volume override if set, otherwise use global + int64_t effective_cutoff = + (stripe->cache_vol->ram_cache_cutoff > 0) ? stripe->cache_vol->ram_cache_cutoff : cache_config_ram_cache_cutoff; // cutoff_check : // doc_len == 0 for the first fragment (it is set from the vector) // The decision on the first fragment is based on // doc->total_len // After that, the decision is based of doc_len (doc_len != 0) - // (cache_config_ram_cache_cutoff == 0) : no cutoffs - cutoff_check = - ((!doc_len && static_cast(doc->total_len) < cache_config_ram_cache_cutoff) || - (doc_len && static_cast(doc_len) < cache_config_ram_cache_cutoff) || !cache_config_ram_cache_cutoff); + // (effective_cutoff == 0) : no cutoffs + cutoff_check = ((!doc_len && static_cast(doc->total_len) < effective_cutoff) || + (doc_len && static_cast(doc_len) < effective_cutoff) || !effective_cutoff); if (cutoff_check && !f.doc_from_ram_cache) { uint64_t o = dir_offset(&dir); stripe->ram_cache->put(read_key, buf.get(), doc->len, http_copy_hdr, o); @@ -635,7 +637,7 @@ CacheVC::scanStripe(int /* event ATS_UNUSED */, Event * /* e ATS_UNUSED */) ReplaceablePtr::ScopedReader hosttable(&theCache->hosttable); - const CacheHostRecord *rec = &hosttable->gen_host_rec; + const CacheHostRecord *rec = hosttable->getGenHostRec(); if (!hostname.empty()) { CacheHostResult res; hosttable->Match(hostname, &res); diff --git a/src/iocore/cache/P_CacheHosting.h b/src/iocore/cache/P_CacheHosting.h index d6dc8fd6c26..d36821c51e3 100644 --- a/src/iocore/cache/P_CacheHosting.h +++ b/src/iocore/cache/P_CacheHosting.h @@ -62,7 +62,9 @@ struct CacheHostRecord { CacheHostRecord() {} }; -void build_vol_hash_table(CacheHostRecord *cp); +void build_vol_hash_table(CacheHostRecord *cp); +CacheHostRecord *createCacheHostRecord(const char *volume_str, char *errbuf, size_t errbufsize); +void destroyCacheHostRecord(CacheHostRecord *rec); struct CacheHostResult { CacheHostRecord *record = nullptr; @@ -229,18 +231,42 @@ class CacheHostTable void Match(std::string_view rdata, CacheHostResult *result) const; void Print() const; + // Getters for Cache::key_to_stripe access + const CacheHostRecord * + getGenHostRec() const + { + return &gen_host_rec; + } + int - getEntryCount() const + getNumEntries() const { return m_numEntries; } + + int + getGenHostRecCacheVols() const + { + return gen_host_rec.num_cachevols; + } + CacheHostMatcher * getHostMatcher() const { return hostMatch.get(); } - static int config_callback(const char *, RecDataT, RecData, void *); + CacheType + getType() const + { + return type; + } + + Cache * + getCache() const + { + return cache; + } void register_config_callback(ReplaceablePtr *p) @@ -248,12 +274,13 @@ class CacheHostTable RecRegisterConfigUpdateCb("proxy.config.cache.hosting_filename", CacheHostTable::config_callback, (void *)p); } - CacheType type = CacheType::HTTP; - Cache *cache = nullptr; - int m_numEntries = 0; - CacheHostRecord gen_host_rec; - private: + static int config_callback(const char *, RecDataT, RecData, void *); + + CacheType type = CacheType::HTTP; + Cache *cache = nullptr; + int m_numEntries = 0; + CacheHostRecord gen_host_rec; std::unique_ptr hostMatch = nullptr; const matcher_tags config_tags = {"hostname", "domain", nullptr, nullptr, nullptr, nullptr, false}; const char *matcher_name = "unknown"; // Used for Debug/Warning/Error messages @@ -276,8 +303,8 @@ struct CacheHostTableConfig : public Continuation { Cache *cache = nullptr; { ReplaceablePtr::ScopedReader hosttable(ppt); - type = hosttable->type; - cache = hosttable->cache; + type = hosttable->getType(); + cache = hosttable->getCache(); } ppt->reset(new CacheHostTable(cache, type)); delete this; @@ -298,6 +325,8 @@ struct ConfigVol { int percent; int avg_obj_size; int fragment_size; + int64_t ram_cache_size; // Per-volume RAM cache size (-1 = use shared allocation) + int64_t ram_cache_cutoff; // Per-volume RAM cache cutoff (-1 = use global cutoff) CacheVol *cachep; LINK(ConfigVol, link); }; diff --git a/src/iocore/cache/P_CacheInternal.h b/src/iocore/cache/P_CacheInternal.h index 8a8ee3c3aa7..b1ef9b1c649 100644 --- a/src/iocore/cache/P_CacheInternal.h +++ b/src/iocore/cache/P_CacheInternal.h @@ -92,6 +92,9 @@ struct EvacuationBlock; extern CacheStatsBlock cache_rsb; +// Global default volumes host record (initialized from proxy.config.cache.default_volumes) +extern CacheHostRecord *default_volumes_host_rec; + // Configuration extern int cache_config_dir_sync_frequency; extern int cache_config_dir_sync_delay; @@ -465,9 +468,11 @@ struct Cache { Action *scan(Continuation *cont, std::string_view hostname = std::string_view{}, int KB_per_second = 2500) const; Action *open_read(Continuation *cont, const CacheKey *key, CacheHTTPHdr *request, const HttpConfigAccessor *params, - CacheFragType type, std::string_view hostname = std::string_view{}) const; + CacheFragType type, std::string_view hostname = std::string_view{}, + const CacheHostRecord *volume_host_rec = nullptr) const; Action *open_write(Continuation *cont, const CacheKey *key, CacheHTTPInfo *old_info, time_t pin_in_cache = 0, - CacheFragType type = CACHE_FRAG_TYPE_HTTP, std::string_view hostname = std::string_view{}) const; + CacheFragType type = CACHE_FRAG_TYPE_HTTP, std::string_view hostname = std::string_view{}, + const CacheHostRecord *volume_host_rec = nullptr) const; static void generate_key(CryptoHash *hash, CacheURL *url); static void generate_key(HttpCacheKey *hash, CacheURL *url, bool ignore_query = false, cache_generation_t generation = -1); @@ -480,7 +485,7 @@ struct Cache { int open_done(); - StripeSM *key_to_stripe(const CacheKey *key, std::string_view hostname) const; + StripeSM *key_to_stripe(const CacheKey *key, std::string_view hostname, const CacheHostRecord *volume_host_rec = nullptr) const; Cache() {} }; diff --git a/src/iocore/cache/Stripe.h b/src/iocore/cache/Stripe.h index 4bffbe7334b..8bd824b4414 100644 --- a/src/iocore/cache/Stripe.h +++ b/src/iocore/cache/Stripe.h @@ -58,6 +58,8 @@ struct CacheVol { int avg_obj_size = -1; // Defer to the records.config if not overriden int fragment_size = -1; // Defer to the records.config if not overriden bool ramcache_enabled = true; + int64_t ram_cache_size = -1; // Per-volume RAM cache size (-1 = use shared allocation) + int64_t ram_cache_cutoff = -1; // Per-volume RAM cache cutoff (-1 = use global cutoff) StripeSM **stripes = nullptr; DiskStripe **disk_stripes = nullptr; LINK(CacheVol, link); diff --git a/src/proxy/ReverseProxy.cc b/src/proxy/ReverseProxy.cc index 341c2c7de40..93de209f985 100644 --- a/src/proxy/ReverseProxy.cc +++ b/src/proxy/ReverseProxy.cc @@ -39,6 +39,7 @@ #include "proxy/http/remap/RemapProcessor.h" #include "proxy/http/remap/UrlRewrite.h" #include "proxy/http/remap/UrlMapping.h" +#include "proxy/http/remap/UrlMappingPathIndex.h" namespace { @@ -168,6 +169,60 @@ reloadUrlRewrite() } } +/** + * Helper function to initialize volume_host_rec for a single url_mapping. + * This is a no-op if the mapping has no volume string or is already initialized. + */ +static void +init_mapping_volume_host_rec(url_mapping &mapping) +{ + char errbuf[256]; + + if (!mapping.initVolumeHostRec(errbuf, sizeof(errbuf))) { + Error("Failed to initialize volume record for @volume=%s: %s", mapping.getVolume().c_str(), errbuf); + } +} + +static void +init_store_volume_host_records(UrlRewrite::MappingsStore &store) +{ + if (store.hash_lookup) { + for (auto &entry : *store.hash_lookup) { + UrlMappingPathIndex *path_index = entry.second; + + if (path_index) { + path_index->foreach_mapping(init_mapping_volume_host_rec); + } + } + } + + for (UrlRewrite::RegexMapping *reg_map = store.regex_list.head; reg_map; reg_map = reg_map->link.next) { + if (reg_map->url_map) { + init_mapping_volume_host_rec(*reg_map->url_map); + } + } +} + +// This is called after the cache is initialized, since we may need the volume_host_records +void +init_remap_volume_host_records() +{ + UrlRewrite *table = rewrite_table; + + if (!table) { + return; + } + + Dbg(dbg_ctl_url_rewrite, "Initializing volume_host_rec for all remap rules after cache init"); + + // Initialize for all mapping stores + init_store_volume_host_records(table->forward_mappings); + init_store_volume_host_records(table->reverse_mappings); + init_store_volume_host_records(table->permanent_redirects); + init_store_volume_host_records(table->temporary_redirects); + init_store_volume_host_records(table->forward_mappings_with_recv_port); +} + int url_rewrite_CB(const char * /* name ATS_UNUSED */, RecDataT /* data_type ATS_UNUSED */, RecData data, void *cookie) { diff --git a/src/proxy/http/HttpCacheSM.cc b/src/proxy/http/HttpCacheSM.cc index 075e81694fc..43bd86eabcd 100644 --- a/src/proxy/http/HttpCacheSM.cc +++ b/src/proxy/http/HttpCacheSM.cc @@ -317,12 +317,20 @@ HttpCacheSM::_schedule_read_retry() Action * HttpCacheSM::do_cache_open_read(const HttpCacheKey &key) { + Action *action_handle = nullptr; + open_read_tries++; ink_assert(pending_action == nullptr); // Initialising read-while-write-inprogress flag this->readwhilewrite_inprogress = false; - Action *action_handle = cacheProcessor.open_read(this, &key, this->read_request_hdr, &http_params); + + if (master_sm && master_sm->t_state.cache_info.volume_host_rec) { + action_handle = cacheProcessor.open_read(this, &key, this->read_request_hdr, &http_params, CACHE_FRAG_TYPE_HTTP, + master_sm->t_state.cache_info.volume_host_rec); + } else { + action_handle = cacheProcessor.open_read(this, &key, this->read_request_hdr, &http_params); + } if (action_handle != ACTION_RESULT_DONE) { pending_action = action_handle; @@ -416,9 +424,15 @@ HttpCacheSM::open_write(const HttpCacheKey *key, URL *url, HTTPHdr *request, Cac return ACTION_RESULT_DONE; } - // INKqa11166 CacheHTTPInfo *info = allow_multiple ? reinterpret_cast(CACHE_ALLOW_MULTIPLE_WRITES) : old_info; - Action *action_handle = cacheProcessor.open_write(this, key, info, pin_in_cache); + Action *action_handle = nullptr; + + if (master_sm && master_sm->t_state.cache_info.volume_host_rec) { + action_handle = + cacheProcessor.open_write(this, key, info, pin_in_cache, CACHE_FRAG_TYPE_HTTP, master_sm->t_state.cache_info.volume_host_rec); + } else { + action_handle = cacheProcessor.open_write(this, key, info, pin_in_cache); + } if (action_handle != ACTION_RESULT_DONE) { pending_action = action_handle; diff --git a/src/proxy/http/remap/RemapConfig.cc b/src/proxy/http/remap/RemapConfig.cc index a1ceac4e0ee..6e6fd322384 100644 --- a/src/proxy/http/remap/RemapConfig.cc +++ b/src/proxy/http/remap/RemapConfig.cc @@ -36,6 +36,9 @@ #include "tscore/Filenames.h" #include "proxy/IPAllow.h" #include "proxy/http/remap/PluginFactory.h" +#include "iocore/cache/Cache.h" + +extern CacheHostRecord *createCacheHostRecord(const char *volume_str, char *errbuf, size_t errbufsize); using namespace std::literals; @@ -48,6 +51,7 @@ namespace DbgCtl dbg_ctl_url_rewrite{"url_rewrite"}; DbgCtl dbg_ctl_remap_plugin{"remap_plugin"}; DbgCtl dbg_ctl_url_rewrite_regex{"url_rewrite_regex"}; + } // end anonymous namespace /** @@ -829,6 +833,14 @@ remap_check_option(const char *const *argv, int argc, unsigned long findmode, in *argptr = &argv[i][9]; } ret_flags |= REMAP_OPTFLG_STRATEGY; + } else if (!strncasecmp(argv[i], "volume=", 7)) { + if ((findmode & REMAP_OPTFLG_VOLUME) != 0) { + idx = i; + } + if (argptr) { + *argptr = &argv[i][7]; + } + ret_flags |= REMAP_OPTFLG_VOLUME; } else { Warning("ignoring invalid remap option '%s'", argv[i]); } @@ -1197,12 +1209,55 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) if ((bti->remap_optflg & REMAP_OPTFLG_MAP_ID) != 0) { int idx = 0; int ret = remap_check_option(bti->argv, bti->argc, REMAP_OPTFLG_MAP_ID, &idx); + if (ret & REMAP_OPTFLG_MAP_ID) { - char *c = strchr(bti->argv[idx], static_cast('=')); + char *c = strchr(bti->argv[idx], static_cast('=')); + new_mapping->map_id = static_cast(atoi(++c)); } } + // Parse @volume= option with comma-separated syntax (@volume=3,4) + for (int i = 0; i < bti->argc; i++) { + if (!strncasecmp(bti->argv[i], "volume=", 7)) { + const char *volume_str = &bti->argv[i][7]; + + if (!volume_str || !*volume_str) { + snprintf(errStrBuf, sizeof(errStrBuf), "Empty @volume= directive at line %d", cln + 1); + errStr = errStrBuf; + goto MAP_ERROR; + } + + for (const char *p = volume_str; *p; p++) { + if (*p != ',' && (*p < '0' || *p > '9')) { + snprintf(errStrBuf, sizeof(errStrBuf), "Invalid character '%c' in @volume=%s at line %d", *p, volume_str, cln + 1); + errStr = errStrBuf; + goto MAP_ERROR; + } + } + + // Check if cache is ready (will be true during config reload, possibly false during initial startup) + if (CacheProcessor::IsCacheEnabled() == CacheInitState::INITIALIZED) { + char volume_errbuf[256]; + CacheHostRecord *rec = createCacheHostRecord(volume_str, volume_errbuf, sizeof(volume_errbuf)); + + if (!rec) { + snprintf(errStrBuf, sizeof(errStrBuf), "Failed to build volume record for @volume=%s at line %d: %s", volume_str, + cln + 1, volume_errbuf); + errStr = errStrBuf; + goto MAP_ERROR; + } + new_mapping->volume_host_rec.store(rec, std::memory_order_release); + Dbg(dbg_ctl_url_rewrite, "[BuildTable] Cache volume directive built: @volume=%s", volume_str); + } else { + // Store the volume string for lazy initialization after cache is ready + new_mapping->setVolume(volume_str); + Dbg(dbg_ctl_url_rewrite, "[BuildTable] Cache volume directive stored (deferred): @volume=%s", volume_str); + } + break; + } + } + map_from = bti->paramv[1]; length = UrlWhack(map_from, &origLength); diff --git a/src/proxy/http/remap/RemapProcessor.cc b/src/proxy/http/remap/RemapProcessor.cc index e14f8c12178..550af9360e6 100644 --- a/src/proxy/http/remap/RemapProcessor.cc +++ b/src/proxy/http/remap/RemapProcessor.cc @@ -171,6 +171,9 @@ RemapProcessor::finish_remap(HttpTransact::State *s, UrlRewrite *table) return false; } + // Pass the volume_host_rec to the transaction state + s->cache_info.volume_host_rec = map->getVolumeHostRec(); + // Do fast ACL filtering (it is safe to check map here) table->PerformACLFiltering(s, map); diff --git a/src/proxy/http/remap/UrlMapping.cc b/src/proxy/http/remap/UrlMapping.cc index 0a3a85236ec..887625c91a7 100644 --- a/src/proxy/http/remap/UrlMapping.cc +++ b/src/proxy/http/remap/UrlMapping.cc @@ -27,6 +27,11 @@ #include "records/RecCore.h" #include "tscore/ink_cap.h" +// Avoid including private header files +struct CacheHostRecord; +extern void destroyCacheHostRecord(CacheHostRecord *rec); +extern CacheHostRecord *createCacheHostRecord(const char *volume_str, char *errbuf, size_t errbufsize); + namespace { DbgCtl dbg_ctl_url_rewrite{"url_rewrite"}; @@ -84,11 +89,39 @@ url_mapping::~url_mapping() delete afr; } + // Destroy any volume hosting records + destroyCacheHostRecord(volume_host_rec.load(std::memory_order_acquire)); + // Destroy the URLs fromURL.destroy(); toURL.destroy(); } +bool +url_mapping::initVolumeHostRec(char *errbuf, size_t errbufsize) +{ + if (_volume_str.empty() || volume_host_rec.load(std::memory_order_acquire) != nullptr) { + return true; + } + + CacheHostRecord *new_rec = createCacheHostRecord(_volume_str.c_str(), errbuf, errbufsize); + + if (!new_rec) { + return false; + } + + CacheHostRecord *expected = nullptr; + + if (volume_host_rec.compare_exchange_strong(expected, new_rec, std::memory_order_acq_rel)) { + Dbg(dbg_ctl_url_rewrite, "Initialized volume_host_rec for @volume=%s", _volume_str.c_str()); + return true; + } else { + // Another thread beat us to it, destroy our copy + destroyCacheHostRecord(new_rec); + return true; + } +} + void url_mapping::Print() const { diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index 8cd99ca6a9c..b7b476716a9 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -79,6 +79,8 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.cache.persist_bad_disks", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_INT, "[01]", RECA_NULL} , + {RECT_CONFIG, "proxy.config.cache.default_volumes", RECD_STRING, "", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , {RECT_CONFIG, "proxy.config.output.logfile.name", RECD_STRING, "traffic.out", RECU_RESTART_TS, RR_REQUIRED, RECC_NULL, nullptr, RECA_NULL} , diff --git a/src/traffic_server/traffic_server.cc b/src/traffic_server/traffic_server.cc index 61982e77168..7a049664a0d 100644 --- a/src/traffic_server/traffic_server.cc +++ b/src/traffic_server/traffic_server.cc @@ -831,6 +831,10 @@ CB_After_Cache_Init() start = ink_atomic_swap(&delay_listen_for_cache, -1); emit_fully_initialized_message(); + // Initialize volume_host_rec for any remap rules with @volume= directives + // that were deferred during startup because cache wasn't ready yet + init_remap_volume_host_records(); + if (1 == start) { // The delay_listen_for_cache value was 1, therefore the main function // delayed the call to start_HttpProxyServer until we got here. We must diff --git a/tests/gold_tests/cache/cache_volume_defaults.replay.yaml b/tests/gold_tests/cache/cache_volume_defaults.replay.yaml new file mode 100644 index 00000000000..648738bf601 --- /dev/null +++ b/tests/gold_tests/cache/cache_volume_defaults.replay.yaml @@ -0,0 +1,217 @@ +# 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. + +meta: + version: "1.0" + +# Configuration section for autest integration +autest: + description: "Test proxy.config.cache.default_volumes configuration" + + dns: + name: "dns" + + server: + name: "server" + + client: + name: "client" + + ats: + name: "ts" + + records_config: + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: "cache|cache_hosting|cache_init" + proxy.config.http.insert_response_via_str: 0 + proxy.config.cache.enable_read_while_writer: 0 + # Set default volumes to volume 2 - this is the key configuration under test + proxy.config.cache.default_volumes: "2" + + volume_config: + # Volume 1: Used for explicit @volume=1 selection + - volume: 1 + scheme: "http" + size: "32M" + + # Volume 2: Used as the default volume via default_volumes setting + - volume: 2 + scheme: "http" + size: "32M" + + # Volume 3: Another volume to verify routing is correct + - volume: 3 + scheme: "http" + size: "32M" + + remap_config: + # Default volume selection (should use default_volumes -> volume 2) + - from: "http://default.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + + # Explicit volume 1 selection (should override default_volumes) + - from: "http://volume1.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + options: + - "@volume=1" + + # Explicit volume 3 selection (should override default_volumes) + - from: "http://volume3.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + options: + - "@volume=3" + +# Proxy verifier test sessions +sessions: + # Test 1: Request using default_volumes (should use volume 2) + # This verifies that when no @volume= is specified, the default_volumes setting is used + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/default_test" + headers: + fields: + - [Host, default.example.com] + - [X-Test-ID, "default-volumes-test"] + - [uuid, 1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Default-Volumes-Test, "success"] + content: + data: "Content using default_volumes" + + proxy-response: + status: 200 + headers: + fields: + - [X-Default-Volumes-Test, { value: "success", as: equal }] + + # Test 2: Request with explicit @volume=1 (should override default_volumes) + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/volume1_test" + headers: + fields: + - [Host, volume1.example.com] + - [X-Test-ID, "volume1-override-test"] + - [uuid, 2] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Volume-Override, "volume1"] + content: + data: "Content explicitly on volume 1" + + proxy-response: + status: 200 + headers: + fields: + - [X-Volume-Override, { value: "volume1", as: equal }] + + # Test 3: Request with explicit @volume=3 (should override default_volumes) + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/volume3_test" + headers: + fields: + - [Host, volume3.example.com] + - [X-Test-ID, "volume3-override-test"] + - [uuid, 3] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Volume-Override, "volume3"] + content: + data: "Content explicitly on volume 3" + + proxy-response: + status: 200 + headers: + fields: + - [X-Volume-Override, { value: "volume3", as: equal }] + + # Test 4: Cache hit test for default_volumes content + # First request to populate cache + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/cached_default" + headers: + fields: + - [Host, default.example.com] + - [X-Test-ID, "cache-hit-setup"] + - [uuid, 4] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Origin-Response, "yes"] + content: + data: "Cached content via default_volumes" + + proxy-response: + status: 200 + headers: + fields: + - [X-Origin-Response, { value: "yes", as: equal }] + + # Test 5: Second request should be a cache hit + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/cached_default" + headers: + fields: + - [Host, default.example.com] + - [X-Test-ID, "cache-hit-verify"] + - [uuid, 5] + + # Server should not be contacted for cache hit + server-response: + status: 500 + reason: "Should not reach server" + + proxy-response: + status: 200 + content: + data: "Cached content via default_volumes" diff --git a/tests/gold_tests/cache/cache_volume_defaults.test.py b/tests/gold_tests/cache/cache_volume_defaults.test.py new file mode 100644 index 00000000000..80f3a1149fd --- /dev/null +++ b/tests/gold_tests/cache/cache_volume_defaults.test.py @@ -0,0 +1,24 @@ +# 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. + +Test.Summary = ''' +Test proxy.config.cache.default_volumes configuration: +- Verify that default_volumes is used as fallback when no other volume selection applies +- Verify that @volume= directive takes priority over default_volumes +- Verify that hosting.config takes priority over default_volumes +''' + +Test.ATSReplayTest(replay_file="cache_volume_defaults.replay.yaml",) diff --git a/tests/gold_tests/cache/cache_volume_features.replay.yaml b/tests/gold_tests/cache/cache_volume_features.replay.yaml new file mode 100644 index 00000000000..77243e2a8ef --- /dev/null +++ b/tests/gold_tests/cache/cache_volume_features.replay.yaml @@ -0,0 +1,254 @@ +# 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. + +meta: + version: "1.0" + +# Configuration section for autest integration +autest: + description: "Test cache volume features: per-volume RAM cache and @volume= directive" + + dns: + name: "dns" + + server: + name: "server" + + client: + name: "client" + + ats: + name: "ts" + + records_config: + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: "cache|cache_hosting|ram_cache" + proxy.config.cache.ram_cache.size: 128M + proxy.config.cache.ram_cache_cutoff: 4K + proxy.config.http.insert_response_via_str: 0 + proxy.config.cache.enable_read_while_writer: 0 + + volume_config: + # Volume 1: Explicit 32MB RAM cache, 8KB cutoff + - volume: 1 + scheme: "http" + size: "64M" + ram_cache_size: "32M" + ram_cache_cutoff: "8K" + + # Volume 2: Explicit 16MB RAM cache, 2KB cutoff + - volume: 2 + scheme: "http" + size: "32M" + ram_cache_size: "16M" + ram_cache_cutoff: "2K" + + # Volume 3: Default RAM cache settings (will use shared pool) + - volume: 3 + scheme: "http" + size: "32M" + + remap_config: + # Default volume selection (no @volume=) + - from: "http://default.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + + # Explicit volume 1 selection + - from: "http://volume1.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + options: + - "@volume=1" + + # Explicit volume 2 selection + - from: "http://volume2.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + options: + - "@volume=2" + + # Invalid volume selection (should fallback) + - from: "http://invalid.example.com/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/" + options: + - "@volume=99" + +# Proxy verifier test sessions +sessions: + # Test 1: Volume selection via @volume=1 + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/test1" + headers: + fields: + - [Host, volume1.example.com] + - [X-Test-ID, "volume1-test"] + - [uuid, 1] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Volume-Test, "1"] + content: + data: "Content for volume 1" + + proxy-response: + status: 200 + headers: + fields: + - [X-Volume-Test, { value: "1", as: equal }] + + # Test 2: Volume selection via @volume=2 + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/test2" + headers: + fields: + - [Host, volume2.example.com] + - [X-Test-ID, "volume2-test"] + - [uuid, 2] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Volume-Test, "2"] + content: + data: "Content for volume 2" + + proxy-response: + status: 200 + headers: + fields: + - [X-Volume-Test, { value: "2", as: equal }] + + # Test 3: Invalid volume selection (should fallback to default) + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/test_invalid" + headers: + fields: + - [Host, invalid.example.com] + - [X-Test-ID, "invalid-volume-test"] + - [uuid, 3] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Volume-Test, "fallback"] + content: + data: "Content for invalid volume (fallback)" + + proxy-response: + status: 200 + headers: + fields: + - [X-Volume-Test, { value: "fallback", as: equal }] + + # Test 4: Default volume selection (no @volume=) + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/default" + headers: + fields: + - [Host, default.example.com] + - [X-Test-ID, "default-volume-test"] + - [uuid, 4] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Volume-Test, "default"] + content: + data: "Content for default volume selection" + + proxy-response: + status: 200 + headers: + fields: + - [X-Volume-Test, { value: "default", as: equal }] + + # Test 5: Cache hit test (verify caching works) + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/cached_content" + headers: + fields: + - [Host, volume2.example.com] + - [X-Test-ID, "cache-hit-setup"] + - [uuid, 5] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [Content-Type, "text/plain"] + - [Cache-Control, "max-age=3600"] + - [X-Origin-Response, "yes"] + content: + data: "This content should be cached" + + proxy-response: + status: 200 + headers: + fields: + - [X-Origin-Response, { value: "yes", as: equal }] + + # Test 6: Second request for cached content (should be cache hit) + - transactions: + - client-request: + method: "GET" + version: "1.1" + url: "/cached_content" + headers: + fields: + - [Host, volume2.example.com] + - [X-Test-ID, "cache-hit-test"] + - [uuid, 6] + + # Server should not be contacted for cache hit + server-response: + status: 500 + reason: "Should not reach server" + + proxy-response: + status: 200 + content: + data: "This content should be cached" diff --git a/tests/gold_tests/cache/cache_volume_features.test.py b/tests/gold_tests/cache/cache_volume_features.test.py new file mode 100644 index 00000000000..71d52167c4c --- /dev/null +++ b/tests/gold_tests/cache/cache_volume_features.test.py @@ -0,0 +1,24 @@ +# 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. + +Test.Summary = ''' +Comprehensive test suite for cache volume features: +- Per-volume RAM cache configuration (ram_cache_size, ram_cache_cutoff) +- @volume= directive in remap.config for volume selection +- Integration between both features +''' + +Test.ATSReplayTest(replay_file="cache_volume_features.replay.yaml",)