blob: f4aa759fbe173a751a06aaebc07e1dab93e071fc [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.ignite.util;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.ignite.IgniteBinary;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.Ignition;
import org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.binary.BinaryObjectBuilder;
import org.apache.ignite.binary.BinaryObjectException;
import org.apache.ignite.binary.BinaryType;
import org.apache.ignite.client.ClientCacheConfiguration;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.configuration.ClientConfiguration;
import org.apache.ignite.internal.binary.BinarySchema;
import org.apache.ignite.internal.binary.BinaryTypeImpl;
import org.apache.ignite.testframework.GridTestUtils;
import org.jetbrains.annotations.Nullable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_INVALID_ARGUMENTS;
import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK;
import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_UNEXPECTED_ERROR;
import static org.apache.ignite.testframework.GridTestUtils.assertContains;
import static org.apache.ignite.testframework.GridTestUtils.assertNotContains;
/**
* Checks command line metadata commands.
*/
public class GridCommandHandlerMetadataTest extends GridCommandHandlerClusterByClassAbstractTest {
/** File system. */
public static final FileSystem FS = FileSystems.getDefault();
/** Types count. */
private static final int TYPES_CNT = 10;
/** */
@Before
public void init() {
injectTestSystemOut();
}
/** */
@After
public void clear() {
crd.binary().types().stream().forEach(type -> crd.context().cacheObjects().removeType(type.typeId()));
}
/**
* Check the command '--meta list'.
* Steps:
* - Creates binary types for a test (by BinaryObjectBuilder);
* - execute the command '--meta list'.
* - Check command output (must contains all created types).
*/
@Test
public void testMetadataList() {
for (int typeNum = 0; typeNum < TYPES_CNT; ++typeNum) {
BinaryObjectBuilder bob = crd.binary().builder("Type_" + typeNum);
for (int fldNum = 0; fldNum <= typeNum; ++fldNum)
bob.setField("fld_" + fldNum, 0);
bob.build();
}
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
String out = testOut.toString();
for (int typeNum = 0; typeNum < TYPES_CNT; ++typeNum)
assertContains(log, out, "typeName=Type_" + typeNum);
}
/**
* Checks that command remove when provided with absent type name prints informative message and
* returns corresponding error code (invalid argument).
*
* Test scenario:
* <ol>
* <li>
* Execute meta --remove command passing non-existing type name.
* </li>
* <li>
* Verify that invalid argument error code is returned, message is printed to command output.
* </li>
* <li>
* Execute meta --remove command passing non-existing type id.
* </li>
* <li>
* Verify that invalid argument error code is returned, message is printed to command output.
* </li>
* </ol>
*/
@Test
public void testMetadataRemoveWrongType() {
String wrongTypeName = "Type01";
assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "remove", "--typeName", wrongTypeName));
assertContains(log, testOut.toString(), "Failed to remove binary type, type not found: " + wrongTypeName);
int wrongTypeId = 42;
assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "remove", "--typeId", Integer.toString(wrongTypeId)));
assertContains(log, testOut.toString(), "Failed to remove binary type, type not found: ");
assertContains(log, testOut.toString(), "0x" +
Integer.toHexString(wrongTypeId).toUpperCase() + " (" + wrongTypeId + ")");
}
/**
* Checks that commands --meta list and --meta remove don't cause registering system or internal classes
* in binary metadata.
*
* Test scenario:
* <ol>
* <li>
* Put a value of a user class to the default cache that requires registering new binary type for that class.
* </li>
* <li>
* Run --meta list command to check that class is registered.
* </li>
* <li>
* Destroy the cache. Remove binary type with --meta remove command.
* </li>
* <li>
* Run --meta list again to check that no additional system or internal classes were registered.
* </li>
* </ol>
*/
@Test
public void testMetadataForInternalClassesIsNotRegistered() {
IgniteCache<Object, Object> dfltCache = grid(0).getOrCreateCache(DEFAULT_CACHE_NAME);
dfltCache.put(1, new TestValue());
Collection<BinaryType> metadata = crd.context().cacheObjects().metadata();
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
assertEquals(metadata.toString(), 1, metadata.size());
assertContains(log, testOut.toString(), "typeName=" + TestValue.class.getTypeName());
grid(0).destroyCache(DEFAULT_CACHE_NAME);
metadata = crd.context().cacheObjects().metadata();
assertEquals(metadata.toString(), 1, metadata.size());
assertEquals(EXIT_CODE_OK, execute("--meta", "remove", "--typeName", TestValue.class.getTypeName()));
metadata = crd.context().cacheObjects().metadata();
assertEquals("Binary metadata is expected to be empty but the following binary types were found: "
+ metadata
.stream()
.map(b -> "BinaryType[typeId=" + b.typeId() + ", typeName=" + b.typeName() + ']')
.collect(Collectors.toList()).toString(),
0,
metadata.size());
}
/**
* Check the command '--meta details'.
* Steps:
* - Creates binary two types for a test (by BinaryObjectBuilder) with several fields and shemas;
* - execute the command '--meta details' for the type Type0 by name
* - check metadata print.
* - execute the command '--meta details' for the type Type0 by type ID on different formats.
* - check metadata print.
*/
@Test
public void testMetadataDetails() {
BinaryObjectBuilder bob0 = crd.binary().builder("TypeName0");
bob0.setField("fld0", 0);
bob0.build();
bob0 = crd.binary().builder("TypeName0");
bob0.setField("fld1", "0");
bob0.build();
bob0 = crd.binary().builder("TypeName0");
bob0.setField("fld0", 1);
bob0.setField("fld2", UUID.randomUUID());
BinaryObject bo0 = bob0.build();
BinaryObjectBuilder bob1 = crd.binary().builder("TypeName1");
bob1.setField("fld0", 0);
bob1.build();
bob1 = crd.binary().builder("TypeName1");
bob1.setField("fld0", 0);
bob1.setField("fld1", new Date());
bob1.setField("fld2", 0.1);
bob1.setField("fld3", new long[]{0, 1, 2, 3});
BinaryObject bo1 = bob1.build();
assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeName", "TypeName0"));
checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(bo0.type().typeId()));
assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeId",
"0x" + Integer.toHexString(crd.context().cacheObjects().typeId("TypeName1"))));
checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(bo1.type().typeId()));
assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeId",
Integer.toString(crd.context().cacheObjects().typeId("TypeName1"))));
checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(bo1.type().typeId()));
}
/**
* Check the command '--meta remove' and '--meta update' with invalid arguments.
* Steps:
* - executes 'remove' command without specified type.
* - checks error code and command output.
* - executes 'remove' command with '--out' option specified as the exist directory
* ('--out' parameter must be a file name).
* - checks error code and command output.
* - executes 'update' command with '--in' option specified as the exist directory
* ('--in' parameter must be a file name)
* - checks error code and command output.
*/
@Test
public void testInvalidArguments() {
String out;
assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "remove"));
out = testOut.toString();
assertContains(log, out, "Check arguments.");
assertContains(log, out, "Type to remove is not specified");
assertContains(log, out, "Please add one of the options: --typeName <type_name> or --typeId <type_id>");
assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "remove", "--typeId", "0", "--out", "target"));
out = testOut.toString();
assertContains(log, out, "Check arguments.");
assertContains(log, out, "Cannot write to output file target.");
assertEquals(EXIT_CODE_INVALID_ARGUMENTS, execute("--meta", "update", "--in", "target"));
out = testOut.toString();
assertContains(log, out, "Check arguments.");
assertContains(log, out, "Cannot read metadata from target");
}
/**
* Check the command '--meta remove' and '--meta update'.
* Steps:
* - creates the type 'Type0'.
* - removes the type by cmdline utility (store removed metadata to specified file).
* - checks that type removed (try to create Type0 with the same field and different type).
* - removes the new type 'Type0' by cmd line utility.
* - restores Typo0 from the file
* - checks restored type.
*/
@Test
public void testRemoveUpdate() throws Exception {
Path typeFile = FS.getPath("type0.bin");
try {
createType("Type0", 0);
// Type of the field cannot be changed.
GridTestUtils.assertThrowsAnyCause(log, () -> {
createType("Type0", "string");
return null;
}, BinaryObjectException.class, "Wrong value has been set");
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", "Type0",
"--out", typeFile.toString()));
// New type must be created successfully after remove the type.
createType("Type0", "string");
assertEquals(EXIT_CODE_OK, execute("--meta", "remove", "--typeName", "Type0"));
// Restore the metadata from file.
assertEquals(EXIT_CODE_OK, execute("--meta", "update", "--in", typeFile.toString()));
// Type of the field cannot be changed.
GridTestUtils.assertThrowsAnyCause(log, () -> {
createType("Type0", "string");
return null;
}, BinaryObjectException.class, "Wrong value has been set");
createType("Type0", 1);
crd.context().cacheObjects().removeType(crd.context().cacheObjects().typeId("Type0"));
createType("Type0", "string");
// Restore the metadata from file.
assertEquals(EXIT_CODE_UNEXPECTED_ERROR, execute("--meta", "update", "--in", typeFile.toString()));
String out = testOut.toString();
assertContains(log, out, "Failed to execute metadata command='update'");
assertContains(log, out, "Type 'Type0' with typeId 110843958 has a " +
"different/incorrect type for field 'fld'.");
assertContains(log, out, "Expected 'String' but 'int' was provided. " +
"The type of an existing field can not be changed");
}
finally {
if (Files.exists(typeFile))
Files.delete(typeFile);
}
}
/**
* Check the all thin connections are dropped on the command '--meta remove' and '--meta update'.
* Steps:
* - opens thin client connection.
* - creates Type0 on thin client side.
* - removes type by cmd line util.
* - executes any command on thin client to detect disconnect.
* - creates Type0 on thin client side with other type of field (checks the type was removed).
*/
@Test
public void testDropThinConnectionsOnRemove() throws Exception {
Path typeFile = FS.getPath("type0.bin");
try (IgniteClient cli = Ignition.startClient(clientConfiguration())) {
createType(cli.binary(), "Type0", 1);
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", "Type0",
"--out", typeFile.toString()));
// Executes command to check disconnect / reconnect.
GridTestUtils.assertThrows(log, () ->
cli.createCache(new ClientCacheConfiguration().setName("test")),
Exception.class, null);
createType(cli.binary(), "Type0", "str");
}
finally {
if (Files.exists(typeFile))
Files.delete(typeFile);
}
}
/**
* Check the all thin connections are dropped on the command '--meta remove' and '--meta update'.
* Steps:
* - opens JDBC thin client connection.
* - executes: CREATE TABLE test(id INT PRIMARY KEY, objVal OTHER).
* - inserts the instance of the 'TestValue' class to the table.
* - removes the type 'TestValue' by cmd line.
* - executes any command on JDBC driver to detect disconnect.
* - checks metadata on client side. It must be empty.
*/
@Test
public void testDropJdbcThinConnectionsOnRemove() throws Exception {
Path typeFile = FS.getPath("type0.bin");
try (Connection conn = DriverManager.getConnection(jdbcThinUrl())) {
try (final Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE test(id INT PRIMARY KEY, objVal OTHER)");
try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO test(id, objVal) VALUES (?, ?)")) {
pstmt.setInt(1, 0);
pstmt.setObject(2, new TestValue());
pstmt.execute();
}
stmt.execute("DELETE FROM test WHERE id >= 0");
}
HashMap<Integer, BinaryType> metasOld = GridTestUtils.getFieldValue(conn, "metaHnd", "cache", "metas");
assertFalse(metasOld.isEmpty());
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", TestValue.class.getName(),
"--out", typeFile.toString()));
// Executes any command to check disconnect.
GridTestUtils.assertThrows(log, () -> {
try (Statement stmt = conn.createStatement()) {
stmt.execute("SELECT * FROM test");
}
return null;
},
SQLException.class, "Failed to communicate with Ignite cluster");
HashMap<Integer, BinaryType> metas = GridTestUtils.getFieldValue(conn, "metaHnd", "cache", "metas");
assertNotSame(metasOld, metas);
assertTrue(metas.isEmpty());
}
finally {
if (Files.exists(typeFile))
Files.delete(typeFile);
}
}
/**
* Check the type successfully merged after remove-recreate-update operations.
* Steps:
* - creates several types with name 'TypeNameX' where X - some index.
* - removes the type by cmdline utility (store removed metadata to specified file).
* - checks that type removed.
* - creates new types with the same names but different field names
* - restores removed types from the file
* - checks all types successfully merged.
*/
@Test
public void testTypeMergedAfterRemoveUpdate() {
String[] typeNames = new String[]{"TypeName0", "TypeName1"};
final int cnt = typeNames.length;
Object[] typeValues = new Object[]{0, LocalDate.now()};
int[] typeIds = new int[cnt];
repeat(cnt, i -> typeIds[i] = crd.binary().builder(typeNames[i])
.setField("fld0", typeValues[i])
.build().type().typeId());
Path[] typeBackups = new Path[typeNames.length];
try {
repeat(cnt, i -> {
Path path = FS.getPath(typeNames[i] + ".bin");
typeBackups[i] = path;
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", typeNames[i],
"--out", path.toString()));
});
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
String out = testOut.toString();
repeat(cnt, i -> assertNotContains(log, out, "typeName=" + typeNames[i]));
repeat(cnt, i -> crd.binary().builder(typeNames[i])
.setField("fld1", typeValues[i])
.build());
repeat(cnt, i -> assertEquals(EXIT_CODE_OK, execute("--meta", "update", "--in",
typeBackups[i].toString())));
repeat(cnt, i -> {
assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeName", typeNames[i]));
checkTypeDetails(log, testOut.toString(), crd.context().cacheObjects().metadata(typeIds[i]));
});
}
finally {
repeat(cnt, i -> {
if (typeBackups[i] != null) {
try {
Files.deleteIfExists(typeBackups[i]);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
/**
* Check the type can't be merged after remove-recreate-update operations
* with incompatible changes.
*
* Steps:
* - creates several types with name 'TypeNameX' where X - some index.
* - removes the type by cmdline utility (store removed metadata to specified file).
* - checks that type removed.
* - creates new types with the same names and same fields but different field types.
* - try to restores and verifies that it fails.
*/
@Test
public void testTypeCantBeMergedAfterRemoveUpdateWithIncompatibleChanges() {
String[] typeNames = new String[]{"TypeName0", "TypeName1"};
final int cnt = typeNames.length;
Object[] typeValues = new Object[]{0, LocalDate.now()};
repeat(cnt, i -> crd.binary().builder(typeNames[i])
.setField("fld0", typeValues[i])
.build().type().typeId());
Path[] typeBackups = new Path[typeNames.length];
try {
repeat(cnt, i -> {
Path path = FS.getPath(typeNames[i] + ".bin");
typeBackups[i] = path;
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", typeNames[i],
"--out", path.toString()));
});
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
String out = testOut.toString();
repeat(cnt, i -> assertNotContains(log, out, "typeName=" + typeNames[i]));
repeat(cnt, i -> crd.binary().builder(typeNames[i])
.setField("fld0", typeValues[cnt - i - 1]) // swap values
.build());
repeat(cnt, i -> {
assertEquals(EXIT_CODE_UNEXPECTED_ERROR, execute("--meta", "update", "--in",
typeBackups[i].toString()));
assertContains(log, testOut.toString(), "The type of an existing field can not be changed");
});
}
finally {
repeat(cnt, i -> {
if (typeBackups[i] != null) {
try {
Files.deleteIfExists(typeBackups[i]);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
/**
* Check that you could remove and update type when metadata store under load.
*
* Steps:
* - creates type with name 'Type0'.
* - start another thread which generates in an infinite loop new types.
* - removes the type by cmdline utility (store removed metadata to specified file).
* - checks that type removed.
* - restore the type.
* - checks that type restored.
*/
@Test
public void testRemoveUpdateUnderLoad() throws Exception {
Path typeFile = FS.getPath("type0.bin");
try {
createType("Type0", 0);
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
assertContains(log, testOut.toString(), "typeName=Type0");
AtomicBoolean stop = new AtomicBoolean(false);
Thread t = new Thread(() -> {
long i = 1;
while (!stop.get())
createType("Type" + i++, i);
});
t.start();
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", "Type0",
"--out", typeFile.toString()));
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
assertNotContains(log, testOut.toString(), "typeName=Type0");
// Restore the metadata from file.
assertEquals(EXIT_CODE_OK, execute("--meta", "update", "--in", typeFile.toString()));
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
assertContains(log, testOut.toString(), "typeName=Type0");
stop.set(true);
t.join(getTestTimeout());
}
finally {
if (Files.exists(typeFile))
Files.delete(typeFile);
}
}
/**
* Checks metadata list/details behaviour after a type removing.
*
* Steps:
* - creates some type.
* - checks that metadata list|details command returns proper type information.
* - removes the type by cmdline utility.
* - checks list command output not contains removed type.
* - checks details command fails with "Type not found" error.
*/
@Test
public void testMetadataListDetailsAfterTypeRemoving() throws IOException {
Path typeFile = FS.getPath("type0.bin");
try {
int typeId = createType("Type0", 0);
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
assertContains(log, testOut.toString(), "typeName=Type0");
assertEquals(EXIT_CODE_OK, execute("--meta", "details", "--typeName", "Type0"));
checkTypeDetails(log, testOut.toString(), crd.binary().type(typeId));
assertEquals(EXIT_CODE_OK, execute("--meta", "remove",
"--typeName", "Type0",
"--out", typeFile.toString()));
assertEquals(EXIT_CODE_OK, execute("--meta", "list"));
assertNotContains(log, testOut.toString(), "typeName=Type0");
assertEquals(EXIT_CODE_UNEXPECTED_ERROR, execute("--meta", "details", "--typeName", "Type0"));
assertContains(log, testOut.toString(), "type not found: " + typeId);
}
finally {
Files.deleteIfExists(typeFile);
}
}
/**
* Repeats {@code cons} {@code cnt} times.
*
* @param cnt Count.
* @param cons Cons.
*/
private void repeat(int cnt, Consumer<Integer> cons) {
for (int i = 0; i < cnt; i++)
cons.accept(i);
}
/**
* @param t Binary type.
*/
private void checkTypeDetails(@Nullable IgniteLogger log, String cmdOut, BinaryType t) {
assertContains(log, cmdOut, "typeId=" + "0x" + Integer.toHexString(t.typeId()).toUpperCase());
assertContains(log, cmdOut, "typeName=" + t.typeName());
assertContains(log, cmdOut, "Fields:");
for (String fldName : t.fieldNames())
assertContains(log, cmdOut, "name=" + fldName + ", type=" + t.fieldTypeName(fldName));
for (BinarySchema s : ((BinaryTypeImpl)t).metadata().schemas())
assertContains(log, cmdOut, "schemaId=0x" + Integer.toHexString(s.schemaId()).toUpperCase());
}
/**
* @param typeName Type name
* @param val Field value.
*/
int createType(String typeName, Object val) {
return createType(crd.binary(), typeName, val);
}
/**
* @param typeName Type name.
* @param val Field value.
*/
int createType(IgniteBinary bin, String typeName, Object val) {
return bin.builder(typeName)
.setField("fld", val)
.build()
.type()
.typeId();
}
/** */
protected ClientConfiguration clientConfiguration() {
return new ClientConfiguration()
.setAddresses("127.0.0.1:10800");
}
/** */
protected String jdbcThinUrl() {
return "jdbc:ignite:thin://127.0.0.1";
}
/**
*
*/
public static class TestValue {
/** */
public final int val = 3;
}
}