ComputeGroupBindingUtil.java
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 org.apache.doris.resource.computegroup;
import org.apache.doris.analysis.ResourceTypeEnum;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Env;
import org.apache.doris.cloud.system.CloudSystemInfoService;
import org.apache.doris.common.Config;
import org.apache.doris.common.UserException;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import org.apache.commons.lang3.StringUtils;
/**
* Validation for the {@code compute_group} property that can be declared on background jobs
* (routine load / async materialized view).
*
* <p>This is a transitional binding: it only adds the ability to <b>declare</b> a compute group,
* it does not change how the group is resolved, checked at runtime, or how failures are handled.
* The property name and its value space are intentionally identical to the final
* {@code (owner, compute_group, workload_group)} design, so that metadata written by this version
* can be read as an explicit "pin" by later versions without any conversion.
*
* <p>Two values are rejected on purpose:
* <ul>
* <li>Any value in non-cloud mode - non-cloud support is not part of this transitional change,
* so no non-cloud metadata will ever carry this key.</li>
* <li>{@code DEFAULT} (case insensitive) - it is reserved by the final design to mean
* "follow the owner's default group at runtime". Allowing a job to pin a group literally
* named {@code DEFAULT} would silently change its behavior after upgrading.</li>
* </ul>
*/
public class ComputeGroupBindingUtil {
/**
* Reserved value in the final binding design: "not pinned, follow the owner's default group".
* Rejected here so that no job can pin a group literally named {@code DEFAULT}.
*/
public static final String RESERVED_DEFAULT = "DEFAULT";
public static final String ERR_NON_CLOUD =
"Property 'compute_group' is only supported in cloud mode for now.";
private ComputeGroupBindingUtil() {
}
/**
* Validates a user declared compute group name.
*
* <p>An empty value means "not declared" and is treated as a no-op by the caller, which must
* not write the key into the job's property map at all.
*
* @param ctx the context of the user executing CREATE / ALTER; privileges are checked against
* this user, matching how {@code workload_group} is validated today
* @param computeGroup the declared name
*/
public static void validateDeclaredComputeGroup(ConnectContext ctx, String computeGroup) throws UserException {
if (StringUtils.isEmpty(computeGroup)) {
return;
}
if (!Config.isCloudMode()) {
throw new UserException(ERR_NON_CLOUD);
}
if (RESERVED_DEFAULT.equalsIgnoreCase(computeGroup)) {
throw new UserException("'" + RESERVED_DEFAULT + "' is a reserved value for property 'compute_group'"
+ " and can not be used as a compute group name here.");
}
if (ctx == null) {
throw new UserException("Can not validate property 'compute_group' without a connect context.");
}
// Same two checks, and the same order, as `USE @<compute group>`.
if (!Env.getCurrentEnv().getAccessManager().checkCloudPriv(ctx.getCurrentUserIdentity(),
computeGroup, PrivPredicate.USAGE, ResourceTypeEnum.CLUSTER)) {
throw new UserException("USAGE denied to user '" + ctx.getQualifiedUser()
+ "' for compute group '" + computeGroup + "'");
}
if (!((CloudSystemInfoService) Env.getCurrentSystemInfo()).getCloudClusterNames().contains(computeGroup)) {
throw new UserException("Compute group '" + computeGroup + "' not found.");
}
}
/**
* Re-checks a job's binding before each of its tasks runs.
*
* <p>Creation-time validation alone is not enough: the groups can be dropped and the owner's
* privileges can be revoked while the job keeps running, and without this check the task would
* silently keep using a group its owner is no longer entitled to, or fail much later with an
* unrelated message such as "no available BE found".
*
* <p>Everything is checked against {@code owner}, the identity the task actually runs as, not
* against whoever created or last altered the job.
*
* @param owner the identity the task runs as
* @param computeGroup the compute group the task will run in, already resolved; empty means the
* job is not bound to a named compute group and there is nothing to check
* @param workloadGroup the workload group declared on the job, empty when none was declared
*/
public static void checkBindingBeforeTask(UserIdentity owner, String computeGroup, String workloadGroup)
throws UserException {
if (owner == null) {
// Jobs created before the owner was persisted; nothing to check them against.
return;
}
boolean computeGroupBound = Config.isCloudMode() && !StringUtils.isEmpty(computeGroup);
if (computeGroupBound) {
// Deliberately not ComputeGroupMgr.getComputeGroupByName() for the existence check: when
// the group is missing that builds a hint message from the thread-local ConnectContext,
// and the callers here are background threads that do not have one.
if (!((CloudSystemInfoService) Env.getCurrentSystemInfo()).getCloudClusterNames()
.contains(computeGroup)) {
throw new UserException("Compute group '" + computeGroup + "' not found.");
}
if (!Env.getCurrentEnv().getAccessManager().checkCloudPriv(owner, computeGroup,
PrivPredicate.USAGE, ResourceTypeEnum.CLUSTER)) {
throw new UserException("USAGE denied to user '" + owner.getQualifiedUser()
+ "' for compute group '" + computeGroup + "'");
}
}
if (!Config.enable_workload_group || StringUtils.isEmpty(workloadGroup)) {
return;
}
if (!Env.getCurrentEnv().getAccessManager()
.checkWorkloadGroupPriv(owner, workloadGroup, PrivPredicate.USAGE)) {
throw new UserException("USAGE denied to user '" + owner.getQualifiedUser()
+ "' for workload group '" + workloadGroup + "'");
}
// A workload group lives in the namespace of a compute group, so it can only be looked up
// once the compute group is known. Without one there is nothing to resolve it against.
// Safe to resolve here: existence was confirmed above, so the hint-message path that needs a
// ConnectContext cannot be reached.
if (computeGroupBound) {
// Throws when the workload group no longer exists under that compute group.
Env.getCurrentEnv().getComputeGroupMgr().getComputeGroupByName(computeGroup)
.getWorkloadGroup(workloadGroup, Env.getCurrentEnv().getWorkloadGroupMgr());
}
}
}