Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c9f58ba
feat(rbac-poc): implement standalone Task Manager Service and integra…
sidhdirenge Jun 29, 2026
03d9761
feat(rbac-poc): pass namespace as routingKey in RemoteTaskExecutor
sidhdirenge Jun 29, 2026
94f9079
feat(rbac-poc): integrate StickyLeaseManager on Task Worker side
sidhdirenge Jun 29, 2026
375876b
feat(rbac-poc): refactor TaskManagerMain to extend AbstractServiceMai…
sidhdirenge Jul 2, 2026
5b162ec
feat(sticky-leases): implement randomized tie-breaking fallback and m…
sidhdirenge Jul 8, 2026
912ce60
feat: Implement Netty Proxy streaming skeleton for Task Manager POC
shruthi713 Jul 17, 2026
3d94f45
feat(poc): Implement Netty Proxy and Gatekeeper IAM reaper
shruthi713 Jul 20, 2026
3de3809
feat(poc): Wire up App Fabric client to Netty Proxy
shruthi713 Jul 21, 2026
92430a8
feat(poc): Wire up Netty Proxy to K8s Discovery service
shruthi713 Jul 21, 2026
f4c54eb
feat(poc): Inject ground truth occupancy headers into Task Worker res…
shruthi713 Jul 21, 2026
45555ad
chore: update logging prefix from sidhdirenge to shruzard
shruthi713 Jul 21, 2026
26a1d1b
chore: add comprehensive logging across Netty POC components with shr…
shruthi713 Jul 21, 2026
3598302
refactor(poc): eliminate legacy task confirmation network callbacks
shruthi713 Jul 21, 2026
6a4f79c
refactor
shruthi713 Jul 21, 2026
f9045ab
refactor: Add Predictive Proxy Expiration and Phase 0/1 Code Cleanup
shruthi713 Jul 21, 2026
fda268d
feat: Reinject 10-minute security cache eviction boundary for Worker
shruthi713 Jul 21, 2026
422a009
feat: Reinject 10-minute security cache eviction boundary for Worker
shruthi713 Jul 21, 2026
8c44f62
rat test
shruthi713 Jul 21, 2026
204fc19
fix: Add license headers and remove dangling try block
shruthi713 Jul 22, 2026
dfe1e85
fix: Remove dangling try block
shruthi713 Jul 22, 2026
50ed55b
chore: Remove deprecated TaskManagerTest
shruthi713 Jul 22, 2026
b4d55ff
fix(checkstyle): wrap lines exceeding 120 chars in cdap-common
shruthi713 Jul 22, 2026
0be04ef
fix(checkstyle): wrap lines exceeding 120 chars in cdap-common
shruthi713 Jul 22, 2026
d39de87
chore: Route RemoteTaskExecutor traffic to task.manager via Constants
shruthi713 Jul 22, 2026
9d72f2c
k8s changes
shruthi713 Jul 22, 2026
e31d6bc
add logs, update activeTasks only after last response
shruthi713 Jul 27, 2026
a42d334
fix checkstyle
shruthi713 Jul 27, 2026
2bbd302
fix(netty-proxy): gracefully drain request body on 429 rejection to p…
shruthi713 Jul 28, 2026
27604c0
fix(netty-proxy): gracefully drain request body on 429 rejection to p…
shruthi713 Jul 28, 2026
9570164
remove hostname fallback
shruthi713 Jul 30, 2026
5d3f335
feat(rbac): increase task worker retry timeout to 90s and prevent fal…
shruthi713 Aug 6, 2026
b5c613f
remove hostname fallback
shruthi713 Aug 6, 2026
8f82d4f
Merge branch 'rbac-netty-proxy-poc-agent' into rbac-netty-proxy-poc
shruthi713 Aug 6, 2026
7367018
fix(rbac): correct configuration key for fallback timeout in RemoteTa…
shruthi713 Aug 6, 2026
746118a
test(rbac): hardcode fallback timeout to 90s for testing
shruthi713 Aug 6, 2026
ea00d91
fix(rbac): prevent occupancy leak on connection closures in ProxyBack…
shruthi713 Aug 6, 2026
9513533
feat(discovery): add live Kubernetes Endpoints watcher to KubeDiscove…
shruthi713 Aug 7, 2026
592c826
fix(netty-proxy): restore SSL support for outbound TaskWorker connect…
shruthi713 Aug 7, 2026
73c485b
feat(discovery): preserve pre-warmed discoverables, SSL, and add live…
shruthi713 Aug 7, 2026
08aa1a2
refactor(proxy): encapsulate endpoints watcher enablement in ProxyFro…
shruthi713 Aug 7, 2026
622ba01
fix(checkstyle): wrap log line exceeding 120 chars in ProxyFrontendHa…
shruthi713 Aug 7, 2026
1d4991a
refactor(retry): change fallback timeout and max retry time from 90s …
shruthi713 Aug 7, 2026
2966cff
feat(retry): include SocketException and transport failures in Remote…
shruthi713 Aug 7, 2026
e759b2d
docs: add comprehensive English architectural comments across proxy, …
shruthi713 Aug 18, 2026
31fb471
fix(proxy): selectively sync ground truth headers only on 409/429 rej…
shruthi713 Aug 19, 2026
8a68d01
refactor(proxy): initialize PodState lastActivityTime cleanly with Sy…
shruthi713 Aug 19, 2026
bf5a38e
chore: remove Dockerfile from PR branch
shruthi713 Aug 19, 2026
fd89a7c
chore: remove _agents/rules/rbac_taskmanager.md from PR branch
shruthi713 Aug 19, 2026
0a2e223
chore: remove _agents/rules/task_manager_context.md from PR branch
shruthi713 Aug 19, 2026
70f8bc6
perf(k8s): eliminate blocking synchronous readNamespacedEndpoints cal…
shruthi713 Aug 19, 2026
27c7b45
refactor(k8s): simplify EndpointsWatcherThread start check to only ch…
shruthi713 Aug 19, 2026
4d9c301
refactor: rename TaskManagerMain to TaskManagerServiceMain
shruthi713 Aug 19, 2026
dd5e7fb
make TaskManager register itself to KubeDiscoverService
shruthi713 Aug 20, 2026
2eb4741
update PodState to use AtomicReference and CAS mechanism for lock fre…
shruthi713 Aug 25, 2026
3482e23
update logs
shruthi713 Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright © 2026 Cask Data, Inc.
#
# Licensed 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.

