Skip to content

feat: add forced-host label for Velocity forced hosts support - #71

Closed
ChipWolf wants to merge 3 commits into
AzisabaNetwork:develop/v3from
marcusk-studio:develop/v3
Closed

ChipWolf wants to merge 3 commits into
AzisabaNetwork:develop/v3from
marcusk-studio:develop/v3

Conversation

@ChipWolf

Copy link
Copy Markdown

Add a new 'forced-host' label/annotation that can be set on Pods and ReplicaSets to configure Velocity's forced hosts functionality. Players connecting via a specific hostname will be routed to the server with the matching forced-host label.

See: https://docs.papermc.io/velocity/configuration/\#forced-hosts-section

Resolves #70

Add a new 'forced-host' label/annotation that can be set on Pods and
ReplicaSets to configure Velocity's forced hosts functionality. Players
connecting via a specific hostname will be routed to the server with
the matching forced-host label.

See: https://docs.papermc.io/velocity/configuration/\#forced-hosts-section
darcusk and others added 2 commits September 12, 2026 18:14
A backend whose Deployment rolls could stay registered in Velocity (and as
a load-balancer endpoint) pointing at the dead pod's IP, with no health
check to evict it. Players routed there get "Unable to connect you to
<server>", and the only remedy today is a manual `kubectl rollout restart`
of the proxy.

Three defects combine to produce this:

1. KuvelServiceHandler.unregisterPod early-returned whenever the server
   name was still mapped to another pod UID, leaving the Velocity
   ServerInfo / LB endpoint bound to the departed pod's IP and never
   verifying the surviving UID was actually a live pod.
2. The 5s RedisServerDiscovery poll's delete path ran unregisterPodOrIgnore
   outside the ReentrantLock that guards registration in processUpdatedPod
   (TOCTOU), and nothing re-derived state from the live pod list each tick.
3. updateLoadBalancerEndpoints (which rebuilds LB membership from the live
   pod list) was only ever called from registerLoadBalancer, never from the
   poll, so a stale endpoint survived in the round-robin rotation.

getServersForStartup already prunes dead entries and rebuilds from live
Running pods on boot -- which is exactly what a proxy restart does. This
change runs that reconciliation continuously.

Changes:

- Level-triggered reconcile in the 5s poll (RedisServerDiscovery.reconcile):
  after diff/delete processing each tick, evict any podUidAndServerNameMap
  UID that is not in the current live-Running labelled-pod set, then call
  KuvelServiceHandler.reconcileLoadBalancerEndpoints() to rebuild every load
  balancer's membership from live pods. The delete path and the sweep now
  run under the same ReentrantLock as registration, closing the TOCTOU.

- KuvelServiceHandler.unregisterPod now reconciles instead of early-
  returning: when the name is still mapped to another UID it verifies that
  UID is a live (Running) pod and rebinds the Velocity ServerInfo / LB
  endpoint to the live pod's IP; only when a genuinely-live pod holds the
  name is teardown skipped. A name is never left bound to a departed UID.

Minimal change, reuses existing helpers (registerPod,
updateLoadBalancerEndpoints) and mirrors getServersForStartup's Running-
phase semantics. The forced-host behaviour from c17652b is preserved.

Co-authored-by: Chip Wolf <hello@chipwolf.uk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…suite from stale branches (#2)

* feat: add workflow_dispatch trigger and upload artifact step for Kuvel build

* Update Java version to 21 and improve build workflow

- Update Java version from 17 to 21 in pom.xml and build.yml
- Switch JDK distribution from adopt to temurin
- Add workflow_dispatch trigger
- Add artifact upload step

* test(forced-host): add unit tests for forced-host concurrency and routing

Rescued from stale branch test/k8s-integration (418bcc0). Adds the
JUnit 5 + Mockito test-scope dependencies and two test classes:

- ForcedHostConcurrencyTest: validates the ConcurrentHashMap +
  CopyOnWriteArrayList pattern used by KuvelServiceHandler for forced
  hosts is safe under concurrent add/remove/iterate.
- ChooseInitialServerListenerTest: covers forced-host routing, fall
  through to initial servers, unregistered-server skip, and empty-list
  cases in ChooseInitialServerListener.

The forced-host source changes from that branch are already present on
develop/v3 (ConcurrentHashMap + CopyOnWriteArrayList, if/return listener),
so only the tests are carried; they compile and pass against the current
source unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(k8s): add forced-host integration harness

Rescued from stale branch test/k8s-integration (418bcc0). Adds a
Kubernetes harness to exercise forced-host routing end to end:

- Dockerfile.kuvel-plugin packages target/Kuvel.jar (matches the
  pom finalName) into a sidecar image copied into the proxy plugins dir.
- backends.yaml, dragonfly.yaml, velocity-config.yaml,
  velocity-proxy.yaml deploy a Paper backend, a Redis (dragonfly)
  instance, and a Velocity proxy wired via the KUVEL_* env overrides
  that KuvelConfig reads, using the kuvel.azisaba.net label prefix and
  the forced-host label key the current plugin expects.
- test_forced_host.py drives the Minecraft handshake against the proxy
  NodePort (30577) to assert forced-host routing.

Labels, jar name, config env vars, and ports are internally consistent
with the current plugin, but this harness has NOT been validated against
a live cluster in this change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Chip Wolf ‮ <hello@chipwolf.uk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@marcusk-studio marcusk-studio closed this by deleting the head repository Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forced hosts

3 participants