This is an automated email from the ASF dual-hosted git repository.
apkhmv 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 4d09fd1644 IGNITE-18729 Add code deployment API (#1698)
4d09fd1644 is described below
commit 4d09fd164455b875ca03e86c93a5a06a0bf2e084
Author: Mikhail <[email protected]>
AuthorDate: Wed Mar 1 12:16:23 2023 +0300
IGNITE-18729 Add code deployment API (#1698)
Co-authored-by: Mikhail Pochatkin <[email protected]>
Co-authored-by: Pavel Tupitsyn <[email protected]>
---
.../src/main/java/org/apache/ignite/Ignite.java | 8 +
.../apache/ignite/deployment/DeploymentUnit.java | 39 ++
.../apache/ignite/deployment/IgniteDeployment.java | 96 +++++
.../org/apache/ignite/deployment/UnitStatus.java | 159 ++++++++
.../ignite/deployment/version/UnitVersion.java | 127 ++++++
.../apache/ignite/deployment/version/Version.java | 65 +++
.../deployment/version/VersionParseException.java | 48 +++
.../ignite/deployment/version/VersionUnitTest.java | 48 +++
.../repl/executor/ItIgnitePicocliCommandsTest.java | 4 +-
.../ignite/internal/client/TcpIgniteClient.java | 7 +
.../org/apache/ignite/client/fakes/FakeIgnite.java | 6 +
.../management/ClusterManagementGroupManager.java | 19 +
modules/code-deployment/build.gradle | 42 ++
.../internal/deployunit/DeploymentManagerImpl.java | 437 +++++++++++++++++++++
.../ignite/internal/deployunit/UnitMeta.java | 143 +++++++
.../internal/deployunit/UnitMetaSerializer.java | 93 +++++
.../DeploymentConfigurationModule.java | 41 ++
.../DeploymentConfigurationSchema.java | 34 ++
.../DeploymentUnitAlreadyExistsException.java | 48 +++
.../exception/DeploymentUnitNotFoundException.java | 35 ++
.../exception/DeploymentUnitReadException.java | 42 ++
.../deployunit/message/DeployUnitMessageTypes.java | 47 +++
.../deployunit/message/DeployUnitRequest.java | 57 +++
.../deployunit/message/DeployUnitResponse.java | 36 ++
.../deployunit/message/UndeployUnitRequest.java | 41 ++
.../deployunit/message/UndeployUnitResponse.java | 36 ++
.../ignite/deployment/UnitMetaSerializerTest.java | 82 ++++
.../apache/ignite/internal/util/IgniteUtils.java | 49 ++-
.../java/org/apache/ignite/lang/ErrorGroups.java | 25 ++
modules/runner/build.gradle | 2 +
.../internal/deployment/ItDeploymentUnitTest.java | 216 ++++++++++
.../org/apache/ignite/internal/app/IgniteImpl.java | 20 +-
settings.gradle | 2 +
33 files changed, 2131 insertions(+), 23 deletions(-)
diff --git a/modules/api/src/main/java/org/apache/ignite/Ignite.java
b/modules/api/src/main/java/org/apache/ignite/Ignite.java
index f01f8779da..4d36bbddaf 100644
--- a/modules/api/src/main/java/org/apache/ignite/Ignite.java
+++ b/modules/api/src/main/java/org/apache/ignite/Ignite.java
@@ -21,6 +21,7 @@ import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import org.apache.ignite.compute.ComputeJob;
import org.apache.ignite.compute.IgniteCompute;
+import org.apache.ignite.deployment.IgniteDeployment;
import org.apache.ignite.network.ClusterNode;
import org.apache.ignite.sql.IgniteSql;
import org.apache.ignite.table.manager.IgniteTables;
@@ -67,6 +68,13 @@ public interface Ignite extends AutoCloseable {
*/
IgniteCompute compute();
+ /**
+ * Returns {@link IgniteDeployment} which can be used to deploy units.
+ *
+ * @return Deployment management object.
+ */
+ IgniteDeployment deployment();
+
/**
* Gets the cluster nodes.
* NOTE: Temporary API to enable Compute until we have proper Cluster API.
diff --git
a/modules/api/src/main/java/org/apache/ignite/deployment/DeploymentUnit.java
b/modules/api/src/main/java/org/apache/ignite/deployment/DeploymentUnit.java
new file mode 100644
index 0000000000..75660a9121
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/deployment/DeploymentUnit.java
@@ -0,0 +1,39 @@
+/*
+ * 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.deployment;
+
+import java.io.InputStream;
+
+/**
+ * Deployment unit interface.
+ */
+public interface DeploymentUnit {
+ /**
+ * Unit name.
+ *
+ * @return Name of deployment unit.
+ */
+ String name();
+
+ /**
+ * Input stream with deployment unit content.
+ *
+ * @return input stream with deployment unit content.
+ */
+ InputStream content();
+}
diff --git
a/modules/api/src/main/java/org/apache/ignite/deployment/IgniteDeployment.java
b/modules/api/src/main/java/org/apache/ignite/deployment/IgniteDeployment.java
new file mode 100644
index 0000000000..5d8e17eab2
--- /dev/null
+++
b/modules/api/src/main/java/org/apache/ignite/deployment/IgniteDeployment.java
@@ -0,0 +1,96 @@
+/*
+ * 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.deployment;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.deployment.version.Version;
+
+/**
+ * Provides access to the Deployment Unit functionality.
+ */
+public interface IgniteDeployment {
+ /**
+ * Deploy provided unit to current node with latest version.
+ *
+ * @param id Unit identifier. Not empty and not null.
+ * @param deploymentUnit Unit content.
+ * @return Future with success or not result.
+ */
+ default CompletableFuture<Boolean> deployAsync(String id, DeploymentUnit
deploymentUnit) {
+ return deployAsync(id, Version.LATEST, deploymentUnit);
+ }
+
+ /**
+ * Deploy provided unit to current node.
+ * After deploy finished, this deployment unit will be place to CMG group
asynchronously.
+ *
+ * @param id Unit identifier. Not empty and not null.
+ * @param version Unit version.
+ * @param deploymentUnit Unit content.
+ * @return Future with success or not result.
+ */
+ CompletableFuture<Boolean> deployAsync(String id, Version version,
DeploymentUnit deploymentUnit);
+
+ /**
+ * Undeploy latest version of unit with corresponding identifier.
+ *
+ * @param id Unit identifier. Not empty and not null.
+ * @return Future completed when unit will be undeployed.
+ * In case when specified unit not exist future will be failed.
+ */
+ default CompletableFuture<Void> undeployAsync(String id) {
+ return undeployAsync(id, Version.LATEST);
+ }
+
+ /**
+ * Undeploy unit with corresponding identifier and version.
+ * Note that unit files will be deleted asynchronously.
+ *
+ * @param id Unit identifier.
+ * @param version Unit version.
+ * @return Future completed when unit will be undeployed.
+ * In case when specified unit not exist future will be failed.
+ */
+ CompletableFuture<Void> undeployAsync(String id, Version version);
+
+ /**
+ * Lists all deployed units.
+ *
+ * @return Future with result.
+ */
+ CompletableFuture<List<UnitStatus>> unitsAsync();
+
+ /**
+ * List all deployed versions of the specified unit.
+ *
+ * @param id Unit identifier. Not empty and not null.
+ * @return Future with list of all available version of unit.
+ * In case when unit with specified identifier not exist future list
will be empty.
+ */
+ CompletableFuture<List<Version>> versionsAsync(String id);
+
+ /**
+ * Return status of unit with provided identifier.
+ *
+ * @param id Unit identifier. Not empty and not null.
+ * @return Future with unit status.
+ * Future will be failed if unit with specified identifier not exist.
+ */
+ CompletableFuture<UnitStatus> statusAsync(String id);
+}
diff --git
a/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java
b/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java
new file mode 100644
index 0000000000..771a0fd8db
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/deployment/UnitStatus.java
@@ -0,0 +1,159 @@
+/*
+ * 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.deployment;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.ignite.deployment.version.Version;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * Deployment unit status.
+ */
+public class UnitStatus {
+ /**
+ * Unit identifier.
+ */
+ private final String id;
+
+ /**
+ * Map from existing unit version to list of nodes consistent ids where
unit deployed.
+ */
+ private final Map<Version, List<String>> versionToConsistentIds;
+
+ /**
+ * Constructor.
+ *
+ * @param id Unit identifier.
+ * @param versionToConsistentIds Map from existing unit version to list
+ * of nodes consistent ids where unit deployed.
+ */
+ private UnitStatus(String id, Map<Version, List<String>>
versionToConsistentIds) {
+ this.id = id;
+ this.versionToConsistentIds =
Collections.unmodifiableMap(versionToConsistentIds);
+ }
+
+ /**
+ * Returns unit identifier.
+ *
+ * @return unit identifier.
+ */
+ public String id() {
+ return id;
+ }
+
+ /**
+ * Returns unit version.
+ *
+ * @return unit version.
+ */
+ public Set<Version> versions() {
+ return Collections.unmodifiableSet(versionToConsistentIds.keySet());
+ }
+
+ /**
+ * Returns consistent ids of nodes for provided version.
+ *
+ * @param version Unit version.
+ * @return consistent ids of nodes for provided version.
+ */
+ public List<String> consistentIds(Version version) {
+ return
Collections.unmodifiableList(versionToConsistentIds.get(version));
+ }
+
+ /**
+ * Builder provider.
+ *
+ * @param id Identifier of unit. Not null and not blank.
+ * @return Instance of {@link UnitStatusBuilder}.
+ */
+ public static UnitStatusBuilder builder(String id) {
+ Objects.requireNonNull(id);
+
+ return new UnitStatusBuilder(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ UnitStatus that = (UnitStatus) o;
+ return Objects.equals(id, that.id) &&
Objects.equals(versionToConsistentIds, that.versionToConsistentIds);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, versionToConsistentIds);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return S.toString(this);
+ }
+
+ /**
+ * Builder for {@link UnitStatus}.
+ */
+ public static class UnitStatusBuilder {
+
+ private final String id;
+ private final Map<Version, List<String>> versionToConsistentIds = new
HashMap<>();
+
+ /**
+ * Constructor.
+ *
+ * @param id unit identifier.
+ */
+ public UnitStatusBuilder(String id) {
+ this.id = id;
+ }
+
+ /**
+ * Append node consistent ids with provided version.
+ *
+ * @param version Unit version.
+ * @param consistentIds Node consistent ids.
+ * @return {@code this} builder for use in a chained invocation.
+ */
+ public UnitStatusBuilder append(Version version, List<String>
consistentIds) {
+ versionToConsistentIds.put(version, consistentIds);
+ return this;
+ }
+
+ /**
+ * Builder status method.
+ *
+ * @return {@link UnitStatus} instance.
+ */
+ public UnitStatus build() {
+ return new UnitStatus(id, versionToConsistentIds);
+ }
+ }
+}
+
diff --git
a/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java
b/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java
new file mode 100644
index 0000000000..02b2df683d
--- /dev/null
+++
b/modules/api/src/main/java/org/apache/ignite/deployment/version/UnitVersion.java
@@ -0,0 +1,127 @@
+/*
+ * 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.deployment.version;
+
+import java.util.Objects;
+
+/**
+ * Implementation of {@link Version} interface based on the three numbers
format,
+ * like x.x.x. where x is short number.
+ */
+public class UnitVersion implements Version {
+ private final short major;
+ private final short minor;
+ private final short patch;
+
+ /**
+ * Constructor.
+ *
+ * @param major Major part of version.
+ * @param minor Minor part of version.
+ * @param patch Patch part of version.
+ */
+ public UnitVersion(short major, short minor, short patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ }
+
+ @Override
+ public String render() {
+ return major + "." + minor + "." + patch;
+ }
+
+ /**
+ * Parse string representation of version to {@link UnitVersion} if
possible.
+ *
+ * @param s String representation of version.
+ * @return Instance of {@link UnitVersion}.
+ * @throws VersionParseException in case when string is not required
{@link UnitVersion} format.
+ */
+ public static UnitVersion parse(String s) {
+ Objects.requireNonNull(s);
+ try {
+ String[] split = s.split("\\.", -1);
+ if (split.length > 3 || split.length == 0) {
+ throw new VersionParseException("Invalid version format");
+ }
+
+ short major = Short.parseShort(split[0]);
+ short minor = split.length > 1 ? Short.parseShort(split[1]) : 0;
+ short patch = split.length > 2 ? Short.parseShort(split[2]) : 0;
+
+ return new UnitVersion(major, minor, patch);
+ } catch (NumberFormatException e) {
+ throw new VersionParseException(e);
+ }
+ }
+
+ @Override
+ public int compareTo(Version o) {
+ if (o == LATEST) {
+ return -1;
+ }
+
+ UnitVersion version = (UnitVersion) o;
+
+ int majorCompare = Short.compare(major, version.major);
+ if (majorCompare != 0) {
+ return majorCompare;
+ }
+
+ int minorCompare = Short.compare(minor, version.minor);
+ if (minorCompare != 0) {
+ return minorCompare;
+ }
+
+ return Short.compare(patch, version.patch);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ UnitVersion version = (UnitVersion) o;
+
+ if (major != version.major) {
+ return false;
+ }
+ if (minor != version.minor) {
+ return false;
+ }
+ return patch == version.patch;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = major;
+ result = 31 * result + minor;
+ result = 31 * result + patch;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return render();
+ }
+}
diff --git
a/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java
b/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java
new file mode 100644
index 0000000000..f9a4e29847
--- /dev/null
+++
b/modules/api/src/main/java/org/apache/ignite/deployment/version/Version.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.deployment.version;
+
+
+/**
+ * Unit version interface. Version implementations should be comparable.
+ */
+public interface Version extends Comparable<Version> {
+ /**
+ * Render version representation in String format.
+ *
+ * @return version string representation.
+ */
+ String render();
+
+ /**
+ * Implementation of {@link Version} interface with special latest logic.
+ * This version have special unique representation. Moreover by convention
+ * this implementation should be oldest version of any another independent
of implementation.
+ */
+ Version LATEST = new Version() {
+ @Override
+ public String render() {
+ return "latest";
+ }
+
+ @Override
+ public int compareTo(Version o) {
+ if (o == LATEST) {
+ return 0;
+ }
+ return 1;
+ }
+ };
+
+ /**
+ * Parse version from String.
+ *
+ * @param s string version representation.
+ * @return Version instance of
+ */
+ static Version parseVersion(String s) {
+ if ("latest".equals(s)) {
+ return LATEST;
+ }
+
+ return UnitVersion.parse(s);
+ }
+}
diff --git
a/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java
b/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java
new file mode 100644
index 0000000000..717035a9c0
--- /dev/null
+++
b/modules/api/src/main/java/org/apache/ignite/deployment/version/VersionParseException.java
@@ -0,0 +1,48 @@
+/*
+ * 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.deployment.version;
+
+/**
+ * Throws when {@link Version} of deployment unit not parsable.
+ */
+public class VersionParseException extends RuntimeException {
+ /**
+ * Constructor.
+ */
+ public VersionParseException() {
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause Cause error.
+ */
+ public VersionParseException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message Error message.
+ */
+ public VersionParseException(String message) {
+ super(message);
+ }
+}
diff --git
a/modules/api/src/test/java/org/apache/ignite/deployment/version/VersionUnitTest.java
b/modules/api/src/test/java/org/apache/ignite/deployment/version/VersionUnitTest.java
new file mode 100644
index 0000000000..bd0ed3cf44
--- /dev/null
+++
b/modules/api/src/test/java/org/apache/ignite/deployment/version/VersionUnitTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.deployment.version;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link Version}.
+ */
+public class VersionUnitTest {
+ @Test
+ public void testParseCorrect() {
+ assertThat(Version.parseVersion("1.1.0"), is(new UnitVersion((short)
1, (short) 1, (short) 0)));
+ assertThat(Version.parseVersion("1.1.01"), is(new UnitVersion((short)
1, (short) 1, (short) 1)));
+ assertThat(Version.parseVersion("1.1"), is(new UnitVersion((short) 1,
(short) 1, (short) 0)));
+ assertThat(Version.parseVersion("1"), is(new UnitVersion((short) 1,
(short) 0, (short) 0)));
+ assertThat(Version.parseVersion("latest"), is(Version.LATEST));
+ }
+
+ @Test
+ public void testParseErrors() {
+ Assertions.assertThrows(VersionParseException.class, () ->
Version.parseVersion("1.1.1.1"));
+ Assertions.assertThrows(VersionParseException.class, () ->
Version.parseVersion(""));
+ Assertions.assertThrows(VersionParseException.class, () ->
Version.parseVersion("version"));
+ Assertions.assertThrows(VersionParseException.class, () ->
Version.parseVersion("1."));
+ Assertions.assertThrows(VersionParseException.class, () ->
Version.parseVersion(String.valueOf(Integer.MAX_VALUE)));
+ Assertions.assertThrows(VersionParseException.class, () ->
Version.parseVersion("1.1f"));
+ }
+}
diff --git
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/core/repl/executor/ItIgnitePicocliCommandsTest.java
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/core/repl/executor/ItIgnitePicocliCommandsTest.java
index 122a9befcc..b4b8a43421 100644
---
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/core/repl/executor/ItIgnitePicocliCommandsTest.java
+++
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/core/repl/executor/ItIgnitePicocliCommandsTest.java
@@ -216,7 +216,7 @@ public class ItIgnitePicocliCommandsTest extends
CliCommandTestInitializedIntegr
// wait for lazy init of node config completer
await("For given parsed words: " + givenParsedLine.words()).until(
() -> complete(givenParsedLine),
- containsInAnyOrder("rest", "compute", "clientConnector",
"raft", "network", "cluster")
+ containsInAnyOrder("rest", "compute", "clientConnector",
"raft", "network", "cluster", "deployment")
);
}
@@ -243,7 +243,7 @@ public class ItIgnitePicocliCommandsTest extends
CliCommandTestInitializedIntegr
// wait for lazy init of node config completer
await("For given parsed words: " + givenParsedLine.words()).until(
() -> complete(givenParsedLine),
- containsInAnyOrder("rest", "clientConnector", "network",
"cluster")
+ containsInAnyOrder("rest", "clientConnector", "network",
"cluster", "deployment")
);
}
diff --git
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
index 8474f280ec..8ab6beaafa 100644
---
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
+++
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
@@ -27,6 +27,7 @@ import java.util.function.BiFunction;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.client.IgniteClientConfiguration;
import org.apache.ignite.compute.IgniteCompute;
+import org.apache.ignite.deployment.IgniteDeployment;
import org.apache.ignite.internal.client.compute.ClientCompute;
import org.apache.ignite.internal.client.io.ClientConnectionMultiplexer;
import org.apache.ignite.internal.client.proto.ClientOp;
@@ -139,6 +140,12 @@ public class TcpIgniteClient implements IgniteClient {
return compute;
}
+ /** {@inheritDoc} */
+ @Override
+ public IgniteDeployment deployment() {
+ throw new UnsupportedOperationException("Deployment management not
implemented yet.");
+ }
+
/** {@inheritDoc} */
@Override
public Collection<ClusterNode> clusterNodes() {
diff --git
a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgnite.java
b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgnite.java
index 51da2b6737..16deb7367f 100644
---
a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgnite.java
+++
b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgnite.java
@@ -22,6 +22,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.apache.ignite.Ignite;
import org.apache.ignite.compute.IgniteCompute;
+import org.apache.ignite.deployment.IgniteDeployment;
import org.apache.ignite.internal.hlc.HybridTimestamp;
import org.apache.ignite.internal.replicator.ReplicationGroupId;
import org.apache.ignite.internal.sql.engine.QueryProcessor;
@@ -167,6 +168,11 @@ public class FakeIgnite implements Ignite {
throw new UnsupportedOperationException("Not implemented yet");
}
+ @Override
+ public IgniteDeployment deployment() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
/** {@inheritDoc} */
@Override
public Collection<ClusterNode> clusterNodes() {
diff --git
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java
index f80852a62a..aa9a678cba 100644
---
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java
+++
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java
@@ -714,6 +714,25 @@ public class ClusterManagementGroupManager implements
IgniteComponent {
}
}
+ /**
+ * Returns a future that, when complete, resolves into a list of node
names that host the CMG.
+ *
+ * @return Future that, when complete, resolves into a list of node names
that host the CMG.
+ */
+ public CompletableFuture<Set<String>> cmgNodes() {
+ if (!busyLock.enterBusy()) {
+ return failedFuture(new NodeStoppingException());
+ }
+
+ try {
+ return raftServiceAfterJoin()
+ .thenCompose(CmgRaftService::readClusterState)
+ .thenApply(ClusterState::cmgNodes);
+ } finally {
+ busyLock.leaveBusy();
+ }
+ }
+
/**
* Returns a future that, when complete, resolves into a logical topology
snapshot.
*
diff --git a/modules/code-deployment/build.gradle
b/modules/code-deployment/build.gradle
new file mode 100644
index 0000000000..2a7af850d6
--- /dev/null
+++ b/modules/code-deployment/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+apply from: "$rootDir/buildscripts/java-core.gradle"
+apply from: "$rootDir/buildscripts/java-integration-test.gradle"
+apply from: "$rootDir/buildscripts/publishing.gradle"
+apply from: "$rootDir/buildscripts/java-junit5.gradle"
+
+description = 'ignite-code-deployment'
+
+dependencies {
+ annotationProcessor project(':ignite-configuration-annotation-processor')
+ annotationProcessor project(':ignite-network-annotation-processor')
+ annotationProcessor libs.auto.service
+
+ implementation libs.auto.service.annotations
+ implementation project(':ignite-core')
+ implementation project(':ignite-api')
+ implementation project(':ignite-network-api')
+ implementation project(':ignite-metastorage-api')
+ implementation project(':ignite-cluster-management')
+ implementation project(':ignite-configuration')
+ implementation project(':ignite-network')
+ implementation project(':ignite-schema')
+
+ testImplementation libs.hamcrest.core
+ testImplementation libs.hamcrest.optional
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java
new file mode 100644
index 0000000000..2c2869a503
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java
@@ -0,0 +1,437 @@
+/*
+ * 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.deployunit;
+
+import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.SYNC;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.exists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.notExists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.revision;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.put;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.stream.Collectors;
+import org.apache.ignite.deployment.DeploymentUnit;
+import org.apache.ignite.deployment.IgniteDeployment;
+import org.apache.ignite.deployment.UnitStatus;
+import org.apache.ignite.deployment.UnitStatus.UnitStatusBuilder;
+import org.apache.ignite.deployment.version.Version;
+import
org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import
org.apache.ignite.internal.deployunit.configuration.DeploymentConfiguration;
+import
org.apache.ignite.internal.deployunit.exception.DeploymentUnitAlreadyExistsException;
+import
org.apache.ignite.internal.deployunit.exception.DeploymentUnitNotFoundException;
+import
org.apache.ignite.internal.deployunit.exception.DeploymentUnitReadException;
+import org.apache.ignite.internal.deployunit.message.DeployUnitMessageTypes;
+import org.apache.ignite.internal.deployunit.message.DeployUnitRequest;
+import org.apache.ignite.internal.deployunit.message.DeployUnitRequestBuilder;
+import org.apache.ignite.internal.deployunit.message.DeployUnitRequestImpl;
+import org.apache.ignite.internal.deployunit.message.DeployUnitResponse;
+import org.apache.ignite.internal.deployunit.message.DeployUnitResponseBuilder;
+import org.apache.ignite.internal.deployunit.message.DeployUnitResponseImpl;
+import org.apache.ignite.internal.deployunit.message.UndeployUnitRequest;
+import org.apache.ignite.internal.deployunit.message.UndeployUnitRequestImpl;
+import org.apache.ignite.internal.deployunit.message.UndeployUnitResponse;
+import org.apache.ignite.internal.deployunit.message.UndeployUnitResponseImpl;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.metastorage.Entry;
+import org.apache.ignite.internal.metastorage.MetaStorageManager;
+import org.apache.ignite.internal.metastorage.dsl.Operation;
+import org.apache.ignite.internal.metastorage.dsl.Operations;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+
+//TODO: rework metastorage keys IGNITE-18870
+/**
+ * Deployment manager implementation.
+ */
+public class DeploymentManagerImpl implements IgniteDeployment,
IgniteComponent {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(DeploymentManagerImpl.class);
+
+ private static final String TMP_SUFFIX = ".tmp";
+
+ private static final String DEPLOY_UNIT_PREFIX = "deploy-unit.";
+
+ private static final String UNITS_PREFIX = DEPLOY_UNIT_PREFIX + "units.";
+
+ /**
+ * Meta storage.
+ */
+ private final MetaStorageManager metaStorage;
+
+ /**
+ * Deployment configuration.
+ */
+ private final DeploymentConfiguration configuration;
+
+ /**
+ * Cluster management group manager.
+ */
+ private final ClusterManagementGroupManager cmgManager;
+
+ /**
+ * In flight futures tracker.
+ */
+ private final InFlightFutures inFlightFutures = new InFlightFutures();
+
+ /**
+ * Cluster service.
+ */
+ private final ClusterService clusterService;
+
+ /**
+ * Folder for units.
+ */
+ private Path unitsFolder;
+
+ /**
+ * Constructor.
+ *
+ * @param clusterService Cluster service.
+ * @param metaStorage Meta storage.
+ * @param workDir Node working directory.
+ * @param configuration Deployment configuration.
+ * @param cmgManager Cluster management group manager.
+ */
+ public DeploymentManagerImpl(ClusterService clusterService,
+ MetaStorageManager metaStorage,
+ Path workDir,
+ DeploymentConfiguration configuration,
+ ClusterManagementGroupManager cmgManager) {
+ this.clusterService = clusterService;
+ this.metaStorage = metaStorage;
+ this.configuration = configuration;
+ this.cmgManager = cmgManager;
+ unitsFolder = workDir;
+ }
+
+ @Override
+ public CompletableFuture<Boolean> deployAsync(String id, Version version,
DeploymentUnit deploymentUnit) {
+ checkId(id);
+ Objects.requireNonNull(version);
+ Objects.requireNonNull(deploymentUnit);
+
+ ByteArray key = new ByteArray(UNITS_PREFIX + id + ":" +
version.render());
+
+ UnitMeta meta = new UnitMeta(id, version, deploymentUnit.name(),
Collections.emptyList());
+
+ Operation put = put(key, UnitMetaSerializer.serialize(meta));
+
+ DeployUnitRequestBuilder builder = DeployUnitRequestImpl.builder();
+
+ try {
+ builder.unitContent(deploymentUnit.content().readAllBytes());
+ } catch (IOException e) {
+ LOG.error("Error to read deployment unit content", e);
+ return CompletableFuture.failedFuture(new
DeploymentUnitReadException(e));
+ }
+ DeployUnitRequest request = builder
+ .unitName(deploymentUnit.name())
+ .id(id)
+ .version(version.render())
+ .build();
+
+ return metaStorage.invoke(notExists(key), put, Operations.noop())
+ .thenCompose(success -> {
+ if (success) {
+ return doDeploy(request);
+ }
+ LOG.error("Failed to deploy meta of unit " + id + ":" +
version);
+ return CompletableFuture.failedFuture(
+ new DeploymentUnitAlreadyExistsException(id,
+ "Unit " + id + ":" + version + " already
exists"));
+ })
+ .thenApply(completed -> {
+ if (completed) {
+ startDeployAsyncToCmg(request);
+ }
+ return completed;
+ });
+ }
+
+ private void startDeployAsyncToCmg(DeployUnitRequest request) {
+ cmgManager.cmgNodes()
+ .thenAccept(nodes -> {
+ for (String node : nodes) {
+ ClusterNode clusterNode =
clusterService.topologyService().getByConsistentId(node);
+ if (clusterNode != null) {
+
inFlightFutures.registerFuture(requestDeploy(clusterNode, request));
+ }
+ }
+ });
+ }
+
+ private CompletableFuture<Boolean> requestDeploy(ClusterNode clusterNode,
DeployUnitRequest request) {
+ return clusterService.messagingService()
+ .invoke(clusterNode, request, Long.MAX_VALUE)
+ .thenCompose(message -> {
+ Throwable error = ((DeployUnitResponse) message).error();
+ if (error != null) {
+ LOG.error("Failed to deploy unit " + request.id() +
":" + request.version()
+ + " to node " + clusterNode, error);
+ return CompletableFuture.failedFuture(error);
+ }
+ return CompletableFuture.completedFuture(true);
+ });
+ }
+
+ @Override
+ public CompletableFuture<Void> undeployAsync(String id, Version version) {
+ checkId(id);
+ Objects.requireNonNull(version);
+
+ ByteArray key = new ByteArray(UNITS_PREFIX + id + ":" + version);
+
+ return metaStorage.invoke(exists(key), Operations.remove(key),
Operations.noop())
+ .thenCompose(success -> {
+ if (success) {
+ return cmgManager.logicalTopology();
+ }
+ return CompletableFuture.failedFuture(new
DeploymentUnitNotFoundException(
+ "Unit " + id + " with version " + version + "
doesn't exist"));
+ }).thenApply(logicalTopologySnapshot -> {
+ for (ClusterNode node : logicalTopologySnapshot.nodes()) {
+ clusterService.messagingService()
+ .invoke(node, UndeployUnitRequestImpl.builder()
+ .id(id)
+ .version(version.render())
+ .build(),
+ Long.MAX_VALUE)
+ .thenAccept(message -> {
+ Throwable error = ((UndeployUnitResponse)
message).error();
+ if (error != null) {
+ LOG.error("Failed to undeploy unit " +
id + ":" + version
+ + " from node " + node, error);
+ }
+ });
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public CompletableFuture<List<UnitStatus>> unitsAsync() {
+ CompletableFuture<List<UnitStatus>> result = new CompletableFuture<>();
+ Map<String, UnitStatusBuilder> map = new HashMap<>();
+ metaStorage.prefix(new ByteArray(UNITS_PREFIX))
+ .subscribe(new Subscriber<>() {
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(Entry item) {
+ UnitMeta meta =
UnitMetaSerializer.deserialize(item.value());
+ map.computeIfAbsent(meta.id(), UnitStatus::builder)
+ .append(meta.version(),
meta.consistentIdLocation());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ result.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+
result.complete(map.values().stream().map(UnitStatusBuilder::build).collect(Collectors.toList()));
+ }
+ });
+ return result;
+ }
+
+ @Override
+ public CompletableFuture<List<Version>> versionsAsync(String id) {
+ checkId(id);
+ CompletableFuture<List<Version>> result = new CompletableFuture<>();
+ metaStorage.prefix(new ByteArray(UNITS_PREFIX + id))
+ .subscribe(new Subscriber<>() {
+ private final List<Version> list = new ArrayList<>();
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(Entry item) {
+ UnitMeta deserialize =
UnitMetaSerializer.deserialize(item.value());
+ list.add(deserialize.version());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ result.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ result.complete(list);
+ }
+ });
+ return result;
+ }
+
+ @Override
+ public CompletableFuture<UnitStatus> statusAsync(String id) {
+ checkId(id);
+ CompletableFuture<UnitStatus> result = new CompletableFuture<>();
+ metaStorage.prefix(new ByteArray(UNITS_PREFIX + id))
+ .subscribe(new Subscriber<>() {
+ private UnitStatusBuilder builder;
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(Entry item) {
+ if (builder == null) {
+ builder = UnitStatus.builder(id);
+ }
+ UnitMeta deserialize =
UnitMetaSerializer.deserialize(item.value());
+ builder.append(deserialize.version(),
deserialize.consistentIdLocation());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ result.completeExceptionally(throwable);
+ }
+
+ @Override
+ public void onComplete() {
+ if (builder != null) {
+ result.complete(builder.build());
+ } else {
+ result.completeExceptionally(
+ new DeploymentUnitNotFoundException("Unit
with " + id + " doesn't exist."));
+ }
+ }
+ });
+ return result;
+ }
+
+ @Override
+ public void start() {
+ unitsFolder =
unitsFolder.resolve(configuration.deploymentLocation().value());
+
clusterService.messagingService().addMessageHandler(DeployUnitMessageTypes.class,
+ (message, senderConsistentId, correlationId) -> {
+ if (message instanceof DeployUnitRequest) {
+ processDeployRequest((DeployUnitRequest) message,
senderConsistentId, correlationId);
+ } else if (message instanceof UndeployUnitRequest) {
+ processUndeployRequest((UndeployUnitRequest) message,
senderConsistentId, correlationId);
+ }
+ });
+ }
+
+ private void processDeployRequest(DeployUnitRequest executeRequest, String
senderConsistentId, long correlationId) {
+ doDeploy(executeRequest).whenComplete((success, throwable) -> {
+ DeployUnitResponseBuilder builder =
DeployUnitResponseImpl.builder();
+ if (throwable != null) {
+ builder.error(throwable);
+ }
+ clusterService.messagingService().respond(senderConsistentId,
+ builder.build(), correlationId);
+ });
+ }
+
+ private void processUndeployRequest(UndeployUnitRequest executeRequest,
String senderConsistentId, long correlationId) {
+ try {
+ Path unitPath = unitsFolder
+ .resolve(executeRequest.id())
+ .resolve(executeRequest.version());
+
+ IgniteUtils.deleteIfExistsThrowable(unitPath);
+ } catch (IOException e) {
+ LOG.error("Failed to undeploy unit " + executeRequest.id() + ":" +
executeRequest.version(), e);
+ clusterService.messagingService()
+ .respond(senderConsistentId,
UndeployUnitResponseImpl.builder().error(e).build(), correlationId);
+ return;
+ }
+
+ clusterService.messagingService()
+ .respond(senderConsistentId,
UndeployUnitResponseImpl.builder().build(), correlationId);
+ }
+
+ private CompletableFuture<Boolean> doDeploy(DeployUnitRequest
executeRequest) {
+ String id = executeRequest.id();
+ String version = executeRequest.version();
+ try {
+ Path unitPath = unitsFolder
+ .resolve(executeRequest.id())
+ .resolve(executeRequest.version())
+ .resolve(executeRequest.unitName());
+
+ Path unitPathTmp = unitPath.resolveSibling(unitPath.getFileName()
+ TMP_SUFFIX);
+
+ Files.createDirectories(unitPathTmp.getParent());
+
+ Files.write(unitPathTmp, executeRequest.unitContent(), CREATE,
SYNC, TRUNCATE_EXISTING);
+ Files.move(unitPathTmp, unitPath, ATOMIC_MOVE, REPLACE_EXISTING);
+ } catch (IOException e) {
+ LOG.error("Failed to deploy unit " + executeRequest.id() + ":" +
executeRequest.version(), e);
+ return CompletableFuture.failedFuture(e);
+ }
+
+ ByteArray key = new ByteArray(UNITS_PREFIX + id + ":" + version);
+ return metaStorage.get(key)
+ .thenCompose(e -> {
+ UnitMeta prev = UnitMetaSerializer.deserialize(e.value());
+
+
prev.addConsistentId(clusterService.topologyService().localMember().name());
+
+ return metaStorage.invoke(
+ revision(key).eq(e.revision()),
+ put(key, UnitMetaSerializer.serialize(prev)),
+ Operations.noop());
+ });
+ }
+
+ @Override
+ public void stop() throws Exception {
+ inFlightFutures.cancelInFlightFutures();
+ }
+
+ private static void checkId(String id) {
+ Objects.requireNonNull(id);
+
+ if (id.isBlank()) {
+ throw new IllegalArgumentException("Id is blank");
+ }
+ }
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitMeta.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitMeta.java
new file mode 100644
index 0000000000..5730f8cc7a
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitMeta.java
@@ -0,0 +1,143 @@
+/*
+ * 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.deployunit;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.ignite.deployment.version.Version;
+
+/**
+ * Unit meta data class.
+ */
+public class UnitMeta {
+ /**
+ * Unit id.
+ */
+ private final String id;
+
+ /**
+ * Unit version.
+ */
+ private final Version version;
+
+ /**
+ * Unit name.
+ */
+ private final String name;
+
+ /**
+ * Consistent ids of nodes with.
+ */
+ private final List<String> consistentIdLocation = new ArrayList<>();
+
+ /**
+ * Constructor.
+ *
+ * @param id Unit identifier.
+ * @param version Unit version.
+ * @param name Unit name.
+ * @param consistentIdLocation Consistent ids of nodes where unit deployed.
+ */
+ public UnitMeta(String id, Version version, String name, List<String>
consistentIdLocation) {
+ this.id = id;
+ this.version = version;
+ this.name = name;
+ this.consistentIdLocation.addAll(consistentIdLocation);
+ }
+
+ public UnitMeta(String id, String name, List<String> consistentIdLocation)
{
+ this(id, Version.LATEST, name, consistentIdLocation);
+ }
+
+ /**
+ * Returns identifier of deployment unit.
+ *
+ * @return Identifier of deployment unit.
+ */
+ public String id() {
+ return id;
+ }
+
+ /**
+ * Returns version of deployment unit.
+ *
+ * @return Version of deployment unit.
+ */
+ public Version version() {
+ return version;
+ }
+
+ /**
+ * Returns name of deployment unit.
+ *
+ * @return name of deployment unit.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns list of nodes consistent id where deployment unit deployed.
+ *
+ * @return List of nodes consistent id where deployment unit deployed.
+ */
+ public List<String> consistentIdLocation() {
+ return consistentIdLocation;
+ }
+
+ /**
+ * Register node as deployment unit holder.
+ *
+ * @param id Consistent identifier of node.
+ */
+ public void addConsistentId(String id) {
+ consistentIdLocation.add(id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ UnitMeta meta = (UnitMeta) o;
+
+ if (id != null ? !id.equals(meta.id) : meta.id != null) {
+ return false;
+ }
+ if (version != null ? !version.equals(meta.version) : meta.version !=
null) {
+ return false;
+ }
+ if (name != null ? !name.equals(meta.name) : meta.name != null) {
+ return false;
+ }
+ return consistentIdLocation.equals(meta.consistentIdLocation);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id != null ? id.hashCode() : 0;
+ result = 31 * result + (version != null ? version.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + consistentIdLocation.hashCode();
+ return result;
+ }
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitMetaSerializer.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitMetaSerializer.java
new file mode 100644
index 0000000000..1101342177
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitMetaSerializer.java
@@ -0,0 +1,93 @@
+/*
+ * 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.deployunit;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.Base64.Encoder;
+import java.util.List;
+import org.apache.ignite.deployment.version.Version;
+
+/**
+ * Serializer for {@link UnitMeta}.
+ */
+public class UnitMetaSerializer {
+ private static final String SEPARATOR = ";";
+
+ /**
+ * Constructor.
+ */
+ private UnitMetaSerializer() {
+
+ }
+
+ /**
+ * Serialize unit meta.
+ *
+ * @param meta Unit meta.
+ * @return Serialized unit meta.
+ */
+ public static byte[] serialize(UnitMeta meta) {
+ StringBuilder sb = new StringBuilder();
+
+ appendWithEncoding(sb, meta.id());
+ appendWithEncoding(sb, meta.version().render());
+ appendWithEncoding(sb, meta.name());
+
+ for (String id : meta.consistentIdLocation()) {
+ appendWithEncoding(sb, id);
+ }
+
+ return sb.toString().getBytes(UTF_8);
+ }
+
+ private static void appendWithEncoding(StringBuilder sb, String content) {
+ Encoder encoder = Base64.getEncoder();
+ sb.append(new String(encoder.encode(content.getBytes(UTF_8)),
UTF_8)).append(SEPARATOR);
+ }
+
+ /**
+ * Deserialize byte array to unit meta.
+ *
+ * @param bytes Byte array.
+ * @return Unit meta.
+ */
+ public static UnitMeta deserialize(byte[] bytes) {
+ String s = new String(bytes, UTF_8);
+
+ String[] split = s.split(SEPARATOR);
+
+ Decoder decoder = Base64.getDecoder();
+
+ String id = new String(decoder.decode(split[0]), UTF_8);
+
+ String version = new String(decoder.decode(split[1]), UTF_8);
+
+ String unitName = new String(decoder.decode(split[2]), UTF_8);
+
+ List<String> ids = new ArrayList<>();
+ for (int i = 3; i < split.length; i++) {
+ ids.add(new String(decoder.decode(split[i]), UTF_8));
+ }
+
+ return new UnitMeta(id, Version.parseVersion(version), unitName, ids);
+ }
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/configuration/DeploymentConfigurationModule.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/configuration/DeploymentConfigurationModule.java
new file mode 100644
index 0000000000..3d0cc121cf
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/configuration/DeploymentConfigurationModule.java
@@ -0,0 +1,41 @@
+/*
+ * 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.deployunit.configuration;
+
+import com.google.auto.service.AutoService;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.ConfigurationModule;
+
+/**
+ * Configuration module for {@link DeploymentConfiguration}.
+ */
+@AutoService(ConfigurationModule.class)
+public class DeploymentConfigurationModule implements ConfigurationModule {
+ @Override
+ public ConfigurationType type() {
+ return ConfigurationType.LOCAL;
+ }
+
+ @Override
+ public Collection<RootKey<?, ?>> rootKeys() {
+ return Collections.singleton(DeploymentConfiguration.KEY);
+ }
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/configuration/DeploymentConfigurationSchema.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/configuration/DeploymentConfigurationSchema.java
new file mode 100644
index 0000000000..a8e3a9d21f
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/configuration/DeploymentConfigurationSchema.java
@@ -0,0 +1,34 @@
+/*
+ * 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.deployunit.configuration;
+
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.Value;
+
+/**
+ * Configuration schema for Compute functionality.
+ */
+@ConfigurationRoot(rootName = "deployment", type = ConfigurationType.LOCAL)
+public class DeploymentConfigurationSchema {
+ /**
+ * Relative path to folder in node working directory where will store all
deployment units content.
+ */
+ @Value(hasDefault = true)
+ public final String deploymentLocation = "deployment";
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitAlreadyExistsException.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitAlreadyExistsException.java
new file mode 100644
index 0000000000..726ef2e04f
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitAlreadyExistsException.java
@@ -0,0 +1,48 @@
+/*
+ * 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.deployunit.exception;
+
+import org.apache.ignite.lang.ErrorGroups.CodeDeployment;
+import org.apache.ignite.lang.IgniteException;
+
+/**
+ * Throws when trying to deploy unit which already exist.
+ */
+public class DeploymentUnitAlreadyExistsException extends IgniteException {
+
+ /**
+ * Unit identifier.
+ */
+ private final String id;
+
+ /**
+ * Constructor.
+ *
+ * @param id Unit identifier.
+ * @param message Error message.
+ */
+ public DeploymentUnitAlreadyExistsException(String id, String message) {
+ super(CodeDeployment.UNIT_ALREADY_EXISTS_ERR, message);
+ this.id = id;
+ }
+
+ public String id() {
+ return id;
+ }
+
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitNotFoundException.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitNotFoundException.java
new file mode 100644
index 0000000000..802338b7ca
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitNotFoundException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.deployunit.exception;
+
+import org.apache.ignite.lang.ErrorGroups.CodeDeployment;
+import org.apache.ignite.lang.IgniteException;
+
+/**
+ * Throws when trying to access information about unit which doesn't exist.
+ */
+public class DeploymentUnitNotFoundException extends IgniteException {
+ /**
+ * Constructor.
+ *
+ * @param message error message.
+ */
+ public DeploymentUnitNotFoundException(String message) {
+ super(CodeDeployment.UNIT_NOT_FOUND_ERR, message);
+ }
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitReadException.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitReadException.java
new file mode 100644
index 0000000000..a985769e55
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/exception/DeploymentUnitReadException.java
@@ -0,0 +1,42 @@
+/*
+ * 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.deployunit.exception;
+
+import org.apache.ignite.lang.ErrorGroups.CodeDeployment;
+import org.apache.ignite.lang.IgniteException;
+
+/**
+ * Throws when deployment unit content failed to read.
+ */
+public class DeploymentUnitReadException extends IgniteException {
+ /**
+ * Constructor.
+ */
+ public DeploymentUnitReadException() {
+ super(CodeDeployment.UNIT_CONTENT_READ_ERR);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause cause error.
+ */
+ public DeploymentUnitReadException(Throwable cause) {
+ super(CodeDeployment.UNIT_CONTENT_READ_ERR, cause);
+ }
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitMessageTypes.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitMessageTypes.java
new file mode 100644
index 0000000000..54337f7fd5
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitMessageTypes.java
@@ -0,0 +1,47 @@
+/*
+ * 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.deployunit.message;
+
+
+import org.apache.ignite.network.annotations.MessageGroup;
+
+/**
+ * Message group for deployment units.
+ */
+@MessageGroup(groupType = 10, groupName = "DeploymentUnit")
+public class DeployUnitMessageTypes {
+ /**
+ * Message type for {@link DeployUnitRequest}.
+ */
+ public static final short DEPLOY_UNIT_REQUEST = 0;
+
+ /**
+ * Message type for {@link DeployUnitResponse}.
+ */
+ public static final short DEPLOY_UNIT_RESPONSE = 1;
+
+ /**
+ * Message type for {@link UndeployUnitRequest}.
+ */
+ public static final short UNDEPLOY_UNIT_REQUEST = 2;
+
+ /**
+ * Message type for {@link UndeployUnitResponse}.
+ */
+ public static final short UNDEPLOY_UNIT_RESPONSE = 3;
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitRequest.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitRequest.java
new file mode 100644
index 0000000000..ddf16bf6d7
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitRequest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.deployunit.message;
+
+import org.apache.ignite.network.NetworkMessage;
+import org.apache.ignite.network.annotations.Transferable;
+
+/**
+ * Deploy unit request.
+ */
+@Transferable(DeployUnitMessageTypes.DEPLOY_UNIT_REQUEST)
+public interface DeployUnitRequest extends NetworkMessage {
+ /**
+ * Returns id of deployment unit.
+ *
+ * @return id of deployment unit
+ */
+ String id();
+
+ /**
+ * Returns version of deployment unit.
+ *
+ * @return version of deployment unit.
+ */
+ String version();
+
+ /**
+ * Returns name of deployment unit.
+ *
+ * @return name of deployment unit.
+ */
+
+ String unitName();
+
+ /**
+ * Returns content of deployment unit.
+ *
+ * @return content of deployment unit.
+ */
+
+ byte[] unitContent();
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitResponse.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitResponse.java
new file mode 100644
index 0000000000..ee298c8a1d
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/DeployUnitResponse.java
@@ -0,0 +1,36 @@
+/*
+ * 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.deployunit.message;
+
+import org.apache.ignite.network.NetworkMessage;
+import org.apache.ignite.network.annotations.Marshallable;
+import org.apache.ignite.network.annotations.Transferable;
+
+/**
+ * Deploy unit response.
+ */
+@Transferable(DeployUnitMessageTypes.DEPLOY_UNIT_RESPONSE)
+public interface DeployUnitResponse extends NetworkMessage {
+ /**
+ * Returns error which happens on deploy process.
+ *
+ * @return error which happens on deploy process.
+ */
+ @Marshallable
+ Throwable error();
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/UndeployUnitRequest.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/UndeployUnitRequest.java
new file mode 100644
index 0000000000..4c4f847670
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/UndeployUnitRequest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.deployunit.message;
+
+import org.apache.ignite.network.NetworkMessage;
+import org.apache.ignite.network.annotations.Transferable;
+
+/**
+ * Undeploy unit request.
+ */
+@Transferable(DeployUnitMessageTypes.UNDEPLOY_UNIT_REQUEST)
+public interface UndeployUnitRequest extends NetworkMessage {
+ /**
+ * Returns id of deployment unit.
+ *
+ * @return id of deployment unit.
+ */
+ String id();
+
+ /**
+ * Returns version of deployment unit.
+ *
+ * @return version of deployment unit.
+ */
+ String version();
+}
diff --git
a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/UndeployUnitResponse.java
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/UndeployUnitResponse.java
new file mode 100644
index 0000000000..e430724ac4
--- /dev/null
+++
b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/message/UndeployUnitResponse.java
@@ -0,0 +1,36 @@
+/*
+ * 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.deployunit.message;
+
+import org.apache.ignite.network.NetworkMessage;
+import org.apache.ignite.network.annotations.Marshallable;
+import org.apache.ignite.network.annotations.Transferable;
+
+/**
+ * Undeploy unit response.
+ */
+@Transferable(DeployUnitMessageTypes.UNDEPLOY_UNIT_RESPONSE)
+public interface UndeployUnitResponse extends NetworkMessage {
+ /**
+ * Returns error which happens on undeploy process.
+ *
+ * @return error which happens on undeploy process.
+ */
+ @Marshallable
+ Throwable error();
+}
diff --git
a/modules/code-deployment/src/test/java/org/apache/ignite/deployment/UnitMetaSerializerTest.java
b/modules/code-deployment/src/test/java/org/apache/ignite/deployment/UnitMetaSerializerTest.java
new file mode 100644
index 0000000000..9f4b841110
--- /dev/null
+++
b/modules/code-deployment/src/test/java/org/apache/ignite/deployment/UnitMetaSerializerTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.deployment;
+
+import static
org.apache.ignite.internal.deployunit.UnitMetaSerializer.deserialize;
+import static
org.apache.ignite.internal.deployunit.UnitMetaSerializer.serialize;
+import static org.hamcrest.Matchers.is;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.ignite.deployment.version.Version;
+import org.apache.ignite.internal.deployunit.UnitMeta;
+import org.apache.ignite.internal.deployunit.UnitMetaSerializer;
+import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for {@link UnitMetaSerializer}.
+ */
+public class UnitMetaSerializerTest {
+ @Test
+ public void testSerializeDeserializeLatest() {
+ UnitMeta meta = new UnitMeta("id", Version.LATEST, "unitName",
Arrays.asList("id1", "id2"));
+
+ byte[] serialize = serialize(meta);
+
+ MatcherAssert.assertThat(deserialize(serialize), is(meta));
+ }
+
+ @Test
+ public void testSerializeDeserializeUnit() {
+ UnitMeta meta = new UnitMeta("id", Version.parseVersion("3.0.0"),
"unitName", Arrays.asList("id1", "id2"));
+
+ byte[] serialize = serialize(meta);
+
+ MatcherAssert.assertThat(deserialize(serialize), is(meta));
+ }
+
+ @Test
+ public void testSerializeDeserializeUnitIncompleteVersion() {
+ UnitMeta meta = new UnitMeta("id", Version.parseVersion("3.0"),
"unitName", Arrays.asList("id1", "id2"));
+
+ byte[] serialize = serialize(meta);
+
+ MatcherAssert.assertThat(deserialize(serialize), is(meta));
+ }
+
+ @Test
+ public void testSerializeDeserializeUnitEmptyConsistentId() {
+ UnitMeta meta = new UnitMeta("id", Version.parseVersion("3.0.0"),
"unitName", Collections.emptyList());
+
+ byte[] serialize = serialize(meta);
+
+ UnitMeta deserialize = deserialize(serialize);
+ MatcherAssert.assertThat(deserialize, is(meta));
+ }
+
+ @Test
+ public void testSerializeDeserializeWithSeparatorCharInIdName() {
+ UnitMeta meta = new UnitMeta("id;", Version.parseVersion("3.0.0"),
"unitName;", Collections.emptyList());
+
+ byte[] serialize = serialize(meta);
+
+ UnitMeta deserialize = deserialize(serialize);
+ MatcherAssert.assertThat(deserialize, is(meta));
+ }
+}
diff --git
a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
index 9bdc8f64b2..efa3d4f15e 100644
---
a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
+++
b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
@@ -510,26 +510,7 @@ public class IgniteUtils {
*/
public static boolean deleteIfExists(Path path) {
try {
- Files.walkFileTree(path, new SimpleFileVisitor<>() {
- @Override
- public FileVisitResult postVisitDirectory(Path dir,
IOException exc) throws IOException {
- if (exc != null) {
- throw exc;
- }
-
- Files.delete(dir);
-
- return FileVisitResult.CONTINUE;
- }
-
- @Override
- public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
- Files.delete(file);
-
- return FileVisitResult.CONTINUE;
- }
- });
-
+ deleteIfExistsThrowable(path);
return true;
} catch (NoSuchFileException e) {
return true;
@@ -538,6 +519,34 @@ public class IgniteUtils {
}
}
+ /**
+ * Deletes a file or a directory with all sub-directories and files.
+ *
+ * @param path File or directory to delete.
+ * @throws IOException if an I/O error is thrown by a visitor method
+ */
+ public static void deleteIfExistsThrowable(Path path) throws IOException {
+ Files.walkFileTree(path, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException
exc) throws IOException {
+ if (exc != null) {
+ throw exc;
+ }
+
+ Files.delete(dir);
+
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes
attrs) throws IOException {
+ Files.delete(file);
+
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
/**
* Checks if assertions enabled.
*
diff --git a/modules/core/src/main/java/org/apache/ignite/lang/ErrorGroups.java
b/modules/core/src/main/java/org/apache/ignite/lang/ErrorGroups.java
index c96b81101b..bed0f9beab 100755
--- a/modules/core/src/main/java/org/apache/ignite/lang/ErrorGroups.java
+++ b/modules/core/src/main/java/org/apache/ignite/lang/ErrorGroups.java
@@ -390,4 +390,29 @@ public class ErrorGroups {
*/
public static final int CONFIG_WRITE_ERR =
NODE_CONFIGURATION_ERR_GROUP.registerErrorCode(3);
}
+
+ /**
+ * Code deployment error group.
+ */
+ public static class CodeDeployment {
+ /**
+ * Code deployment error group.
+ */
+ public static final ErrorGroup CODE_DEPLOYMENT_ERR_GROUP =
ErrorGroup.newGroup("CODEDEPLOY", 13);
+
+ /**
+ * Access to non-existing deployment unit.
+ */
+ public static final int UNIT_NOT_FOUND_ERR =
CODE_DEPLOYMENT_ERR_GROUP.registerErrorCode(1);
+
+ /**
+ * Unit duplicate error.
+ */
+ public static final int UNIT_ALREADY_EXISTS_ERR =
CODE_DEPLOYMENT_ERR_GROUP.registerErrorCode(2);
+
+ /**
+ * Deployment unit content read error.
+ */
+ public static final int UNIT_CONTENT_READ_ERR =
CODE_DEPLOYMENT_ERR_GROUP.registerErrorCode(3);
+ }
}
diff --git a/modules/runner/build.gradle b/modules/runner/build.gradle
index c1484c0bc3..a1f0650691 100644
--- a/modules/runner/build.gradle
+++ b/modules/runner/build.gradle
@@ -55,6 +55,7 @@ dependencies {
implementation project(':ignite-replicator')
implementation project(':ignite-distribution-zones')
implementation project(':ignite-placement-driver')
+ implementation project(':ignite-code-deployment')
implementation libs.jetbrains.annotations
implementation libs.micronaut.inject
implementation libs.micronaut.validation
@@ -114,6 +115,7 @@ dependencies {
integrationTestImplementation project(':ignite-metastorage')
integrationTestImplementation project(':ignite-table')
integrationTestImplementation project(':ignite-transactions')
+ integrationTestImplementation project(':ignite-code-deployment')
integrationTestImplementation project(':ignite-jdbc')
integrationTestImplementation(testFixtures(project(':ignite-core')))
integrationTestImplementation(testFixtures(project(':ignite-configuration')))
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/deployment/ItDeploymentUnitTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/deployment/ItDeploymentUnitTest.java
new file mode 100644
index 0000000000..32659a44dc
--- /dev/null
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/deployment/ItDeploymentUnitTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.deployment;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import org.apache.ignite.deployment.DeploymentUnit;
+import org.apache.ignite.deployment.UnitStatus;
+import org.apache.ignite.deployment.UnitStatus.UnitStatusBuilder;
+import org.apache.ignite.deployment.version.Version;
+import org.apache.ignite.internal.AbstractClusterIntegrationTest;
+import org.apache.ignite.internal.app.IgniteImpl;
+import
org.apache.ignite.internal.deployunit.configuration.DeploymentConfiguration;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests for {@link org.apache.ignite.deployment.IgniteDeployment}.
+ */
+public class ItDeploymentUnitTest extends AbstractClusterIntegrationTest {
+ private static final long REPLICA_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
+ private static final long SIZE_IN_BYTES = 1024L;
+
+ private Path dummyFile;
+
+ @BeforeEach
+ public void generateDummy() throws IOException {
+ dummyFile = workDir.resolve("dummy.txt");
+
+ if (!Files.exists(dummyFile)) {
+ try (SeekableByteChannel channel = Files.newByteChannel(dummyFile,
WRITE, CREATE)) {
+ channel.position(SIZE_IN_BYTES - 4);
+
+ ByteBuffer buf = ByteBuffer.allocate(4).putInt(2);
+ buf.rewind();
+ channel.write(buf);
+ }
+ }
+ }
+
+ @Test
+ public void testDeploy() throws Exception {
+ Unit unit = deployAndVerify("test", Version.parseVersion("1.1.0"), 1);
+
+ IgniteImpl cmg = cluster.node(0);
+ waitUnitReplica(cmg, unit);
+
+ CompletableFuture<List<UnitStatus>> list =
node(2).deployment().unitsAsync();
+ UnitStatusBuilder builder =
UnitStatus.builder(unit.id).append(unit.version,
List.of(unit.deployedNode.name(), cmg.name()));
+ assertThat(list, willBe(List.of(builder.build())));
+ }
+
+ @Test
+ public void testDeployUndeploy() throws Exception {
+ Unit unit = deployAndVerify("test", Version.parseVersion("1.1.0"), 1);
+ unit.undeploy();
+
+ CompletableFuture<List<UnitStatus>> list =
node(2).deployment().unitsAsync();
+ assertThat(list, willBe(Collections.emptyList()));
+ }
+
+ @Test
+ public void testDeployTwoUnits() throws Exception {
+ String id = "test";
+ Unit unit1 = deployAndVerify(id, Version.parseVersion("1.1.0"), 1);
+ Unit unit2 = deployAndVerify(id, Version.parseVersion("1.1.1"), 2);
+
+ IgniteImpl cmg = cluster.node(0);
+ waitUnitReplica(cmg, unit1);
+ waitUnitReplica(cmg, unit2);
+
+ CompletableFuture<UnitStatus> list =
node(2).deployment().statusAsync(id);
+ UnitStatusBuilder status = UnitStatus.builder(id)
+ .append(unit1.version, List.of(unit1.deployedNode.name(),
cmg.name()))
+ .append(unit2.version, List.of(unit2.deployedNode.name(),
cmg.name()));
+ assertThat(list, willBe(status.build()));
+
+ CompletableFuture<List<Version>> versions =
node(2).deployment().versionsAsync(unit1.id);
+ assertThat(versions, willBe(List.of(unit1.version, unit2.version)));
+ }
+
+ @Test
+ public void testDeployTwoUnitsAndUndeployOne() throws Exception {
+ Unit unit1 = deployAndVerify("test", Version.parseVersion("1.1.0"), 1);
+ Unit unit2 = deployAndVerify("test", Version.parseVersion("1.1.1"), 2);
+
+ IgniteImpl cmg = cluster.node(0);
+ waitUnitReplica(cmg, unit1);
+ waitUnitReplica(cmg, unit2);
+
+ CompletableFuture<UnitStatus> list =
node(2).deployment().statusAsync(unit2.id);
+ UnitStatusBuilder builder = UnitStatus.builder(unit1.id)
+ .append(unit1.version, List.of(unit1.deployedNode.name(),
cmg.name()))
+ .append(unit2.version, List.of(unit2.deployedNode.name(),
cmg.name()));
+ assertThat(list, willBe(builder.build()));
+
+ unit2.undeploy();
+ CompletableFuture<List<Version>> newVersions =
node(2).deployment().versionsAsync(unit1.id);
+ assertThat(newVersions, willBe(List.of(unit1.version)));
+ }
+
+ private Unit deployAndVerify(String id, Version version, int nodeIndex) {
+ IgniteImpl entryNode = node(nodeIndex);
+
+ CompletableFuture<Boolean> deploy = entryNode.deployment()
+ .deployAsync(id, version, fromPath(dummyFile));
+
+ assertThat(deploy, willBe(true));
+
+ Unit unit = new Unit(entryNode, id, version);
+ assertTrue(Files.exists(getNodeUnitFile(unit)));
+
+ return unit;
+ }
+
+ private Path getNodeUnitFile(Unit unit) {
+ return getNodeUnitFile(unit.deployedNode, unit.id, unit.version);
+ }
+
+ private Path getNodeUnitFile(IgniteImpl node, String unitId, Version
unitVersion) {
+ String deploymentFolder = node.nodeConfiguration()
+ .getConfiguration(DeploymentConfiguration.KEY)
+ .deploymentLocation().value();
+ Path resolve = workDir.resolve(node.name()).resolve(deploymentFolder);
+ return resolve.resolve(unitId)
+ .resolve(unitVersion.render())
+ .resolve(dummyFile.getFileName());
+ }
+
+ private void waitUnitReplica(IgniteImpl ignite, Unit unit) throws
InterruptedException {
+ Path unitPath = getNodeUnitFile(ignite, unit.id, unit.version);
+ assertTrue(IgniteTestUtils.waitForCondition(() -> {
+ try {
+ return Files.exists(unitPath) && Files.size(unitPath) ==
SIZE_IN_BYTES;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }, REPLICA_TIMEOUT));
+ }
+
+ private void waitUnitClean(IgniteImpl ignite, Unit unit) throws
InterruptedException {
+ Path unitPath = getNodeUnitFile(ignite, unit.id, unit.version);
+ assertTrue(IgniteTestUtils.waitForCondition(() ->
!Files.exists(unitPath), REPLICA_TIMEOUT));
+ }
+
+ class Unit {
+ private final IgniteImpl deployedNode;
+
+ private final String id;
+
+ private final Version version;
+
+ Unit(IgniteImpl deployedNode, String id, Version version) {
+ this.deployedNode = deployedNode;
+ this.id = id;
+ this.version = version;
+ }
+
+ void undeploy() throws InterruptedException {
+ deployedNode.deployment().undeployAsync(id, version);
+ waitUnitClean(deployedNode, this);
+ }
+ }
+
+ static DeploymentUnit fromPath(Path path) {
+ Objects.requireNonNull(path);
+ return new DeploymentUnit() {
+
+ @Override
+ public String name() {
+ return path.getFileName().toString();
+ }
+
+ @Override
+ public InputStream content() {
+ try {
+ return new FileInputStream(path.toFile());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ }
+}
diff --git
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index c246c3525b..9b015328c9 100644
---
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -42,6 +42,7 @@ import org.apache.ignite.Ignite;
import org.apache.ignite.IgnitionManager;
import org.apache.ignite.client.handler.ClientHandlerModule;
import org.apache.ignite.compute.IgniteCompute;
+import org.apache.ignite.deployment.IgniteDeployment;
import org.apache.ignite.internal.baseline.BaselineManager;
import
org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
import
org.apache.ignite.internal.cluster.management.configuration.ClusterManagementConfiguration;
@@ -66,6 +67,8 @@ import
org.apache.ignite.internal.configuration.ServiceLoaderModulesProvider;
import org.apache.ignite.internal.configuration.storage.ConfigurationStorage;
import
org.apache.ignite.internal.configuration.storage.DistributedConfigurationStorage;
import
org.apache.ignite.internal.configuration.storage.LocalFileConfigurationStorage;
+import org.apache.ignite.internal.deployunit.DeploymentManagerImpl;
+import
org.apache.ignite.internal.deployunit.configuration.DeploymentConfiguration;
import org.apache.ignite.internal.distributionzones.DistributionZoneManager;
import
org.apache.ignite.internal.distributionzones.configuration.DistributionZonesConfiguration;
import org.apache.ignite.internal.hlc.HybridClock;
@@ -73,6 +76,7 @@ import org.apache.ignite.internal.hlc.HybridClockImpl;
import org.apache.ignite.internal.index.IndexManager;
import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.manager.IgniteComponent;
import org.apache.ignite.internal.metastorage.MetaStorageManager;
import org.apache.ignite.internal.metastorage.impl.MetaStorageManagerImpl;
import
org.apache.ignite.internal.metastorage.server.persistence.RocksDbKeyValueStorage;
@@ -249,6 +253,8 @@ public class IgniteImpl implements Ignite {
/** Metric manager. */
private final MetricManager metricManager;
+ private final IgniteDeployment deploymentManager;
+
private final DistributionZoneManager distributionZoneManager;
/** Creator for volatile {@link
org.apache.ignite.internal.raft.storage.LogStorageFactory} instances. */
@@ -497,6 +503,12 @@ public class IgniteImpl implements Ignite {
sql,
() -> cmgMgr.clusterState().thenApply(s ->
s.clusterTag().clusterId())
);
+
+ deploymentManager = new DeploymentManagerImpl(clusterSvc,
+ metaStorageMgr,
+ workDir,
+
nodeConfigRegistry.getConfiguration(DeploymentConfiguration.KEY),
+ cmgMgr);
}
private RestComponent createRestComponent(String name) {
@@ -627,7 +639,8 @@ public class IgniteImpl implements Ignite {
distributedTblMgr,
indexManager,
qryEngine,
- clientHandlerModule
+ clientHandlerModule,
+ (IgniteComponent) deploymentManager
);
} catch (NodeStoppingException e) {
throw new CompletionException(e);
@@ -750,6 +763,11 @@ public class IgniteImpl implements Ignite {
return compute;
}
+ @Override
+ public IgniteDeployment deployment() {
+ return deploymentManager;
+ }
+
/** {@inheritDoc} */
@Override
public Collection<ClusterNode> clusterNodes() {
diff --git a/settings.gradle b/settings.gradle
index 5ee56ae73d..3632092a4a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -65,6 +65,7 @@ include(':packaging')
include(':ignite-replicator')
include(':ignite-distribution-zones')
include(':ignite-placement-driver')
+include(':ignite-code-deployment')
project(":ignite-examples").projectDir = file('examples')
project(":ignite-page-memory").projectDir = file('modules/page-memory')
@@ -115,6 +116,7 @@ project(":packaging-db").projectDir = file('packaging/db')
project(":packaging").projectDir = file('packaging')
project(":ignite-distribution-zones").projectDir =
file('modules/distribution-zones')
project(":ignite-placement-driver").projectDir =
file('modules/placement-driver')
+project(":ignite-code-deployment").projectDir = file('modules/code-deployment')
ext.isCiServer = System.getenv().containsKey("IGNITE_CI")