FROM us-east1-docker.pkg.dev/j145774183a931adb-tp/cdf-dev-shru/cloud-data-fusion:latest
# For OSS CDAP, use "FROM gcr.io/cdapio/cdap:latest"

RUN rm -rf /opt/cdap/master/lib/io.cdap.cdap.cdap-common-6.12.0-SNAPSHOT.jar

COPY cdap-common/target/cdap-common-6.12.0-SNAPSHOT.jar /opt/cdap/master/lib/io.cdap.cdap.cdap-common-6.12.0-SNAPSHOT.jar

RUN rm -rf /opt/cdap/master/ext/environments/k8s/io.cdap.cdap.cdap-kubernetes-6.12.0-SNAPSHOT.jar


COPY cdap-kubernetes/target/cdap-kubernetes-6.12.0-SNAPSHOT.jar /opt/cdap/master/ext/environments/k8s/io.cdap.cdap.cdap-kubernetes-6.12.0-SNAPSHOT.jar


RUN chmod -R 755 /opt/cdap
18 changes: 18 additions & 0 deletions _agents/rules/rbac_taskmanager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# CDAP RBAC Warm Sticky Leases & Task Manager Service

When working on the RBAC Everywhere feature, Namespaced Service Accounts (NSA), or Task Worker pod scaling in this repository:

1. **Architecture Context**:
* Refer to the detailed Warm Sticky Lease research notes here:
[rbac_taskmanager_research_notes.md](file:///usr/local/google/home/venkataramansh/.gemini/jetski/brain/2099d0c8-9e2b-4db5-a81a-5cfb437c1660/rbac_taskmanager_research_notes.md)
* This feature resolves the "429 collision storm" and cold-start latencies (~40s) by shifting tenant isolation from the request level to the namespace/pod lease level.

2. **Core Routing Rules**:
* **Direct Routing**: `RemoteClient` must bypass K8s round-robin load balancing. It resolves the headless task-worker service via DNS expansion to individual pod IPs, queries the `TaskManager` service to resolve the lease, and routes directly to the leased pod IP.
* **Lease Registry**: The `TaskManager` service holds the lease maps in-memory to prevent Spanner database write contention. It uses a `ReentrantLock` to serialize checks/actions and prevent race conditions.
* **Local Guard**: Individual Task Workers use `StickyLeaseManager` as a fail-safe to reject mismatching namespace requests locally with a `429`.

3. **Key Classes**:
* App Fabric / Client: `RemoteClient`, `RemoteTaskExecutor`, `KubeDiscoveryService`
* Task Manager: `TaskManager`, `TaskManagerHttpHandler`, `TaskManagerService`, `TaskManagerMain`
* Task Worker: `StickyLeaseManager`, `TaskWorkerHttpHandlerInternal`
19 changes: 19 additions & 0 deletions _agents/rules/task_manager_context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Task Manager & Sticky Lease Context

## Architecture
* **Environment:** Cloud Data Fusion (CDF) on GKE (ZooKeeper-free).
* **Service Discovery:** Headless DNS expansion to pod IPs.
* **Coordination:** Standalone single-replica HTTP `TaskManager` service.
* **Local Guard:** `StickyLeaseManager` on Task Worker pods enforcing "First-Write Wins" lease lock.

## Concurrency & Workload Limits
* **Concurrency:** Max 10 concurrent tasks per pod (enforced by `podActiveTaskCounts` / `activeTasks`).
* **Lifetime Limit:** Max 10 tasks before reset (enforced by `podTotalTaskProcessedCounts`).
* *Increment in `resolve`:* Enforces strict limit of 10 tasks started (safe, current behavior).
* *Increment in `finish`:* Allows better utilization but pod can process up to 19 tasks due to concurrency.

## Reliability & Recovery
* **Downtime:** `RemoteClient` falls back to local consistent hashing if `TaskManager` is down.
* **State Recovery:** To recover from `TaskManager` restarts without polling, use a self-correction pattern:
* Task Worker returns `409 Conflict` (with active namespace in body) on lease mismatch.
* `RemoteClient` parses 409 and notifies `TaskManager` to update its lease map.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,17 @@ private File sendRequest(ArtifactId artifactId, boolean unpack)
Multimap<String, String> headers = httpRequest.getHeaders();
internalAuthenticator.applyInternalAuthenticationHeaders(headers::put);

HttpResponse httpResponse = HttpRequests.execute(httpRequest);
long startTime = System.currentTimeMillis();
HttpResponse httpResponse;
try {
httpResponse = HttpRequests.execute(httpRequest);
} catch (IOException e) {
LOG.error("shruzard ArtifactLocalizerClient request for {} failed after {} ms with error: {}",
artifactId, System.currentTimeMillis() - startTime, e.getMessage());
throw e;
}
LOG.info("shruzard ArtifactLocalizerClient request for {} completed in {} ms (response code: {})",
artifactId, System.currentTimeMillis() - startTime, httpResponse.getResponseCode());

if (httpResponse.getResponseCode() != HttpURLConnection.HTTP_OK) {
if (httpResponse.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public static final class Service {
public static final String INTERNAL_ROUTER = "internal.router";
public static final String AUTHENTICATION = "authentication";
public static final String TASK_WORKER = "task.worker";
public static final String TASK_MANAGER = "task.manager";
public static final String SYSTEM_WORKER = "system.worker";
public static final String ARTIFACT_LOCALIZER = "artifact.localizer";
public static final String SYSTEM_METRICS_EXPORTER = "system.metrics.exporter";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright © 2026 Cask Data, Inc.
*
* Licensed 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.
*/

package io.cdap.cdap.common.internal.remote;

/**
* Tracks the routing state and load for a given worker pod IP.
*/
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;
}
}
Comment on lines +26 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

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;
            }
        }
    }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright © 2026 Cask Data, Inc.
