This is an automated email from the ASF dual-hosted git repository.
bnolsen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new fb691b5306 Configurable Hash Algorithm and Parameters for Consistent
Hash Parent Selection (#12567)
fb691b5306 is described below
commit fb691b53066e1b291fd1be2aa379c699e89bfb1f
Author: Phil Sorber <[email protected]>
AuthorDate: Wed Nov 19 12:39:29 2025 -0700
Configurable Hash Algorithm and Parameters for Consistent Hash Parent
Selection (#12567)
* Make Parent Selection Hash configurable and add SipHash-1-3 and Wyhash
v4.1.
* Remove WyHash and add more configs.
* Remove redundancy.
---
configs/records.yaml.default.in | 1 +
configs/strategies.schema.json | 23 +++
doc/admin-guide/files/parent.config.en.rst | 62 +++++++++
doc/admin-guide/files/records.yaml.en.rst | 57 ++++++++
doc/admin-guide/files/strategies.yaml.en.rst | 56 ++++++++
include/proxy/ParentConsistentHash.h | 7 +-
include/proxy/ParentSelection.h | 12 ++
include/proxy/http/remap/NextHopConsistentHash.h | 8 +-
include/tscore/HashSip.h | 147 +++++++++++++++++--
src/proxy/CMakeLists.txt | 4 +
src/proxy/ParentConsistentHash.cc | 48 ++++---
src/proxy/ParentSelection.cc | 60 ++++++++
src/proxy/http/remap/NextHopConsistentHash.cc | 73 ++++++++--
.../http/remap/unit-tests/nexthop_test_stubs.cc | 28 ++++
src/proxy/{ => unit_tests}/CMakeLists.txt | 42 +-----
src/proxy/unit_tests/main.cc | 51 +++++++
src/proxy/unit_tests/stub.cc | 26 ++++
src/proxy/unit_tests/test_ParentHashConfig.cc | 62 +++++++++
src/records/RecordsConfig.cc | 2 +
src/tscore/CMakeLists.txt | 2 +-
src/tscore/HashSip.cc | 141 -------------------
src/tscore/unit_tests/test_HashAlgorithms.cc | 155 +++++++++++++++++++++
22 files changed, 843 insertions(+), 224 deletions(-)
diff --git a/configs/records.yaml.default.in b/configs/records.yaml.default.in
index f59a1ffbdc..225ae80239 100644
--- a/configs/records.yaml.default.in
+++ b/configs/records.yaml.default.in
@@ -130,6 +130,7 @@ records:
##############################################################################
parent_proxy:
retry_time: 300
+ consistent_hash_algorithm: siphash24
##############################################################################
# Security. Docs:
diff --git a/configs/strategies.schema.json b/configs/strategies.schema.json
index b9a8500c41..8f1116c3a1 100644
--- a/configs/strategies.schema.json
+++ b/configs/strategies.schema.json
@@ -157,6 +157,29 @@
"url"
]
},
+ "hash_algorithm": {
+ "type": "string",
+ "description": "when using consistent_hash, this specifies
the hash algorithm to use",
+ "enum": [
+ "siphash24",
+ "siphash13"
+ ]
+ },
+ "hash_seed0": {
+ "type": "integer",
+ "description": "when using consistent_hash, this specifies
the first 64 bits of the hash seed (decimal integer)",
+ "minimum": 0
+ },
+ "hash_seed1": {
+ "type": "integer",
+ "description": "when using consistent_hash, this specifies
the second 64 bits of the hash seed (decimal integer)",
+ "minimum": 0
+ },
+ "hash_replicas": {
+ "type": "integer",
+ "description": "when using consistent_hash, this specifies
the number of virtual nodes (replicas) per host",
+ "minimum": 1
+ },
"go_direct": {
"type": "boolean",
"description": "wether, true/false, users of the strategy
may bypass parents and go directly to the origin"
diff --git a/doc/admin-guide/files/parent.config.en.rst
b/doc/admin-guide/files/parent.config.en.rst
index 9fe59116ef..7b8fa4a56d 100644
--- a/doc/admin-guide/files/parent.config.en.rst
+++ b/doc/admin-guide/files/parent.config.en.rst
@@ -278,6 +278,12 @@ The following list shows the possible actions and their
allowed values.
The other traffic is unaffected. Once the downed parent becomes
available, the traffic distribution returns to the pre-down
state.
+
+ The hash algorithm used for consistent hashing can be configured via
+ :ts:cv:`proxy.config.http.parent_proxy.consistent_hash_algorithm`.
Available
+ algorithms are ``siphash24`` (default) and ``siphash13`` (faster).
+ See the records.yaml documentation for details.
+
- ``latched`` - The first parent in the list is marked as primary and is
always chosen until connection errors cause it to be marked down. When
this occurs the next parent in the list then becomes primary. The
primary
@@ -318,6 +324,62 @@ The following list shows the possible actions and their
allowed values.
- ``false`` - The default. Do not ignore the host status.
+.. _parent-config-format-hash-algorithm:
+
+``hash_algorithm``
+ When using ``round_robin=consistent_hash``, this specifies the hash
algorithm to use
+ for this specific rule, overriding the global default set in
:ts:cv:`proxy.config.http.parent_proxy.consistent_hash_algorithm`.
+
+ Allowed values:
+
+ - ``siphash24`` - SipHash-2-4 (default). Cryptographically strong,
DoS-resistant.
+ - ``siphash13`` - SipHash-1-3. ~50% faster than SipHash-2-4, still
DoS-resistant.
+
+ Example::
+
+ dest_domain=. parent="p1.x.com:80,p2.x.com:80"
round_robin=consistent_hash hash_algorithm=siphash13
+
+.. _parent-config-format-hash-seed0:
+
+``hash_seed0``
+ When using ``round_robin=consistent_hash``, this specifies the first 64
bits of the hash seed (key).
+
+ - For SipHash algorithms, this is the first half of the 128-bit
cryptographic key (k0)
+ - For future 64-bit algorithms, this will be the full seed value
+
+ Value must be specified as a decimal integer (e.g. ``12345678901234567``).
Default is ``0``.
+
+ Example::
+
+ dest_domain=. parent="p1.x.com:80,p2.x.com:80"
round_robin=consistent_hash hash_seed0=1234567890
+
+.. _parent-config-format-hash-seed1:
+
+``hash_seed1``
+ When using ``round_robin=consistent_hash``, this specifies the second 64
bits of the hash seed (key).
+
+ - For SipHash algorithms, this is the second half of the 128-bit
cryptographic key (k1)
+ - For future 64-bit algorithms, this value is ignored
+
+ Value must be specified as a decimal integer (e.g.
``9876543210987654321``). Default is ``0``.
+
+ Example::
+
+ dest_domain=. parent="p1.x.com:80,p2.x.com:80"
round_robin=consistent_hash hash_seed0=1234567890 hash_seed1=9876543210
+
+.. _parent-config-format-hash-replicas:
+
+``hash_replicas``
+ When using ``round_robin=consistent_hash``, this specifies the number of
virtual nodes (replicas)
+ per parent host on the consistent hash ring.
+
+ Increasing the replica count improves the distribution of requests across
parent proxies but uses
+ more memory. Must be greater than 0. Default is ``1024``.
+
+ Example::
+
+ dest_domain=. parent="p1.x.com:80,p2.x.com:80"
round_robin=consistent_hash hash_replicas=2048
+
Examples
========
diff --git a/doc/admin-guide/files/records.yaml.en.rst
b/doc/admin-guide/files/records.yaml.en.rst
index 2b35828a9b..d1a722fc82 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -1496,6 +1496,63 @@ Parent Proxy Configuration
``2`` Mark the host down. This is the default.
===== ======================================================================
+.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_algorithm
STRING siphash24
+
+ Selects the hash algorithm used for consistent hash parent selection. This
setting
+ only affects parent selection when ``round_robin=consistent_hash`` is
configured in
+ :file:`parent.config`. The hash algorithm determines how requests are
distributed
+ across parent proxies.
+
+ ==============
================================================================================
+ Value Description
+ ==============
================================================================================
+ ``siphash24`` SipHash-2-4 (default). Cryptographically strong,
DoS-resistant hash function.
+ ``siphash13`` SipHash-1-3. ~50% faster than SipHash-2-4, still
DoS-resistant.
+ ==============
================================================================================
+
+ .. warning::
+
+ Changing this setting will cause requests to be redistributed
differently across
+ parent proxies. This can lead to cache churn and increased origin load
during the
+ transition period. Plan the migration carefully and consider doing it
during
+ low-traffic periods.
+
+.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_seed0 INT 0
+
+ The first 64 bits of the hash seed (key) for consistent hash parent
selection.
+ This setting only affects parent selection when
``round_robin=consistent_hash`` is configured.
+
+ - For SipHash algorithms, this forms the first half of the 128-bit
cryptographic key (k0)
+ - For future 64-bit hash algorithms (like XXH3), this is the full seed value
+
+ The value must be specified as a decimal integer (e.g.
``12345678901234567``). Default is ``0``.
+ Per-rule configuration is available in :file:`parent.config` using
``hash_seed0=<value>``.
+ Per-strategy configuration is available in :file:`strategies.yaml` using
``hash_seed0: <value>``.
+
+.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_seed1 INT 0
+
+ The second 64 bits of the hash seed (key) for consistent hash parent
selection.
+ This setting only affects parent selection when
``round_robin=consistent_hash`` is configured.
+
+ - For SipHash algorithms, this forms the second half of the 128-bit
cryptographic key (k1)
+ - For future 64-bit hash algorithms, this value is ignored
+
+ The value must be specified as a decimal integer (e.g.
``9876543210987654321``). Default is ``0``.
+ Per-rule configuration is available in :file:`parent.config` using
``hash_seed1=<value>``.
+ Per-strategy configuration is available in :file:`strategies.yaml` using
``hash_seed1: <value>``.
+
+.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_replicas INT
1024
+
+ The number of virtual nodes (replicas) per parent host on the consistent
hash ring.
+ This setting only affects parent selection when
``round_robin=consistent_hash`` is configured.
+
+ Increasing the replica count improves the distribution of requests across
parent proxies
+ but uses more memory. The default value of 1024 provides good distribution
in most scenarios.
+
+ Must be greater than 0. Default is ``1024``.
+ Per-rule configuration is available in :file:`parent.config` using
``hash_replicas=<value>``.
+ Per-strategy configuration is available in :file:`strategies.yaml` using
``hash_replicas: <value>``.
+
.. ts:cv:: CONFIG
proxy.config.http.parent_proxy.enable_parent_timeout_markdowns INT 0
:reloadable:
:overridable:
diff --git a/doc/admin-guide/files/strategies.yaml.en.rst
b/doc/admin-guide/files/strategies.yaml.en.rst
index 5f3fad0c0d..ec7c223c8a 100644
--- a/doc/admin-guide/files/strategies.yaml.en.rst
+++ b/doc/admin-guide/files/strategies.yaml.en.rst
@@ -201,6 +201,62 @@ Each **strategy** in the list may using the following
parameters:
#. **parent**: Use the parent URL as set via the API
:cpp:func:`TSHttpTxnParentSelectionUrlSet`.
This again is likely set via an existing plugin such as the **cachekey**
plugin.
+- **hash_algorithm**: The hash algorithm used by the **consistent_hash**
policy. This parameter allows
+ per-strategy selection of the hash algorithm. If not specified, defaults to
**siphash24** (or the value
+ set in :ts:cv:`proxy.config.http.parent_proxy.consistent_hash_algorithm`).
Use one of:
+
+ #. **siphash24**: (**default**) SipHash-2-4. Cryptographically strong,
DoS-resistant hash function.
+ #. **siphash13**: SipHash-1-3. ~50% faster than SipHash-2-4, still
DoS-resistant.
+
+ Example:
+
+ .. code-block:: yaml
+
+ policy: consistent_hash
+ hash_algorithm: siphash13
+
+- **hash_seed0**: The first 64 bits of the hash seed (key) for the
**consistent_hash** policy.
+
+ - For SipHash algorithms, this forms the first half of the 128-bit
cryptographic key (k0)
+ - For future 64-bit hash algorithms, this is the full seed value
+
+ Value must be specified as a decimal integer. Default is **0**.
+
+ Example:
+
+ .. code-block:: yaml
+
+ policy: consistent_hash
+ hash_seed0: 12345678901234567
+
+- **hash_seed1**: The second 64 bits of the hash seed (key) for the
**consistent_hash** policy.
+
+ - For SipHash algorithms, this forms the second half of the 128-bit
cryptographic key (k1)
+ - For future 64-bit hash algorithms, this value is ignored
+
+ Value must be specified as a decimal integer. Default is **0**.
+
+ Example:
+
+ .. code-block:: yaml
+
+ policy: consistent_hash
+ hash_seed0: 12345678901234567
+ hash_seed1: 9876543210987654321
+
+- **hash_replicas**: The number of virtual nodes (replicas) per host on the
consistent hash ring for
+ the **consistent_hash** policy.
+
+ Increasing the replica count improves the distribution of requests across
hosts but uses more memory.
+ Must be greater than 0. Default is **1024**.
+
+ Example:
+
+ .. code-block:: yaml
+
+ policy: consistent_hash
+ hash_replicas: 2048
+
- **go_direct**: A boolean value indicating whether a transaction may bypass
proxies and go direct to the origin. Defaults to **true**
- **parent_is_proxy**: A boolean value which indicates if the groups of hosts
are proxy caches or origins. **true** (default) means all the hosts used in
the remap are |TS| caches. **false** means the hosts are origins that the next
hop strategies may use for load balancing and/or failover.
- **cache_peer_result**: A boolean value that is only used when the **policy**
is 'consistent_hash' and a **peering_ring** mode is used for the strategy. When
set to true, the default, all responses from upstream and peer endpoints are
allowed to be cached. Setting this to false will disable caching responses
received from a peer host. Only responses from upstream origins or parents will
be cached for this strategy.
diff --git a/include/proxy/ParentConsistentHash.h
b/include/proxy/ParentConsistentHash.h
index afcc387393..4a38c16a61 100644
--- a/include/proxy/ParentConsistentHash.h
+++ b/include/proxy/ParentConsistentHash.h
@@ -38,14 +38,15 @@
//
class ParentConsistentHash : public ParentSelectionStrategy
{
- // there are two hashes PRIMARY parents
- // and SECONDARY parents.
- ATSHash64Sip24 hash[2];
+ std::unique_ptr<ATSHash64> hash[2];
std::unique_ptr<ATSConsistentHash> chash[2];
pRecord *parents[2];
bool foundParents[2][MAX_PARENTS];
bool ignore_query;
int secondary_mode;
+ ParentHashAlgorithm selected_algorithm;
+ uint64_t hash_seed0;
+ uint64_t hash_seed1;
public:
static const int PRIMARY = 0;
diff --git a/include/proxy/ParentSelection.h b/include/proxy/ParentSelection.h
index 09726f5036..5960cfda18 100644
--- a/include/proxy/ParentSelection.h
+++ b/include/proxy/ParentSelection.h
@@ -35,11 +35,13 @@
#include "proxy/ControlMatcher.h"
#include "records/RecProcess.h"
#include "tscore/ConsistentHash.h"
+#include "tscore/Hash.h"
#include "tscore/Tokenizer.h"
#include "tscore/ink_apidefs.h"
#include "proxy/HostStatus.h"
#include <algorithm>
+#include <memory>
#include <vector>
#define MAX_PARENTS 64
@@ -73,6 +75,8 @@ enum class ParentRetry_t {
BOTH = 3
};
+enum class ParentHashAlgorithm { SIPHASH24 = 0, SIPHASH13 };
+
struct UnavailableServerResponseCodes {
UnavailableServerResponseCodes(char *val);
~UnavailableServerResponseCodes(){};
@@ -163,6 +167,10 @@ public:
int max_unavailable_server_retries = 1;
int secondary_mode = 1;
bool ignore_self_detect = false;
+ ParentHashAlgorithm consistent_hash_algorithm =
ParentHashAlgorithm::SIPHASH24;
+ uint64_t consistent_hash_seed0 = 0;
+ uint64_t consistent_hash_seed1 = 0;
+ int consistent_hash_replicas = 1024; // Number of virtual nodes per host
(int to match ATSConsistentHash constructor)
};
// If the parent was set by the external customer api,
@@ -444,6 +452,10 @@ public:
// Helper Functions
ParentRecord *createDefaultParent(char *val);
+// Hash utility functions
+ParentHashAlgorithm parseHashAlgorithm(std::string_view name);
+std::unique_ptr<ATSHash64> createHashInstance(ParentHashAlgorithm algo,
uint64_t seed0, uint64_t seed1);
+
// Unit Test Functions
void show_result(ParentResult *aParentResult);
void br(HttpRequestData *h, const char *os_hostname, sockaddr const *dest_ip =
nullptr); // short for build request
diff --git a/include/proxy/http/remap/NextHopConsistentHash.h
b/include/proxy/http/remap/NextHopConsistentHash.h
index 7f3c29e8e1..12d0ca9b5d 100644
--- a/include/proxy/http/remap/NextHopConsistentHash.h
+++ b/include/proxy/http/remap/NextHopConsistentHash.h
@@ -46,8 +46,12 @@ class NextHopConsistentHash : public NextHopSelectionStrategy
uint64_t getHashKey(uint64_t sm_id, const HttpRequestData &hrdata, ATSHash64
*h);
public:
- NHHashKeyType hash_key = NHHashKeyType::PATH_HASH_KEY;
- NHHashUrlType hash_url = NHHashUrlType::REQUEST;
+ NHHashKeyType hash_key = NHHashKeyType::PATH_HASH_KEY;
+ NHHashUrlType hash_url = NHHashUrlType::REQUEST;
+ std::string hash_algorithm = "siphash24"; // Default hash algorithm name
+ uint64_t hash_seed0 = 0; // First 64 bits of hash seed
+ uint64_t hash_seed1 = 0; // Second 64 bits of hash seed
+ int hash_replicas = 1024; // Number of virtual nodes per
host (int to match ATSConsistentHash constructor)
NextHopConsistentHash() = delete;
NextHopConsistentHash(const std::string_view name, const NHPolicyType
&policy, ts::Yaml::Map &n);
diff --git a/include/tscore/HashSip.h b/include/tscore/HashSip.h
index ce94404dc8..a26d3c1057 100644
--- a/include/tscore/HashSip.h
+++ b/include/tscore/HashSip.h
@@ -23,22 +23,145 @@
#include "tscore/Hash.h"
#include <cstdint>
+#include <cstring>
/*
- Siphash is a Hash Message Authentication Code and can take a key.
+ SipHash is a Hash Message Authentication Code and can take a key.
If you don't care about MAC use the void constructor and it will use
a zero key for you.
- */
-struct ATSHash64Sip24 : ATSHash64 {
- ATSHash64Sip24();
- ATSHash64Sip24(const unsigned char key[16]);
- ATSHash64Sip24(std::uint64_t key0, std::uint64_t key1);
- void update(const void *data, std::size_t len) override;
- void final() override;
- std::uint64_t get() const override;
- void clear() override;
+ Template parameters:
+ - c_rounds: number of compression rounds per message block
+ - d_rounds: number of finalization rounds
+*/
+
+#define SIP_BLOCK_SIZE 8
+
+#define ROTL64(a, b) (((a) << (b)) | ((a) >> (64 - b)))
+
+static inline std::uint64_t
+U8TO64_LE(const std::uint8_t *p)
+{
+ std::uint64_t result;
+ std::memcpy(&result, p, sizeof(result));
+ return result;
+}
+
+#define SIPCOMPRESS(x0, x1, x2, x3) \
+ x0 += x1; \
+ x2 += x3; \
+ x1 = ROTL64(x1, 13); \
+ x3 = ROTL64(x3, 16); \
+ x1 ^= x0; \
+ x3 ^= x2; \
+ x0 = ROTL64(x0, 32); \
+ x2 += x1; \
+ x0 += x3; \
+ x1 = ROTL64(x1, 17); \
+ x3 = ROTL64(x3, 21); \
+ x1 ^= x2; \
+ x3 ^= x0; \
+ x2 = ROTL64(x2, 32);
+
+template <int c_rounds, int d_rounds> struct ATSHashSip : ATSHash64 {
+ ATSHashSip() { this->clear(); }
+
+ ATSHashSip(const unsigned char key[16]) : k0(U8TO64_LE(key)),
k1(U8TO64_LE(key + sizeof(k0))) { this->clear(); }
+
+ ATSHashSip(std::uint64_t key0, std::uint64_t key1) : k0(key0), k1(key1) {
this->clear(); }
+
+ void
+ update(const void *data, std::size_t len) override
+ {
+ std::size_t i, blocks;
+ unsigned char *m;
+ std::uint64_t mi;
+ std::uint8_t block_off = 0;
+
+ if (!finalized) {
+ m = (unsigned char *)data;
+ total_len += len;
+
+ if (len + block_buffer_len < SIP_BLOCK_SIZE) {
+ std::memcpy(block_buffer + block_buffer_len, m, len);
+ block_buffer_len += len;
+ } else {
+ if (block_buffer_len > 0) {
+ block_off = SIP_BLOCK_SIZE - block_buffer_len;
+ std::memcpy(block_buffer + block_buffer_len, m, block_off);
+
+ mi = U8TO64_LE(block_buffer);
+ v3 ^= mi;
+ for (int r = 0; r < c_rounds; r++) {
+ SIPCOMPRESS(v0, v1, v2, v3);
+ }
+ v0 ^= mi;
+ }
+
+ for (i = block_off, blocks = ((len - block_off) & ~(SIP_BLOCK_SIZE -
1)); i < blocks; i += SIP_BLOCK_SIZE) {
+ mi = U8TO64_LE(m + i);
+ v3 ^= mi;
+ for (int r = 0; r < c_rounds; r++) {
+ SIPCOMPRESS(v0, v1, v2, v3);
+ }
+ v0 ^= mi;
+ }
+
+ block_buffer_len = (len - block_off) & (SIP_BLOCK_SIZE - 1);
+ std::memcpy(block_buffer, m + block_off + blocks, block_buffer_len);
+ }
+ }
+ }
+
+ void
+ final() override
+ {
+ std::uint64_t last7;
+ int i;
+
+ if (!finalized) {
+ last7 = static_cast<std::uint64_t>(total_len & 0xff) << 56;
+
+ for (i = block_buffer_len - 1; i >= 0; i--) {
+ last7 |= static_cast<std::uint64_t>(block_buffer[i]) << (i * 8);
+ }
+
+ v3 ^= last7;
+ for (int r = 0; r < c_rounds; r++) {
+ SIPCOMPRESS(v0, v1, v2, v3);
+ }
+ v0 ^= last7;
+ v2 ^= 0xff;
+ for (int r = 0; r < d_rounds; r++) {
+ SIPCOMPRESS(v0, v1, v2, v3);
+ }
+ hfinal = v0 ^ v1 ^ v2 ^ v3;
+ finalized = true;
+ }
+ }
+
+ std::uint64_t
+ get() const override
+ {
+ if (finalized) {
+ return hfinal;
+ } else {
+ return 0;
+ }
+ }
+
+ void
+ clear() override
+ {
+ v0 = k0 ^ 0x736f6d6570736575ull;
+ v1 = k1 ^ 0x646f72616e646f6dull;
+ v2 = k0 ^ 0x6c7967656e657261ull;
+ v3 = k1 ^ 0x7465646279746573ull;
+ finalized = false;
+ total_len = 0;
+ block_buffer_len = 0;
+ }
private:
unsigned char block_buffer[8] = {0};
@@ -53,3 +176,7 @@ private:
std::size_t total_len = 0;
bool finalized = false;
};
+
+// Standard SipHash variants
+using ATSHash64Sip24 = ATSHashSip<2, 4>;
+using ATSHash64Sip13 = ATSHashSip<1, 3>;
diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt
index f145034fb6..93c1669784 100644
--- a/src/proxy/CMakeLists.txt
+++ b/src/proxy/CMakeLists.txt
@@ -55,4 +55,8 @@ if(TS_USE_QUIC)
add_subdirectory(http3)
endif()
+if(BUILD_TESTING)
+ add_subdirectory(unit_tests)
+endif()
+
clang_tidy_check(proxy)
diff --git a/src/proxy/ParentConsistentHash.cc
b/src/proxy/ParentConsistentHash.cc
index a9dab9a1d3..ce0cc46c21 100644
--- a/src/proxy/ParentConsistentHash.cc
+++ b/src/proxy/ParentConsistentHash.cc
@@ -23,6 +23,7 @@
#include <atomic>
#include "proxy/HostStatus.h"
#include "proxy/ParentConsistentHash.h"
+#include "tscore/HashSip.h"
namespace
{
@@ -38,21 +39,26 @@ ParentConsistentHash::ParentConsistentHash(ParentRecord
*parent_record)
parents[SECONDARY] = parent_record->secondary_parents;
ignore_query = parent_record->ignore_query;
secondary_mode = parent_record->secondary_mode;
+ selected_algorithm = parent_record->consistent_hash_algorithm;
+ hash_seed0 = parent_record->consistent_hash_seed0;
+ hash_seed1 = parent_record->consistent_hash_seed1;
ink_zero(foundParents);
- chash[PRIMARY] = std::make_unique<ATSConsistentHash>();
+ hash[PRIMARY] = createHashInstance(selected_algorithm, hash_seed0,
hash_seed1);
+ chash[PRIMARY] =
std::make_unique<ATSConsistentHash>(parent_record->consistent_hash_replicas);
for (i = 0; i < parent_record->num_parents; i++) {
- chash[PRIMARY]->insert(&(parent_record->parents[i]),
parent_record->parents[i].weight, (ATSHash64 *)&hash[PRIMARY]);
+ chash[PRIMARY]->insert(&(parent_record->parents[i]),
parent_record->parents[i].weight, hash[PRIMARY].get());
}
if (parent_record->num_secondary_parents > 0) {
Dbg(dbg_ctl_parent_select, "ParentConsistentHash(): initializing the
secondary parents hash.");
- chash[SECONDARY] = std::make_unique<ATSConsistentHash>();
+ hash[SECONDARY] = createHashInstance(selected_algorithm, hash_seed0,
hash_seed1);
+ chash[SECONDARY] =
std::make_unique<ATSConsistentHash>(parent_record->consistent_hash_replicas);
for (i = 0; i < parent_record->num_secondary_parents; i++) {
chash[SECONDARY]->insert(&(parent_record->secondary_parents[i]),
parent_record->secondary_parents[i].weight,
- (ATSHash64 *)&hash[SECONDARY]);
+ hash[SECONDARY].get());
}
} else {
chash[SECONDARY] = nullptr;
@@ -110,8 +116,8 @@ ParentConsistentHash::getPathHash(HttpRequestData *hrdata,
ATSHash64 *h)
// Helper function to abstract calling ATSConsistentHash lookup_by_hashval()
vs lookup().
static pRecord *
-chash_lookup(ATSConsistentHash *fhash, uint64_t path_hash,
ATSConsistentHashIter *chashIter, bool *wrap_around,
- ATSHash64Sip24 *hash, bool *chash_init, bool *mapWrapped)
+chash_lookup(ATSConsistentHash *fhash, uint64_t path_hash,
ATSConsistentHashIter *chashIter, bool *wrap_around, ATSHash64 *hash,
+ bool *chash_init, bool *mapWrapped)
{
pRecord *prtmp;
@@ -134,18 +140,18 @@ void
ParentConsistentHash::selectParent(bool first_call, ParentResult *result,
RequestData *rdata,
unsigned int /* fail_threshold ATS_UNUSED
*/, unsigned int retry_time)
{
- ATSHash64Sip24 hash;
- ATSConsistentHash *fhash;
- HttpRequestData *request_info = static_cast<HttpRequestData *>(rdata);
- bool firstCall = first_call;
- bool parentRetry = false;
- bool wrap_around[2] = {false, false};
- int lookups = 0;
- uint64_t path_hash = 0;
- uint32_t last_lookup;
- pRecord *prtmp = nullptr, *pRec = nullptr;
- HostStatus &pStatus = HostStatus::instance();
- TSHostStatus host_stat = TSHostStatus::TS_HOST_STATUS_INIT;
+ std::unique_ptr<ATSHash64> hash = createHashInstance(selected_algorithm,
hash_seed0, hash_seed1);
+ ATSConsistentHash *fhash;
+ HttpRequestData *request_info = static_cast<HttpRequestData
*>(rdata);
+ bool firstCall = first_call;
+ bool parentRetry = false;
+ bool wrap_around[2] = {false, false};
+ int lookups = 0;
+ uint64_t path_hash = 0;
+ uint32_t last_lookup;
+ pRecord *prtmp = nullptr, *pRec = nullptr;
+ HostStatus &pStatus = HostStatus::instance();
+ TSHostStatus host_stat = TSHostStatus::TS_HOST_STATUS_INIT;
Dbg(dbg_ctl_parent_select, "ParentConsistentHash::%s(): Using a consistent
hash parent selection strategy.", __func__);
ink_assert(numParents(result) > 0 || result->rec->go_direct == true);
@@ -193,10 +199,10 @@ ParentConsistentHash::selectParent(bool first_call,
ParentResult *result, Reques
}
// Do the initial parent look-up.
- path_hash = getPathHash(request_info, (ATSHash64 *)&hash);
+ path_hash = getPathHash(request_info, hash.get());
fhash = chash[last_lookup].get();
do { // search until we've selected a different parent if !firstCall
- prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup],
&wrap_around[last_lookup], &hash,
+ prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup],
&wrap_around[last_lookup], hash.get(),
&result->chash_init[last_lookup],
&result->mapWrapped[last_lookup]);
lookups++;
if (prtmp) {
@@ -283,7 +289,7 @@ ParentConsistentHash::selectParent(bool first_call,
ParentResult *result, Reques
}
}
fhash = chash[last_lookup].get();
- prtmp = chash_lookup(fhash, path_hash,
&result->chashIter[last_lookup], &wrap_around[last_lookup], &hash,
+ prtmp = chash_lookup(fhash, path_hash,
&result->chashIter[last_lookup], &wrap_around[last_lookup], hash.get(),
&result->chash_init[last_lookup],
&result->mapWrapped[last_lookup]);
lookups++;
if (prtmp) {
diff --git a/src/proxy/ParentSelection.cc b/src/proxy/ParentSelection.cc
index a83f0f2a72..0870bcb9bf 100644
--- a/src/proxy/ParentSelection.cc
+++ b/src/proxy/ParentSelection.cc
@@ -56,6 +56,33 @@ DbgCtl
ParentResult::dbg_ctl_parent_select{"parent_select"};
static DbgCtl &dbg_ctl_parent_select{ParentResult::dbg_ctl_parent_select};
static DbgCtl dbg_ctl_parent_config{"parent_config"};
+ParentHashAlgorithm
+parseHashAlgorithm(std::string_view name)
+{
+ if (name == "siphash24") {
+ return ParentHashAlgorithm::SIPHASH24;
+ } else if (name == "siphash13") {
+ return ParentHashAlgorithm::SIPHASH13;
+ } else {
+ Warning("Unknown hash algorithm '%.*s', defaulting to siphash24",
static_cast<int>(name.size()), name.data());
+ return ParentHashAlgorithm::SIPHASH24;
+ }
+}
+
+std::unique_ptr<ATSHash64>
+createHashInstance(ParentHashAlgorithm algo, uint64_t seed0, uint64_t seed1)
+{
+ switch (algo) {
+ case ParentHashAlgorithm::SIPHASH24:
+ return std::make_unique<ATSHash64Sip24>(seed0, seed1);
+ case ParentHashAlgorithm::SIPHASH13:
+ return std::make_unique<ATSHash64Sip13>(seed0, seed1);
+ default:
+ Warning("Unknown hash algorithm %d, using SipHash-2-4",
static_cast<int>(algo));
+ return std::make_unique<ATSHash64Sip24>(seed0, seed1);
+ }
+}
+
ParentSelectionPolicy::ParentSelectionPolicy()
{
int32_t retry_time = 0;
@@ -647,6 +674,22 @@ ParentRecord::Init(matcher_line *line_info)
self_detect = static_cast<int>(rec_self_detect.value());
}
+ if (auto
rec_hash_algo{RecGetRecordStringAlloc("proxy.config.http.parent_proxy.consistent_hash_algorithm")};
rec_hash_algo) {
+ consistent_hash_algorithm = parseHashAlgorithm(rec_hash_algo.value());
+ }
+
+ if (auto
rec_hash_seed0{RecGetRecordInt("proxy.config.http.parent_proxy.consistent_hash_seed0")};
rec_hash_seed0) {
+ consistent_hash_seed0 = static_cast<uint64_t>(rec_hash_seed0.value());
+ }
+
+ if (auto
rec_hash_seed1{RecGetRecordInt("proxy.config.http.parent_proxy.consistent_hash_seed1")};
rec_hash_seed1) {
+ consistent_hash_seed1 = static_cast<uint64_t>(rec_hash_seed1.value());
+ }
+
+ if (auto
rec_hash_replicas{RecGetRecordInt("proxy.config.http.parent_proxy.consistent_hash_replicas")};
rec_hash_replicas) {
+ consistent_hash_replicas = static_cast<int>(rec_hash_replicas.value());
+ }
+
for (int i = 0; i < MATCHER_MAX_TOKENS; i++) {
used = false;
label = line_info->line[0][i];
@@ -754,6 +797,23 @@ ParentRecord::Init(matcher_line *line_info)
ignore_self_detect = false;
}
used = true;
+ } else if (strcasecmp(label, "hash_algorithm") == 0) {
+ consistent_hash_algorithm = parseHashAlgorithm(val);
+ used = true;
+ } else if (strcasecmp(label, "hash_seed0") == 0) {
+ consistent_hash_seed0 = static_cast<uint64_t>(strtoull(val, nullptr,
10));
+ used = true;
+ } else if (strcasecmp(label, "hash_seed1") == 0) {
+ consistent_hash_seed1 = static_cast<uint64_t>(strtoull(val, nullptr,
10));
+ used = true;
+ } else if (strcasecmp(label, "hash_replicas") == 0) {
+ int v = atoi(val);
+ if (v > 0) {
+ consistent_hash_replicas = v;
+ used = true;
+ } else {
+ errPtr = "invalid argument to hash_replicas directive. Argument must
be greater than 0.";
+ }
}
// Report errors generated by ProcessParents();
if (errPtr != nullptr) {
diff --git a/src/proxy/http/remap/NextHopConsistentHash.cc
b/src/proxy/http/remap/NextHopConsistentHash.cc
index 855655063d..848e78a751 100644
--- a/src/proxy/http/remap/NextHopConsistentHash.cc
+++ b/src/proxy/http/remap/NextHopConsistentHash.cc
@@ -27,6 +27,7 @@
#include "iocore/utils/Machine.h"
#include "tsutil/YamlCfg.h"
#include "proxy/http/remap/NextHopConsistentHash.h"
+#include "proxy/ParentSelection.h"
// hash_key strings.
constexpr std::string_view hash_key_url = "url";
@@ -57,17 +58,17 @@ std::shared_ptr<HostRecord>
NextHopConsistentHash::chashLookup(const std::shared_ptr<ATSConsistentHash>
&ring, uint32_t cur_ring, ParentResult &result,
HttpRequestData &request_info, bool
*wrapped, uint64_t sm_id)
{
- uint64_t hash_key = 0;
- ATSHash64Sip24 hash;
- HostRecord *host_rec = nullptr;
- ATSConsistentHashIter *iter = &result.chashIter[cur_ring];
+ uint64_t hash_key = 0;
+ std::unique_ptr<ATSHash64> hash =
createHashInstance(parseHashAlgorithm(hash_algorithm), hash_seed0, hash_seed1);
+ HostRecord *host_rec = nullptr;
+ ATSConsistentHashIter *iter = &result.chashIter[cur_ring];
if (result.chash_init[cur_ring] == false) {
- hash_key = getHashKey(sm_id, request_info, &hash);
+ hash_key = getHashKey(sm_id, request_info, hash.get());
host_rec = static_cast<HostRecord
*>(ring->lookup_by_hashval(hash_key, iter, wrapped));
result.chash_init[cur_ring] = true;
} else {
- host_rec = static_cast<HostRecord *>(ring->lookup(nullptr, iter, wrapped,
&hash));
+ host_rec = static_cast<HostRecord *>(ring->lookup(nullptr, iter, wrapped,
hash.get()));
}
bool wrap_around = *wrapped;
*wrapped = (result.mapWrapped[cur_ring] && *wrapped) ? true : false;
@@ -91,7 +92,7 @@ NextHopConsistentHash::~NextHopConsistentHash()
NextHopConsistentHash::NextHopConsistentHash(const std::string_view name,
const NHPolicyType &policy, ts::Yaml::Map &n)
: NextHopSelectionStrategy(name, policy, n)
{
- ATSHash64Sip24 hash;
+ std::unique_ptr<ATSHash64> hash;
try {
if (n["hash_url"]) {
@@ -140,9 +141,61 @@ NextHopConsistentHash::NextHopConsistentHash(const
std::string_view name, const
"', this strategy will be ignored.");
}
+ // Parse hash_algorithm
+ try {
+ if (n["hash_algorithm"]) {
+ hash_algorithm = n["hash_algorithm"].Scalar();
+ if (hash_algorithm != "siphash24" && hash_algorithm != "siphash13") {
+ NH_Note("Invalid 'hash_algorithm' value, '%s', for the strategy named
'%s', using default 'siphash24'.",
+ hash_algorithm.c_str(), strategy_name.c_str());
+ hash_algorithm = "siphash24";
+ }
+ }
+ } catch (std::exception &ex) {
+ throw std::invalid_argument("Error parsing the strategy named '" +
strategy_name + "' due to '" + ex.what() +
+ "', this strategy will be ignored.");
+ }
+
+ // Parse hash_seed0
+ try {
+ if (n["hash_seed0"]) {
+ hash_seed0 = n["hash_seed0"].as<uint64_t>();
+ }
+ } catch (std::exception &ex) {
+ throw std::invalid_argument("Error parsing the strategy named '" +
strategy_name + "' due to '" + ex.what() +
+ "', this strategy will be ignored.");
+ }
+
+ // Parse hash_seed1
+ try {
+ if (n["hash_seed1"]) {
+ hash_seed1 = n["hash_seed1"].as<uint64_t>();
+ }
+ } catch (std::exception &ex) {
+ throw std::invalid_argument("Error parsing the strategy named '" +
strategy_name + "' due to '" + ex.what() +
+ "', this strategy will be ignored.");
+ }
+
+ // Parse hash_replicas
+ try {
+ if (n["hash_replicas"]) {
+ hash_replicas = n["hash_replicas"].as<int>();
+ if (hash_replicas <= 0) {
+ NH_Note("Invalid 'hash_replicas' value, %d, for the strategy named
'%s', must be > 0, using default 1024.", hash_replicas,
+ strategy_name.c_str());
+ hash_replicas = 1024;
+ }
+ }
+ } catch (std::exception &ex) {
+ throw std::invalid_argument("Error parsing the strategy named '" +
strategy_name + "' due to '" + ex.what() +
+ "', this strategy will be ignored.");
+ }
+
+ hash = createHashInstance(parseHashAlgorithm(hash_algorithm), hash_seed0,
hash_seed1);
+
// load up the hash rings.
for (uint32_t i = 0; i < groups; i++) {
- std::shared_ptr<ATSConsistentHash> hash_ring =
std::make_shared<ATSConsistentHash>();
+ std::shared_ptr<ATSConsistentHash> hash_ring =
std::make_shared<ATSConsistentHash>(hash_replicas);
for (uint32_t j = 0; j < host_groups[i].size(); j++) {
// ATSConsistentHash needs the raw pointer.
HostRecord *p = host_groups[i][j].get();
@@ -154,11 +207,11 @@ NextHopConsistentHash::NextHopConsistentHash(const
std::string_view name, const
}
p->group_index = host_groups[i][j]->group_index;
p->host_index = host_groups[i][j]->host_index;
- hash_ring->insert(p, p->weight, &hash);
+ hash_ring->insert(p, p->weight, hash.get());
NH_Dbg(NH_DBG_CTL, "Loading hash rings - ring: %d, host record: %d,
name: %s, hostname: %s, strategy: %s", i, j, p->name,
p->hostname.c_str(), strategy_name.c_str());
}
- hash.clear();
+ hash->clear();
rings.push_back(std::move(hash_ring));
}
}
diff --git a/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc
b/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc
index 9302ca1447..d5945d30c6 100644
--- a/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc
+++ b/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc
@@ -209,3 +209,31 @@ HostStatus::setHostStatus(const std::string_view host,
TSHostStatus status, unsi
NH_Dbg(DbgCtl{"next_hop"}, "setting host status for '%.*s' to %s",
static_cast<int>(host.size()), host.data(),
HostStatusNames[status]);
}
+
+// Stub implementations for Parent Selection hash utilities
+#include "proxy/ParentSelection.h"
+#include "tscore/Hash.h"
+#include "tscore/HashSip.h"
+
+ParentHashAlgorithm
+parseHashAlgorithm(std::string_view name)
+{
+ if (name == "siphash13") {
+ return ParentHashAlgorithm::SIPHASH13;
+ } else {
+ return ParentHashAlgorithm::SIPHASH24;
+ }
+}
+
+std::unique_ptr<ATSHash64>
+createHashInstance(ParentHashAlgorithm algo, uint64_t seed0, uint64_t seed1)
+{
+ switch (algo) {
+ case ParentHashAlgorithm::SIPHASH24:
+ return std::make_unique<ATSHash64Sip24>(seed0, seed1);
+ case ParentHashAlgorithm::SIPHASH13:
+ return std::make_unique<ATSHash64Sip13>(seed0, seed1);
+ default:
+ return std::make_unique<ATSHash64Sip24>(seed0, seed1);
+ }
+}
diff --git a/src/proxy/CMakeLists.txt b/src/proxy/unit_tests/CMakeLists.txt
similarity index 51%
copy from src/proxy/CMakeLists.txt
copy to src/proxy/unit_tests/CMakeLists.txt
index f145034fb6..b21fb32715 100644
--- a/src/proxy/CMakeLists.txt
+++ b/src/proxy/unit_tests/CMakeLists.txt
@@ -15,44 +15,14 @@
#
#######################
-add_library(
- proxy STATIC
- private/SSLProxySession.cc
- CacheControl.cc
- ControlBase.cc
- ControlMatcher.cc
- HostStatus.cc
- IPAllow.cc
- ParentConsistentHash.cc
- ParentRoundRobin.cc
- ParentSelectionStrategy.cc
- ParentSelection.cc
- Plugin.cc
- PluginVC.cc
- ProtocolProbeSessionAccept.cc
- ProxySession.cc
- ProxyTransaction.cc
- ReverseProxy.cc
- Transform.cc
- FetchSM.cc
- PluginHttpConnect.cc
+add_executable(
+ test_proxy main.cc test_ParentHashConfig.cc
"${PROJECT_SOURCE_DIR}/src/iocore/net/libinknet_stub.cc" stub.cc
)
-add_library(ts::proxy ALIAS proxy)
-target_link_libraries(
- proxy
- PUBLIC ts::inkcache ts::inkevent ts::tsutil ts::tscore ts::inknet
- PRIVATE ts::rpcpublichandlers ts::jsonrpc_protocol ts::inkutils
ts::tsapibackend
-)
-
-add_subdirectory(hdrs)
-add_subdirectory(shared)
-add_subdirectory(http)
-add_subdirectory(http2)
-add_subdirectory(logging)
+target_link_libraries(test_proxy PRIVATE Catch2::Catch2WithMain ts::http
ts::proxy ts::tscore ts::records ts::inkevent)
-if(TS_USE_QUIC)
- add_subdirectory(http3)
+if(NOT APPLE)
+ target_link_options(test_proxy PRIVATE -Wl,--allow-multiple-definition)
endif()
-clang_tidy_check(proxy)
+add_catch2_test(NAME test_proxy COMMAND $<TARGET_FILE:test_proxy>)
diff --git a/src/proxy/unit_tests/main.cc b/src/proxy/unit_tests/main.cc
new file mode 100644
index 0000000000..a4b3b9453c
--- /dev/null
+++ b/src/proxy/unit_tests/main.cc
@@ -0,0 +1,51 @@
+/** @file
+
+ The main file for proxy unit tests
+
+ @section license License
+
+ 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 <catch2/catch_test_macros.hpp>
+#include <catch2/reporters/catch_reporter_event_listener.hpp>
+#include <catch2/reporters/catch_reporter_registrars.hpp>
+
+#include "tscore/Layout.h"
+#include "iocore/eventsystem/EventSystem.h"
+#include "records/RecordsConfig.h"
+#include "iocore/utils/diags.i"
+
+struct DiagnosticsListener : Catch::EventListenerBase {
+ using EventListenerBase::EventListenerBase;
+
+ void
+ testRunStarting(Catch::TestRunInfo const & /* testRunInfo */) override
+ {
+ Layout::create();
+ init_diags("", nullptr);
+ RecProcessInit();
+ LibRecordsConfigInit();
+ }
+
+ void
+ testRunEnded(Catch::TestRunStats const & /* testRunStats */) override
+ {
+ }
+};
+
+CATCH_REGISTER_LISTENER(DiagnosticsListener);
diff --git a/src/proxy/unit_tests/stub.cc b/src/proxy/unit_tests/stub.cc
new file mode 100644
index 0000000000..a1a95fe8cc
--- /dev/null
+++ b/src/proxy/unit_tests/stub.cc
@@ -0,0 +1,26 @@
+/** @file
+
+ Stub file for test_proxy
+
+ @section license License
+
+ 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 "proxy/IPAllow.h"
+
+uint8_t IpAllow::subjects[IpAllow::Subject::MAX_SUBJECTS];
diff --git a/src/proxy/unit_tests/test_ParentHashConfig.cc
b/src/proxy/unit_tests/test_ParentHashConfig.cc
new file mode 100644
index 0000000000..31e9ab1496
--- /dev/null
+++ b/src/proxy/unit_tests/test_ParentHashConfig.cc
@@ -0,0 +1,62 @@
+/** @file
+
+ Unit tests for Parent Selection hash algorithm configuration
+
+ @section license License
+
+ 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 "proxy/ParentSelection.h"
+#include <catch2/catch_test_macros.hpp>
+
+// Helper function to test parseHashAlgorithm (normally static, exposed here
for testing)
+extern ParentHashAlgorithm parseHashAlgorithm(std::string_view name);
+
+TEST_CASE("parseHashAlgorithm - Valid inputs", "[ParentSelection]")
+{
+ REQUIRE(parseHashAlgorithm("siphash24") == ParentHashAlgorithm::SIPHASH24);
+ REQUIRE(parseHashAlgorithm("siphash13") == ParentHashAlgorithm::SIPHASH13);
+}
+
+TEST_CASE("parseHashAlgorithm - Invalid inputs fallback to default",
"[ParentSelection]")
+{
+ REQUIRE(parseHashAlgorithm("invalid") == ParentHashAlgorithm::SIPHASH24);
+ REQUIRE(parseHashAlgorithm("") == ParentHashAlgorithm::SIPHASH24);
+ REQUIRE(parseHashAlgorithm("SIPHASH24") == ParentHashAlgorithm::SIPHASH24);
// case sensitive
+ REQUIRE(parseHashAlgorithm("xxh3") == ParentHashAlgorithm::SIPHASH24);
// not yet implemented
+ REQUIRE(parseHashAlgorithm("md5") == ParentHashAlgorithm::SIPHASH24);
+}
+
+TEST_CASE("parseHashAlgorithm - Case sensitivity", "[ParentSelection]")
+{
+ // Should be case-sensitive - uppercase should fall back to default
+ REQUIRE(parseHashAlgorithm("SipHash24") == ParentHashAlgorithm::SIPHASH24);
+}
+
+TEST_CASE("ParentHashAlgorithm - Backward compatibility", "[ParentSelection]")
+{
+ // Verify default is siphash24 for backward compatibility
+ REQUIRE(parseHashAlgorithm("siphash24") == ParentHashAlgorithm::SIPHASH24);
+
+ // Verify enum default value is SIPHASH24
+ ParentHashAlgorithm default_algo = ParentHashAlgorithm::SIPHASH24;
+ REQUIRE(static_cast<int>(default_algo) == 0);
+
+ // Verify that unrecognized values fall back to siphash24
+ REQUIRE(parseHashAlgorithm("unknown") == ParentHashAlgorithm::SIPHASH24);
+}
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index 323589661c..396a8d8d2e 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -439,6 +439,8 @@ static constexpr RecordElement RecordsConfig[] =
,
{RECT_CONFIG, "proxy.config.http.parent_proxy.disable_parent_markdowns",
RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
,
+ {RECT_CONFIG, "proxy.config.http.parent_proxy.consistent_hash_algorithm",
RECD_STRING, "siphash24", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr,
RECA_NULL}
+ ,
{RECT_CONFIG, "proxy.config.http.forward.proxy_auth_to_parent", RECD_INT,
"0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
,
diff --git a/src/tscore/CMakeLists.txt b/src/tscore/CMakeLists.txt
index 0693798b4a..b9f856f56c 100644
--- a/src/tscore/CMakeLists.txt
+++ b/src/tscore/CMakeLists.txt
@@ -44,7 +44,6 @@ add_library(
FrequencyCounter.cc
Hash.cc
HashFNV.cc
- HashSip.cc
HostLookup.cc
InkErrno.cc
JeMiAllocator.cc
@@ -155,6 +154,7 @@ if(BUILD_TESTING)
unit_tests/test_Extendible.cc
unit_tests/test_Encoding.cc
unit_tests/test_FrequencyCounter.cc
+ unit_tests/test_HashAlgorithms.cc
unit_tests/test_HKDF.cc
unit_tests/test_Histogram.cc
unit_tests/test_History.cc
diff --git a/src/tscore/HashSip.cc b/src/tscore/HashSip.cc
deleted file mode 100644
index c2487255fe..0000000000
--- a/src/tscore/HashSip.cc
+++ /dev/null
@@ -1,141 +0,0 @@
-/**
-
-Algorithm Info:
-https://131002.net/siphash/
-
-Based off of implementation:
-https://github.com/floodyberry/siphash
-
- */
-
-#include "tscore/HashSip.h"
-#include <cstring>
-
-using namespace std;
-
-#define SIP_BLOCK_SIZE 8
-
-#define ROTL64(a, b) (((a) << (b)) | ((a) >> (64 - b)))
-
-#define U8TO64_LE(p) *(const uint64_t *)(p)
-
-#define SIPCOMPRESS(x0, x1, x2, x3) \
- x0 += x1; \
- x2 += x3; \
- x1 = ROTL64(x1, 13); \
- x3 = ROTL64(x3, 16); \
- x1 ^= x0; \
- x3 ^= x2; \
- x0 = ROTL64(x0, 32); \
- x2 += x1; \
- x0 += x3; \
- x1 = ROTL64(x1, 17); \
- x3 = ROTL64(x3, 21); \
- x1 ^= x2; \
- x3 ^= x0; \
- x2 = ROTL64(x2, 32);
-
-ATSHash64Sip24::ATSHash64Sip24()
-{
- this->clear();
-}
-
-ATSHash64Sip24::ATSHash64Sip24(const unsigned char key[16]) :
k0(U8TO64_LE(key)), k1(U8TO64_LE(key + sizeof(k0)))
-{
- this->clear();
-}
-
-ATSHash64Sip24::ATSHash64Sip24(uint64_t key0, uint64_t key1) : k0(key0),
k1(key1)
-{
- this->clear();
-}
-
-void
-ATSHash64Sip24::update(const void *data, size_t len)
-{
- size_t i, blocks;
- unsigned char *m;
- uint64_t mi;
- uint8_t block_off = 0;
-
- if (!finalized) {
- m = (unsigned char *)data;
- total_len += len;
-
- if (len + block_buffer_len < SIP_BLOCK_SIZE) {
- memcpy(block_buffer + block_buffer_len, m, len);
- block_buffer_len += len;
- } else {
- if (block_buffer_len > 0) {
- block_off = SIP_BLOCK_SIZE - block_buffer_len;
- memcpy(block_buffer + block_buffer_len, m, block_off);
-
- mi = U8TO64_LE(block_buffer);
- v3 ^= mi;
- SIPCOMPRESS(v0, v1, v2, v3);
- SIPCOMPRESS(v0, v1, v2, v3);
- v0 ^= mi;
- }
-
- for (i = block_off, blocks = ((len - block_off) & ~(SIP_BLOCK_SIZE -
1)); i < blocks; i += SIP_BLOCK_SIZE) {
- mi = U8TO64_LE(m + i);
- v3 ^= mi;
- SIPCOMPRESS(v0, v1, v2, v3);
- SIPCOMPRESS(v0, v1, v2, v3);
- v0 ^= mi;
- }
-
- block_buffer_len = (len - block_off) & (SIP_BLOCK_SIZE - 1);
- memcpy(block_buffer, m + block_off + blocks, block_buffer_len);
- }
- }
-}
-
-void
-ATSHash64Sip24::final()
-{
- uint64_t last7;
- int i;
-
- if (!finalized) {
- last7 = static_cast<uint64_t>(total_len & 0xff) << 56;
-
- for (i = block_buffer_len - 1; i >= 0; i--) {
- last7 |= static_cast<uint64_t>(block_buffer[i]) << (i * 8);
- }
-
- v3 ^= last7;
- SIPCOMPRESS(v0, v1, v2, v3);
- SIPCOMPRESS(v0, v1, v2, v3);
- v0 ^= last7;
- v2 ^= 0xff;
- SIPCOMPRESS(v0, v1, v2, v3);
- SIPCOMPRESS(v0, v1, v2, v3);
- SIPCOMPRESS(v0, v1, v2, v3);
- SIPCOMPRESS(v0, v1, v2, v3);
- hfinal = v0 ^ v1 ^ v2 ^ v3;
- finalized = true;
- }
-}
-
-uint64_t
-ATSHash64Sip24::get() const
-{
- if (finalized) {
- return hfinal;
- } else {
- return 0;
- }
-}
-
-void
-ATSHash64Sip24::clear()
-{
- v0 = k0 ^ 0x736f6d6570736575ull;
- v1 = k1 ^ 0x646f72616e646f6dull;
- v2 = k0 ^ 0x6c7967656e657261ull;
- v3 = k1 ^ 0x7465646279746573ull;
- finalized = false;
- total_len = 0;
- block_buffer_len = 0;
-}
diff --git a/src/tscore/unit_tests/test_HashAlgorithms.cc
b/src/tscore/unit_tests/test_HashAlgorithms.cc
new file mode 100644
index 0000000000..7c69648b02
--- /dev/null
+++ b/src/tscore/unit_tests/test_HashAlgorithms.cc
@@ -0,0 +1,155 @@
+/** @file
+
+ Unit tests for hash algorithms (SipHash-1-3, SipHash-2-4)
+
+ @section license License
+
+ 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 "tscore/HashSip.h"
+#include <catch2/catch_test_macros.hpp>
+#include <string>
+#include <vector>
+
+TEST_CASE("HashSip13 - Deterministic output", "[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash1, hash2;
+ const char *input = "test";
+
+ hash1.update(input, 4);
+ hash1.final();
+
+ hash2.update(input, 4);
+ hash2.final();
+
+ REQUIRE(hash1.get() == hash2.get());
+ REQUIRE(hash1.get() != 0);
+}
+
+TEST_CASE("HashSip13 - Empty input", "[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash;
+ hash.update("", 0);
+ hash.final();
+ REQUIRE(hash.get() != 0);
+}
+
+TEST_CASE("HashSip13 - Single byte", "[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash;
+ hash.update("a", 1);
+ hash.final();
+ REQUIRE(hash.get() != 0);
+}
+
+TEST_CASE("HashSip13 - Block boundaries", "[libts][HashSip13]")
+{
+ std::vector<size_t> sizes = {7, 8, 9, 16, 17, 31, 32, 33};
+ std::string input(64, 'x');
+
+ for (auto size : sizes) {
+ ATSHash64Sip13 hash;
+ hash.update(input.c_str(), size);
+ hash.final();
+ REQUIRE(hash.get() != 0);
+ }
+}
+
+TEST_CASE("HashSip13 - Incremental vs single update", "[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash1, hash2;
+
+ hash1.update("hello", 5);
+ hash1.update(" world", 6);
+ hash1.final();
+
+ hash2.update("hello world", 11);
+ hash2.final();
+
+ REQUIRE(hash1.get() == hash2.get());
+}
+
+TEST_CASE("HashSip13 - Typical URL paths", "[libts][HashSip13]")
+{
+ std::vector<std::string> urls = {"/", "/index.html", "/api/v1/users/123",
"/images/photos/vacation/beach/2024/photo_12345.jpg"};
+
+ for (const auto &url : urls) {
+ ATSHash64Sip13 hash;
+ hash.update(url.c_str(), url.size());
+ hash.final();
+ REQUIRE(hash.get() != 0);
+ }
+}
+
+TEST_CASE("HashSip13 - Long URLs with query strings", "[libts][HashSip13]")
+{
+ std::string long_url = "/search?";
+ for (int i = 0; i < 200; i++) {
+ long_url += "parameter" + std::to_string(i) + "=some_longer_value" +
std::to_string(i) + "&";
+ }
+
+ ATSHash64Sip13 hash;
+ hash.update(long_url.c_str(), long_url.size());
+ hash.final();
+ REQUIRE(hash.get() != 0);
+ REQUIRE(long_url.size() > 2000);
+}
+
+TEST_CASE("HashSip13 - Different inputs produce different hashes",
"[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash1, hash2;
+
+ hash1.update("parent1", 7);
+ hash1.final();
+
+ hash2.update("parent2", 7);
+ hash2.final();
+
+ REQUIRE(hash1.get() != hash2.get());
+}
+
+TEST_CASE("HashSip13 - Clear and reuse", "[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash;
+
+ hash.update("first", 5);
+ hash.final();
+ uint64_t first_result = hash.get();
+
+ hash.clear();
+ hash.update("first", 5);
+ hash.final();
+
+ REQUIRE(hash.get() == first_result);
+}
+
+TEST_CASE("HashSip13 - Comparison with SipHash-2-4", "[libts][HashSip13]")
+{
+ ATSHash64Sip13 hash13;
+ ATSHash64Sip24 hash24;
+ const char *input = "test";
+
+ hash13.update(input, 4);
+ hash13.final();
+
+ hash24.update(input, 4);
+ hash24.final();
+
+ REQUIRE(hash13.get() != 0);
+ REQUIRE(hash24.get() != 0);
+}