Skip to content

feat(filter): add ring hash, subset, zone-aware, and priority LB strategies - #949

Open
abdallahsamabd wants to merge 1 commit into
praxis-proxy:mainfrom
abdallahsamabd:feat/loadbalance
Open

feat(filter): add ring hash, subset, zone-aware, and priority LB strategies#949
abdallahsamabd wants to merge 1 commit into
praxis-proxy:mainfrom
abdallahsamabd:feat/loadbalance

Conversation

@abdallahsamabd

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements the four remaining load balancing algorithms from the proposal (#913):

  • Ring hash: consistent hashing with pluggable hash functions (FNV-1a, xxHash64, MurmurHash3), configurable virtual node density, and O(log N) binary-search lookup on a sorted ring.
  • Subset LB: filters endpoints by metadata key-value labels, applies an inner strategy (round-robin, least-connections, P2C, or random) within the matched subset, with configurable fallback policy (any_endpoint or no_endpoint).
  • Zone-aware: prefers same-zone endpoints, spilling to all zones when local healthy capacity drops below a configurable threshold (default 70%).
  • Priority levels: groups endpoints into primary/failover tiers by priority field, using an overprovisioning factor (default 140%) to determine when to spill to the next tier.

Also extends the Endpoint config with metadata, zone, and priority fields to support the composite strategies. Includes example configs, integration tests, and unit tests for all new algorithms.

Which issue(s) does this relate to?

Part of #109

Checklist

  • Signed off all commits (git commit -s)
  • Tests added or updated
  • Documentation updated (if applicable)
  • make lint && make test passes locally

Does this introduce a breaking change?

No. The new metadata, zone, and priority fields on Endpoint::Weighted all have serde defaults ({}, null, null) so existing configs continue to work unchanged.

@abdallahsamabd
abdallahsamabd requested review from a team August 11, 2026 11:25
@praxis-bot-app

Copy link
Copy Markdown

PR too large: 1859 lines added (limit: 750, excludes Cargo files, tests, docs, examples, and benchmarks). Please split into smaller PRs. Add skip/pr-conventions label to override.

@abdallahsamabd
abdallahsamabd force-pushed the feat/loadbalance branch 2 times, most recently from 294b972 to 8237f65 Compare August 11, 2026 12:08
@abdallahsamabd abdallahsamabd added the skip/pr-conventions Skip conventions checks for PRs label Aug 11, 2026

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Adds ring hash, subset, zone-aware, and priority LB strategies with config types, unit tests, integration tests, and example configs.

Overall: Well-structured implementation. The four strategies follow established patterns (health-aware selection, panic mode fallback, weighted endpoints). Config types use deny_unknown_fields and sensible defaults. Test coverage is thorough with both unit and integration tests. One correctness bug in the xxHash64 implementation and two convention issues.

Severity Count
Critical 0
Large 1
Medium 2

Note: tests/integration/tests/suite/examples/retry_policy.rs appears unrelated to this PR's LB feature work. Consider splitting it into its own PR for cleaner history.

Comment thread filter/src/load_balancing/ring_hash.rs Outdated
fn xxhash64(s: &str) -> u64 {
const PRIME1: u64 = 0x9E37_79B1_85EB_CA87;
const PRIME2: u64 = 0x14DE_F9DE_A2F7_9CD6;
const PRIME3: u64 = 0x0165_6738_5AF0_6C85;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Large] The PRIME3 constant 0x0165_6738_5AF0_6C85 does not match the xxHash64 specification (XXH_PRIME64_3 = 0x0165_667B_19E3_779F in xxhash.h). This causes the xxhash hash function to produce non-standard output despite being documented as "xxHash 64-bit" in the config enum. The incorrect constant affects tail processing for any input whose length mod 8 is between 4 and 7, which covers roughly half of all virtual node keys.

Change to:

const PRIME3: u64 = 0x0165_667B_19E3_779F;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the next push. The constant was wrong; corrected to 0x1656_67B1_9E37_79F9 per xxhash.h. Tests updated and passing

/// Hash function to use for the ring. Defaults to FNV-1a.
#[serde(default)]
pub hash_function: HashFunction,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] virtual_nodes (here), min_local_healthy_pct (line 193), and overprovisioning_factor (line 218) are constrained numerics with no parse-time validation. Per project conventions, use #[serde(try_from)] with newtype wrappers:

  • virtual_nodes must be >= 1: a value of 0 creates an empty ring, silently routing all requests through the panic-mode fallback.
  • min_local_healthy_pct must be <= 100: values above 100 make the zone-aware strategy always spill to all endpoints, defeating its purpose.
  • overprovisioning_factor must be >= 1: a value of 0 causes healthy_count * 0 >= total * 100 to always be false, making every tier fail the capacity check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread examples/README.md Outdated
