github-actions[bot] commented on code in PR #66913:
URL: https://github.com/apache/doris/pull/66913#discussion_r3838204870
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -692,52 +692,86 @@ public void startSplit(int numBackends) throws
UserException {
}
public void doStartSplit() throws UserException {
- TableScan scan = createTableScan();
- CompletableFuture.runAsync(() -> {
- AtomicReference<CloseableIterable<FileScanTask>> taskRef = new
AtomicReference<>();
- try {
- preExecutionAuthenticator.execute(
- () -> {
- long startTime = System.currentTimeMillis();
- try {
- CloseableIterable<FileScanTask> fileScanTasks
= planFileScanTask(scan);
- taskRef.set(fileScanTasks);
- CloseableIterator<FileScanTask> iterator =
fileScanTasks.iterator();
- while (splitAssignment.needMoreSplit() &&
iterator.hasNext()) {
- try {
- splitAssignment.addToQueue(
-
Lists.newArrayList(createIcebergSplit(iterator.next())));
- } catch (UserException e) {
- throw new RuntimeException(e);
+ IcebergTableCacheValue.Lease planningLease =
retainPlanningGeneration();
+ TableScan scan;
+ try {
+ scan = createTableScan();
+ } catch (UserException | RuntimeException | Error t) {
+ planningLease.close();
+ throw t;
+ }
+ Future<?> planningFuture;
+ try {
+ planningFuture =
Env.getCurrentEnv().getExtMetaCacheMgr().getScheduleExecutor().submit(() -> {
+ AtomicReference<CloseableIterable<FileScanTask>> taskRef = new
AtomicReference<>();
+ try {
+ preExecutionAuthenticator.execute(
+ () -> {
+ long startTime = System.currentTimeMillis();
+ try {
+ CloseableIterable<FileScanTask>
fileScanTasks = planFileScanTask(scan);
+ taskRef.set(fileScanTasks);
+ CloseableIterator<FileScanTask> iterator =
fileScanTasks.iterator();
+ while (splitAssignment.needMoreSplit() &&
iterator.hasNext()) {
+ try {
+ splitAssignment.addToQueue(
+
Lists.newArrayList(createIcebergSplit(iterator.next())));
+ } catch (UserException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ } finally {
+ if (getSummaryProfile() != null) {
+
getSummaryProfile().addExternalTableGetFileScanTasksTime(
+ System.currentTimeMillis() -
startTime);
}
- }
- } finally {
- if (getSummaryProfile() != null) {
-
getSummaryProfile().addExternalTableGetFileScanTasksTime(
- System.currentTimeMillis() -
startTime);
}
}
+ );
+ splitAssignment.finishSchedule();
+ recordManifestCacheProfile();
+ } catch (Exception e) {
+ Optional<NotSupportedException> opt =
checkNotSupportedException(e);
+ if (opt.isPresent()) {
+ splitAssignment.setException(new
UserException(opt.get().getMessage(), opt.get()));
+ } else {
+ splitAssignment.setException(new
UserException(e.getMessage(), e));
+ }
+ } finally {
+ if (taskRef.get() != null) {
+ try {
+ taskRef.get().close();
+ } catch (IOException e) {
+ // ignore
}
- );
- splitAssignment.finishSchedule();
- recordManifestCacheProfile();
- } catch (Exception e) {
- Optional<NotSupportedException> opt =
checkNotSupportedException(e);
- if (opt.isPresent()) {
- splitAssignment.setException(new
UserException(opt.get().getMessage(), opt.get()));
- } else {
- splitAssignment.setException(new
UserException(e.getMessage(), e));
- }
- } finally {
- if (taskRef.get() != null) {
- try {
- taskRef.get().close();
- } catch (IOException e) {
- // ignore
}
+ planningLease.close();
}
- }
- }, Env.getCurrentEnv().getExtMetaCacheMgr().getScheduleExecutor());
+ });
+ } catch (RuntimeException | Error t) {
+ planningLease.close();
+ throw t;
+ }
+ // Cancellation interrupts the worker, while the lease remains owned
by its actual-terminal finally.
+ splitAssignment.addCloseable(() -> planningFuture.cancel(true));
Review Comment:
[P1] Register this cancellation handle atomically with stop
`SplitAssignment.addCloseable()` mutates a plain `ArrayList`, while `stop()`
can concurrently traverse that same list with fail-fast `forEach`. If teardown
races this registration, the add can make `stop()` throw
`ConcurrentModificationException` before it notifies waiters and before
`FileQueryScanNode.stop()` removes the split sources. The following `isStop()`
check only repairs a clean missed-registration race; it cannot finish an
already-aborted stop. Please provide a synchronized register-or-close operation
shared with `stop()`, and cover registration versus stop with a barrier test.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/MetaCacheEntry.java:
##########
@@ -107,7 +127,13 @@ public MetaCacheEntry(String name, @Nullable Function<K,
V> loader, CacheSpec ca
maxSize,
true,
null);
- this.loadingData =
cacheFactory.buildCache(this::loadFromDefaultLoader, refreshExecutor);
+ if (retirementListener != null) {
+ this.loadingData = cacheFactory.buildCacheWithAsyncRemovalListener(
Review Comment:
[P1] Make admitted-value retirement non-droppable
This delegates retirement of admitted Hudi/Iceberg values to Caffeine on the
bounded shared refresh executor. That executor's rejection policy throws after
saturation/timeout or shutdown, so a rejected removal-listener dispatch never
reaches `retirementListener`; the evicted filesystem view or
table/FileIO/catalog generation then remains pinned. The inline fallback below
protects only suppressed values that never stayed in Caffeine. Please give
admitted eviction/invalidation/refresh replacement the same non-droppable
cleanup guarantee, and test an admitted resource removal with a rejecting
listener executor.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -502,6 +506,12 @@ public boolean isCached() {
// query with a random sql
public void execute() throws Exception {
+ synchronized (executionAdmissionLock) {
+ if (pendingCancelReason != null) {
Review Comment:
[P1] Apply this admission guard to the proxy entry point
Ordinary forwarded execution never calls this overload:
`ConnectProcessor.proxyExecute()` invokes `executor.queryRetry(queryId)`
directly. A pre-registration cancel is therefore replayed into
`pendingCancelReason`, but the master still enters parsing/planning because
neither `queryRetry(TUniqueId)` nor `execute(TUniqueId)` checks it. FE-local
work can complete after cancellation, and external planning can block before
any coordinator exists to signal. Centralize this check in the shared query-ID
entry path and add a proxy-execution barrier test; the new test covers only
zero-argument `execute()`.
##########
fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSqlConnectPoolMgr.java:
##########
@@ -56,6 +58,12 @@ public int registerConnection(ConnectContext ctx) {
@Override
public void unregisterConnection(ConnectContext ctx) {
+ // Reject new publications first, then signal the active query before
waiting for an admitted
+ // GetFlightInfo publisher. Waiting before cancellation can deadlock
KILL CONNECTION behind the
+ // publisher whose query must be canceled in order to leave
publication.
+ ctx.sealFlightSqlDeferredExecutors();
+ ctx.cancelQuery(new Status(TStatusCode.CANCELLED, "arrow flight
connection closed"));
Review Comment:
[P1] Keep teardown running when cancellation fails
For a forwarded query, this call can throw: `StmtExecutor.cancel()` wraps an
RPC or journal-wait failure from `MasterOpExecutor.cancel()` in
`RuntimeException`. That aborts this method before it drains deferred
executors, closes the channel/transaction, and removes the connection and token
mappings, so a transient leader/RPC failure can strand the entire Flight
session. Please make the teardown legs failure-atomic (for example, nested
`finally` cleanup with aggregated reporting), and add a test where
`cancelQuery()` throws but every later cleanup still runs.
##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java:
##########
@@ -556,19 +557,141 @@ public void alterJob(AlterJobCommand alterJobCommand)
throws AnalysisException,
@Override
public void updateJobStatus(JobStatus status) throws JobException {
+ AbstractStreamingTask taskToCancel = null;
+ boolean waitForTask = JobStatus.PAUSED.equals(status);
lock.writeLock().lock();
try {
+ if ((JobStatus.PAUSED.equals(status) ||
JobStatus.STOPPED.equals(status))
+ && status != getJobStatus()) {
+ taskToCancel = runningStreamTask;
+ }
+ JobStatus previousStatus = getJobStatus();
super.updateJobStatus(status);
- if (JobStatus.PAUSED.equals(getJobStatus())) {
- clearRunningStreamTask(status);
+ if (previousStatus != getJobStatus()) {
+ statusEpoch++;
}
if (isFinalStatus()) {
Env.getCurrentGlobalTransactionMgr().getCallbackFactory().removeCallback(getJobId());
}
log.info("Streaming insert job {} update status to {}",
getJobId(), getJobStatus());
+ } catch (RuntimeException | JobException e) {
+ if (taskToCancel != null) {
+ runningStreamTask = taskToCancel;
+ }
+ throw e;
+ } finally {
+ lock.writeLock().unlock();
+ }
+ if (taskToCancel != null && waitForTask) {
+ // The task owner can need this job's write lock while finishing
transaction callbacks.
+ // Cancel and wait only after publishing the status and releasing
the job lock.
+ taskToCancel.cancel(waitForTask);
+ }
+ }
+
+ /** Applies a validated manual transition while reason, status, and
cancellation share one job lock. */
+ public void updateManualJobStatus(JobStatus status, FailureReason reason)
throws JobException {
+ AbstractStreamingTask taskToWait = null;
+ AbstractStreamingTask taskToRelease = null;
+ JobStatus publishedStatus = JobStatus.RUNNING.equals(status) ?
JobStatus.PENDING : status;
+ lock.writeLock().lock();
+ try {
+ validateManualStatusTransition(status);
+ resetFailureInfo(reason);
+ if (JobStatus.PAUSED.equals(status) && runningStreamTask != null) {
+ needRebuildReader = true;
+ taskToRelease = runningStreamTask;
+ }
+ if (JobStatus.PAUSED.equals(status) ||
JobStatus.STOPPED.equals(status)) {
+ taskToWait = runningStreamTask;
+ if (taskToWait != null) {
+ // Linearize manual termination with callbacks before
publishing the new job status.
+ // Task-specific RPCs and waits remain outside the job
lock.
+ taskToWait.publishCancellation();
+ }
+ }
+ // RESUME must re-enter PENDING so the scheduler creates a
successor before publishing RUNNING.
+ super.updateJobStatus(publishedStatus);
+ statusEpoch++;
+ if (isFinalStatus()) {
+
Env.getCurrentGlobalTransactionMgr().getCallbackFactory().removeCallback(getJobId());
+ }
+ log.info("Streaming insert job {} manually updated status to {}",
getJobId(), getJobStatus());
} finally {
lock.writeLock().unlock();
}
+ boolean readerReleased = true;
+ if (taskToRelease != null) {
+ readerReleased = !(taskToRelease instanceof StreamingMultiTblTask)
+ || ((StreamingMultiTblTask)
taskToRelease).releaseRemoteReaderAndWait();
+ if (!(taskToRelease instanceof StreamingMultiTblTask)) {
+ taskToRelease.releaseRemoteReader();
+ }
+ }
+ if (taskToWait != null) {
+ taskToWait.cancel(true);
+ if (readerReleased && taskToWait.canHandoffAfterCancellation()) {
Review Comment:
[P1] Preserve the snapshot runtime owner for terminal cleanup
This clears the task after successful PAUSE handoff or an execution-terminal
STOP, but snapshot rounds may select an arbitrary runtime BE and DROP later
learns that exact `/api/close` destination only from `runningStreamTask`.
PAUSE's `/api/releaseReader` deliberately keeps the CDC slot, and STOP can
clear without either release or close; after this clear, `cleanMeta()` falls
back to the bound/random BE and can close the wrong host, leaving the live slot
behind. Retain a terminal cleanup owner until DROP consumes it or close through
the captured runtime BE, and test PAUSE/STOP-then-DROP with different runtime
and bound BEs.
##########
fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingMultiTblTask.java:
##########
@@ -390,6 +402,39 @@ public void releaseRemoteReader() {
}
}
+ /** Wait for the BE to acknowledge reader release before allowing a
successor to reuse the source. */
+ boolean releaseRemoteReaderAndWait() {
+ if (runningBackendId <= 0) {
Review Comment:
[P1] Linearize reader admission with PAUSE
This returns success when the worker has not published `runningBackendId`
yet, but that worker checks cancellation only before `resolveBackend()`; it can
publish the backend and send `/api/writeRecords` immediately after PAUSE
observes this branch. `cancel(true)` does not wait for this task, and the
manually paused job has no scheduler pass to retry predecessor release, so
PAUSE can complete while a new CDC reader starts and remains owned
indefinitely. Please serialize backend/reader admission with cancellation
(including a post-resolution cancel check) and add a barrier test for PAUSE
between the run check and backend publication.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/ConnectProcessor.java:
##########
@@ -773,8 +777,17 @@ public TMasterOpResult proxyExecute(TMasterOpRequest
request) throws TException
LOG.warn("Process one query failed because unknown reason: ", e);
ctx.getState().setError(ErrorCode.ERR_UNKNOWN_ERROR, "Unexpected
exception: " + e.getMessage());
}
- // no matter the master execute success or fail, the master must
transfer the result to follower
- // and tell the follower the current journalID.
+ try {
+ return buildProxyResult(request, executor);
+ } finally {
+ if (ctx.getStatementContext() != null) {
+ ctx.getStatementContext().close();
Review Comment:
[P1] Do not let resource-close failure skip forwarded cleanup
`StatementContext.close()` can rethrow a resource's close failure from this
`finally`. That makes `proxyExecute()` exit before
`FrontendServiceImpl.forward()` reaches `ConnectContext.remove()` and the
proxy-query map callback, leaving the finished context registered for
cancellation and installed on the reusable Thrift worker. Please put
thread-local and identity-checked proxy-map removal in an outer `finally` while
preserving the close error, and add a forwarded execution test with a throwing
statement resource.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java:
##########
@@ -784,6 +785,9 @@ private boolean shouldQueue() {
// A call to Exec() must precede all other member function calls.
@Override
public void exec() throws Exception {
+ if (isQueryCancelled()) {
Review Comment:
[P1] Close the queue-token publication race with cancel
This check and the later `shouldQueue()` check can both pass, then
cancellation can run before `queueToken` is assigned. `cancel()` sees no token
and only marks the coordinator canceled; this thread subsequently publishes a
waiting token and blocks in `QueueToken.get()`, whose future does not observe
coordinator status, so the canceled query stays pinned until queue timeout.
Nereids has the same gap before `setQueueInfo()`. Linearize token publication
with cancellation or recheck-and-cancel immediately after publication, with
classic and Nereids barrier tests.
##########
fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java:
##########
@@ -1214,6 +1227,10 @@ public TMasterOpResult forward(TMasterOpRequest params)
throws TException {
Runnable clearCallback = () -> {};
if (params.isSetQueryId()) {
proxyQueryIdToConnCtx.put(params.getQueryId(), context);
+ if (pendingProxyQueryCancels.remove(params.getQueryId()) != null) {
Review Comment:
[P1] Preserve cancellation across forwarded RPC retries
`FEOpExecutor.forward()` can retry the same request/query ID after a
transport failure, while the original attempt may also have reached this FE.
This one-shot `remove` lets the first arrival consume the pre-registration
cancel; a delayed/retried second arrival then registers and executes
uncanceled. Overlapping attempts also overwrite the single map value, and
either key-only cleanup can remove the other's publication. Please keep
cancellation durable for the whole retry-arrival window and make
registration/removal identity-safe, with a same-query-ID overlapping-attempt
test.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]