feat(rbac): Netty Proxy & Sticky Lease Manager for Scalable Multi-Tenant Task Workers - #16203
feat(rbac): Netty Proxy & Sticky Lease Manager for Scalable Multi-Tenant Task Workers#16203shruthi713 wants to merge 55 commits into
Conversation
…te into RemoteClient
…n and use CommonNettyHttpServiceFactory
…ismatch rejection count reversion in TaskManager
- Replaced TaskManagerHttpHandler with standalone Netty ServerBootstrap - Added ProxyFrontendHandler for zero-copy (.retain()) chunk streaming and connection queuing - Added ProxyBackendHandler for worker response relay and reverse backpressure
- Update Proxy to use thread-safe single PodState map with synchronized blocks - Add StickyLeaseManager lifecycle hooks for IAM Context injection - Remove per-task IAM sidecar wipes to enable Warm Boot leasing
- Modify RemoteClient to exclusively route to the new L7 proxy natively - Inject X-CDF-Namespace headers into all worker outbound traffic - Remove manual task /finish ping as Netty proxy implicitly intercepts worker state - Implement 60-second glass-break direct-fallback if Proxy is completely unreachable
- Delete background polling thread in TaskManagerService - Inject DiscoveryServiceClient directly into ProxyFrontendHandler - Execute synchronous, real-time ZK watch evaluations directly on the EventLoop using Twill's local cache - Resolve actual active Kubernetes Pod IPs instead of mocked local ports
…ponses - Expose X-Active-Tasks and X-Leased-Namespace from StickyLeaseManager - Ensure headers populate on both successful launches and lease rejections (429/409) - Enables Netty Gatekeeper to perform self-healing routing map corrections autonomously
- Removed ThreadLocal pod tracking from App Fabric RemoteClient - Deleted notifyTaskManagerFinished endpoint and logic - Relies entirely on the Netty Proxy passive response header interception for state synchronization
- Added timestamp tracking to PodState and ProxyBackendHandler to predict tenant lease expiration - Upgraded ProxyFrontendHandler 3-step hierarchy to safely route 35s expired Developer loads to enterprise idle pods - Deleted legacy TaskManager.java dead code - Replaced sidhirange with shruzard prefixes
…revent TCP Connection Reset
…revent TCP Connection Reset
…lback if proxy is busy
# Conflicts: # cdap-common/src/main/java/io/cdap/cdap/common/internal/remote/PodState.java # cdap-common/src/main/java/io/cdap/cdap/common/internal/remote/ProxyBackendHandler.java # cdap-common/src/main/java/io/cdap/cdap/common/internal/remote/ProxyFrontendHandler.java # cdap-common/src/main/java/io/cdap/cdap/common/internal/remote/RemoteTaskExecutor.java # cdap-common/src/main/java/io/cdap/cdap/common/internal/remote/StickyLeaseManager.java # task-manager-service.yaml
…ryService and fail-fast eviction in ProxyFrontendHandler
…ions in ProxyFrontendHandler
… endpoints watcher
…ntendHandler constructor
…TaskExecutor retry predicates
There was a problem hiding this comment.
Code Review
This pull request introduces a centralized Task Manager service and a Sticky Lease mechanism to support warm sticky leases for RBAC, shifting tenant isolation to the namespace/pod lease level. Key changes include the addition of Netty-based proxy handlers (ProxyFrontendHandler and ProxyBackendHandler), real-time Kubernetes Endpoints watching in KubeDiscoveryService, and routing updates in RemoteClient and RemoteTaskExecutor. The review feedback highlights several critical issues: a blocking synchronous Kubernetes API call in the discovery service, a security vulnerability due to disabled SSL certificate validation in the proxy, potential race conditions/NPEs in the task worker handler, a hardcoded namespace in the task manager URL, and an opportunity to optimize PodState to be lock-free to avoid blocking Netty event loops.
| CoreV1Api api = getCoreApi(); | ||
| V1Endpoints endpoints = api.readNamespacedEndpoints(meta.getName(), namespace, null); |
There was a problem hiding this comment.
Making a synchronous, blocking call to api.readNamespacedEndpoints inside toDiscoverables (which runs on the service watcher thread) is a major performance risk. This thread is responsible for updating service discovery state for the entire CDAP instance. Blocking it on network calls to the Kubernetes API server can cause severe delays in service discovery updates, especially under load or if the API server is slow. Instead of synchronous queries, rely entirely on the asynchronous EndpointsWatcherThread for services that require pod-level discovery, or restrict this behavior to only the specific services that need it (e.g., task workers) rather than all ClusterIP services.
| SslContext sslCtx = SslContextBuilder.forClient() | ||
| .trustManager(InsecureTrustManagerFactory.INSTANCE).build(); |
There was a problem hiding this comment.
Using InsecureTrustManagerFactory.INSTANCE disables SSL certificate validation, making the connection to the backend worker vulnerable to Man-in-the-Middle (MitM) attacks. In enterprise multi-tenant environments, this is a significant security risk. You should configure a proper SslContext with a trust manager that validates the backend certificates using the cluster's CA or the appropriate trust store.
| String currentLease = stickyLeaseManager.getCurrentLease() != null ? | ||
| stickyLeaseManager.getCurrentLease().getNamespace() : ""; |
There was a problem hiding this comment.
Calling stickyLeaseManager.getCurrentLease() multiple times in a check-then-act manner introduces a race condition. If the lease is released by another thread between the null check and the getNamespace() call, a NullPointerException will be thrown. Store the result of getCurrentLease() in a local variable first.
| String currentLease = stickyLeaseManager.getCurrentLease() != null ? | |
| stickyLeaseManager.getCurrentLease().getNamespace() : ""; | |
| io.cdap.cdap.proto.id.NamespaceId currentLeaseId = stickyLeaseManager.getCurrentLease(); | |
| String currentLease = currentLeaseId != null ? currentLeaseId.getNamespace() : ""; |
| String currentLease = stickyLeaseManager.getCurrentLease() != null ? | ||
| stickyLeaseManager.getCurrentLease().getNamespace() : ""; |
There was a problem hiding this comment.
Calling stickyLeaseManager.getCurrentLease() multiple times in a check-then-act manner introduces a race condition. If the lease is released by another thread between the null check and the getNamespace() call, a NullPointerException will be thrown. Store the result of getCurrentLease() in a local variable first.
| String currentLease = stickyLeaseManager.getCurrentLease() != null ? | |
| stickyLeaseManager.getCurrentLease().getNamespace() : ""; | |
| io.cdap.cdap.proto.id.NamespaceId currentLeaseId = stickyLeaseManager.getCurrentLease(); | |
| String currentLease = currentLeaseId != null ? currentLeaseId.getNamespace() : ""; |
| private static final Logger LOG = LoggerFactory.getLogger(RemoteClient.class); | ||
|
|
||
|
|
||
| private static final String TASK_MANAGER_URL = "http://cdap-task-manager.default.svc.cluster.local:11025"; |
There was a problem hiding this comment.
Hardcoding the Kubernetes namespace default in the TASK_MANAGER_URL (http://cdap-task-manager.default.svc.cluster.local:11025) prevents CDAP from working correctly if it is deployed in a different namespace. The namespace should be dynamically resolved (e.g., from the environment or pod metadata) or made configurable via CConfiguration.
| public class PodState { | ||
| private String leasedNamespace; | ||
| private int inflightRequests; | ||
| private long lastActivityTime; | ||
|
|
||
| public PodState(String leasedNamespace, int inflightRequests) { | ||
| this.leasedNamespace = leasedNamespace; | ||
| this.inflightRequests = inflightRequests; | ||
| // Subtract 40 seconds worth of nanos to instantly trigger predictions on boot | ||
| this.lastActivityTime = System.nanoTime() | ||
| - java.util.concurrent.TimeUnit.SECONDS.toNanos(40); | ||
| } | ||
|
|
||
| public String getLeasedNamespace() { | ||
| return leasedNamespace; | ||
| } | ||
|
|
||
| public void setLeasedNamespace(String leasedNamespace) { | ||
| this.leasedNamespace = leasedNamespace; | ||
| } | ||
|
|
||
| public int getInflightRequests() { | ||
| return inflightRequests; | ||
| } | ||
|
|
||
| public void setInflightRequests(int inflightRequests) { | ||
| this.inflightRequests = inflightRequests; | ||
| } | ||
|
|
||
| public long getLastActivityTime() { | ||
| return lastActivityTime; | ||
| } | ||
|
|
||
| public void setLastActivityTime(long lastActivityTime) { | ||
| this.lastActivityTime = lastActivityTime; | ||
| } | ||
| } |
There was a problem hiding this comment.
To avoid blocking the Netty event loop threads with synchronized blocks in ProxyFrontendHandler and ProxyBackendHandler, refactor PodState to be completely lock-free. This can be achieved by using an immutable State record/class and updating it atomically via AtomicReference.
public class PodState {
private static class State {
final String leasedNamespace;
final int inflightRequests;
final long lastActivityTime;
State(String leasedNamespace, int inflightRequests, long lastActivityTime) {
this.leasedNamespace = leasedNamespace;
this.inflightRequests = inflightRequests;
this.lastActivityTime = lastActivityTime;
}
}
private final java.util.concurrent.atomic.AtomicReference<State> stateRef;
public PodState(String leasedNamespace, int inflightRequests) {
this.stateRef = new java.util.concurrent.atomic.AtomicReference<>(new State(
leasedNamespace,
inflightRequests,
System.nanoTime() - java.util.concurrent.TimeUnit.SECONDS.toNanos(40)
));
}
public String getLeasedNamespace() {
return stateRef.get().leasedNamespace;
}
public int getInflightRequests() {
return stateRef.get().inflightRequests;
}
public long getLastActivityTime() {
return stateRef.get().lastActivityTime;
}
public boolean tryAcquireWarmLease(String namespace, int maxConcurrency) {
while (true) {
State current = stateRef.get();
if (!namespace.equals(current.leasedNamespace) || current.inflightRequests >= maxConcurrency) {
return false;
}
State next = new State(current.leasedNamespace, current.inflightRequests + 1, System.nanoTime());
if (stateRef.compareAndSet(current, next)) {
return true;
}
}
}
public boolean tryClaimIdleLease(String namespace, long idleTimeoutNanos) {
while (true) {
State current = stateRef.get();
boolean isUnleased = current.leasedNamespace == null || current.leasedNamespace.isEmpty();
boolean isExpiredIdle = current.inflightRequests == 0
&& (System.nanoTime() - current.lastActivityTime > idleTimeoutNanos);
if (current.inflightRequests != 0 || (!isUnleased && !isExpiredIdle)) {
return false;
}
State next = new State(namespace, 1, System.nanoTime());
if (stateRef.compareAndSet(current, next)) {
return true;
}
}
}
public void decrementInflightRequests() {
while (true) {
State current = stateRef.get();
State next = new State(current.leasedNamespace, Math.max(0, current.inflightRequests - 1), System.nanoTime());
if (stateRef.compareAndSet(current, next)) {
return;
}
}
}
public void updateFromHeader(String activeTasksStr, String leasedNamespace) {
while (true) {
State current = stateRef.get();
int nextInflight = current.inflightRequests;
if (activeTasksStr != null) {
nextInflight = Integer.parseInt(activeTasksStr);
}
String nextNamespace = leasedNamespace != null ? leasedNamespace : current.leasedNamespace;
State next = new State(nextNamespace, nextInflight, System.nanoTime());
if (stateRef.compareAndSet(current, next)) {
return;
}
}
}
}…lease management, and routing classes
…ections and release occupancy on LastHttpContent
…stem.currentTimeMillis()
…l from WatcherThread and rely on asynchronous EndpointsWatcherThread
…eck endpointsWatcherEnabled
…e synchronization
Summary & Objective
This PR implements the Netty Proxy & Sticky Lease Architecture for CDAP Task Workers under Namespaced Service Accounts (RBAC).
In enterprise multi-tenant environments with dynamic Workload Identity bindings, this L7 proxy decouples AppFabric from Task Worker lifecycle delays, enabling up to 10 concurrent requests per warm Task Worker pod while strictly preserving namespace tenant isolation.
Architectural Highlights & Key Changes
1. In-Memory Lock-Free Lease Governance (
StickyLeaseManager.java)namespace -> pod endpointusing lock-freeConcurrentHashMap.AtomicInteger(capped at 10 concurrent tasks/pod).2. High-Performance Zero-Copy Netty Proxy (
ProxyFrontendHandler.java&ProxyBackendHandler.java)X-CDF-Namespaceheader, reserves lease slots fromStickyLeaseManager, and streamsByteBufchunks with zero JVM heap copies (ReferenceCountUtil.retain()).LastHttpContent, and catches 409 Conflict (split-brain recovery) to invalidate stale leases and reroute to a healthy pod.3. Sub-Millisecond Pod Discovery (
KubeDiscoveryService.java)Endpointswatcher via Fabric8 Kubernetes Client.4. Client-Side Routing & Fallback (
RemoteTaskExecutor.java)task.manager.service.urlwith theX-CDF-Namespaceheader.Verification & Load Test Results