| [random.yaml](configs/traffic-management/random.yaml) | Selects an upstream endpoint at random, weighted by endpoint weight |
| [rate-limiting.yaml](configs/traffic-management/rate-limiting.yaml) | Token bucket rate limiter with per-IP or global modes |
| [redirect.yaml](configs/traffic-management/redirect.yaml) | Returns a 3xx redirect without contacting any upstream |
| [ring-hash.yaml](configs/traffic-management/ring-hash.yaml) | Configurable ring-hash with pluggable hash function and virtual node density |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] subset-lb.yaml and zone-aware.yaml break the table's alphabetical ordering. subset-lb (su) should come after static-response (st), and zone-aware (z) should be the last entry, after weighted-load-balancing (w). Move both lines to their correct positions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now generated via cargo xtask sync-example-readme --fix which enforces ordering automatically

@abdallahsamabd
abdallahsamabd force-pushed the feat/loadbalance branch 2 times, most recently from 9133c5e to c3954eb Compare August 11, 2026 13:59
…tegies

Signed-off-by: Abdallah Samara <abdallahsamabd@gmail.com>

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental Review (post-fix)

The PRIME3 fix from the previous review was applied correctly, and the examples/README.md ordering is now alphabetical. However, the xxHash64 implementation still has three incorrect prime constants.

Severity Count
Large 1
Medium 0
Small 0

const PRIME2: u64 = 0x14DE_F9DE_A2F7_9CD6;
const PRIME3: u64 = 0x1656_67B1_9E37_79F9;
const PRIME4: u64 = 0x27D4_EB2F_1656_67C5;
const PRIME5: u64 = 0x1656_67B1_9E37_79F9;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Large] PRIME3 was fixed, but three other xxHash64 constants are still incorrect. Most visibly, PRIME5 is identical to PRIME3 (0x1656_67B1_9E37_79F9), which is always wrong -- no hash algorithm reuses the same constant for two different purposes.

Full comparison against the xxHash64 specification:

Constant Code value Canonical value Status
PRIME1 0x9E37_79B1_85EB_CA87 0x9E37_79B1_85EB_CA87 correct
PRIME2 0x14DE_F9DE_A2F7_9CD6 0xC2B2_AE3D_27D4_EB4F wrong
PRIME3 0x1656_67B1_9E37_79F9 0x1656_67B1_9E37_79F9 correct
PRIME4 0x27D4_EB2F_1656_67C5 0x85EB_CA77_C2B2_AE63 wrong (has PRIME64_5 value)
PRIME5 0x1656_67B1_9E37_79F9 0x27D4_EB2F_1656_67C5 wrong (duplicates PRIME3)

Note that the same wrong PRIME2 is also used in xxh64_round and the wrong PRIME4 in xxh64_merge_round. All six occurrences need updating.

Consider adding a reference-vector test (hash a known string, assert the exact u64 output) to catch constant errors at compile time.

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Adds ring hash, subset, zone-aware, and priority LB strategies with config types, validation, unit tests, integration tests, and example configs.

Overall: Solid implementation. The four strategies follow established patterns, config types use deny_unknown_fields and sensible defaults, the validation module catches invalid bounds, and test coverage is thorough. Two issues found: a behavioral gap in subset fallback and field ordering convention violations.

Severity Count
Critical 0
Large 0
Medium 2

if let Some(strategy) = &self.subset_strategy {
let result = strategy.select(hash_key, health);
if result.is_some() {
return result;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] When all subset endpoints are unhealthy, the inner strategy's panic mode returns an unhealthy endpoint, making result.is_some() true and bypassing the fallback_policy. This means fallback_policy: any_endpoint never activates on health degradation -- only on an empty selector match.

Scenario: three canary endpoints all fail health checks; two stable endpoints are healthy. The subset's inner RoundRobin panic-routes to an unhealthy canary instead of falling back to a healthy stable endpoint.

Add a health-aware check before returning the subset result. For example, store the subset endpoint indices at construction and skip to fallback when all of them are unhealthy:

if let Some(strategy) = &self.subset_strategy {
    if !self.all_subset_unhealthy(health) {
        let result = strategy.select(hash_key, health);
        if result.is_some() {
            return result;
        }
    }
}

Also add a test: construct a Subset with health state where all subset endpoints are marked unhealthy and fallback_policy: AnyEndpoint, then assert the selected endpoint comes from outside the subset.

pub struct RingHashOpts {
/// Name of the request header to use as the hash key.
#[serde(default)]
pub header: Option<String>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Three of the four new config structs have non-alphabetical field ordering, violating the project convention ("Struct fields: name first, then alphabetical").

RingHashOpts — current: header, hash_function, virtual_nodes; correct: hash_function, header, virtual_nodes.

SubsetOpts (line 155) — current: selector, inner_strategy, fallback_policy; correct: fallback_policy, inner_strategy, selector.

ZoneAwareOpts (line 187) — current: local_zone, inner_strategy, min_local_healthy_pct; correct: inner_strategy, local_zone, min_local_healthy_pct.

Reorder each struct's fields alphabetically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip/pr-conventions Skip conventions checks for PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants