This is an automated email from the ASF dual-hosted git repository.
ibessonov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 9ad42b7 IGNITE-14748 Implemented ability to persist ordering of named
list configuration elements. (#208)
9ad42b7 is described below
commit 9ad42b75e54323d927d0a1a665d6a8a8be274c7c
Author: ibessonov <[email protected]>
AuthorDate: Tue Jul 20 10:25:36 2021 +0300
IGNITE-14748 Implemented ability to persist ordering of named list
configuration elements. (#208)
---
.../configuration/ConfigurationChangerTest.java | 34 +--
.../configuration/TestConfigurationChanger.java | 51 +++++
.../notifications/ConfigurationListenerTest.java | 2 +-
.../storage/TestConfigurationStorage.java | 7 +-
.../{sample => tree}/ConfigurationArrayTest.java | 3 +-
.../ConstructableTreeNodeTest.java | 13 +-
.../configuration/tree/NamedListOrderTest.java | 239 +++++++++++++++++++++
.../{sample => tree}/TraversableTreeNodeTest.java | 33 ++-
.../configuration/util/ConfigurationUtilTest.java | 208 ++++++++++--------
.../validation/ValidationUtilTest.java | 5 +-
.../ignite/configuration/NamedListChange.java | 58 +++--
.../apache/ignite/configuration/NamedListView.java | 4 +-
modules/configuration/pom.xml | 7 +
.../configuration/ConfigurationChanger.java | 12 +-
.../configuration/ConfigurationManager.java | 3 +-
.../configuration/tree/ConfigurationVisitor.java | 2 +-
.../internal/configuration/tree/NamedListNode.java | 131 ++++++++---
.../internal/configuration/tree/OrderedMap.java | 199 +++++++++++++++++
.../configuration/util/ConfigurationFlattener.java | 212 ++++++++++++++++++
.../util/ConfigurationNotificationsUtil.java | 4 +-
.../configuration/util/ConfigurationUtil.java | 225 +++++++------------
.../configuration/tree/OrderedMapTest.java | 150 +++++++++++++
.../runner/app/AbstractSchemaChangeTest.java | 10 +-
.../SchemaConfigurationConverterTest.java | 2 +-
.../internal/table/distributed/TableManager.java | 2 +-
25 files changed, 1242 insertions(+), 374 deletions(-)
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
index b69a14b..ef7503f 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
@@ -24,7 +24,6 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import org.apache.ignite.configuration.ConfigurationChangeException;
-import org.apache.ignite.configuration.RootKey;
import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.ConfigValue;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
@@ -38,14 +37,12 @@ import org.apache.ignite.configuration.validation.Validator;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
import org.apache.ignite.internal.configuration.storage.Data;
import
org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
-import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.ignite.internal.configuration.AConfiguration.KEY;
import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.superRootPatcher;
@@ -111,7 +108,7 @@ public class ConfigurationChangerTest {
public void testSimpleConfigurationChange() throws Exception {
final TestConfigurationStorage storage = new
TestConfigurationStorage();
- ConfigurationChanger changer = new TestConfigurationChanger();
+ ConfigurationChanger changer = new TestConfigurationChanger(cgen);
changer.addRootKey(KEY);
changer.register(storage);
@@ -135,11 +132,11 @@ public class ConfigurationChangerTest {
public void testModifiedFromAnotherStorage() throws Exception {
final TestConfigurationStorage storage = new
TestConfigurationStorage();
- ConfigurationChanger changer1 = new TestConfigurationChanger();
+ ConfigurationChanger changer1 = new TestConfigurationChanger(cgen);
changer1.addRootKey(KEY);
changer1.register(storage);
- ConfigurationChanger changer2 = new TestConfigurationChanger();
+ ConfigurationChanger changer2 = new TestConfigurationChanger(cgen);
changer2.addRootKey(KEY);
changer2.register(storage);
@@ -180,11 +177,11 @@ public class ConfigurationChangerTest {
public void testModifiedFromAnotherStorageWithIncompatibleChanges() throws
Exception {
final TestConfigurationStorage storage = new
TestConfigurationStorage();
- ConfigurationChanger changer1 = new TestConfigurationChanger();
+ ConfigurationChanger changer1 = new TestConfigurationChanger(cgen);
changer1.addRootKey(KEY);
changer1.register(storage);
- ConfigurationChanger changer2 = new TestConfigurationChanger();
+ ConfigurationChanger changer2 = new TestConfigurationChanger(cgen);
changer2.addRootKey(KEY);
changer2.register(storage);
@@ -223,7 +220,7 @@ public class ConfigurationChangerTest {
public void testFailedToWrite() {
final TestConfigurationStorage storage = new
TestConfigurationStorage();
- ConfigurationChanger changer = new TestConfigurationChanger();
+ ConfigurationChanger changer = new TestConfigurationChanger(cgen);
changer.addRootKey(KEY);
storage.fail(true);
@@ -281,7 +278,7 @@ public class ConfigurationChangerTest {
@Test
public void defaultsOnInit() throws Exception {
- var changer = new TestConfigurationChanger();
+ var changer = new TestConfigurationChanger(cgen);
changer.addRootKey(DefaultsConfiguration.KEY);
@@ -308,21 +305,4 @@ public class ConfigurationChangerTest {
assertEquals("bar", root.childsList().get("name").defStr());
}
- private static class TestConfigurationChanger extends ConfigurationChanger
{
- TestConfigurationChanger() {
- super((oldRoot, newRoot, revision) -> completedFuture(null));
- }
-
- /** {@inheritDoc} */
- @Override public void addRootKey(RootKey<?, ?> rootKey) {
- super.addRootKey(rootKey);
-
- cgen.compileRootSchema(rootKey.schemaClass());
- }
-
- /** {@inheritDoc} */
- @Override public InnerNode createRootNode(RootKey<?, ?> rootKey) {
- return cgen.instantiateNode(rootKey.schemaClass());
- }
- }
}
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java
new file mode 100644
index 0000000..23f27d3
--- /dev/null
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java
@@ -0,0 +1,51 @@
+/*
+ * 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.internal.configuration;
+
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
+import org.apache.ignite.internal.configuration.tree.InnerNode;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+
+/** Implementation of {@link ConfigurationChanger} to be used in tests. Has no
support of listeners. */
+public class TestConfigurationChanger extends ConfigurationChanger {
+ /** Runtime implementations generator for node classes. */
+ private final ConfigurationAsmGenerator cgen;
+
+ /**
+ * @param cgen Runtime implementations generator for node classes. Will be
used to instantiate nodes objects.
+ */
+ public TestConfigurationChanger(ConfigurationAsmGenerator cgen) {
+ super((oldRoot, newRoot, revision) -> completedFuture(null));
+
+ this.cgen = cgen;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void addRootKey(RootKey<?, ?> rootKey) {
+ super.addRootKey(rootKey);
+
+ cgen.compileRootSchema(rootKey.schemaClass());
+ }
+
+ /** {@inheritDoc} */
+ @Override public InnerNode createRootNode(RootKey<?, ?> rootKey) {
+ return cgen.instantiateNode(rootKey.schemaClass());
+ }
+}
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
index d95790f..ec1bbcb 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
@@ -238,7 +238,7 @@ public class ConfigurationListenerTest {
log.clear();
configuration.change(parent ->
- parent.changeElements(elements -> elements.update("name", element
-> element.changeStr("foo")))
+ parent.changeElements(elements -> elements.createOrUpdate("name",
element -> element.changeStr("foo")))
).get(1, SECONDS);
assertEquals(List.of("parent", "elements", "update"), log);
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/storage/TestConfigurationStorage.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/storage/TestConfigurationStorage.java
index ce5965a..c8530ed 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/storage/TestConfigurationStorage.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/storage/TestConfigurationStorage.java
@@ -17,12 +17,11 @@
package org.apache.ignite.internal.configuration.storage;
import java.io.Serializable;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.ignite.configuration.annotation.ConfigurationType;
@@ -31,10 +30,10 @@ import
org.apache.ignite.configuration.annotation.ConfigurationType;
*/
public class TestConfigurationStorage implements ConfigurationStorage {
/** Map to store values. */
- private Map<String, Serializable> map = new ConcurrentHashMap<>();
+ private Map<String, Serializable> map = new HashMap<>();
/** Change listeners. */
- private List<ConfigurationStorageListener> listeners = new ArrayList<>();
+ private List<ConfigurationStorageListener> listeners = new
CopyOnWriteArrayList<>();
/** Storage version. */
private AtomicLong version = new AtomicLong(0);
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/ConfigurationArrayTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/ConfigurationArrayTest.java
similarity index 98%
rename from
modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/ConfigurationArrayTest.java
rename to
modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/ConfigurationArrayTest.java
index 28ed24b..a0422ff 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/ConfigurationArrayTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/ConfigurationArrayTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.configuration.sample;
+package org.apache.ignite.internal.configuration.tree;
import java.util.Random;
import java.util.UUID;
@@ -23,7 +23,6 @@ import java.util.stream.Stream;
import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
-import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/ConstructableTreeNodeTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNodeTest.java
similarity index 91%
rename from
modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/ConstructableTreeNodeTest.java
rename to
modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNodeTest.java
index 7abed25..6d1503c 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/ConstructableTreeNodeTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/ConstructableTreeNodeTest.java
@@ -15,14 +15,11 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.configuration.sample;
+package org.apache.ignite.internal.configuration.tree;
-import java.util.Collections;
+import java.util.List;
import java.util.NoSuchElementException;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
-import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
-import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
-import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -49,11 +46,11 @@ public class ConstructableTreeNodeTest {
cgen = null;
}
- public static <P extends InnerNode & ParentView & ParentChange> P
newParentInstance() {
+ public static <P extends InnerNode & ParentChange> P newParentInstance() {
return
(P)cgen.instantiateNode(TraversableTreeNodeTest.ParentConfigurationSchema.class);
}
- public static <C extends InnerNode & ChildView & ChildChange> C
newChildInstance() {
+ public static <C extends InnerNode & ChildChange> C newChildInstance() {
return
(C)cgen.instantiateNode(TraversableTreeNodeTest.ChildConfigurationSchema.class);
}
@@ -84,7 +81,7 @@ public class ConstructableTreeNodeTest {
assertNotNull(parentNode.elements());
assertNotSame(elements, parentNode.elements());
- assertEquals(Collections.emptySet(),
parentNode.elements().namedListKeys());
+ assertEquals(List.of(), parentNode.elements().namedListKeys());
// Inner node.
NamedElementView element = elements.get("name");
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListOrderTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListOrderTest.java
new file mode 100644
index 0000000..16637c6
--- /dev/null
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListOrderTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.internal.configuration.tree;
+
+import java.util.Map;
+import org.apache.ignite.configuration.NamedListChange;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.internal.configuration.TestConfigurationChanger;
+import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
+import
org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/** Test for keys ordering in named list nodes. */
+public class NamedListOrderTest {
+ /** Root that has a single named list. */
+ @ConfigurationRoot(rootName = "a")
+ public static class AConfigurationSchema {
+ /** */
+ @NamedConfigValue
+ public BConfigurationSchema b;
+ }
+
+ /** Named list element node that in inself contains another named list. */
+ @Config
+ public static class BConfigurationSchema {
+ /** Every named list element node must have at least one configuration
field that is not named list. */
+ @Value(hasDefault = true)
+ public String c = "foo";
+
+ @NamedConfigValue
+ public BConfigurationSchema b;
+ }
+
+ /** Runtime implementations generator. */
+ private static ConfigurationAsmGenerator cgen;
+
+ /** Test configuration storage. */
+ private TestConfigurationStorage storage;
+
+ /** Test configuration changer. */
+ private TestConfigurationChanger changer;
+
+ /** Instantiates {@link #cgen}. */
+ @BeforeAll
+ public static void beforeAll() {
+ cgen = new ConfigurationAsmGenerator();
+ }
+
+ /** Nullifies {@link #cgen} to prevent memory leak from having runtime
ClassLoader accessible from GC root. */
+ @AfterAll
+ public static void afterAll() {
+ cgen = null;
+ }
+
+ /** */
+ @BeforeEach
+ public void before() {
+ storage = new TestConfigurationStorage();
+
+ changer = new TestConfigurationChanger(cgen);
+ changer.addRootKey(AConfiguration.KEY);
+ changer.register(storage);
+ }
+
+ /** */
+ @AfterEach
+ public void after() {
+ changer.stop();
+ }
+
+ /**
+ * Tests that there are no unnecessary {@code <idx>} values in the storage
after all basic named list operations.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void storageData() throws Exception {
+ // Manually instantiate configuration instance.
+ var a = (AConfiguration)cgen.instantiateCfg(AConfiguration.KEY,
changer);
+
+ // Create values on several layers at the same time. They all should
have <idx> = 0.
+ a.b().change(b -> b.create("X", x -> x.changeB(xb -> xb.create("Z0",
z0 -> {})))).get();
+
+ assertEquals(
+ Map.of(
+ "a.b.X.c", "foo",
+ "a.b.X.<idx>", 0,
+ "a.b.X.b.Z0.c", "foo",
+ "a.b.X.b.Z0.<idx>", 0
+ ),
+ storage.readAll().values()
+ );
+
+ BConfiguration x = a.b().get("X");
+
+ // Append new key. It should have <idx> = 1.
+ x.b().change(xb -> xb.create("Z5", z5 -> {})).get();
+
+ assertEquals(
+ Map.of(
+ "a.b.X.c", "foo",
+ "a.b.X.<idx>", 0,
+ "a.b.X.b.Z0.c", "foo",
+ "a.b.X.b.Z0.<idx>", 0,
+ "a.b.X.b.Z5.c", "foo",
+ "a.b.X.b.Z5.<idx>", 1
+ ),
+ storage.readAll().values()
+ );
+
+ // Insert new key somewhere in the middle. Index of Z5 should be
updated to 2.
+ x.b().change(xb -> xb.create(1, "Z2", z2 -> {})).get();
+
+ assertEquals(
+ Map.of(
+ "a.b.X.c", "foo",
+ "a.b.X.<idx>", 0,
+ "a.b.X.b.Z0.c", "foo",
+ "a.b.X.b.Z0.<idx>", 0,
+ "a.b.X.b.Z2.c", "foo",
+ "a.b.X.b.Z2.<idx>", 1,
+ "a.b.X.b.Z5.c", "foo",
+ "a.b.X.b.Z5.<idx>", 2
+ ),
+ storage.readAll().values()
+ );
+
+ // Insert new key somewhere in the middle. Indexes of Z3 and Z5 should
be updated to 2 and 3.
+ x.b().change(xb -> xb.createAfter("Z2", "Z3", z3 -> {})).get();
+
+ assertEquals(
+ Map.of(
+ "a.b.X.c", "foo",
+ "a.b.X.<idx>", 0,
+ "a.b.X.b.Z0.c", "foo",
+ "a.b.X.b.Z0.<idx>", 0,
+ "a.b.X.b.Z2.c", "foo",
+ "a.b.X.b.Z2.<idx>", 1,
+ "a.b.X.b.Z3.c", "foo",
+ "a.b.X.b.Z3.<idx>", 2,
+ "a.b.X.b.Z5.c", "foo",
+ "a.b.X.b.Z5.<idx>", 3
+ ),
+ storage.readAll().values()
+ );
+
+ // Delete key from the middle. Indexes of Z3 and Z5 should be updated
to 1 and 2.
+ x.b().change(xb -> xb.delete("Z2")).get();
+
+ assertEquals(
+ Map.of(
+ "a.b.X.c", "foo",
+ "a.b.X.<idx>", 0,
+ "a.b.X.b.Z0.c", "foo",
+ "a.b.X.b.Z0.<idx>", 0,
+ "a.b.X.b.Z3.c", "foo",
+ "a.b.X.b.Z3.<idx>", 1,
+ "a.b.X.b.Z5.c", "foo",
+ "a.b.X.b.Z5.<idx>", 2
+ ),
+ storage.readAll().values()
+ );
+
+ // Delete values on several layers simultaneously. Storage must be
empty after that.
+ a.b().change(b -> b.delete("X")).get();
+
+ assertEquals(
+ Map.of(),
+ storage.readAll().values()
+ );
+ }
+
+ /** Tests exceptions described in methods signatures. */
+ @Test
+ public void creationErrors() throws Exception {
+ // Manually instantiate configuration instance.
+ var a = (AConfiguration)cgen.instantiateCfg(AConfiguration.KEY,
changer);
+
+ a.b().change(b -> b.create("X", x -> {}).create("Y", y -> {})).get();
+
+ // Dirty cast, but appropriate for this particular test.
+ var b = (NamedListChange<BView>)a.b().value();
+
+ // NPE in keys.
+ assertThrows(NullPointerException.class, () -> b.create(null, z ->
{}));
+ assertThrows(NullPointerException.class, () -> b.createOrUpdate(null,
z -> {}));
+ assertThrows(NullPointerException.class, () -> b.create(0, null, z ->
{}));
+ assertThrows(NullPointerException.class, () -> b.createAfter(null,
"Z", z -> {}));
+ assertThrows(NullPointerException.class, () -> b.createAfter("X",
null, z -> {}));
+
+ // NPE in closures.
+ assertThrows(NullPointerException.class, () -> b.create("Z", null));
+ assertThrows(NullPointerException.class, () -> b.createOrUpdate("Z",
null));
+ assertThrows(NullPointerException.class, () -> b.create(0, "Z", null));
+ assertThrows(NullPointerException.class, () -> b.createAfter("X", "Z",
null));
+
+ // Already existing keys.
+ assertThrows(IllegalArgumentException.class, () -> b.create("X", x ->
{}));
+ assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x
-> {}));
+ assertThrows(IllegalArgumentException.class, () -> b.createAfter("X",
"Y", y -> {}));
+
+ // Nonexistent preceding key.
+ assertThrows(IllegalArgumentException.class, () -> b.createAfter("A",
"Z", z -> {}));
+
+ // Wrong indexes.
+ assertThrows(IndexOutOfBoundsException.class, () -> b.create(-1, "Z",
z -> {}));
+ assertThrows(IndexOutOfBoundsException.class, () -> b.create(3, "Z", z
-> {}));
+
+ // Create after delete.
+ b.delete("X");
+ assertThrows(IllegalArgumentException.class, () -> b.create("X", x ->
{}));
+ assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x
-> {}));
+ }
+}
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/TraversableTreeNodeTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java
similarity index 88%
rename from
modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/TraversableTreeNodeTest.java
rename to
modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java
index 15eb5ca..1894f6a 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/sample/TraversableTreeNodeTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/tree/TraversableTreeNodeTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.ignite.internal.configuration.sample;
+package org.apache.ignite.internal.configuration.tree;
import java.io.Serializable;
import java.util.ArrayList;
@@ -29,16 +29,11 @@ import
org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.configuration.validation.Immutable;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
-import org.apache.ignite.internal.configuration.tree.ConfigurationVisitor;
-import org.apache.ignite.internal.configuration.tree.InnerNode;
-import org.apache.ignite.internal.configuration.tree.NamedListNode;
-import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
-import static java.util.Collections.emptySet;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -64,11 +59,11 @@ public class TraversableTreeNodeTest {
cgen = null;
}
- public static <P extends InnerNode & ParentView & ParentChange> P
newParentInstance() {
+ public static <P extends InnerNode & ParentChange> P newParentInstance() {
return (P)cgen.instantiateNode(ParentConfigurationSchema.class);
}
- public static <C extends InnerNode & ChildView & ChildChange> C
newChildInstance() {
+ public static <C extends InnerNode & ChildChange> C newChildInstance() {
return (C)cgen.instantiateNode(ChildConfigurationSchema.class);
}
@@ -178,7 +173,7 @@ public class TraversableTreeNodeTest {
// Named list node must always be instantiated.
assertNotNull(elementsNode);
- parentNode.changeElements(elements -> elements.update("key", element
-> {}));
+ parentNode.changeElements(elements -> elements.createOrUpdate("key",
element -> {}));
assertNotSame(elementsNode, parentNode.elements());
}
@@ -188,11 +183,13 @@ public class TraversableTreeNodeTest {
*/
@Test
public void putRemoveNamedConfiguration() {
- var elementsNode = newParentInstance().elements();
+ var elementsNode =
(NamedListChange<NamedElementChange>)newParentInstance().elements();
- assertEquals(emptySet(), elementsNode.namedListKeys());
+ assertEquals(List.of(), elementsNode.namedListKeys());
- ((NamedListChange<?>)elementsNode).update("keyPut", element -> {});
+ elementsNode.createOrUpdate("keyPut", element -> {});
+
+ assertThrows(IllegalArgumentException.class, () ->
elementsNode.create("keyPut", element -> {}));
assertThat(elementsNode.namedListKeys(), hasItem("keyPut"));
@@ -202,7 +199,7 @@ public class TraversableTreeNodeTest {
assertNull(elementNode.strCfg());
- ((NamedListChange<NamedElementChange>)elementsNode).update("keyPut",
element -> element.changeStrCfg("val"));
+ elementsNode.createOrUpdate("keyPut", element ->
element.changeStrCfg("val"));
// Assert that consecutive put methods create new object every time.
assertNotSame(elementNode, elementsNode.get("keyPut"));
@@ -211,21 +208,21 @@ public class TraversableTreeNodeTest {
assertEquals("val", elementNode.strCfg());
- ((NamedListChange<?>)elementsNode).delete("keyPut");
+ elementsNode.delete("keyPut");
assertThat(elementsNode.namedListKeys(),
CoreMatchers.hasItem("keyPut"));
assertNull(elementsNode.get("keyPut"));
- ((NamedListChange<?>)elementsNode).delete("keyRemove");
+ elementsNode.delete("keyPut");
// Assert that "remove" method creates null element inside of the node.
- assertThat(elementsNode.namedListKeys(), hasItem("keyRemove"));
+ assertThat(elementsNode.namedListKeys(), hasItem("keyPut"));
- assertNull(elementsNode.get("keyRemove"));
+ assertNull(elementsNode.get("keyPut"));
// Assert that once you remove something from list, you can't put it
back again with different set of fields.
- assertThrows(IllegalStateException.class, () ->
((NamedListChange<?>)elementsNode).update("keyRemove", element -> {}));
+ assertThrows(IllegalArgumentException.class, () ->
elementsNode.createOrUpdate("keyPut", element -> {}));
}
/**
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
index 7e1d69f..0b7fe20 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
@@ -17,8 +17,10 @@
package org.apache.ignite.internal.configuration.util;
+import java.io.Serializable;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.ConfigValue;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
@@ -27,14 +29,24 @@ import
org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.internal.configuration.SuperRoot;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
import org.apache.ignite.internal.configuration.tree.InnerNode;
import org.apache.ignite.internal.configuration.tree.TraversableTreeNode;
+import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
-import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
+import static
org.apache.ignite.internal.configuration.tree.NamedListNode.ORDER_IDX;
+import static
org.apache.ignite.internal.configuration.util.ConfigurationFlattener.createFlattenedUpdatesMap;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.aMapWithSize;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anEmptyMap;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@@ -58,7 +70,7 @@ public class ConfigurationUtilTest {
cgen = null;
}
- public static <P extends InnerNode & ParentView & ParentChange> P
newParentInstance() {
+ public static <P extends InnerNode & ParentChange> P newParentInstance() {
return (P)cgen.instantiateNode(ParentConfigurationSchema.class);
}
@@ -122,13 +134,16 @@ public class ConfigurationUtilTest {
public String str;
}
- /** */
+ /**
+ * Tests that {@link ConfigurationUtil#find(List, TraversableTreeNode)}
finds proper node when provided with correct
+ * path.
+ */
@Test
public void findSuccessfully() {
var parent = newParentInstance();
parent.changeElements(elements ->
- elements.update("name", element ->
+ elements.createOrUpdate("name", element ->
element.changeChild(child ->
child.changeStr("value")
)
@@ -161,14 +176,17 @@ public class ConfigurationUtilTest {
);
}
- /** */
+ /**
+ * Tests that {@link ConfigurationUtil#find(List, TraversableTreeNode)}
returns null when path points to nonexistent
+ * named list element.
+ */
@Test
public void findNulls() {
var parent = newParentInstance();
assertNull(ConfigurationUtil.find(List.of("elements", "name"),
parent));
- parent.changeElements(elements -> elements.update("name", element ->
{}));
+ parent.changeElements(elements -> elements.createOrUpdate("name",
element -> {}));
assertNull(ConfigurationUtil.find(List.of("elements", "name",
"child"), parent));
@@ -177,7 +195,10 @@ public class ConfigurationUtilTest {
assertNull(ConfigurationUtil.find(List.of("elements", "name", "child",
"str"), parent));
}
- /** */
+ /**
+ * Tests that {@link ConfigurationUtil#find(List, TraversableTreeNode)}
throws {@link KeyNotFoundException} when
+ * provided with a wrong path.
+ */
@Test
public void findUnsuccessfully() {
var parent = newParentInstance();
@@ -187,7 +208,7 @@ public class ConfigurationUtilTest {
() -> ConfigurationUtil.find(List.of("elements", "name", "child"),
parent)
);
- parent.changeElements(elements -> elements.update("name", element ->
{}));
+ parent.changeElements(elements -> elements.createOrUpdate("name",
element -> {}));
assertThrows(
KeyNotFoundException.class,
@@ -202,7 +223,9 @@ public class ConfigurationUtilTest {
);
}
- /** */
+ /**
+ * Tests convertion of flat map to a prefix map.
+ */
@Test
public void toPrefixMap() {
assertEquals(
@@ -226,18 +249,22 @@ public class ConfigurationUtilTest {
);
}
- /** */
+ /**
+ * Tests that patching of configuration node with a prefix map works fine
when prefix map is valid.
+ */
@Test
public void fillFromPrefixMapSuccessfully() {
var parentNode = newParentInstance();
ConfigurationUtil.fillFromPrefixMap(parentNode, Map.of(
"elements", Map.of(
- "name1", Map.of(
- "child", Map.of("str", "value1")
- ),
"name2", Map.of(
- "child", Map.of("str", "value2")
+ "child", Map.of("str", "value2"),
+ ORDER_IDX, 1
+ ),
+ "name1", Map.of(
+ "child", Map.of("str", "value1"),
+ ORDER_IDX, 0
)
)
));
@@ -246,13 +273,15 @@ public class ConfigurationUtilTest {
assertEquals("value2",
parentNode.elements().get("name2").child().str());
}
- /** */
+ /**
+ * Tests that patching of configuration node with a prefix map works fine
when prefix map is valid.
+ */
@Test
public void fillFromPrefixMapSuccessfullyWithRemove() {
var parentNode = newParentInstance();
parentNode.changeElements(elements ->
- elements.update("name", element ->
+ elements.createOrUpdate("name", element ->
element.changeChild(child -> {})
)
);
@@ -264,64 +293,74 @@ public class ConfigurationUtilTest {
assertNull(parentNode.elements().get("node"));
}
- /** */
+ /**
+ * Tests that conversion from "changer" lambda to a flat map of updates
for the storage works properly.
+ */
@Test
- public void nodeToFlatMap() {
- var parentNode = newParentInstance();
+ public void flattenedUpdatesMap() {
+ var superRoot = new SuperRoot(key -> null,
Map.of(ParentConfiguration.KEY, newParentInstance()));
- assertEquals(
- emptyMap(),
- ConfigurationUtil.nodeToFlatMap(null, new SuperRoot(key -> null,
Map.of(
- ParentConfiguration.KEY,
- parentNode
- )))
- );
+ assertThat(flattenedMap(superRoot, parent -> {}), is(anEmptyMap()));
- // No defaults in this test so everything must be initialized
explicitly.
- parentNode.changeElements(elements ->
- elements.create("name", element ->
- element.changeChild(child ->
- child.changeStr("foo")
+ assertThat(
+ flattenedMap(superRoot, parent -> parent
+ .changeElements(elements -> elements
+ .create("name", element -> element
+ .changeChild(child -> child.changeStr("foo"))
+ )
)
- )
+ ),
+ is(allOf(
+ aMapWithSize(2),
+ hasEntry("root.elements.name.child.str", (Serializable)"foo"),
+ hasEntry("root.elements.name.<idx>", 0)
+ ))
);
- assertEquals(
- singletonMap("root.elements.name.child.str", "foo"),
- ConfigurationUtil.nodeToFlatMap(null, new SuperRoot(key -> null,
Map.of(
- ParentConfiguration.KEY,
- parentNode
- )))
+ assertThat(
+ flattenedMap(superRoot, parent -> parent
+ .changeElements(elements1 -> elements1.delete("void"))
+ ),
+ is(anEmptyMap())
);
- assertEquals(
- emptyMap(),
- ConfigurationUtil.nodeToFlatMap(new SuperRoot(key -> null, Map.of(
- ParentConfiguration.KEY,
- parentNode
- )), new SuperRoot(key -> null, singletonMap(
- ParentConfiguration.KEY,
- (InnerNode)newParentInstance().changeElements(elements ->
- elements.delete("void")
- )
- )))
+ assertThat(
+ flattenedMap(superRoot, parent -> parent
+ .changeElements(elements -> elements.delete("name"))
+ ),
+ is(allOf(
+ aMapWithSize(2),
+ hasEntry("root.elements.name.child.str", null),
+ hasEntry("root.elements.name.<idx>", null)
+ ))
);
+ }
- assertEquals(
- singletonMap("root.elements.name.child.str", null),
- ConfigurationUtil.nodeToFlatMap(new SuperRoot(key -> null, Map.of(
- ParentConfiguration.KEY,
- parentNode
- )), new SuperRoot(key -> null, singletonMap(
- ParentConfiguration.KEY,
- (InnerNode)newParentInstance().changeElements(elements ->
- elements.delete("name")
- )
- )))
- );
+ /**
+ * Patches super root and returns flat representation of the changes.
Passed {@code superRoot} object will contain
+ * patched tree when method execution is completed.
+ *
+ * @param superRoot Super root to patch.
+ * @param patch Closure to cnahge parent node.
+ * @return Flat map with all changes from the patch.
+ */
+ @NotNull private Map<String, Serializable> flattenedMap(SuperRoot
superRoot, Consumer<ParentChange> patch) {
+ // Preserve a copy of the super root to use it as a golden source of
data.
+ SuperRoot originalSuperRoot = superRoot.copy();
+
+ // Make a copy of the root insode of the superRoot. This copy will be
used for further patching.
+ superRoot.construct(ParentConfiguration.KEY.key(), new
ConfigurationSource() {});
+
+ // Patch root node.
+ patch.accept((ParentChange)superRoot.getRoot(ParentConfiguration.KEY));
+
+ // Create flat diff between two super trees.
+ return createFlattenedUpdatesMap(originalSuperRoot, superRoot);
}
- /** */
+ /**
+ * Tests basic invariants of {@link
ConfigurationUtil#patch(ConstructableTreeNode, TraversableTreeNode)} method.
+ */
@Test
public void patch() {
var originalRoot = newParentInstance();
@@ -333,8 +372,8 @@ public class ConfigurationUtilTest {
);
// Updating config.
- ParentView updatedRoot = ConfigurationUtil.patch(originalRoot,
(TraversableTreeNode)newParentInstance().changeElements(elements ->
- elements.update("name1", element ->
+ ParentView updatedRoot = ConfigurationUtil.patch(originalRoot,
(TraversableTreeNode)copy(originalRoot).changeElements(elements ->
+ elements.createOrUpdate("name1", element ->
element.changeChild(child -> child.changeStr("value2"))
)
));
@@ -348,8 +387,8 @@ public class ConfigurationUtilTest {
assertEquals("value2",
updatedRoot.elements().get("name1").child().str());
// Expanding config.
- ParentView expandedRoot = ConfigurationUtil.patch(originalRoot,
(TraversableTreeNode)newParentInstance().changeElements(elements ->
- elements.update("name2", element ->
+ ParentView expandedRoot = ConfigurationUtil.patch(originalRoot,
(TraversableTreeNode)copy(originalRoot).changeElements(elements ->
+ elements.createOrUpdate("name2", element ->
element.changeChild(child -> child.changeStr("value2"))
)
));
@@ -364,7 +403,7 @@ public class ConfigurationUtilTest {
assertEquals("value2",
expandedRoot.elements().get("name2").child().str());
// Shrinking config.
- ParentView shrinkedRoot =
(ParentView)ConfigurationUtil.patch((InnerNode)expandedRoot,
(TraversableTreeNode)newParentInstance().changeElements(elements ->
+ ParentView shrinkedRoot =
(ParentView)ConfigurationUtil.patch((InnerNode)expandedRoot,
(TraversableTreeNode)copy(expandedRoot).changeElements(elements ->
elements.delete("name1")
));
@@ -376,33 +415,12 @@ public class ConfigurationUtilTest {
assertNotNull(shrinkedRoot.elements().get("name2"));
}
- /** */
- @Test
- public void cleanupMatchingValues() {
- var curParent = newParentInstance();
-
- curParent.changeElements(elements -> elements
- .create("missing", element -> {})
- .create("match", element -> element.changeChild(child ->
child.changeStr("match")))
- .create("mismatch", element -> element.changeChild(child ->
child.changeStr("foo")))
- );
-
- var newParent = newParentInstance();
-
- newParent.changeElements(elements -> elements
- .create("extra", element -> {})
- .create("match", element -> element.changeChild(child ->
child.changeStr("match")))
- .create("mismatch", element -> element.changeChild(child ->
child.changeStr("bar")))
- );
-
- ConfigurationUtil.cleanupMatchingValues(curParent, newParent);
-
- // Old node stayed intact.
- assertEquals("match", curParent.elements().get("match").child().str());
- assertEquals("foo",
curParent.elements().get("mismatch").child().str());
-
- // New node was modified.
- assertNull(newParent.elements().get("match").child().str());
- assertEquals("bar",
newParent.elements().get("mismatch").child().str());
+ /**
+ * @param parent {@link ParentView} object.
+ * @return Copy of the {@code parent} objects cast to {@link ParentChange}.
+ * @see ConstructableTreeNode#copy()
+ */
+ private ParentChange copy(ParentView parent) {
+ return (ParentChange)((ConstructableTreeNode)parent).copy();
}
}
diff --git
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
index a090d33..1a95c85 100644
---
a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
+++
b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/internal/configuration/validation/ValidationUtilTest.java
@@ -45,7 +45,6 @@ import org.junit.jupiter.api.Test;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-import static java.util.Collections.emptySet;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** */
@@ -175,8 +174,8 @@ public class ValidationUtilTest {
@Override public void validate(NamedListValidation annotation,
ValidationContext<NamedListView<?>> ctx) {
assertEquals("root.elements", ctx.currentKey());
- assertEquals(emptySet(), ctx.getOldValue().namedListKeys());
- assertEquals(emptySet(), ctx.getNewValue().namedListKeys());
+ assertEquals(List.of(), ctx.getOldValue().namedListKeys());
+ assertEquals(List.of(), ctx.getNewValue().namedListKeys());
ctx.addIssue(new ValidationIssue("bar"));
}
diff --git
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
index 3a9af1b..a6f8405 100644
---
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
+++
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListChange.java
@@ -20,29 +20,62 @@ package org.apache.ignite.configuration;
import java.util.function.Consumer;
/** */
-public interface NamedListChange<Change> {
+public interface NamedListChange<Change> extends NamedListView<Change> {
/**
- * Update the value in named list configuration.
+ * Creates a new value in the named list configuration.
*
* @param key Key for the value to be created.
- * @param valConsumer Closure to modify value associated with the key.
Object of type {@code T},
- * passed to the closure, must not be reused anywhere else.
+ * @param valConsumer Closure to modify the value associated with the key.
Closure parameter must not be leaked
+ * outside the scope of the closure.
* @return {@code this} for chaining.
+ *
+ * @throws NullPointerException If one of the parameters is null.
+ * @throws IllegalArgumentException If an element with the given name
already exists.
*/
- //TODO Replace with "createOrUpdate"
NamedListChange<Change> create(String key, Consumer<Change> valConsumer);
/**
- * Update the value in named list configuration.
+ * Creates a new value at the given position in the named list
configuration.
+ *
+ * @param index Index of the inserted element.
+ * @param key Key for the value to be created.
+ * @param valConsumer Closure to modify the value associated with the key.
Closure parameter must not be leaked
+ * outside the scope of the closure.
+ * @return {@code this} for chaining.
+ *
+ * @throws NullPointerException If one of the parameters is null.
+ * @throws IndexOutOfBoundsException If index is negative of exceeds the
size of the list.
+ * @throws IllegalArgumentException If an element with the given name
already exists.
+ */
+ NamedListChange<Change> create(int index, String key, Consumer<Change>
valConsumer);
+
+ /**
+ * Create a new value after a given precedingKey key in the named list
configuration.
+ *
+ * @param precedingKey Name of the preceding element.
+ * @param key Key for the value to be created.
+ * @param valConsumer Closure to modify the value associated with the key.
Closure parameter must not be leaked
+ * outside the scope of the closure.
+ * @return {@code this} for chaining.
+ *
+ * @throws NullPointerException If one of parameters is null.
+ * @throws IllegalArgumentException If element with given name already
exists
+ * or if {@code precedingKey} element doesn't exist.
+ */
+ NamedListChange<Change> createAfter(String precedingKey, String key,
Consumer<Change> valConsumer);
+
+ /**
+ * Updates a value in the named list configuration. If the value cannot be
found, creates a new one instead.
*
* @param key Key for the value to be updated.
- * @param valConsumer Closure to modify value associated with the key.
Object of type {@code T},
- * passed to the closure, must not be reused anywhere else.
+ * @param valConsumer Closure to modify the value associated with the key.
Closure parameter must not be leaked
+ * outside the scope of the closure.
* @return {@code this} for chaining.
*
- * @throws IllegalStateException If {@link #delete(String)} has been
invoked with the same key previously.
+ * @throws NullPointerException If one of parameters is null.
+ * @throws IllegalArgumentException If {@link #delete(String)} has been
invoked with the same key previously.
*/
- NamedListChange<Change> update(String key, Consumer<Change> valConsumer)
throws IllegalStateException;
+ NamedListChange<Change> createOrUpdate(String key, Consumer<Change>
valConsumer);
/**
* Remove the value from named list configuration.
@@ -50,7 +83,8 @@ public interface NamedListChange<Change> {
* @param key Key for the value to be removed.
* @return {@code this} for chaining.
*
- * @throws IllegalStateException If {@link #update(String, Consumer)} has
been invoked with the same key previously.
+ * @throws NullPointerException If key is null.
+ * @throws IllegalArgumentException If {@link #createOrUpdate(String,
Consumer)} has been invoked with the same key previously.
*/
- NamedListChange<Change> delete(String key) throws IllegalStateException;
+ NamedListChange<Change> delete(String key);
}
diff --git
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
index 5016bfb..1e1c0ea 100644
---
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
+++
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/NamedListView.java
@@ -17,14 +17,14 @@
package org.apache.ignite.configuration;
-import java.util.Set;
+import java.util.List;
/** */
public interface NamedListView<T> {
/**
* @return Immutable collection of keys contained within this list.
*/
- Set<String> namedListKeys();
+ List<String> namedListKeys();
/**
* Returns value associated with the passed key.
diff --git a/modules/configuration/pom.xml b/modules/configuration/pom.xml
index d10e833..9af6f5e 100644
--- a/modules/configuration/pom.xml
+++ b/modules/configuration/pom.xml
@@ -53,5 +53,12 @@
<groupId>com.typesafe</groupId>
<artifactId>config</artifactId>
</dependency>
+
+ <!-- Test dependencies. -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
index ec84b8b..e54121a 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
@@ -50,11 +50,10 @@ import org.jetbrains.annotations.Nullable;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.stream.Collectors.toList;
+import static
org.apache.ignite.internal.configuration.util.ConfigurationFlattener.createFlattenedUpdatesMap;
import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.addDefaults;
-import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.cleanupMatchingValues;
import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.fillFromPrefixMap;
import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.nodePatcher;
-import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.nodeToFlatMap;
import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.patch;
import static
org.apache.ignite.internal.configuration.util.ConfigurationUtil.toPrefixMap;
@@ -290,6 +289,8 @@ public abstract class ConfigurationChanger {
}
};
+ source.reset();
+
source.descend(collector);
assert !storagesTypes.isEmpty();
@@ -357,9 +358,6 @@ public abstract class ConfigurationChanger {
src.descend(changes);
- // It is necessary to reinitialize default values every time.
- // Possible use case that explicitly requires it: creation of
the same named list entry with slightly
- // different set of values and different dynamic defaults at
the same time.
SuperRoot patchedSuperRoot = patch(curRoots, changes);
SuperRoot defaultsNode = new SuperRoot(rootCreator());
@@ -368,9 +366,7 @@ public abstract class ConfigurationChanger {
SuperRoot patchedChanges = patch(changes, defaultsNode);
- cleanupMatchingValues(curRoots, patchedChanges);
-
- Map<String, Serializable> allChanges = nodeToFlatMap(curRoots,
patchedChanges);
+ Map<String, Serializable> allChanges =
createFlattenedUpdatesMap(curRoots, patchedChanges);
// Unlikely but still possible.
if (allChanges.isEmpty())
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
index 7aebf4e..1cb81e4 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
@@ -37,8 +37,7 @@ import
org.apache.ignite.internal.configuration.storage.ConfigurationStorage;
/**
* Configuration manager is responsible for handling configuration lifecycle
and provides configuration API.
*/
-// TODO: IGNITE-14586 Remove @SuppressWarnings when implementation provided.
-@SuppressWarnings("WeakerAccess") public class ConfigurationManager {
+public class ConfigurationManager {
/** Configuration registry. */
private final ConfigurationRegistry confRegistry;
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConfigurationVisitor.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConfigurationVisitor.java
index 825bbbd..58238d6 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConfigurationVisitor.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/ConfigurationVisitor.java
@@ -41,7 +41,7 @@ public interface ConfigurationVisitor<T> {
* @param node Inner configuration node.
* @return Anything that implementation decides to return.
*/
- default T visitInnerNode(String key, InnerNode node) { //TODO IGNITE-14372
Pass interface, not implementation.
+ default T visitInnerNode(String key, InnerNode node) {
return null;
}
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
index 55c8f20..0cd3874 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/NamedListNode.java
@@ -18,22 +18,27 @@
package org.apache.ignite.internal.configuration.tree;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
import java.util.Objects;
-import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.apache.ignite.configuration.NamedListChange;
-import org.apache.ignite.configuration.NamedListView;
-/** */
-public final class NamedListNode<N extends InnerNode> implements
NamedListView<N>, NamedListChange<N>, TraversableTreeNode,
ConstructableTreeNode {
- /** */
- public final Supplier<N> valSupplier;
+/**
+ * Configuration node implementation for the collection of named {@link
InnerNode}s. Unlike implementations of
+ * {@link InnerNode}, this class is used for every named list in configuration.
+ *
+ * @param <N> Type of the {@link InnerNode} that is stored in named list node
object.
+ */
+public final class NamedListNode<N extends InnerNode> implements
NamedListChange<N>, TraversableTreeNode, ConstructableTreeNode {
+ /** Name of a synthetic configuration property that describes the order of
elements in a named list. */
+ public static final String ORDER_IDX = "<idx>";
- /** */
- private final Map<String, N> map;
+ /** Supplier of new node objects when new list element node has to be
created. */
+ private final Supplier<N> valSupplier;
+
+ /** Internal container for named list element. Maps keys to named list
elements nodes. */
+ private final OrderedMap<N> map;
/**
* Default constructor.
@@ -42,7 +47,7 @@ public final class NamedListNode<N extends InnerNode>
implements NamedListView<N
*/
public NamedListNode(Supplier<N> valSupplier) {
this.valSupplier = valSupplier;
- map = new HashMap<>();
+ map = new OrderedMap<>();
}
/**
@@ -52,7 +57,7 @@ public final class NamedListNode<N extends InnerNode>
implements NamedListView<N
*/
private NamedListNode(NamedListNode<N> node) {
valSupplier = node.valSupplier;
- map = new HashMap<>(node.map);
+ map = new OrderedMap<>(node.map);
}
/** {@inheritDoc} */
@@ -61,8 +66,8 @@ public final class NamedListNode<N extends InnerNode>
implements NamedListView<N
}
/** {@inheritDoc} */
- @Override public final Set<String> namedListKeys() {
- return Collections.unmodifiableSet(map.keySet());
+ @Override public final List<String> namedListKeys() {
+ return Collections.unmodifiableList(map.keys());
}
/** {@inheritDoc} */
@@ -76,11 +81,67 @@ public final class NamedListNode<N extends InnerNode>
implements NamedListView<N
}
/** {@inheritDoc} */
- @Override public final NamedListChange<N> update(String key, Consumer<N>
valConsumer) {
+ @Override public NamedListChange<N> create(String key, Consumer<N>
valConsumer) {
+ Objects.requireNonNull(key, "key");
+ Objects.requireNonNull(valConsumer, "valConsumer");
+
+ checkNewKey(key);
+
+ N val = valSupplier.get();
+
+ map.put(key, val);
+
+ valConsumer.accept(val);
+
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override public NamedListChange<N> create(int index, String key,
Consumer<N> valConsumer) {
+ Objects.requireNonNull(key, "key");
+ Objects.requireNonNull(valConsumer, "valConsumer");
+
+ if (index < 0 || index > map.size())
+ throw new IndexOutOfBoundsException(index);
+
+ checkNewKey(key);
+
+ N val = valSupplier.get();
+
+ map.putByIndex(index, key, val);
+
+ valConsumer.accept(val);
+
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override public NamedListChange<N> createAfter(String precedingKey,
String key, Consumer<N> valConsumer) {
+ Objects.requireNonNull(precedingKey, "precedingKey");
+ Objects.requireNonNull(key, "key");
+ Objects.requireNonNull(valConsumer, "valConsumer");
+
+ if (!map.containsKey(precedingKey))
+ throw new IllegalArgumentException("Element with name " +
precedingKey + " doesn't exist.");
+
+ checkNewKey(key);
+
+ N val = valSupplier.get();
+
+ map.putAfter(precedingKey, key, val);
+
+ valConsumer.accept(val);
+
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override public final NamedListChange<N> createOrUpdate(String key,
Consumer<N> valConsumer) {
+ Objects.requireNonNull(key, "key");
Objects.requireNonNull(valConsumer, "valConsumer");
if (map.containsKey(key) && map.get(key) == null)
- throw new IllegalStateException("You can't create entity that has
just been deleted [key=" + key + ']');
+ throw new IllegalArgumentException("You can't create entity that
has just been deleted [key=" + key + ']');
N val = map.get(key);
@@ -94,9 +155,26 @@ public final class NamedListNode<N extends InnerNode>
implements NamedListView<N
return this;
}
+ /**
+ * Checks that this new key can be inserted into the map.
+ * @param key New key.
+ * @throws IllegalArgumentException If key already exists.
+ */
+ private void checkNewKey(String key) {
+ if (map.containsKey(key)) {
+ if (map.get(key) == null)
+ throw new IllegalArgumentException("You can't create entity
that has just been deleted [key=" + key + ']');
+
+ throw new IllegalArgumentException("Element with name " + key + "
already exists.");
+ }
+ }
+
/** {@inheritDoc} */
@Override public NamedListChange<N> delete(String key) {
- map.put(key, null);
+ Objects.requireNonNull(key, "key");
+
+ if (map.containsKey(key))
+ map.put(key, null);
return this;
}
@@ -110,18 +188,13 @@ public final class NamedListNode<N extends InnerNode>
implements NamedListView<N
map.remove(key);
}
- /** {@inheritDoc} */
- @Override public NamedListChange<N> create(String key, Consumer<N>
valConsumer) {
- Objects.requireNonNull(valConsumer, "valConsumer");
-
- N val = map.get(key);
-
- if (val == null)
- map.put(key, val = valSupplier.get());
-
- valConsumer.accept(val);
-
- return this;
+ /**
+ * Reorders keys in the map.
+ *
+ * @param orderedKeys List of keys in new order. Must have the same set of
keys in it.
+ */
+ public void reorderKeys(List<String> orderedKeys) {
+ map.reorderKeys(orderedKeys);
}
/** {@inheritDoc} */
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java
new file mode 100644
index 0000000..d67d3f3
--- /dev/null
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/tree/OrderedMap.java
@@ -0,0 +1,199 @@
+/*
+ * 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.internal.configuration.tree;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Simplified map class that preserves keys order.
+ *
+ * @param <V> Type of the value.
+ */
+class OrderedMap<V> {
+ /** Underlying hash map. */
+ private final Map<String, V> map;
+
+ /** Ordered keys. */
+ private final List<String> orderedKeys;
+
+ /** Default constructor. */
+ OrderedMap() {
+ map = new HashMap<>();
+ orderedKeys = new ArrayList<>();
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param other Source of keys/values to copy from.
+ */
+ OrderedMap(OrderedMap<V> other) {
+ map = new HashMap<>(other.map);
+ orderedKeys = new ArrayList<>(other.orderedKeys);
+ }
+
+ /**
+ * Same as {@link Map#containsKey(Object)}.
+ *
+ * @param key Key to check.
+ * @return {@code true} if map contains the key.
+ */
+ public boolean containsKey(String key) {
+ return map.containsKey(key);
+ }
+
+ /**
+ * Same as {@link Map#get(Object)}.
+ *
+ * @param key Key to search.
+ * @return Value associated with the key or {@code null} is it's not found.
+ */
+ public V get(String key) {
+ return map.get(key);
+ }
+
+ /**
+ * Same as {@link Map#remove(Object)}.
+ *
+ * @param key Key to remove.
+ * @return Previous value associated with the key or {@code null} if the
map had no such key.
+ */
+ public V remove(String key) {
+ V res = map.remove(key);
+
+ if (res != null)
+ orderedKeys.remove(key);
+
+ return res;
+ }
+
+ /**
+ * Inserts a value into the map under the specified key. If the key was
not present in the map, it will be ordered last.
+ * ordering index will be used.
+ *
+ * @param key Key to put.
+ * @param value Value associated with the key.
+ */
+ public void put(String key, V value) {
+ if (map.put(key, value) == null)
+ orderedKeys.add(key);
+ }
+
+ /**
+ * Inserts a value into the map under the specified key. The key will be
positioned at the given index, shifting any
+ * existing values at that position to the right. Key must not be present
in the map when the method is called.
+ *
+ * @param idx Ordering index for the key. Can't be negative. Every value
bigger or equal than {@code size()} is
+ * treated like {@code size()}.
+ * @param key Key to put.
+ * @param value Value associated with the key.
+ */
+ public void putByIndex(int idx, String key, V value) {
+ assert !map.containsKey(key) : key + " " + map;
+
+ if (idx >= orderedKeys.size())
+ orderedKeys.add(key);
+ else
+ orderedKeys.add(idx, key);
+
+ map.put(key, value);
+ }
+
+ /**
+ * Put value to the map at the position after the {@code precedingKey}.
Key must not be present in the map when the
+ * method is called.
+ *
+ * @param precedingKey Previous key for the new key. Last key will be used
if this one is missing from the map.
+ * @param key Key to put.
+ * @param value Value associated with the key.
+ */
+ public void putAfter(String precedingKey, String key, V value) {
+ assert map.containsKey(precedingKey) : precedingKey + " " + map;
+ assert !map.containsKey(key) : key + " " + map;
+
+ int idx = orderedKeys.indexOf(precedingKey);
+
+ putByIndex(idx + 1, key, value);
+ }
+
+ /**
+ * Re-associates the value under the {@code oldKey} to the {@code newKey}.
Does nothing if the {@code oldKey}
+ * is not present in the map.
+ * was missing from the map.
+ *
+ * @param oldKey Old key.
+ * @param newKey New key.
+ *
+ * @throws IllegalArgumentException If both {@code oldKey} and {@code
newKey} already exist in the map.
+ */
+ public void rename(String oldKey, String newKey) {
+ if (!map.containsKey(oldKey))
+ return;
+
+ if (map.containsKey(newKey))
+ throw new IllegalArgumentException();
+
+ int idx = orderedKeys.indexOf(oldKey);
+
+ orderedKeys.set(idx, newKey);
+
+ V value = map.remove(oldKey);
+
+ map.put(newKey, value);
+ }
+
+ /**
+ * @return List of keys.
+ */
+ public List<String> keys() {
+ return new ArrayList<>(orderedKeys);
+ }
+
+ /**
+ * Reorders keys in the map.
+ *
+ * @param orderedKeys List of keys in new order. Must have the same set of
keys in it.
+ */
+ public void reorderKeys(List<String> orderedKeys) {
+ assert map.keySet().equals(Set.copyOf(orderedKeys)) : map.keySet() + "
: " + orderedKeys;
+
+ this.orderedKeys.clear();
+
+ this.orderedKeys.addAll(orderedKeys);
+ }
+
+ /**
+ * @return Size of the map.
+ */
+ public int size() {
+ return map.size();
+ }
+
+ /**
+ * Clears the map.
+ */
+ public void clear() {
+ map.clear();
+
+ orderedKeys.clear();
+ }
+}
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
new file mode 100644
index 0000000..61600c9
--- /dev/null
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
@@ -0,0 +1,212 @@
+/*
+ * 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.internal.configuration.util;
+
+import java.io.Serializable;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.ignite.internal.configuration.SuperRoot;
+import org.apache.ignite.internal.configuration.tree.InnerNode;
+import org.apache.ignite.internal.configuration.tree.NamedListNode;
+
+/** Utility class that has {@link
ConfigurationFlattener#createFlattenedUpdatesMap(SuperRoot, SuperRoot)} method.
*/
+public class ConfigurationFlattener {
+ /**
+ * Convert a traversable tree to a map of qualified keys to values.
+ *
+ * @param curRoot Current root tree.
+ * @param updates Tree with updates.
+ * @return Map of changes.
+ */
+ public static Map<String, Serializable>
createFlattenedUpdatesMap(SuperRoot curRoot, SuperRoot updates) {
+ // Resulting map.
+ Map<String, Serializable> resMap = new HashMap<>();
+
+ // This method traverses two trees at the same time. In order to reuse
the visitor object it's decided to
+ // use an explicit stack for nodes of the "old" tree. We need to reuse
the visitor object because it accumulates
+ // "full" keys in dot-separated notation. Please refer to
KeysTrackingConfigurationVisitor for details..
+ Deque<InnerNode> oldInnerNodesStack = new ArrayDeque<>();
+
+ oldInnerNodesStack.push(curRoot);
+
+ // Explicit access to the children of super root guarantees that
"oldInnerNodesStack" is never empty and thus
+ // we don't need null-checks when calling Deque#peek().
+ updates.traverseChildren(new FlattenerVisitor(oldInnerNodesStack,
resMap));
+
+ assert oldInnerNodesStack.peek() == curRoot : oldInnerNodesStack;
+
+ return resMap;
+ }
+
+ /**
+ * @param node Named list node.
+ * @return Map that contains same keys and their positions as values.
+ */
+ private static Map<String, Integer> keysToOrderIdx(NamedListNode<?> node) {
+ Map<String, Integer> res = new HashMap<>();
+
+ int idx = 0;
+
+ for (String key : node.namedListKeys()) {
+ if (node.get(key) != null)
+ res.put(key, idx++);
+ }
+
+ return res;
+ }
+
+ /**
+ * Visitor that collects diff between "old" and "new" trees into a flat
map.
+ */
+ private static class FlattenerVisitor extends
KeysTrackingConfigurationVisitor<Object> {
+ /** Old nodes stack for recursion. */
+ private final Deque<InnerNode> oldInnerNodesStack;
+
+ /** Map with the result. */
+ private final Map<String, Serializable> resMap;
+
+ /** Flag indicates that "old" and "new" trees are literally the same
at the moment. */
+ private boolean singleTreeTraversal;
+
+ /**
+ * Makes sense only if {@link #singleTreeTraversal} is {@code true}.
Helps distinguishing creation from
+ * deletion. Always {@code false} if {@link #singleTreeTraversal} is
{@code false}.
+ */
+ private boolean deletion;
+
+ FlattenerVisitor(Deque<InnerNode> oldInnerNodesStack, Map<String,
Serializable> resMap) {
+ this.oldInnerNodesStack = oldInnerNodesStack;
+ this.resMap = resMap;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Void doVisitLeafNode(String key, Serializable newVal)
{
+ // Read same value from old tree.
+ Serializable oldVal = oldInnerNodesStack.peek().traverseChild(key,
ConfigurationUtil.leafNodeVisitor());
+
+ // Do not put duplicates into the resulting map.
+ if (singleTreeTraversal || !Objects.deepEquals(oldVal, newVal))
+ resMap.put(currentKey(), deletion ? null : newVal);
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Void doVisitInnerNode(String key, InnerNode newNode) {
+ // Read same node from old tree.
+ InnerNode oldNode = oldInnerNodesStack.peek().traverseChild(key,
ConfigurationUtil.innerNodeVisitor());
+
+ // Skip subtree that has not changed.
+ if (oldNode == newNode && !singleTreeTraversal)
+ return null;
+
+ if (oldNode == null)
+ visitAsymmetricInnerNode(newNode, false);
+ else {
+ oldInnerNodesStack.push(oldNode);
+
+ newNode.traverseChildren(this);
+
+ oldInnerNodesStack.pop();
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <N extends InnerNode> Void
doVisitNamedListNode(String key, NamedListNode<N> newNode) {
+ // Read same named list node from old tree.
+ NamedListNode<?> oldNode =
oldInnerNodesStack.peek().traverseChild(key,
ConfigurationUtil.namedListNodeVisitor());
+
+ // Skip subtree that has not changed.
+ if (oldNode == newNode && !singleTreeTraversal)
+ return null;
+
+ // Old keys ordering can be ignored if we either create or delete
everything.
+ Map<String, Integer> oldKeysToOrderIdxMap = singleTreeTraversal ?
null
+ : keysToOrderIdx(oldNode);
+
+ // New keys ordering can be ignored if we delete everything.
+ Map<String, Integer> newKeysToOrderIdxMap = deletion ? null
+ : keysToOrderIdx(newNode);
+
+ for (String namedListKey : newNode.namedListKeys()) {
+ withTracking(namedListKey, true, false, () -> {
+ InnerNode oldNamedElement = oldNode.get(namedListKey);
+ InnerNode newNamedElement = newNode.get(namedListKey);
+
+ // Deletion of nonexistent element.
+ if (oldNamedElement == null && newNamedElement == null)
+ return null;
+
+ // Skip element that has not changed.
+ // Its index can be different though, so we don't
"continue" straight away.
+ if (singleTreeTraversal || oldNamedElement !=
newNamedElement) {
+ if (newNamedElement == null)
+ visitAsymmetricInnerNode(oldNamedElement, true);
+ else if (oldNamedElement == null)
+ visitAsymmetricInnerNode(newNamedElement, false);
+ else {
+ oldInnerNodesStack.push(oldNamedElement);
+
+ newNamedElement.traverseChildren(this);
+
+ oldInnerNodesStack.pop();
+ }
+ }
+
+ Integer newIdx = newKeysToOrderIdxMap == null ? null :
newKeysToOrderIdxMap.get(namedListKey);
+ Integer oldIdx = oldKeysToOrderIdxMap == null ? null :
oldKeysToOrderIdxMap.get(namedListKey);
+
+ // We should "persist" changed indexes only.
+ if (newIdx != oldIdx || singleTreeTraversal ||
newNamedElement == null) {
+ String orderKey = currentKey() +
NamedListNode.ORDER_IDX;
+
+ resMap.put(orderKey, deletion || newNamedElement ==
null ? null : newIdx);
+ }
+
+ return null;
+ });
+ }
+
+ return null;
+ }
+
+ /**
+ * Here we must list all joined keys belonging to deleted or created
element. The only way to do it is to
+ * traverse the entire configuration tree unconditionally.
+ */
+ private void visitAsymmetricInnerNode(InnerNode node, boolean delete) {
+ assert !singleTreeTraversal;
+ assert node != null;
+
+ oldInnerNodesStack.push(node);
+ singleTreeTraversal = true;
+ deletion = delete;
+
+ node.traverseChildren(this);
+
+ deletion = false;
+ singleTreeTraversal = false;
+ oldInnerNodesStack.pop();
+ }
+ }
+}
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
index bbdb88b..34b0558 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationNotificationsUtil.java
@@ -98,8 +98,8 @@ public class ConfigurationNotificationsUtil {
// This is optimization, we could use
"NamedListConfiguration#get" directly, but we don't want to.
- Set<String> oldNames = oldNamedList.namedListKeys();
- Set<String> newNames = newNamedList.namedListKeys();
+ List<String> oldNames = oldNamedList.namedListKeys();
+ List<String> newNames = newNamedList.namedListKeys();
Map<String, ConfigurationProperty<?, ?>>
namedListCfgMembers = namedListCfg.members();
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
index 8e89b16..58e3a9d 100644
---
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
@@ -21,15 +21,13 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
-import java.util.Objects;
import java.util.RandomAccess;
import java.util.stream.Collectors;
+import org.apache.ignite.configuration.NamedListView;
import org.apache.ignite.configuration.RootKey;
-import org.apache.ignite.internal.configuration.SuperRoot;
import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
import org.apache.ignite.internal.configuration.tree.ConfigurationVisitor;
import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
@@ -252,22 +250,24 @@ public class ConfigurationUtil {
/** {@inheritDoc} */
@Override public void descend(ConstructableTreeNode node) {
+ if (node instanceof NamedListNode) {
+ descendToNamedListNode((NamedListNode<?>)node);
+
+ return;
+ }
+
for (Map.Entry<String, ?> entry : map.entrySet()) {
String key = entry.getKey();
Object val = entry.getValue();
assert val == null || val instanceof Map || val instanceof
Serializable;
- if (val == null) {
- if (node instanceof NamedListNode) {
- // Given that this particular method is applied to
modify existing trees rather than
- // creating new trees, a "hack" is required in
this place. "construct" is designed to create
- // "change" objects, thus it would just nullify
named list element instead of deleting it.
- ((NamedListNode<?>)node).forceDelete(key);
- }
- else
- node.construct(key, null);
- }
+ // Ordering of indexes must be skipped here because they
make no sense in this context.
+ if (key.equals(NamedListNode.ORDER_IDX))
+ continue;
+
+ if (val == null)
+ node.construct(key, null);
else if (val instanceof Map)
node.construct(key, new
InnerConfigurationSource((Map<String, ?>)val));
else {
@@ -277,101 +277,73 @@ public class ConfigurationUtil {
}
}
}
- }
-
- var src = new InnerConfigurationSource(prefixMap);
-
- src.descend(node);
- }
-
- /**
- * Convert a traversable tree to a map of qualified keys to values.
- *
- * @param curRoot Current root tree.
- * @param updates Tree with updates.
- * @return Map of changes.
- */
- public static Map<String, Serializable> nodeToFlatMap(
- SuperRoot curRoot,
- SuperRoot updates
- ) {
- Map<String, Serializable> values = new HashMap<>();
- updates.traverseChildren(new KeysTrackingConfigurationVisitor<>() {
- /** Write nulls instead of actual values. Makes sense for
deletions from named lists. */
- private boolean writeNulls;
-
- /** {@inheritDoc} */
- @Override public Map<String, Serializable> doVisitLeafNode(String
key, Serializable val) {
- if (val != null)
- values.put(currentKey(), writeNulls ? null : val);
+ /**
+ * Specific implementation of {@link
#descend(ConstructableTreeNode)} that descends into named list node and
+ * sets a proper ordering to named list elements.
+ *
+ * @param node Named list node under construction.
+ */
+ private void descendToNamedListNode(NamedListNode<?> node) {
+ // This list must be mutable and RandomAccess.
+ var orderedKeys = new
ArrayList<>(((NamedListView<?>)node).namedListKeys());
- return values;
- }
+ for (Map.Entry<String, ?> entry : map.entrySet()) {
+ String key = entry.getKey();
+ Object val = entry.getValue();
- /** {@inheritDoc} */
- @Override public Map<String, Serializable> doVisitInnerNode(String
key, InnerNode node) {
- if (node == null)
- return null;
+ assert val == null || val instanceof Map || val instanceof
Serializable;
- node.traverseChildren(this);
+ if (val == null) {
+ // Given that this particular method is applied to
modify existing trees rather than
+ // creating new trees, a "hack" is required in this
place. "construct" is designed to create
+ // "change" objects, thus it would just nullify named
list element instead of deleting it.
+ node.forceDelete(key);
+ }
+ else if (val instanceof Map) {
+ // For every named list entry modification we must
take its index into account.
+ // We do this by modifying "orderedKeys" when index is
explicitly passed.
+ Object idxObj = ((Map<?,
?>)val).get(NamedListNode.ORDER_IDX);
- return values;
- }
+ if (idxObj != null) {
+ assert idxObj instanceof Integer : val;
- /** {@inheritDoc} */
- @Override public <N extends InnerNode> Map<String, Serializable>
doVisitNamedListNode(String key, NamedListNode<N> node) {
- for (String namedListKey : node.namedListKeys()) {
- N namedElement = node.get(namedListKey);
-
- withTracking(namedListKey, true, false, () -> {
- if (namedElement == null)
- visitDeletedNamedListElement();
- else
- namedElement.traverseChildren(this);
-
- return null;
- });
- }
+ int idx = (Integer)idxObj;
- return values;
- }
+ if (idx >= orderedKeys.size()) {
+ // Updates can come in arbitrary order. This
means that array may be too small
+ // during batch creation. In this case we have
to insert enough nulls before
+ // invoking "add" method for actual key.
+ orderedKeys.ensureCapacity(idx + 1);
- /**
- * Here we must list all joined keys belonging to deleted element.
The only way to do it is to traverse
- * cached configuration tree and write {@code null} into all
values.
- */
- private void visitDeletedNamedListElement() {
- // It must be impossible to delete something inside of the
element that we're currently deleting.
- assert !writeNulls;
+ while (idx != orderedKeys.size())
+ orderedKeys.add(null);
- Object originalNamedElement = null;
+ orderedKeys.add(key);
+ }
+ else
+ orderedKeys.set(idx, key);
+ }
- List<String> currentPath = currentPath();
+ node.construct(key, new
InnerConfigurationSource((Map<String, ?>)val));
+ }
+ else {
+ assert val instanceof Serializable;
- try {
- // This code can in fact be better optimized for deletion
scenario,
- // but there's no point in doing that, since the operation
is so rare and it will
- // complicate code even more.
- originalNamedElement = find(currentPath, curRoot);
- }
- catch (KeyNotFoundException ignore) {
- // May happen, not a big deal. This means that element
never existed in the first place.
+ node.construct(key, new
LeafConfigurationSource((Serializable)val));
+ }
}
- if (originalNamedElement != null) {
- assert originalNamedElement instanceof InnerNode :
currentPath;
-
- writeNulls = true;
-
- ((InnerNode)originalNamedElement).traverseChildren(this);
-
- writeNulls = false;
- }
+ node.reorderKeys(orderedKeys.size() > node.size()
+ ? orderedKeys.subList(0, node.size())
+ : orderedKeys
+ );
}
- });
+ }
- return values;
+ var src = new InnerConfigurationSource(prefixMap);
+
+ src.descend(node);
}
/**
@@ -539,65 +511,6 @@ public class ConfigurationUtil {
};
}
- /**
- * Nullifies leaves in {@code newNode} node that are equal to the
corresponding leaf values in {@code curNode}.
- * In this context we view {@code curNode} as a full configuration node
with all the data, while {@code newNode}
- * contains only updates that we plan to apply to the {@code curNode} in
the future.
- * @param curNode Node to look for definitive values.
- * @param newNode Node to nullify matching values.
- */
- public static void cleanupMatchingValues(InnerNode curNode, InnerNode
newNode) {
- if (curNode == null || newNode == null)
- return;
-
- assert curNode.getClass() == newNode.getClass();
-
- newNode.traverseChildren(new ConfigurationVisitor<Void>() {
- /** {@inheritDoc} */
- @Override public Void visitLeafNode(String key, Serializable
newVal) {
- if (Objects.deepEquals(curNode.traverseChild(key,
leafNodeVisitor()), newVal))
- newNode.construct(key, null);
-
- return null;
- }
-
- /** {@inheritDoc} */
- @Override public Void visitInnerNode(String key, InnerNode
newInnerNode) {
- InnerNode oldInnerNode = curNode.traverseChild(key,
innerNodeVisitor());
-
- if (oldInnerNode == newInnerNode)
- newNode.construct(key, null);
- else
- cleanupMatchingValues(oldInnerNode, newInnerNode);
-
- return null;
- }
-
- /** {@inheritDoc} */
- @Override public <N extends InnerNode> Void
visitNamedListNode(String key, NamedListNode<N> newNamedList) {
- NamedListNode<?> curNamedList = curNode.traverseChild(key,
namedListNodeVisitor());
-
- if (curNamedList == newNamedList) {
- newNode.construct(key, null);
-
- return null;
- }
-
- for (String name : new
LinkedHashSet<>(newNamedList.namedListKeys())) {
- InnerNode oldElement = curNamedList.get(name);
- N newElement = newNamedList.get(name);
-
- if (oldElement == newElement)
- newNamedList.forceDelete(name);
- else
- cleanupMatchingValues(oldElement, newElement);
- }
-
- return null;
- }
- });
- }
-
/** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
private static class PatchLeafConfigurationSource implements
ConfigurationSource {
/** */
@@ -644,6 +557,9 @@ public class ConfigurationUtil {
@Override public void descend(ConstructableTreeNode dstNode) {
assert srcNode.getClass() == dstNode.getClass() :
srcNode.getClass() + " : " + dstNode.getClass();
+ if (srcNode == dstNode)
+ return;
+
srcNode.traverseChildren(new ConfigurationVisitor<>() {
@Override public Void visitLeafNode(String key, Serializable
val) {
if (val != null)
@@ -690,12 +606,15 @@ public class ConfigurationUtil {
@Override public void descend(ConstructableTreeNode dstNode) {
assert srcNode.getClass() == dstNode.getClass();
+ if (srcNode == dstNode)
+ return;
+
for (String key : srcNode.namedListKeys()) {
InnerNode node = srcNode.get(key);
if (node == null)
((NamedListNode<?>)dstNode).forceDelete(key); // Same as
in fillFromPrefixMap.
- else
+ else if (((NamedListView<?>)dstNode).get(key) != node)
dstNode.construct(key, new
PatchInnerConfigurationSource(node));
}
}
diff --git
a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java
new file mode 100644
index 0000000..580d181
--- /dev/null
+++
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/OrderedMapTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.internal.configuration.tree;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/** Test with basic {@link OrderedMap} invariants. */
+public class OrderedMapTest {
+ /** Map instance. */
+ private final OrderedMap<String> map = new OrderedMap<>();
+
+ /** Tests put/get/remove consistency on a single key. */
+ @Test
+ public void putGetRemove() {
+ assertNull(map.get("key1"));
+
+ map.put("key", "value");
+
+ assertEquals(1, map.size());
+ assertEquals("value", map.get("key"));
+
+ map.remove("key");
+
+ assertNull(map.get("key1"));
+ }
+
+ /** Tests that {@link OrderedMap#put(String, Object)} preserves order. */
+ @Test
+ public void keysOrder() {
+ map.put("key1", "value1");
+ map.put("key2", "value2");
+
+ assertEquals(2, map.size());
+ assertEquals(List.of("key1", "key2"), map.keys());
+
+ map.clear();
+
+ assertEquals(0, map.size());
+ assertEquals(List.of(), map.keys());
+
+ map.put("key2", "value2");
+ map.put("key1", "value1");
+
+ assertEquals(2, map.size());
+ assertEquals(List.of("key2", "key1"), map.keys());
+
+ map.put("key2", "value3");
+ assertEquals(List.of("key2", "key1"), map.keys());
+ }
+
+ /** Tests that {@link OrderedMap#putByIndex(int, String, Object)}
preserves order. */
+ @Test
+ public void putByIndex() {
+ map.putByIndex(0, "key1", "value1");
+
+ map.putByIndex(0, "key2", "value2");
+
+ assertEquals(List.of("key2", "key1"), map.keys());
+
+ map.putByIndex(1, "key3", "value3");
+
+ assertEquals(List.of("key2", "key3", "key1"), map.keys());
+
+ map.putByIndex(3, "key4", "value4");
+
+ assertEquals(List.of("key2", "key3", "key1", "key4"), map.keys());
+
+ map.putByIndex(5, "key5", "value5");
+
+ assertEquals(List.of("key2", "key3", "key1", "key4", "key5"),
map.keys());
+ }
+
+ /** Tests that {@link OrderedMap#putAfter(String, String, Object)}
preserves order. */
+ @Test
+ public void putAfter() {
+ map.put("key1", "value1");
+
+ assertEquals(List.of("key1"), map.keys());
+
+ map.putAfter("key1", "key2", "value2");
+
+ assertEquals(List.of("key1", "key2"), map.keys());
+
+ map.putAfter("key1", "key3", "value3");
+
+ assertEquals(List.of("key1", "key3", "key2"), map.keys());
+
+ map.putAfter("key2", "key4", "value4");
+
+ assertEquals(List.of("key1", "key3", "key2", "key4"), map.keys());
+ }
+
+ /** Tests basic invariants of {@link OrderedMap#rename(String, String)}
method. */
+ @Test
+ public void rename() {
+ map.put("key1", "value1");
+ map.put("key2", "value2");
+ map.put("key3", "value3");
+
+ map.rename("key2", "key4");
+
+ assertEquals("value2", map.get("key4"));
+
+ assertFalse(map.containsKey("key2"));
+
+ assertEquals(List.of("key1", "key4", "key3"), map.keys());
+ }
+
+ /** Tests that {@link OrderedMap#reorderKeys(List)} reorders keys
properly. */
+ @Test
+ public void reorderKeys() {
+ map.put("key1", "value1");
+ map.put("key2", "value2");
+ map.put("key3", "value3");
+
+ assertEquals(List.of("key1", "key2", "key3"), map.keys());
+
+ map.reorderKeys(List.of("key2", "key1", "key3"));
+
+ assertEquals(List.of("key2", "key1", "key3"), map.keys());
+
+ map.reorderKeys(List.of("key2", "key3", "key1"));
+
+ assertEquals(List.of("key2", "key3", "key1"), map.keys());
+
+ map.reorderKeys(List.of("key1", "key3", "key2"));
+
+ assertEquals(List.of("key1", "key3", "key2"), map.keys());
+ }
+}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/AbstractSchemaChangeTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/AbstractSchemaChangeTest.java
index 8070888..fbe794c 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/AbstractSchemaChangeTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/AbstractSchemaChangeTest.java
@@ -119,7 +119,7 @@ abstract class AbstractSchemaChangeTest {
});
tblChanger.changeColumns(listChanger ->
- listChanger.update(colKey, colChanger ->
colChanger.changeType(c -> c.changeType("STRING")))
+ listChanger.createOrUpdate(colKey, colChanger ->
colChanger.changeType(c -> c.changeType("STRING")))
);
})
);
@@ -147,7 +147,7 @@ abstract class AbstractSchemaChangeTest {
});
tblChanger.changeColumns(listChanger ->
- listChanger.update(colKey, colChanger ->
colChanger.changeNullable(false))
+ listChanger.createOrUpdate(colKey, colChanger ->
colChanger.changeNullable(false))
);
})
);
@@ -175,7 +175,7 @@ abstract class AbstractSchemaChangeTest {
});
tblChanger.changeColumns(listChanger ->
- listChanger.update(colKey, colChanger ->
colChanger.changeNullable(true))
+ listChanger.createOrUpdate(colKey, colChanger ->
colChanger.changeNullable(true))
);
})
);
@@ -258,7 +258,7 @@ abstract class AbstractSchemaChangeTest {
});
tblChanger.changeColumns(listChanger ->
- listChanger.update(colKey, colChanger ->
colChanger.changeName(newName))
+ listChanger.createOrUpdate(colKey, colChanger ->
colChanger.changeName(newName))
);
})
);
@@ -280,7 +280,7 @@ abstract class AbstractSchemaChangeTest {
});
tblChanger.changeColumns(listChanger ->
- listChanger.update(colKey, colChanger ->
colChanger.changeDefaultValue(defSup.get().toString()))
+ listChanger.createOrUpdate(colKey, colChanger ->
colChanger.changeDefaultValue(defSup.get().toString()))
);
})
);
diff --git
a/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaConfigurationConverterTest.java
b/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaConfigurationConverterTest.java
index 8ca9951..0237f60 100644
---
a/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaConfigurationConverterTest.java
+++
b/modules/schema/src/test/java/org/apache/ignite/internal/schema/configuration/SchemaConfigurationConverterTest.java
@@ -83,7 +83,7 @@ public class SchemaConfigurationConverterTest {
confRegistry.getConfiguration(TablesConfiguration.KEY).change(
ch -> {
SchemaConfigurationConverter.createTable(tbl, ch)
- .changeTables(tblsCh -> tblsCh.create(tbl.canonicalName(),
+ .changeTables(tblsCh ->
tblsCh.createOrUpdate(tbl.canonicalName(),
tblCh -> tblCh.changeReplicas(1)));
}).get();
}
diff --git
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
index 116d4ea..b8e8a7e 100644
---
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
+++
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java
@@ -575,7 +575,7 @@ public class TableManager extends Producer<TableEvent,
TableEventParameters> imp
try {
configurationMgr.configurationRegistry()
.getConfiguration(TablesConfiguration.KEY).tables().change(change ->
- change.update(name, tableChange)).get();
+ change.createOrUpdate(name, tableChange)).get();
}
catch (InterruptedException | ExecutionException e) {
LOG.error("Table wasn't created [name=" + name + ']', e);