Code review comment: I am wondering whether it wouldn't be simpler to change `result.handleAsync` to `result.whenAsync` and then to add the result of this operation to the `incompleteFuturesTracker`? That way we are sure that we will have handled the result before doing any other operations (e.g. failing/completing checkpoints).

Code under review:
public CompletableFuture<Acknowledge> sendEvent(OperatorEvent evt) {
        if (!isReady()) {
            throw new FlinkRuntimeException("SubtaskGateway is not ready, task not yet running.");
        }

        final SerializedValue<OperatorEvent> serializedEvent;
        try {
            serializedEvent = new SerializedValue<>(evt);
        } catch (IOException e) {
            
            
            throw new FlinkRuntimeException("Cannot serialize operator event", e);
        }

        final Callable<CompletableFuture<Acknowledge>> sendAction =
                subtaskAccess.createEventSendAction(serializedEvent);

        final CompletableFuture<Acknowledge> result = new CompletableFuture<>();
        FutureUtils.assertNoException(
                result.handleAsync(
                        (success, failure) -> {
                            if (failure != null) {
                                if (subtaskAccess.isStillRunning()) {
                                    String msg =
                                            String.format(
                                                    EVENT_LOSS_ERROR_MESSAGE,
                                                    evt,
                                                    subtaskAccess.subtaskName());
                                    subtaskAccess.triggerTaskFailover(
                                            new FlinkException(msg, failure));
                                }

                                nonSuccessFuturesTrack.removeFailedFuture(result);
                            }
                            return null;
                        },
                        sendingExecutor));

        sendingExecutor.execute(
                () -> {
                    nonSuccessFuturesTrack.trackFuture(result);
                    sender.sendEvent(sendAction, result);
                });
        return result;
    }

Line commented on: nonSuccessFuturesTrack.removeFailedFuture(result);

Revised code:
public CompletableFuture<Acknowledge> sendEvent(OperatorEvent evt) {
        if (!isReady()) {
            throw new FlinkRuntimeException("SubtaskGateway is not ready, task not yet running.");
        }

        final SerializedValue<OperatorEvent> serializedEvent;
        try {
            serializedEvent = new SerializedValue<>(evt);
        } catch (IOException e) {
            
            
            throw new FlinkRuntimeException("Cannot serialize operator event", e);
        }

        final Callable<CompletableFuture<Acknowledge>> sendAction =
                subtaskAccess.createEventSendAction(serializedEvent);

        final CompletableFuture<Acknowledge> sendResult = new CompletableFuture<>();
        final CompletableFuture<Acknowledge> result =
                sendResult.whenCompleteAsync(
                        (success, failure) -> {
                            if (failure != null && subtaskAccess.isStillRunning()) {
                                String msg =
                                        String.format(
                                                EVENT_LOSS_ERROR_MESSAGE,
                                                evt,
                                                subtaskAccess.subtaskName());
                                Runnables.assertNoException(
                                        () ->
                                                subtaskAccess.triggerTaskFailover(
                                                        new FlinkException(msg, failure)));
                            }
                        },
                        sendingExecutor);

        sendingExecutor.execute(
                () -> {
                    sender.sendEvent(sendAction, sendResult);
                    incompleteFuturesTracker.trackFutureWhileIncomplete(result);
                });
        return result;
    }