blob: 4bc821a03baa7fb078f52d3f2d2519e10f70275e [file] [log] [blame]
// 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.qe;
import org.apache.doris.analysis.SetType;
import org.apache.doris.analysis.SetVar;
import org.apache.doris.analysis.StringLiteral;
import org.apache.doris.analysis.SysVariableDesc;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.PatternMatcher;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.persist.EditLog;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Lists;
import org.apache.commons.lang.SerializationUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
// Variable manager, merge session variable and global variable
public class VariableMgr {
private static final Logger LOG = LogManager.getLogger(VariableMgr.class);
// variable have this flag means that every session have a copy of this variable,
// and can modify its own variable.
public static final int SESSION = 1;
// Variables with this flag have only one instance in one process.
public static final int GLOBAL = 2;
// Variables with this flag only exist in each session.
public static final int SESSION_ONLY = 4;
// Variables with this flag can only be read.
public static final int READ_ONLY = 8;
// Variables with this flag can not be seen with `SHOW VARIABLES` statement.
public static final int INVISIBLE = 16;
// Map variable name to variable context which have enough information to change variable value.
private static ImmutableMap<String, VarContext> ctxByVarName;
// global session variable
private static SessionVariable globalSessionVariable;
// Global read/write lock to protect access global variable.
private static final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private static final Lock rlock = rwlock.readLock();
private static final Lock wlock = rwlock.writeLock();
// Form map from variable name to its field in Java class.
static {
// Session value
globalSessionVariable = new SessionVariable();
ImmutableSortedMap.Builder<String, VarContext> builder =
ImmutableSortedMap.orderedBy(String.CASE_INSENSITIVE_ORDER);
for (Field field : SessionVariable.class.getDeclaredFields()) {
VarAttr attr = field.getAnnotation(VarAttr.class);
if (attr == null) {
continue;
}
field.setAccessible(true);
builder.put(attr.name(),
new VarContext(field, globalSessionVariable, SESSION | attr.flag(),
getValue(globalSessionVariable, field)));
}
// Variables only exist in global environment.
for (Field field : GlobalVariable.class.getDeclaredFields()) {
VarAttr attr = field.getAnnotation(VarAttr.class);
if (attr == null) {
continue;
}
if ((field.getModifiers() & Modifier.STATIC) == 0) {
LOG.warn("Field in GlobalVariable with VarAttr annotation must have static modifier.");
continue;
}
field.setAccessible(true);
builder.put(attr.name(),
new VarContext(field, null, GLOBAL | attr.flag(), getValue(null, field)));
}
ctxByVarName = builder.build();
}
public static Lock readLock() {
return rlock;
}
public static Lock writeLock() {
return wlock;
}
public static SessionVariable getGlobalSessionVariable() {
return globalSessionVariable;
}
// Set value to a variable
private static boolean setValue(Object obj, Field field, String value) throws DdlException {
VarAttr attr = field.getAnnotation(VarAttr.class);
String convertedVal = VariableVarConverters.convert(attr.name(), value);
try {
switch (field.getType().getSimpleName()) {
case "boolean":
if (convertedVal.equalsIgnoreCase("ON")
|| convertedVal.equalsIgnoreCase("TRUE")
|| convertedVal.equalsIgnoreCase("1")) {
field.setBoolean(obj, true);
} else if (convertedVal.equalsIgnoreCase("OFF")
|| convertedVal.equalsIgnoreCase("FALSE")
|| convertedVal.equalsIgnoreCase("0")) {
field.setBoolean(obj, false);
} else {
throw new IllegalAccessException();
}
break;
case "byte":
field.setByte(obj, Byte.valueOf(convertedVal));
break;
case "short":
field.setShort(obj, Short.valueOf(convertedVal));
break;
case "int":
field.setInt(obj, Integer.valueOf(convertedVal));
break;
case "long":
field.setLong(obj, Long.valueOf(convertedVal));
break;
case "float":
field.setFloat(obj, Float.valueOf(convertedVal));
break;
case "double":
field.setDouble(obj, Double.valueOf(convertedVal));
break;
case "String":
field.set(obj, convertedVal);
break;
default:
// Unsupported type variable.
ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_TYPE_FOR_VAR, attr.name());
}
} catch (NumberFormatException e) {
ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_TYPE_FOR_VAR, attr.name());
} catch (IllegalAccessException e) {
ErrorReport.reportDdlException(ErrorCode.ERR_WRONG_VALUE_FOR_VAR, attr.name(), value);
}
return true;
}
public static SessionVariable newSessionVariable() {
wlock.lock();
try {
return (SessionVariable) SerializationUtils.clone(globalSessionVariable);
} finally {
wlock.unlock();
}
}
// Check if this setVar can
private static void checkUpdate(SetVar setVar, int flag) throws DdlException {
if ((flag & READ_ONLY) != 0) {
ErrorReport.reportDdlException(ErrorCode.ERR_VARIABLE_IS_READONLY, setVar.getVariable());
}
if (setVar.getType() == SetType.GLOBAL && (flag & SESSION_ONLY) != 0) {
ErrorReport.reportDdlException(ErrorCode.ERR_LOCAL_VARIABLE, setVar.getVariable());
}
if (setVar.getType() != SetType.GLOBAL && (flag & GLOBAL) != 0) {
ErrorReport.reportDdlException(ErrorCode.ERR_GLOBAL_VARIABLE, setVar.getVariable());
}
}
// Get from show name to field
public static void setVar(SessionVariable sessionVariable, SetVar setVar) throws DdlException {
VarContext ctx = ctxByVarName.get(setVar.getVariable());
if (ctx == null) {
ErrorReport.reportDdlException(ErrorCode.ERR_UNKNOWN_SYSTEM_VARIABLE, setVar.getVariable());
}
// Check variable attribute and setVar
checkUpdate(setVar, ctx.getFlag());
// Check variable time_zone value is valid
if (setVar.getVariable().toLowerCase().equals("time_zone")) {
setVar = new SetVar(
setVar.getType(), setVar.getVariable(),
new StringLiteral(TimeUtils.checkTimeZoneValidAndStandardize(setVar.getValue().getStringValue())));
}
// To modify to default value.
VarAttr attr = ctx.getField().getAnnotation(VarAttr.class);
String value;
// If value is null, this is `set variable = DEFAULT`
if (setVar.getValue() != null) {
value = setVar.getValue().getStringValue();
} else {
value = ctx.getDefaultValue();
if (value == null) {
ErrorReport.reportDdlException(ErrorCode.ERR_NO_DEFAULT, attr.name());
}
}
if (setVar.getType() == SetType.GLOBAL) {
wlock.lock();
try {
setValue(ctx.getObj(), ctx.getField(), value);
} finally {
wlock.unlock();
}
writeGlobalVariableUpdate(globalSessionVariable, "update global variables");
} else {
// set global variable should not affect variables of current session.
// global variable will only make effect when connecting in.
setValue(sessionVariable, ctx.getField(), value);
}
}
// global variable persistence
public static void write(DataOutputStream out) throws IOException {
globalSessionVariable.write(out);
}
public static void read(DataInputStream in) throws IOException, DdlException {
wlock.lock();
try {
globalSessionVariable.readFields(in);
} finally {
wlock.unlock();
}
}
private static void writeGlobalVariableUpdate(SessionVariable variable, String msg) {
EditLog editLog = Catalog.getInstance().getEditLog();
editLog.logGlobalVariable(variable);
}
public static void replayGlobalVariable(SessionVariable variable) throws IOException, DdlException {
wlock.lock();
try {
for (Field field : SessionVariable.class.getDeclaredFields()) {
VarAttr attr = field.getAnnotation(VarAttr.class);
if (attr == null) {
continue;
}
field.setAccessible(true);
VarContext ctx = ctxByVarName.get(attr.name());
if (ctx.getFlag() == SESSION) {
String value = getValue(variable, ctx.getField());
setValue(ctx.getObj(), ctx.getField(), value);
}
}
} finally {
wlock.unlock();
}
}
// Get variable value through variable name, used to satisfy statement like `SELECT @@comment_version`
public static void fillValue(SessionVariable var, SysVariableDesc desc) throws AnalysisException {
VarContext ctx = ctxByVarName.get(desc.getName());
if (ctx == null) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_UNKNOWN_SYSTEM_VARIABLE, desc.getName());
}
if (desc.getSetType() == SetType.GLOBAL) {
rlock.lock();
try {
fillValue(ctx.getObj(), ctx.getField(), desc);
} finally {
rlock.unlock();
}
} else {
fillValue(var, ctx.getField(), desc);
}
}
private static void fillValue(Object obj, Field field, SysVariableDesc desc) {
try {
switch (field.getType().getSimpleName()) {
case "boolean":
desc.setType(Type.BOOLEAN);
desc.setBoolValue(field.getBoolean(obj));
break;
case "byte":
desc.setType(Type.TINYINT);
desc.setIntValue(field.getByte(obj));
break;
case "short":
desc.setType(Type.SMALLINT);
desc.setIntValue(field.getShort(obj));
break;
case "int":
desc.setType(Type.INT);
desc.setIntValue(field.getInt(obj));
break;
case "long":
desc.setType(Type.BIGINT);
desc.setIntValue(field.getLong(obj));
break;
case "float":
desc.setType(Type.FLOAT);
desc.setFloatValue(field.getFloat(obj));
break;
case "double":
desc.setType(Type.DOUBLE);
desc.setFloatValue(field.getDouble(obj));
break;
case "String":
desc.setType(Type.VARCHAR);
desc.setStringValue((String) field.get(obj));
break;
default:
desc.setType(Type.VARCHAR);
desc.setStringValue("");
break;
}
} catch (IllegalAccessException e) {
LOG.warn("Access failed.", e);
}
}
// Get variable value through variable name, used to satisfy statement like `SELECT @@comment_version`
public static String getValue(SessionVariable var, SysVariableDesc desc) throws AnalysisException {
VarContext ctx = ctxByVarName.get(desc.getName());
if (ctx == null) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_UNKNOWN_SYSTEM_VARIABLE, desc.getName());
}
if (desc.getSetType() == SetType.GLOBAL) {
rlock.lock();
try {
return getValue(ctx.getObj(), ctx.getField());
} finally {
rlock.unlock();
}
} else {
return getValue(var, ctx.getField());
}
}
private static String getValue(Object obj, Field field) {
try {
switch (field.getType().getSimpleName()) {
case "boolean":
return Boolean.toString(field.getBoolean(obj));
case "byte":
return Byte.toString(field.getByte(obj));
case "short":
return Short.toString(field.getShort(obj));
case "int":
return Integer.toString(field.getInt(obj));
case "long":
return Long.toString(field.getLong(obj));
case "float":
return Float.toString(field.getFloat(obj));
case "double":
return Double.toString(field.getDouble(obj));
case "String":
return (String) field.get(obj);
default:
return "";
}
} catch (IllegalAccessException e) {
LOG.warn("Access failed.", e);
}
return "";
}
// Dump all fields
public static List<List<String>> dump(SetType type, SessionVariable sessionVar, PatternMatcher matcher) {
List<List<String>> rows = Lists.newArrayList();
// Hold the read lock when session dump, because this option need to access global variable.
rlock.lock();
try {
for (Map.Entry<String, VarContext> entry : ctxByVarName.entrySet()) {
// Filter variable not match to the regex.
if (matcher != null && !matcher.match(entry.getKey())) {
continue;
}
VarContext ctx = entry.getValue();
List<String> row = Lists.newArrayList();
row.add(entry.getKey());
if (type != SetType.GLOBAL && ctx.getObj() == globalSessionVariable) {
// In this condition, we may retrieve session variables for caller.
row.add(getValue(sessionVar, ctx.getField()));
} else {
row.add(getValue(ctx.getObj(), ctx.getField()));
}
if (row.size() > 1 && row.get(0).equalsIgnoreCase(SessionVariable.SQL_MODE)) {
try {
row.set(1, SqlModeHelper.decode(Long.valueOf(row.get(1))));
} catch (DdlException e) {
row.set(1, "");
LOG.warn("Decode sql mode failed");
}
}
rows.add(row);
}
} finally {
rlock.unlock();
}
// Sort all variables by variable name.
Collections.sort(rows, new Comparator<List<String>>() {
@Override
public int compare(List<String> o1, List<String> o2) {
return o1.get(0).compareTo(o2.get(0));
}
});
return rows;
}
@Retention(RetentionPolicy.RUNTIME)
public static @interface VarAttr {
// Name in show variables and set statement;
String name();
int flag() default 0;
// TODO(zhaochun): min and max is not used.
String minValue() default "0";
String maxValue() default "0";
}
private static class VarContext {
private Field field;
private Object obj;
private int flag;
private String defaultValue;
public VarContext(Field field, Object obj, int flag, String defaultValue) {
this.field = field;
this.obj = obj;
this.flag = flag;
this.defaultValue = defaultValue;
}
public Field getField() {
return field;
}
public Object getObj() {
return obj;
}
public int getFlag() {
return flag;
}
public String getDefaultValue() {
return defaultValue;
}
}
}