package com.tesora.dve.variables; /* * #%L * Tesora Inc. * Database Virtualization Engine * %% * Copyright (C) 2011 - 2014 Tesora Inc. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.util.EnumSet; import java.util.Locale; import com.tesora.dve.common.catalog.CatalogDAO; import com.tesora.dve.common.catalog.VariableConfig; import com.tesora.dve.errmap.AvailableErrors; import com.tesora.dve.errmap.ErrorInfo; import com.tesora.dve.exceptions.PECodingException; import com.tesora.dve.exceptions.PEException; import com.tesora.dve.server.connectionmanager.SSConnection; import com.tesora.dve.sql.SchemaException; import com.tesora.dve.sql.schema.VariableScopeKind; import com.tesora.dve.sql.util.Functional; import com.tesora.dve.sql.util.UnaryFunction; import com.tesora.dve.sql.util.UnaryPredicate; /* * Every nonuser variable has an associated VariableConfig record in the catalog. * The value in the catalog means different things depending on the scopes. If the * variable has a global scope, then the catalog value is the persistent value, which * is used to initialize the variable upon startup. If the variable has no global scope, * then it is the default value of a session variable (also used to initialize the variable * upon startup, but not alterable during forward processing via set global.foo. * * The persistent value is modified via alter dve set .... * */ public class VariableHandler<Type> { public static final String DEFAULT_KEYWORD = "DEFAULT"; public static final String NULL_VALUE = "NULL"; // name of the variable private final String variableName; // information about the type, valid values, etc. private final ValueMetadata<Type> metadata; // valid scopes private final EnumSet<VariableScopeKind> scopes; // the compiled in default private final Type compiledDefaultOnMissing; // options private final EnumSet<VariableOption> options; // help private final String help; public VariableHandler(String name, ValueMetadata<Type> md, EnumSet<VariableScopeKind> applies, Type defaultOnMissing, EnumSet<VariableOption> options) { this(name,md,applies,defaultOnMissing,options,null); } public VariableHandler(String name, ValueMetadata<Type> md, EnumSet<VariableScopeKind> applies, Type defaultOnMissing, EnumSet<VariableOption> options, String help) { this.variableName = name.toLowerCase(Locale.ENGLISH); this.metadata = md; this.scopes = applies; this.compiledDefaultOnMissing = defaultOnMissing; this.options = options; String actualHelp = help; if (actualHelp == null && options.contains(VariableOption.EMULATED)) actualHelp = "emulated"; else if (actualHelp == null) actualHelp = ""; this.help = actualHelp; } public String getName() { return variableName; } public String toString() { return variableName; } public ValueMetadata<Type> getMetadata() { return metadata; } public EnumSet<VariableScopeKind> getScopes() { return scopes; } public EnumSet<VariableOption> getOptions() { return options; } public String getDescription() { return help; } public Type getDefaultOnMissing() { return compiledDefaultOnMissing; } public boolean isDVEOnly() { return !options.contains(VariableOption.EMULATED); } public boolean isEmulatedPassthrough() { return options.contains(VariableOption.EMULATED) && options.contains(VariableOption.PASSTHROUGH); } public static final UnaryPredicate<VariableHandler<?>> isGlobalPredicate = new UnaryPredicate<VariableHandler<?>>() { @Override public boolean test(VariableHandler<?> object) { return object.getScopes().contains(VariableScopeKind.GLOBAL); } }; public static final UnaryPredicate<VariableHandler<?>> isSessionPredicate = new UnaryPredicate<VariableHandler<?>>() { @Override public boolean test(VariableHandler<?> object) { return object.getScopes().contains(VariableScopeKind.SESSION); } }; // for global values the SSConn is not needed - need to extract an interface for that public Type getValue(VariableStoreSource source, VariableScopeKind vs) { if (!scopes.contains(vs)) throw new VariableException("Attempt to obtain unsupported scope " + vs.name() + " value from variable " + variableName); if (vs == VariableScopeKind.GLOBAL) { // global map. if the global map is the transient map, use that instead (for the tests) if (source == null || source.getGlobalVariableStore().isServer()) return ServerGlobalVariableStore.INSTANCE.getValue(this); else return source.getGlobalVariableStore().getValue(this); } else { // session map return source.getSessionVariableStore().getValue(this); } } // convenience - if a variable only has one scope kind - get the value public Type getValue(VariableStoreSource source) { if (scopes.size() > 1) throw new VariableException("No variable scope specified for variable " + getName() + " which supports " + scopes.toString()); return getValue(source,scopes.iterator().next()); } // maybe a little easier than saying session or global public Type getSessionValue(VariableStoreSource source) { return getValue(source,VariableScopeKind.SESSION); } public Type getGlobalValue(VariableStoreSource source) { return getValue(source,VariableScopeKind.GLOBAL); } public void setValue(VariableStoreSource conn, VariableScopeKind scope, String newValue) throws PEException { if (scope == VariableScopeKind.SESSION) setSessionValue(conn,newValue); else if (scope == VariableScopeKind.GLOBAL) setGlobalValue(conn,newValue); else throw new PEException("Unknown scope for set: " + scope); } public void setSessionValue(VariableStoreSource conn, String newValue) throws PEException { if (options.contains(VariableOption.READONLY)) throw new PEException(String.format("Variable '%s' not settable as session variable",getName())); if (!scopes.contains(VariableScopeKind.SESSION)) throw new PECodingException("Attempt to set non existent session variable " + variableName); AbstractVariableStore sessionValues = conn.getSessionVariableStore(); Type t = toInternal(newValue); sessionValues.setValue(this, t); onSessionValueChange(conn,t); } public void setGlobalValue(VariableStoreSource conn, String newValue) throws PEException { if (options.contains(VariableOption.READONLY)) throw new PEException(String.format("Unable to set readonly variable '%s'",getName())); if (!scopes.contains(VariableScopeKind.GLOBAL)) throw new PECodingException("Attempt to set non existent global variable " + variableName); Type t = toInternal(newValue); // push it down first, in case there's a problem pushdownGlobalValue(conn,t); // the global variable store propagates the change message ServerGlobalVariableStore.INSTANCE.setValue(this, t); } public void pushdownGlobalValue(VariableStoreSource conn, Type t) throws PEException { // after broadcasting, set the global value if (conn instanceof SSConnection) { SSConnection ssConnection = (SSConnection) conn; String anything = getGlobalAssignmentClause(t); if (anything != null) ssConnection.updateGlobalVariableState(String.format("set %s",anything)); } } public void setPersistentValue(VariableStoreSource conn, final String newValue) throws PEException { // validate Type t = toInternal(newValue); if (conn instanceof SSConnection) { CatalogDAO c = ((SSConnection)conn).getCatalogDAO(); persistValue(c,newValue); } // broadcast; note that we skip access checking here, because persistent set always updates // the global map. (i.e. setting the pers value for a session variable should effect new sesssions) ServerGlobalVariableStore.INSTANCE.setValue(this, t); pushdownGlobalValue(conn,t); } public void persistValue(final CatalogDAO c, final String newValue) throws PEException { try { c.begin(); VariableConfig vc = c.findVariableConfig(getName()); vc.setValue(newValue); c.commit(); } catch (Throwable e) { throw new PEException("Cannot set variable " + getName(), e); } } // this method is used to update dve state on the various servers // any underlying mysql state would have been handled by the originating dve server public void onGlobalValueChange(Type newValue) throws PEException { // nothing by default } public void onSessionValueChange(VariableStoreSource conn, Type newValue) throws PEException { if (isEmulatedPassthrough()) { if (conn instanceof SSConnection) { SSConnection ssCon = (SSConnection) conn; ssCon.updateWorkerState(); } } } public String getSessionAssignmentClause(String value) { if (scopes.contains(VariableScopeKind.SESSION) && isEmulatedPassthrough()) return String.format("%s=%s",getName(),value); return null; } public String getGlobalAssignmentClause(Type value) { if (scopes.contains(VariableScopeKind.GLOBAL) && isEmulatedPassthrough()) return String.format("@@global.%s = %s",getName(),toExternal(value)); return null; } public VariableConfig lookupPersistentConfig(CatalogDAO c) throws PEException { return c.findVariableConfig(getName(), true); } public VariableConfig buildNewConfig() { // String name, String valueType, String value, String scopes, boolean emulated, String helpText) { return new VariableConfig(getName(), getMetadata().getTypeName(), toRow(getDefaultOnMissing()), convertScopes(getScopes()), convertOptions(getOptions()), getDescription()); } public static String convertScopes(EnumSet<VariableScopeKind> scopes) { return Functional.join(scopes, ",", new UnaryFunction<String,VariableScopeKind>() { @Override public String evaluate(VariableScopeKind object) { return object.name(); } }); } public static EnumSet<VariableScopeKind> convertScopes(String in) { String[] bits = in.split(","); EnumSet<VariableScopeKind> out = EnumSet.noneOf(VariableScopeKind.class); for(String s : bits) { out.add(VariableScopeKind.valueOf(s.trim())); } return out; } public static String convertOptions(EnumSet<VariableOption> options) { return Functional.join(options, ",", new UnaryFunction<String,VariableOption>() { @Override public String evaluate(VariableOption object) { return object.name(); } }); } public static EnumSet<VariableOption> convertOptions(String in) { String[] bits = in.split(","); EnumSet<VariableOption> out = EnumSet.noneOf(VariableOption.class); for(String s : bits) out.add(VariableOption.valueOf(s.trim())); return out; } // for catalog helper public VariableConfig initialiseCatalog(CatalogDAO c) throws PEException { VariableConfig vc = buildNewConfig(); c.persistToCatalog(vc); return vc; } public Type initialise(CatalogDAO c) throws PEException { VariableConfig conf = c.findVariableConfig(getName(),false); Type out = null; if (conf != null) { out = toInternal(conf.getValue()); if (scopes.contains(VariableScopeKind.GLOBAL)) { // check with the global version too. we're going to go directly to the global variable store ValueReference<Type> existing = ServerGlobalVariableStore.INSTANCE.getReference(this); if (existing != null) { out = existing.get(); } } } else { conf = buildNewConfig(); out = getDefaultOnMissing(); try { c.begin(); c.persistToCatalog(conf); c.commit(); } catch (Throwable t) { c.rollback(t); throw new PEException("Unable to initialise catalog for variable '" + getName() + "'"); } } return out; } public String toExternal(Type in) { return getMetadata().convertToExternal(in); } public String toRow(Type in) { if (in == null) return NULL_VALUE; return getMetadata().toRow(in); } public Type toInternal(String in) throws PEException { if (DEFAULT_KEYWORD.equalsIgnoreCase(in)) { return this.getDefaultOnMissing(); } else if ((in == null) || NULL_VALUE.equals(in)) { if (options.contains(VariableOption.NULLABLE)) { return null; } throw new SchemaException(new ErrorInfo(AvailableErrors.WRONG_VALUE_FOR_VARIABLE, this.getName(), "NULL")); } return getMetadata().convertToInternal(getName(),in); } // the map does not support nulls public String toMap(Type in) { if (in == null) return NULL_VALUE; return toExternal(in); } }