*
* Licensed 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.
*/

package io.cdap.cdap.common.internal.remote;

import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpResponse;

import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ProxyBackendHandler extends ChannelInboundHandlerAdapter {

private static final Logger LOG = LoggerFactory.getLogger(ProxyBackendHandler.class);


private final Channel inboundChannel;
private final Map<String, PodState> podRegistry;
private final String targetWorkerAddress;
private boolean decremented = false;

public ProxyBackendHandler(Channel inboundChannel, Map<String, PodState> podRegistry, String targetWorkerAddress) {
this.inboundChannel = inboundChannel;
this.podRegistry = podRegistry;
this.targetWorkerAddress = targetWorkerAddress;
}

private synchronized void decrementInflight() {
if (!decremented) {
PodState state = podRegistry.get(targetWorkerAddress);
if (state != null) {
synchronized (state) {
state.setInflightRequests(Math.max(0, state.getInflightRequests() - 1));
state.setLastActivityTime(System.nanoTime());
}
}
decremented = true;
}
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpResponse) {
HttpResponse resp = (HttpResponse) msg;
PodState state = podRegistry.get(targetWorkerAddress);
if (state != null) {
// Thread-safe update from Worker Ground Truth headers
synchronized (state) {
String activeTasksStr = resp.headers().get("X-Active-Tasks");
String leasedNamespace = resp.headers().get("X-Leased-Namespace");
// Treat Task Worker as strict source of truth for load ONLY if it rejects us
if (resp.status().code() == 429 || resp.status().code() == 409) {
if (activeTasksStr != null) {
state.setInflightRequests(Integer.parseInt(activeTasksStr));
}
}

if (leasedNamespace != null) {
state.setLeasedNamespace(leasedNamespace);
}

state.setLastActivityTime(System.nanoTime());

if (activeTasksStr != null || leasedNamespace != null) {
LOG.info("shruzard - ProxyBackendHandler: Header Sync "
+ "PodState for {}. Local Occupancy: {}, Remote Tasks: {}, Namespace: {}",
targetWorkerAddress, state.getInflightRequests(), activeTasksStr,
state.getLeasedNamespace());
}
}
}
}

if (msg instanceof io.netty.handler.codec.http.LastHttpContent) {
decrementInflight();
}

// Forward worker responses directly back to the client
inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
ctx.channel().read();
} else {
future.channel().close();
}
});
}

@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) {
// Backend Worker channel is saturated; pause reading from App Fabric client
if (inboundChannel != null && inboundChannel.isActive()) {
inboundChannel.config().setAutoRead(ctx.channel().isWritable());
}
ctx.fireChannelWritabilityChanged();
}

@Override
public void channelInactive(ChannelHandlerContext ctx) {
decrementInflight();
ProxyFrontendHandler.closeOnFlush(inboundChannel);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
decrementInflight();
cause.printStackTrace();
ProxyFrontendHandler.closeOnFlush(ctx.channel());
}
}
Loading