diff --git a/CHANGES.txt b/CHANGES.txt index 062811b158..9107756a87 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,7 @@ * Fix BulkLoader ignoring cipher suites options (CASSANDRA-18582) * Migrate Python optparse to argparse (CASSANDRA-17914) Merged from 3.11: + * Fix NPE when using udfContext in UDF after a restart of a node (CASSANDRA-18739) * Moved jflex from runtime to build dependencies (CASSANDRA-18664) Merged from 3.0: * Backport of CASSANDRA-16905 Further restrict schema column drop/recreate conversions (CASSANDRA-18760) diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java b/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java index d4bdf203a2..134dea570a 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDFContextImpl.java @@ -22,12 +22,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.schema.CQLTypeParser; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.cql3.functions.types.*; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.utils.ByteBufferUtil; /** @@ -35,19 +39,25 @@ import org.apache.cassandra.utils.ByteBufferUtil; */ public final class UDFContextImpl implements UDFContext { - private final KeyspaceMetadata keyspaceMetadata; private final Map> byName = new HashMap<>(); private final TypeCodec[] argCodecs; private final TypeCodec returnCodec; + private final String keyspace; - UDFContextImpl(List argNames, TypeCodec[] argCodecs, TypeCodec returnCodec, - KeyspaceMetadata keyspaceMetadata) + /* + metadata can not be retrieved within the constructor as the keyspace itself may be being + constructed and an NPE will result. + */ + private final Supplier metadata; + + UDFContextImpl(List argNames, TypeCodec[] argCodecs, TypeCodec returnCodec, String keyspace) { for (int i = 0; i < argNames.size(); i++) byName.put(argNames.get(i).toString(), argCodecs[i]); this.argCodecs = argCodecs; this.returnCodec = returnCodec; - this.keyspaceMetadata = keyspaceMetadata; + this.keyspace = keyspace; + this.metadata = Suppliers.memoize(() -> Schema.instance.getKeyspaceMetadata(keyspace)); } public UDTValue newArgUDTValue(String argName) @@ -67,9 +77,9 @@ public final class UDFContextImpl implements UDFContext public UDTValue newUDTValue(String udtName) { - Optional udtType = keyspaceMetadata.types.get(ByteBufferUtil.bytes(udtName)); + Optional udtType = metadata.get().types.get(ByteBufferUtil.bytes(udtName)); DataType dataType = UDHelper.driverType(udtType.orElseThrow( - () -> new IllegalArgumentException("No UDT named " + udtName + " in keyspace " + keyspaceMetadata.name) + () -> new IllegalArgumentException("No UDT named " + udtName + " in keyspace " + keyspace) )); return newUDTValue(dataType); } @@ -91,7 +101,7 @@ public final class UDFContextImpl implements UDFContext public TupleValue newTupleValue(String cqlDefinition) { - AbstractType abstractType = CQLTypeParser.parse(keyspaceMetadata.name, cqlDefinition, keyspaceMetadata.types); + AbstractType abstractType = CQLTypeParser.parse(keyspace, cqlDefinition, metadata.get().types); DataType dataType = UDHelper.driverType(abstractType); return newTupleValue(dataType); } diff --git a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java index 3863f89828..04bd8265f5 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDFunction.java @@ -232,9 +232,7 @@ public abstract class UDFunction extends AbstractFunction implements ScalarFunct this.argCodecs = UDHelper.codecsFor(argDataTypes); this.returnCodec = UDHelper.codecFor(returnDataType); this.calledOnNullInput = calledOnNullInput; - KeyspaceMetadata keyspaceMetadata = Schema.instance.getKeyspaceMetadata(name.keyspace); - this.udfContext = new UDFContextImpl(argNames, argCodecs, returnCodec, - keyspaceMetadata); + this.udfContext = new UDFContextImpl(argNames, argCodecs, returnCodec, name.keyspace); } public static UDFunction tryCreate(FunctionName name, diff --git a/test/distributed/org/apache/cassandra/distributed/test/UDFTest.java b/test/distributed/org/apache/cassandra/distributed/test/UDFTest.java new file mode 100644 index 0000000000..37ec95d5f5 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/UDFTest.java @@ -0,0 +1,65 @@ +/* + * 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.cassandra.distributed.test; + +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; + +import static java.lang.Boolean.TRUE; +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows; +import static org.apache.cassandra.distributed.shared.AssertUtils.row; + +public class UDFTest extends TestBaseImpl +{ + @Test + public void testUDFContextKeyspaceNotNull() throws Throwable + { + /* + To test the keyspace is not null in the context we call UDFContext.newUDTValue() because it + relies on the keyspace not being null. + */ + String[] createStmts = { + "CREATE TABLE " + KEYSPACE + ".current (city text, PRIMARY KEY (city))", + "CREATE TYPE IF NOT EXISTS " + KEYSPACE + ".aggst (lt int, ge int)", + "CREATE FUNCTION " + KEYSPACE + ".udf_not_null ()\n" + + "CALLED ON NULL INPUT\n" + + "RETURNS boolean LANGUAGE java AS $$\n" + + "udfContext.newUDTValue(\"aggst\");\n" + + "return Boolean.TRUE;\n" + + "$$;", + }; + + try (Cluster cluster = init(Cluster.create(1, config -> config.set("enable_user_defined_functions", "true")))) + { + for (String stmt : createStmts) + cluster.schemaChange(stmt); + + cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".current (city) VALUES ('Helsinki')", ALL); + + assertRows(cluster.coordinator(1).execute("SELECT " + KEYSPACE + ".udf_not_null() AS m FROM " + KEYSPACE + ".current", ALL), row(TRUE)); + + cluster.get(1).shutdown().get(); + cluster.get(1).startup(); + + assertRows(cluster.coordinator(1).execute("SELECT " + KEYSPACE + ".udf_not_null() AS m FROM " + KEYSPACE + ".current", ALL), row(TRUE)); + } + } +}