From 928de5eea4a2f5683755bc249558ec8c4e16355d Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Sat, 14 Mar 2026 12:53:52 +0300 Subject: [PATCH 01/15] IGNITE-28207 IgniteClassPath creation --- .idea/inspectionProfiles/Project_Default.xml | 10 +- .../ignite/internal/util/lang/GridFunc.java | 14 ++ .../IgniteControlUtilityTestSuite2.java | 4 +- .../util/GridCommandHandlerClassPathTest.java | 73 ++++++++ .../ignite/internal/GridKernalContext.java | 6 + .../internal/GridKernalContextImpl.java | 12 ++ .../apache/ignite/internal/IgniteKernal.java | 2 + .../classpath/ClassPathProcessor.java | 169 ++++++++++++++++++ .../internal/classpath/IgniteClassPath.java | 93 ++++++++++ .../classpath/IgniteClassPathState.java | 30 ++++ .../internal/client/thin/ClientOperation.java | 3 + .../internal/client/thin/TcpIgniteClient.java | 52 ++++++ .../management/IgniteCommandRegistry.java | 4 +- .../classpath/ClassPathCommand.java | 30 ++++ .../classpath/ClassPathCreateCommand.java | 131 ++++++++++++++ .../classpath/ClassPathCreateCommandArg.java | 67 +++++++ .../classpath/ClassPathStartCreationTask.java | 51 ++++++ .../persistence/filename/NodeFileTree.java | 18 ++ .../persistence/filename/SharedFileTree.java | 8 + .../reader/StandaloneGridKernalContext.java | 6 + .../platform/client/ClientMessageParser.java | 7 + .../classpath/ClientFileUploadRequest.java | 141 +++++++++++++++ .../internal/visor/VisorOneNodeTask.java | 2 + 23 files changed, 928 insertions(+), 5 deletions(-) create mode 100644 modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCommand.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommandArg.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathStartCreationTask.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 0af10ada908d7..230bf03e768a9 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -369,7 +369,6 @@ - - @@ -625,7 +623,13 @@ - + + + diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java index 8bf794062ef45..6c38c62040684 100755 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/lang/GridFunc.java @@ -2153,4 +2153,18 @@ public static Collection emptyIfNull(@Nullable Collection col) { public static Map emptyIfNull(@Nullable Map map) { return map == null ? Collections.emptyMap() : map; } + + /** + * @param arr Array. + * @param el Element. + * @return Element index or {@code -1} if not found. + */ + public static int indexOf(T[] arr, T el) { + for (int i = 0; i < arr.length; i++) { + if (Objects.equals(arr[i], el)) + return i; + } + + return -1; + } } diff --git a/modules/control-utility/src/test/java/org/apache/ignite/testsuites/IgniteControlUtilityTestSuite2.java b/modules/control-utility/src/test/java/org/apache/ignite/testsuites/IgniteControlUtilityTestSuite2.java index d7ed2b5124613..d60fe72b6d98d 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/testsuites/IgniteControlUtilityTestSuite2.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/testsuites/IgniteControlUtilityTestSuite2.java @@ -22,6 +22,7 @@ import org.apache.ignite.util.CacheMetricsCommandTest; import org.apache.ignite.util.CdcCommandTest; import org.apache.ignite.util.CdcResendCommandTest; +import org.apache.ignite.util.GridCommandHandlerClassPathTest; import org.apache.ignite.util.GridCommandHandlerConsistencyBinaryTest; import org.apache.ignite.util.GridCommandHandlerConsistencyCountersTest; import org.apache.ignite.util.GridCommandHandlerConsistencyRepairCorrectnessAtomicTest; @@ -77,7 +78,8 @@ SecurityCommandHandlerPermissionsTest.class, - IdleVerifyDumpTest.class + IdleVerifyDumpTest.class, + GridCommandHandlerClassPathTest.class }) public class IgniteControlUtilityTestSuite2 { } diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java new file mode 100644 index 0000000000000..6b3cfad39d3ce --- /dev/null +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.util; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Collectors; +import org.junit.Test; + +import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK; + +/** + * Test for --classpath command. + */ +public class GridCommandHandlerClassPathTest extends GridCommandHandlerAbstractTest { + /** {@inheritDoc} */ + @Override protected void beforeTestsStarted() throws Exception { + cleanPersistenceDir(); + + startGrids(2); + + super.beforeTestsStarted(); + } + + /** Tests --create command. */ + @Test + public void testCreate() throws Exception { + String jars = Files.list(Path.of(getClass().getClassLoader().getResource(".").getPath() + "../")) + .map(Path::toAbsolutePath) + .map(Path::toString) + .filter(f -> f.endsWith("jar")) + .collect(Collectors.joining(",")); + + Path dir = Path.of(getClass().getClassLoader().getResource(".").getPath() + "../../../core/target"); + + System.out.println("dir = " + dir); + + String coreJars = Files.list(dir) + .map(Path::toAbsolutePath) + .map(Path::toString) + .filter(f -> f.endsWith("jar")) + .collect(Collectors.joining(",")); + + jars += "," + coreJars; + + injectTestSystemOut(); + + final TestCommandHandler hnd = newCommandHandler(createTestLogger()); + + assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", "mysuperapp", "--files", jars)); + + String outStr = testOut.toString(); + + stopAllGrids(); + + System.out.println(outStr); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java index 742105bf4da59..e9a0a1a14afd3 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContext.java @@ -26,6 +26,7 @@ import org.apache.ignite.internal.binary.BinaryMarshaller; import org.apache.ignite.internal.cache.query.index.IndexProcessor; import org.apache.ignite.internal.cache.transform.CacheObjectTransformerProcessor; +import org.apache.ignite.internal.classpath.ClassPathProcessor; import org.apache.ignite.internal.managers.checkpoint.GridCheckpointManager; import org.apache.ignite.internal.managers.collision.GridCollisionManager; import org.apache.ignite.internal.managers.communication.GridIoManager; @@ -648,6 +649,11 @@ public interface GridKernalContext extends Iterable { */ public RollingUpgradeProcessor rollingUpgrade(); + /** + * @return Class path processor. + */ + public ClassPathProcessor classPath(); + /** * Executor that is in charge of processing user async continuations. * diff --git a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java index 2a7c7f66afa67..831d2a71b6f74 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/GridKernalContextImpl.java @@ -42,6 +42,7 @@ import org.apache.ignite.internal.binary.GridBinaryMarshaller; import org.apache.ignite.internal.cache.query.index.IndexProcessor; import org.apache.ignite.internal.cache.transform.CacheObjectTransformerProcessor; +import org.apache.ignite.internal.classpath.ClassPathProcessor; import org.apache.ignite.internal.maintenance.MaintenanceProcessor; import org.apache.ignite.internal.managers.checkpoint.GridCheckpointManager; import org.apache.ignite.internal.managers.collision.GridCollisionManager; @@ -367,6 +368,10 @@ public class GridKernalContextImpl implements GridKernalContext, Externalizable @GridToStringExclude private RollingUpgradeProcessor rollUpProc; + /** Classpath processor. */ + @GridToStringExclude + private ClassPathProcessor classPathProc; + /** */ private Thread.UncaughtExceptionHandler hnd; @@ -603,6 +608,8 @@ else if (comp instanceof PerformanceStatisticsProcessor) perfStatProc = (PerformanceStatisticsProcessor)comp; else if (comp instanceof RollingUpgradeProcessor) rollUpProc = (RollingUpgradeProcessor)comp; + else if (comp instanceof ClassPathProcessor) + classPathProc = (ClassPathProcessor)comp; else if (comp instanceof IndexProcessor) indexProc = (IndexProcessor)comp; else if (!(comp instanceof DiscoveryNodeValidationProcessor @@ -1117,6 +1124,11 @@ public void recoveryMode(boolean recoveryMode) { return rollUpProc; } + /** {@inheritDoc} */ + @Override public ClassPathProcessor classPath() { + return classPathProc; + } + /** {@inheritDoc} */ @Override public Executor getAsyncContinuationExecutor() { return config().getAsyncContinuationExecutor() == null diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java index ebaa9d0bbabcd..5c73d1de07c34 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java @@ -90,6 +90,7 @@ import org.apache.ignite.internal.binary.BinaryUtils; import org.apache.ignite.internal.cache.query.index.IndexProcessor; import org.apache.ignite.internal.cache.transform.CacheObjectTransformerProcessor; +import org.apache.ignite.internal.classpath.ClassPathProcessor; import org.apache.ignite.internal.cluster.ClusterGroupAdapter; import org.apache.ignite.internal.cluster.IgniteClusterEx; import org.apache.ignite.internal.maintenance.MaintenanceProcessor; @@ -1008,6 +1009,7 @@ public void start( startManager(new GridFailoverManager(ctx)); startManager(new GridCollisionManager(ctx)); startManager(new GridIndexingManager(ctx)); + startProcessor(new ClassPathProcessor(ctx)); // Assign discovery manager to context before other processors start so they // are able to register custom event listener. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java new file mode 100644 index 0000000000000..4b9e24f743a81 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -0,0 +1,169 @@ +/* + * 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.classpath; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.UUID; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteException; +import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.processors.GridProcessorAdapter; +import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; +import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.util.typedef.internal.A; +import org.apache.ignite.internal.util.typedef.internal.U; + +import static org.apache.ignite.internal.classpath.IgniteClassPathState.CREATING; + +/** + * TODO: + * 1. How to check data integrity on start? + * Do we want to do this for txt file or for jar only? + * 2. Check and remove obsolete icp from dist on start. + * Do we want to have some flag to skip remove in this case? (if we preparing for ICP registration). + * 3. Should we include CP into snapshots and dumps? + */ +public class ClassPathProcessor extends GridProcessorAdapter { + /** Prefix for metastorage keys. */ + public static final String METASTORE_PREFIX = "classpath."; + + /** + * @param ctx Kernal context. + */ + public ClassPathProcessor(GridKernalContext ctx) { + super(ctx); + } + + /** + * Register new classpath in metastorage it same name not exists. + * Fails if exists. + * + * @param name + * @param files + * @param lengths + * @return + */ + public UUID startCreation(String name, String[] files, long[] lengths) { + assert files.length == lengths.length : "wrong arrays lengths"; + A.ensure(U.alphanumericUnderscore(name), "Classpath name must satisfy the following name pattern: a-zA-Z0-9_"); + + String key = METASTORE_PREFIX + name; + + IgniteClassPath icp = new IgniteClassPath(UUID.randomUUID(), name, files, lengths); + + try { + if (!ctx.distributedMetastorage().compareAndSet(key, null, icp)) + throw new IgniteException("Classpath alreay exists: " + name); + } + catch (IgniteCheckedException e) { + throw new IgniteException(e); + } + + try { + NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); + + File root = ft.classPath(name); + + NodeFileTree.mkdir(root, "Ignite Class Path root: " + name); + + log.info("New classpath registered [root = " + root + ", icp=" + icp + ']'); + + return icp.id(); + } + catch (Exception e) { + try { + ctx.distributedMetastorage().remove(key); + } + catch (IgniteCheckedException ex) { + log.error("Can't remove metastorage key for IgniteClassPath: " + key, e); + } + + throw e; + } + } + + /** + * @param icpID ClassPath ID. + * @param name File name. + * @param offset Offset to write data to. + * @param bytesCnt Bytes count in batch to write. + * @param batch Batch. + */ + public void uploadFilePart( + UUID icpID, + String name, + long offset, + int bytesCnt, + byte[] batch + ) throws IgniteCheckedException, IOException { + try { + IgniteClassPath icp = search(icpID); + + if (F.indexOf(icp.files(), name) == -1) + throw new IllegalArgumentException("Unknown lib [icp=" + icp.name() + ", unknown_lib=" + name + ']'); + + File lib = new File(ctx.pdsFolderResolver().fileTree().classPath(icp.name()), name); + + if (offset == 0) { + log.info("Creating new classpath file: " + lib); + + if (!lib.createNewFile()) + throw new IgniteException("File exists: " + lib); + } + + try (RandomAccessFile raf = new RandomAccessFile(lib, "rw")) { + if (raf.length() < offset) { + throw new IgniteException("Wrong offset [icp=" + icp.name() + ", lib=" + name + ", " + + "fileLength=" + raf.length() + ", offset=" + offset + ']'); + } + + raf.seek(offset); + raf.write(batch, 0, bytesCnt); + } + } + catch (Exception e) { + log.error("UploadFilePart:", e); + + throw e; + } + } + + /** + * @param icpID ClassPath ID. + * @return Class path. + * @throws IgniteCheckedException If failed. + */ + private IgniteClassPath search(UUID icpID) throws IgniteCheckedException { + IgniteClassPath[] icp = new IgniteClassPath[1]; + + ctx.distributedMetastorage().iterate(METASTORE_PREFIX, (key, icp0) -> { + if (icpID.equals(((IgniteClassPath)icp0).id())) + icp[0] = (IgniteClassPath)icp0; + }); + + if (icp[0] == null) + throw new IgniteException("ClassPath not found: " + icpID); + + if (icp[0].state() != CREATING) + throw new IgniteException("ClassPath in wrong state [expected=" + CREATING + ", status=" + icp[0].state() + ']'); + + return icp[0]; + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java new file mode 100644 index 0000000000000..78c2127723976 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.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.classpath; + +import java.io.Serializable; +import java.util.UUID; +import org.apache.ignite.internal.util.typedef.internal.S; + +/** + * Class path POJO. + */ +public class IgniteClassPath implements Serializable { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + private UUID id; + + /** */ + private String name; + + /** */ + private String[] files; + + /** */ + private long[] lengths; + + /** */ + private IgniteClassPathState state; + + /** + * @param id Unique id of classpath. + * @param name User provided name. + * @param files Files to include to classpath. + */ + public IgniteClassPath(UUID id, String name, String[] files, long[] lengths) { + this.id = id; + this.name = name; + this.files = files; + this.lengths = lengths; + this.state = IgniteClassPathState.CREATING; + } + + /** */ + public IgniteClassPathState state() { + return state; + } + + /** */ + public void state(IgniteClassPathState state) { + this.state = state; + } + + /** */ + public UUID id() { + return id; + } + + /** */ + public String name() { + return name; + } + + /** */ + public String[] files() { + return files; + } + + /** */ + public long[] lengths() { + return lengths; + } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(IgniteClassPath.class, this); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java new file mode 100644 index 0000000000000..595bec09fc129 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java @@ -0,0 +1,30 @@ +/* + * 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.classpath; + +/** */ +public enum IgniteClassPathState { + /** Creationg process in progress. */ + CREATING, + + /** Ready for usage. */ + READY, + + /** Marked for removal. Newly started code can't use corresponding classpath. */ + REMOVING +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java index 112dc47c7125a..747dfd5867dff 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ClientOperation.java @@ -294,6 +294,9 @@ public enum ClientOperation { /** IgniteSet.iterator page. */ OP_SET_ITERATOR_GET_PAGE(9023), + /** File upload. */ + FILE_UPLOAD(9030), + /** Stop warmup. */ OP_STOP_WARMUP(10000); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index b21eca8578160..21e8b45149260 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -18,12 +18,17 @@ package org.apache.ignite.internal.client.thin; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.EventListener; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; @@ -55,6 +60,7 @@ import org.apache.ignite.client.events.ClientLifecycleEventListener; import org.apache.ignite.client.events.ClientStartEvent; import org.apache.ignite.client.events.ClientStopEvent; +import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.configuration.BinaryConfiguration; import org.apache.ignite.configuration.ClientConfiguration; import org.apache.ignite.configuration.ClientTransactionConfiguration; @@ -625,6 +631,52 @@ private void retrieveBinaryConfiguration(ClientConfiguration cfg) { marsh.setBinaryConfiguration(resCfg); } + /** + * @param node Node to upload file to. + * @param icpID Classpath ID. + * @param file File to upload. + */ + public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws IOException { + List uploadNode = Collections.singletonList(node.id()); + + String name = file.getFileName().toString(); + byte[] batch = new byte[(int)(U.MB)]; + + try (InputStream fis = Files.newInputStream(file)) { + long[] offset = new long[] {0}; + int[] bytesCnt = new int[1]; + + // We want to create empty file on the server side. + bytesCnt[0] = fis.read(batch); + + do { + ch.service( + ClientOperation.FILE_UPLOAD, + ch -> { + try (BinaryWriterEx w = BinaryUtils.writer(marsh.context(), ch.out(), null)) { + w.writeUuid(node.id()); + w.writeUuid(icpID); + w.writeString(name); + w.writeLong(offset[0]); + w.writeInt(bytesCnt[0]); + w.writeByteArray(batch); + } + }, + in -> null, + uploadNode // Request can still be sent to a random (default) node. + ); + + offset[0] += bytesCnt[0]; + bytesCnt[0] = fis.read(batch); + } + while (bytesCnt[0] > 0); + + // Check all data read and sent. + if (offset[0] != Files.size(file)) + throw new IOException("Can't read all data from file"); + } + } + /** * Thin client implementation of {@link BinaryMetadataHandler}. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java b/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java index 1f28f1cfbd24c..ec61e61f34667 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/IgniteCommandRegistry.java @@ -25,6 +25,7 @@ import org.apache.ignite.internal.management.cache.CacheCommand; import org.apache.ignite.internal.management.cdc.CdcCommand; import org.apache.ignite.internal.management.checkpoint.CheckpointCommand; +import org.apache.ignite.internal.management.classpath.ClassPathCommand; import org.apache.ignite.internal.management.consistency.ConsistencyCommand; import org.apache.ignite.internal.management.defragmentation.DefragmentationCommand; import org.apache.ignite.internal.management.diagnostic.DiagnosticCommand; @@ -77,7 +78,8 @@ public IgniteCommandRegistry() { new DefragmentationCommand(), new PerformanceStatisticsCommand(), new CdcCommand(), - new ConsistencyCommand() + new ConsistencyCommand(), + new ClassPathCommand() ); U.loadService(CommandsProvider.class).forEach(p -> p.commands().forEach(this::register)); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCommand.java new file mode 100644 index 0000000000000..f297278661da7 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCommand.java @@ -0,0 +1,30 @@ +/* + * 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.management.classpath; + +import org.apache.ignite.internal.management.api.CommandRegistryImpl; + +/** Command to manage IgniteClassPath. */ +public class ClassPathCommand extends CommandRegistryImpl { + /** */ + public ClassPathCommand() { + super( + new ClassPathCreateCommand() + ); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java new file mode 100644 index 0000000000000..2d25cccc642e1 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java @@ -0,0 +1,131 @@ +/* + * 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.management.classpath; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import org.apache.ignite.Ignite; +import org.apache.ignite.IgniteException; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.internal.client.thin.TcpIgniteClient; +import org.apache.ignite.internal.management.api.CommandUtils; +import org.apache.ignite.internal.management.api.NativeCommand; +import org.apache.ignite.internal.util.typedef.F; +import org.jetbrains.annotations.Nullable; + +/** + * + */ +public class ClassPathCreateCommand implements NativeCommand { + /** {@inheritDoc} */ + @Override public String description() { + return "Create ClassPath instance from set of local file"; + } + + /** {@inheritDoc} */ + @Override public Class argClass() { + return ClassPathCreateCommandArg.class; + } + + /** {@inheritDoc} */ + @Override public Void execute( + @Nullable IgniteClient client, + @Nullable Ignite ignite, + ClassPathCreateCommandArg arg, + Consumer printer + ) throws Exception { + if (client != null) { + create(client, arg, printer); + + return null; + } + + throw new UnsupportedOperationException("Creating with the Ignite instance not supported at a time."); + } + + /** */ + private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg, Consumer printer) throws Exception { + TcpIgniteClient cli = (TcpIgniteClient)client; + + List files = new ArrayList<>(arg.files.length); + long[] lengths = new long[arg.files.length]; + + for (int i = 0; i < arg.files.length; i++) { + Path f = Path.of(arg.files[i]); + + if (!Files.exists(f)) + throw new IllegalArgumentException("File not exists: " + f); + + files.add(f); + + lengths[i] = Files.size(f); + } + + arg.lengths = lengths; + // We don't want to send full path to server nodes. + // Server nodes require files names, only. + arg.files = fileNames(files); + + ClusterNode uploadNode = uploadNode(client); + + printer.accept("Upload node: " + uploadNode.id()); + + UUID icpID = CommandUtils.execute(client, null, ClassPathStartCreationTask.class, arg, Collections.singletonList(uploadNode)); + + printer.accept("New classpath registered [uploadNode=" + uploadNode.id() + ", name=" + arg.name + ", id=" + icpID.toString() + ']'); + printer.accept("Starting to upload files:"); + + for (Path file : files) { + printer.accept(String.valueOf(file.toAbsolutePath())); + cli.uploadClasspathFile(uploadNode, icpID, file); + printer.accept("DONE"); + } + } + + /** */ + private static ClusterNode uploadNode(IgniteClient client) { + Collection nodes = CommandUtils.nodes(client, null); + + if (F.isEmpty(nodes)) + throw new IgniteException("Cluster empty"); + + Collection servers = CommandUtils.servers(nodes); + + if (F.isEmpty(servers)) + throw new IgniteException("No server nodes"); + + return F.first(servers); + } + + /** */ + private static String[] fileNames(List files) { + String[] fileNames = new String[files.size()]; + + for (int i = 0; i < fileNames.length; i++) + fileNames[i] = files.get(i).getFileName().toString(); + + return fileNames; + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommandArg.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommandArg.java new file mode 100644 index 0000000000000..586654424a80d --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommandArg.java @@ -0,0 +1,67 @@ +/* + * 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.management.classpath; + +import org.apache.ignite.internal.Order; +import org.apache.ignite.internal.dto.IgniteDataTransferObject; +import org.apache.ignite.internal.management.api.Argument; + +/** */ +public class ClassPathCreateCommandArg extends IgniteDataTransferObject { + /** */ + private static final long serialVersionUID = 0; + + /** */ + @Order(0) + @Argument(description = "Name of the classpath") + String name; + + /** */ + @Order(1) + @Argument(description = "Files to add to classpath") + String[] files; + + /** */ + @Order(2) + long[] lengths; + + /** */ + public String name() { + return name; + } + + /** */ + public void name(String name) { + this.name = name; + } + + /** */ + public String[] files() { + return files; + } + + /** */ + public void files(String[] files) { + this.files = files; + } + + /** */ + public long[] lengths() { + return lengths; + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathStartCreationTask.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathStartCreationTask.java new file mode 100644 index 0000000000000..c7c11a1c7fce8 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathStartCreationTask.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.management.classpath; + +import java.util.UUID; +import org.apache.ignite.IgniteException; +import org.apache.ignite.internal.visor.VisorJob; +import org.apache.ignite.internal.visor.VisorOneNodeTask; +import org.jetbrains.annotations.Nullable; + +/** */ +public class ClassPathStartCreationTask extends VisorOneNodeTask { + /** */ + private static final long serialVersionUID = 0L; + + /** {@inheritDoc} */ + @Override protected VisorJob job(ClassPathCreateCommandArg arg) { + return new ClassPathStartCreationJob(arg, debug); + } + + /** */ + private static class ClassPathStartCreationJob extends VisorJob { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + protected ClassPathStartCreationJob(@Nullable ClassPathCreateCommandArg arg, boolean debug) { + super(arg, debug); + } + + /** {@inheritDoc} */ + @Override protected UUID run(@Nullable ClassPathCreateCommandArg arg) throws IgniteException { + return ignite.context().classPath().startCreation(arg.name, arg.files, arg.lengths); + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java index 159eeb77db7c9..2100a9fe0d0f6 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java @@ -295,6 +295,9 @@ public class NodeFileTree extends SharedFileTree { /** Path to the storage directory. */ private final File nodeStorage; + /** Path to the root classpath directory. */ + private final File icp; + /** * Key is the path from {@link DataStorageConfiguration#getExtraStoragePaths()}, may be relative. Value is storage. * @see DataStorageConfiguration#getExtraStoragePaths() @@ -334,6 +337,7 @@ public NodeFileTree(File root, String folderName) { this.folderName = folderName; binaryMeta = new File(binaryMetaRoot, folderName); + icp = new File(icpRoot, folderName); nodeStorage = rootRelative(DB_DIR); checkpoint = new File(nodeStorage, CHECKPOINT_DIR); wal = rootRelative(DFLT_WAL_PATH); @@ -382,6 +386,7 @@ protected NodeFileTree(IgniteConfiguration cfg, File root, String folderName, bo this.folderName = folderName; binaryMeta = new File(binaryMetaRoot, folderName); + icp = new File(icpRoot, folderName); DataStorageConfiguration dsCfg = cfg.getDataStorageConfiguration(); @@ -431,6 +436,11 @@ public File binaryMeta() { return binaryMeta; } + /** @return Root directory for Ignite class path files. */ + public File classPathRoot() { + return icp; + } + /** @return Path to the directory containing active WAL segments. */ public @Nullable File wal() { return wal; @@ -1082,6 +1092,14 @@ public File maintenanceFile() { return new File(nodeStorage, MAINTENANCE_FILE_NAME); } + /** + * @param name IgniteClassPath name. + * @return IgniteClassPath directory. + */ + public File classPath(String name) { + return new File(icp, name); + } + /** * @param includeMeta If {@code true} then include metadata directory into results. * @param filter Cache group names to filter. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SharedFileTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SharedFileTree.java index 33df1ed636040..199b9a00e38d4 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SharedFileTree.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/SharedFileTree.java @@ -36,6 +36,7 @@ * ├── db ← db (shared between all local nodes). * │ ├── binary_meta ← binaryMetaRoot (shared between all local nodes). * │ ├── marshaller ← marshaller (shared between all local nodes). + * │ ├── classpath ← classpath (shared between all local nodes). * └── snapshots ← snpsRoot (shared between all local nodes). * * @@ -48,6 +49,9 @@ public class SharedFileTree { /** Name of marshaller mappings folder. */ public static final String MARSHALLER_DIR = "marshaller"; + /** Name of classpath folder. */ + public static final String CLASSPATH_DIR = "classpath"; + /** Database default folder. */ protected static final String DB_DIR = "db"; @@ -60,6 +64,9 @@ public class SharedFileTree { /** Path to the directory containing marshaller files. */ private final File marshaller; + /** Path to the directory containing classpath files. */ + protected final File icpRoot; + /** Path to the snapshot root directory. */ private final File snpsRoot; @@ -77,6 +84,7 @@ protected SharedFileTree(File root, String snpsRoot) { marshaller = Paths.get(rootStr, DB_DIR, MARSHALLER_DIR).toFile(); binaryMetaRoot = Paths.get(rootStr, DB_DIR, BINARY_METADATA_DIR).toFile(); + icpRoot = Paths.get(rootStr, DB_DIR, CLASSPATH_DIR).toFile(); } /** diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java index 84af46b6e31d9..a14437e56ee9f 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java @@ -44,6 +44,7 @@ import org.apache.ignite.internal.binary.GridBinaryMarshaller; import org.apache.ignite.internal.cache.query.index.IndexProcessor; import org.apache.ignite.internal.cache.transform.CacheObjectTransformerProcessor; +import org.apache.ignite.internal.classpath.ClassPathProcessor; import org.apache.ignite.internal.managers.checkpoint.GridCheckpointManager; import org.apache.ignite.internal.managers.collision.GridCollisionManager; import org.apache.ignite.internal.managers.communication.GridIoManager; @@ -747,6 +748,11 @@ private void setField(IgniteEx kernal, String name, Object val) throws NoSuchFie return null; } + /** {@inheritDoc} */ + @Override public ClassPathProcessor classPath() { + return null; + } + /** {@inheritDoc} */ @Override public Executor getAsyncContinuationExecutor() { return null; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java index d0f54ef2136ba..4d344fc3f1216 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java @@ -76,6 +76,7 @@ import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheScanQueryRequest; import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlFieldsQueryRequest; import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlQueryRequest; +import org.apache.ignite.internal.processors.platform.client.classpath.ClientFileUploadRequest; import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterChangeStateRequest; import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGetDataCenterNodesRequest; import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGetStateRequest; @@ -410,6 +411,9 @@ public class ClientMessageParser implements ClientListenerMessageParser { /** Get service topology. */ private static final short OP_SERVICE_GET_TOPOLOGY = 7003; + /** File upload. */ + public static final short FILE_UPLOAD = 9030; + /** Operations that are performed before a node is joined to the topology. */ /** Stop warmup. */ private static final short OP_STOP_WARMUP = 10000; @@ -735,6 +739,9 @@ public ClientListenerRequest decode(BinaryReaderEx reader) { case OP_SERVICE_GET_TOPOLOGY: return new ClientServiceTopologyRequest(reader); + case FILE_UPLOAD: + return new ClientFileUploadRequest(reader); + case OP_STOP_WARMUP: return new ClientCacheStopWarmupRequest(reader); } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java new file mode 100644 index 0000000000000..2546be36b18c8 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java @@ -0,0 +1,141 @@ +/* + * 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.processors.platform.client.classpath; + +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteException; +import org.apache.ignite.binary.BinaryRawReader; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.internal.GridClosureCallMode; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext; +import org.apache.ignite.internal.processors.platform.client.ClientRequest; +import org.apache.ignite.internal.processors.platform.client.ClientResponse; +import org.apache.ignite.lang.IgniteCallable; +import org.apache.ignite.resources.IgniteInstanceResource; + +import static org.apache.ignite.internal.processors.task.TaskExecutionOptions.options; + +/** */ +public class ClientFileUploadRequest extends ClientRequest { + /** */ + private final FileUploadPartCallable data; + + /** + * Creates the file upload request. + * + * @param reader Reader. + */ + public ClientFileUploadRequest(BinaryRawReader reader) { + super(reader); + + data = new FileUploadPartCallable( + reader.readUuid(), + reader.readUuid(), + reader.readString(), + reader.readLong(), + reader.readInt(), + reader.readByteArray() + ); + } + + /** {@inheritDoc} */ + @Override public ClientResponse process(ClientConnectionContext ctx) { + // TODO: add backward compatibility support. + + try { + // Forward request to upload node. + if (!ctx.kernalContext().localNodeId().equals(data.uploadNodeId)) { + ClusterNode uploadNode = ctx.kernalContext().cluster().get().node(data.uploadNodeId); + + if (uploadNode == null) { + throw new IgniteException("Upload node not found [localNode=" + ctx.kernalContext().localNodeId() + + ", uploadNode=" + data.uploadNodeId + ", icp=" + data.name + ']'); + } + + ctx.kernalContext().closure().callAsync( + GridClosureCallMode.BALANCE, + data, + options(Collections.singletonList(uploadNode)).withFailoverDisabled() + ).get(); + + return new ClientResponse(requestId()); + } + + ctx.kernalContext().classPath().uploadFilePart(data.icpId, data.name, data.offset, data.bytesCnt, data.batch); + + return new ClientResponse(requestId()); + } + catch (IgniteCheckedException | IOException e) { + return new ClientResponse(requestId(), e.getMessage()); + } + } + + /** */ + private static class FileUploadPartCallable implements IgniteCallable { + /** */ + private static final long serialVersionUID = 0L; + + /** Upload node ID. */ + private final UUID uploadNodeId; + + /** ClassPath ID. */ + private final UUID icpId; + + /** File name. */ + private final String name; + + /** Offset to write data to. */ + private final long offset; + + /** Bytes count in batch to write. */ + private final int bytesCnt; + + /** Batch. */ + private final byte[] batch; + + /** */ + public FileUploadPartCallable(UUID uploadNodeId, UUID icpId, String name, long offset, int bytesCnt, byte[] batch) { + this.uploadNodeId = uploadNodeId; + this.icpId = icpId; + this.name = name; + this.offset = offset; + this.bytesCnt = bytesCnt; + this.batch = batch; + } + + /** Auto-injected grid instance. */ + @IgniteInstanceResource + private transient IgniteEx ignite; + + /** {@inheritDoc} */ + @Override public Void call() throws Exception { + UUID locNodeId = ignite.localNode().id(); + + assert uploadNodeId.equals(locNodeId) + : "Forwarded to wrong node [uploadNode=" + uploadNodeId + ", localNode=" + locNodeId + ']'; + + ignite.context().classPath().uploadFilePart(icpId, name, offset, bytesCnt, batch); + + return null; + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java index 505e615654d43..beb51c6a5d56e 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java @@ -31,6 +31,8 @@ public abstract class VisorOneNodeTask extends VisorMultiNodeTask @Override protected Collection jobNodes(VisorTaskArgument arg) { Collection nodes = super.jobNodes(arg); + System.out.println("nodes = " + nodes); + assert nodes.size() == 1 : nodes; return nodes; From fae5368144c7e63c68c19f7c7f5a894f449ccc50 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Sat, 14 Mar 2026 13:20:40 +0300 Subject: [PATCH 02/15] IGNITE-28207 IgniteClassPath creation --- .idea/inspectionProfiles/Project_Default.xml | 10 +++------- .../management/classpath/ClassPathCreateCommand.java | 1 + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 230bf03e768a9..0af10ada908d7 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -369,6 +369,7 @@ + + @@ -623,13 +625,7 @@ - - - + diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java index 2d25cccc642e1..b63555d7fa464 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java @@ -97,6 +97,7 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg printer.accept("New classpath registered [uploadNode=" + uploadNode.id() + ", name=" + arg.name + ", id=" + icpID.toString() + ']'); printer.accept("Starting to upload files:"); + // TODO: add pretty print here. for (Path file : files) { printer.accept(String.valueOf(file.toAbsolutePath())); cli.uploadClasspathFile(uploadNode, icpID, file); From 78a61f50f7c71a98643ef49cf9014fea6967618a Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Sat, 14 Mar 2026 16:42:01 +0300 Subject: [PATCH 03/15] IGNITE-28207 IgniteClassPath creation --- .../apache/ignite/internal/IgniteKernal.java | 2 +- .../ClassPathDeployToAllRequest.java | 38 +++++ .../ClassPathDeployToAllResponse.java | 27 +++ .../classpath/ClassPathProcessor.java | 160 ++++++++++++++---- .../classpath/ClassPathCreateCommand.java | 2 + .../classpath/ClassPathDistributeTask.java | 62 +++++++ .../util/distributed/DistributedProcess.java | 7 +- 7 files changed, 267 insertions(+), 31 deletions(-) create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java diff --git a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java index 5c73d1de07c34..dd5176f9cb8e2 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/IgniteKernal.java @@ -1009,7 +1009,6 @@ public void start( startManager(new GridFailoverManager(ctx)); startManager(new GridCollisionManager(ctx)); startManager(new GridIndexingManager(ctx)); - startProcessor(new ClassPathProcessor(ctx)); // Assign discovery manager to context before other processors start so they // are able to register custom event listener. @@ -1020,6 +1019,7 @@ public void start( // Start the encryption manager after assigning the discovery manager to context, so it will be // able to register custom event listener. startManager(new GridEncryptionManager(ctx)); + startProcessor(new ClassPathProcessor(ctx)); startProcessor(new PdsConsistentIdProcessor(ctx)); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java new file mode 100644 index 0000000000000..dcf75b91eb177 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java @@ -0,0 +1,38 @@ +/* + * 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.classpath; + +import java.io.Serializable; +import java.util.UUID; +import org.apache.ignite.internal.util.distributed.DistributedProcess; + +/** + * Class path deploy to all request for {@link DistributedProcess} initiate message. + */ +public class ClassPathDeployToAllRequest implements Serializable { + /** Serial version uid. */ + private static final long serialVersionUID = 0L; + + /** Ignite class path id. */ + final UUID icpId; + + /** */ + public ClassPathDeployToAllRequest(UUID icpId) { + this.icpId = icpId; + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java new file mode 100644 index 0000000000000..0bab548ff6945 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java @@ -0,0 +1,27 @@ +/* + * 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.classpath; + +import java.io.Serializable; +import org.apache.ignite.internal.util.distributed.DistributedProcess; + +/** + * Class path deploy to all response for {@link DistributedProcess} initiate message. + */ +public class ClassPathDeployToAllResponse implements Serializable { +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java index 4b9e24f743a81..8b515fc8a5137 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -20,17 +20,25 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.IgniteInternalFuture; import org.apache.ignite.internal.processors.GridProcessorAdapter; import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; +import org.apache.ignite.internal.util.distributed.DistributedProcess; +import org.apache.ignite.internal.util.future.GridFinishedFuture; +import org.apache.ignite.internal.util.future.GridFutureAdapter; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.A; import org.apache.ignite.internal.util.typedef.internal.U; +import org.jetbrains.annotations.Nullable; import static org.apache.ignite.internal.classpath.IgniteClassPathState.CREATING; +import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.CLASSPATH_DEPLOY_TO_ALL; /** * TODO: @@ -44,37 +52,43 @@ public class ClassPathProcessor extends GridProcessorAdapter { /** Prefix for metastorage keys. */ public static final String METASTORE_PREFIX = "classpath."; + /** Distribute process that distributes new Ignite class path across all server nodes. */ + private final DistributedProcess deployToAllProc; + + /** */ + private final Map> deployToAllFuts = new ConcurrentHashMap<>(); + /** * @param ctx Kernal context. */ public ClassPathProcessor(GridKernalContext ctx) { super(ctx); + + deployToAllProc = new DistributedProcess<>( + ctx, + CLASSPATH_DEPLOY_TO_ALL, + this::startDeployToAllProcess, + this::processDeployToAllResult + ); } /** * Register new classpath in metastorage it same name not exists. * Fails if exists. * - * @param name - * @param files - * @param lengths - * @return + * @param name Class path name. + * @param files Files included. + * @param lengths Files lengths. + * @return Class path id. */ public UUID startCreation(String name, String[] files, long[] lengths) { assert files.length == lengths.length : "wrong arrays lengths"; - A.ensure(U.alphanumericUnderscore(name), "Classpath name must satisfy the following name pattern: a-zA-Z0-9_"); - String key = METASTORE_PREFIX + name; + A.ensure(U.alphanumericUnderscore(name), "Classpath name must satisfy the following name pattern: a-zA-Z0-9_"); IgniteClassPath icp = new IgniteClassPath(UUID.randomUUID(), name, files, lengths); - try { - if (!ctx.distributedMetastorage().compareAndSet(key, null, icp)) - throw new IgniteException("Classpath alreay exists: " + name); - } - catch (IgniteCheckedException e) { - throw new IgniteException(e); - } + toMetastorage(icp, null); try { NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); @@ -89,10 +103,10 @@ public UUID startCreation(String name, String[] files, long[] lengths) { } catch (Exception e) { try { - ctx.distributedMetastorage().remove(key); + ctx.distributedMetastorage().remove(metastorageKey(icp)); } catch (IgniteCheckedException ex) { - log.error("Can't remove metastorage key for IgniteClassPath: " + key, e); + log.error("Can't remove metastorage key for IgniteClassPath: " + metastorageKey(icp), e); } throw e; @@ -112,9 +126,9 @@ public void uploadFilePart( long offset, int bytesCnt, byte[] batch - ) throws IgniteCheckedException, IOException { + ) throws IOException { try { - IgniteClassPath icp = search(icpID); + IgniteClassPath icp = fromMetastorage(icpID); if (F.indexOf(icp.files(), name) == -1) throw new IllegalArgumentException("Unknown lib [icp=" + icp.name() + ", unknown_lib=" + name + ']'); @@ -145,25 +159,113 @@ public void uploadFilePart( } } + /** + * @param icpId ClassPath ID. + * @return + */ + public IgniteInternalFuture distributeToAllNodes(UUID icpId) { + GridFutureAdapter fut = new GridFutureAdapter<>(); + + synchronized (this) { + IgniteClassPath icp = fromMetastorage(icpId); + + ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId); + + if (deployToAllFuts.put(icpId, fut) != null) + return new GridFinishedFuture<>(new IllegalStateException("Distribute to all process started, already: " + icp)); + + deployToAllProc.start(icpId, req); + } + + return fut; + } + + /** + * @param req Request on snapshot creation. + * @return Future which will be completed when a snapshot has been started. + */ + private IgniteInternalFuture startDeployToAllProcess(ClassPathDeployToAllRequest req) { + IgniteClassPath icp = fromMetastorage(req.icpId); + + if (deployToAllFuts.containsKey(req.icpId)) { + log.info("Upload node skip download [icp=" + icp + ']'); + + return new GridFinishedFuture<>(new ClassPathDeployToAllResponse()); + } + + log.info("Starting download new classpath [icp=" + icp + ']'); + + return new GridFinishedFuture<>(new ClassPathDeployToAllResponse()); + } + + /** + * @param id Request id. + * @param res Results. + * @param err Errors. + */ + private void processDeployToAllResult(UUID id, Map res, Map err) { + GridFutureAdapter fut = deployToAllFuts.remove(id); + + // Only upload node manage the process. + if (fut == null) { + if (log.isDebugEnabled()) + log.debug("Unknown distribute process [id=" + id + ']'); + + return; + } + + IgniteClassPath icp = fromMetastorage(id); + + // TODO: check this exception not failed all node. + if (!fut.onDone("OK")) { + throw new IllegalStateException("Distribute process in wrong state " + + "[canceled=" + fut.isCancelled() + ", failed=" + fut.isFailed() + ", done=" + fut.isDone() + ']'); + } + + icp.state(IgniteClassPathState.READY); + + log.info("Deploy to all DONE!"); + } + /** * @param icpID ClassPath ID. * @return Class path. - * @throws IgniteCheckedException If failed. */ - private IgniteClassPath search(UUID icpID) throws IgniteCheckedException { - IgniteClassPath[] icp = new IgniteClassPath[1]; + private IgniteClassPath fromMetastorage(UUID icpID) { + try { + IgniteClassPath[] icp = new IgniteClassPath[1]; + + ctx.distributedMetastorage().iterate(METASTORE_PREFIX, (key, icp0) -> { + if (icpID.equals(((IgniteClassPath)icp0).id())) + icp[0] = (IgniteClassPath)icp0; + }); - ctx.distributedMetastorage().iterate(METASTORE_PREFIX, (key, icp0) -> { - if (icpID.equals(((IgniteClassPath)icp0).id())) - icp[0] = (IgniteClassPath)icp0; - }); + if (icp[0] == null) + throw new IgniteException("ClassPath not found: " + icpID); - if (icp[0] == null) - throw new IgniteException("ClassPath not found: " + icpID); + if (icp[0].state() != CREATING) + throw new IgniteException("ClassPath in wrong state [expected=" + CREATING + ", status=" + icp[0].state() + ']'); + + return icp[0]; + } + catch (IgniteCheckedException e) { + throw new IgniteException(e); + } + } - if (icp[0].state() != CREATING) - throw new IgniteException("ClassPath in wrong state [expected=" + CREATING + ", status=" + icp[0].state() + ']'); + /** */ + private void toMetastorage(IgniteClassPath icp, @Nullable IgniteClassPath prev) { + try { + if (!ctx.distributedMetastorage().compareAndSet(metastorageKey(icp), prev, icp)) + throw new IgniteException("Classpath alreay exists: " + icp.name()); + } + catch (IgniteCheckedException e) { + throw new IgniteException(e); + } + } - return icp[0]; + /** */ + private static String metastorageKey(IgniteClassPath icp) { + return METASTORE_PREFIX + icp.name(); } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java index b63555d7fa464..e6d6458e7af78 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java @@ -103,6 +103,8 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg cli.uploadClasspathFile(uploadNode, icpID, file); printer.accept("DONE"); } + + CommandUtils.execute(client, null, ClassPathDistributeTask.class, icpID, Collections.singletonList(uploadNode)); } /** */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java new file mode 100644 index 0000000000000..06b117e480d70 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java @@ -0,0 +1,62 @@ +/* + * 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.management.classpath; + +import java.util.UUID; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteException; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.visor.VisorJob; +import org.apache.ignite.internal.visor.VisorOneNodeTask; +import org.jetbrains.annotations.Nullable; + +/** */ +public class ClassPathDistributeTask extends VisorOneNodeTask { + /** */ + private static final long serialVersionUID = 0L; + + /** {@inheritDoc} */ + @Override protected VisorJob job(UUID arg) { + return new ClassPathDistributeJob(arg, debug); + } + + /** */ + private static class ClassPathDistributeJob extends VisorJob { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + protected ClassPathDistributeJob(@Nullable UUID arg, boolean debug) { + super(arg, debug); + } + + /** {@inheritDoc} */ + @Override protected Void run(@Nullable UUID arg) throws IgniteException { + IgniteInternalFuture fut = ignite.context().classPath().distributeToAllNodes(arg); + + try { + fut.get(); + + return null; + } + catch (IgniteCheckedException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/DistributedProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/DistributedProcess.java index 8b1ed6955d256..573c27889f0a3 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/DistributedProcess.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/util/distributed/DistributedProcess.java @@ -508,6 +508,11 @@ public enum DistributedProcessType { /** * Snapshot partitions validation. */ - CHECK_SNAPSHOT_PARTS + CHECK_SNAPSHOT_PARTS, + + /** + * Deploy new classpath to all nodes. + */ + CLASSPATH_DEPLOY_TO_ALL; } } From 9c3f016dd8b2452de08bbcf6ea158637e416f763 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Mon, 16 Mar 2026 11:19:43 +0300 Subject: [PATCH 04/15] IGNITE-28207 IgniteClassPath creation --- .../ClassPathDeployToAllRequest.java | 6 +- .../classpath/ClassPathProcessor.java | 4 +- .../classpath/DownloadClassPathTask.java | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java index dcf75b91eb177..e3e2fa63113d9 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java @@ -31,8 +31,12 @@ public class ClassPathDeployToAllRequest implements Serializable { /** Ignite class path id. */ final UUID icpId; + /** Node containing class path files received from client. */ + final UUID uploadNodeId; + /** */ - public ClassPathDeployToAllRequest(UUID icpId) { + public ClassPathDeployToAllRequest(UUID icpId, UUID uploadNodeId) { this.icpId = icpId; + this.uploadNodeId = uploadNodeId; } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java index 8b515fc8a5137..364dbedc7258a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -169,7 +169,7 @@ public IgniteInternalFuture distributeToAllNodes(UUID icpId) { synchronized (this) { IgniteClassPath icp = fromMetastorage(icpId); - ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId); + ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId, ctx.localNodeId()); if (deployToAllFuts.put(icpId, fut) != null) return new GridFinishedFuture<>(new IllegalStateException("Distribute to all process started, already: " + icp)); @@ -195,7 +195,7 @@ private IgniteInternalFuture startDeployToAllProce log.info("Starting download new classpath [icp=" + icp + ']'); - return new GridFinishedFuture<>(new ClassPathDeployToAllResponse()); + return new DownloadClassPathTask(ctx, icp).call(); } /** diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java new file mode 100644 index 0000000000000..b8a8f19a182b1 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java @@ -0,0 +1,95 @@ +/* + * 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.classpath; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import org.apache.ignite.IgniteLogger; +import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.util.future.GridFutureAdapter; + +/** + * + */ +public class DownloadClassPathTask implements Callable> { + /** Kernal context. */ + private final GridKernalContext ctx; + + /** Logger. */ + private final IgniteLogger log; + + /** Ignite class path. */ + private final IgniteClassPath icp; + + /** Resulting future. */ + private final GridFutureAdapter res; + + /** + * @param ctx Kernal context. + * @param icp Ignite class path. + */ + public DownloadClassPathTask(GridKernalContext ctx, IgniteClassPath icp) { + this.ctx = ctx; + this.log = ctx.log(DownloadClassPathTask.class); + this.icp = icp; + this.res = new GridFutureAdapter<>(); + } + + /** {@inheritDoc} */ + @Override public IgniteInternalFuture call() { + // TODO: rewrite to async code (listen for each future completion. + Stream.of(icp.files()) + .map(DownloadClassPathFileTask::new) + .map(ctx.pools().getPeerClassLoadingExecutorService()::submit) + .forEach(fileFut -> { + if (res.isDone()) + fileFut.cancel(true); + + try { + fileFut.get(); + } + catch (ExecutionException | InterruptedException e) { + if (!res.isDone()) { + log.warning("Classpath download files error: " + icp, e); + + res.onDone(e); + } + } + }); + + return res; + } + + /** */ + private class DownloadClassPathFileTask implements Runnable { + /** */ + private final String name; + + /** */ + public DownloadClassPathFileTask(String name) { + this.name = name; + } + + /** {@inheritDoc} */ + @Override public void run() { + log.info("Downloading file: " + name); + } + } +} From 6fd1dec09661fb20e7ede713d536b8362a792f66 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Wed, 29 Apr 2026 12:36:30 +0300 Subject: [PATCH 05/15] IGNITE-28207 WIP --- .../util/GridCommandHandlerClassPathTest.java | 4 + .../ClassPathDeployToAllRequest.java | 7 +- .../ClassPathDeployToAllResponse.java | 4 +- .../classpath/ClassPathProcessor.java | 107 ++------------- .../classpath/DeployToAllProcess.java | 126 ++++++++++++++++++ .../classpath/DownloadClassPathTask.java | 3 +- .../client/thin/PayloadInputChannel.java | 2 +- .../client/thin/ReliableChannelEx.java | 16 +++ .../client/thin/ReliableChannelImpl.java | 44 +++++- .../internal/client/thin/TcpClientCache.java | 15 +++ .../internal/client/thin/TcpIgniteClient.java | 9 +- .../classpath/ClassPathDistributeTask.java | 2 +- .../internal/visor/VisorOneNodeTask.java | 2 - 13 files changed, 224 insertions(+), 117 deletions(-) create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index 6b3cfad39d3ce..ad142cd002972 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -70,4 +70,8 @@ public void testCreate() throws Exception { System.out.println(outStr); } + + + // TODO check empty file creation. + // TODO add in production code checks of files integriy. Perform file integrity check on startup. } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java index e3e2fa63113d9..3d8adbf6b512b 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java @@ -17,17 +17,14 @@ package org.apache.ignite.internal.classpath; -import java.io.Serializable; import java.util.UUID; import org.apache.ignite.internal.util.distributed.DistributedProcess; +import org.apache.ignite.plugin.extensions.communication.Message; /** * Class path deploy to all request for {@link DistributedProcess} initiate message. */ -public class ClassPathDeployToAllRequest implements Serializable { - /** Serial version uid. */ - private static final long serialVersionUID = 0L; - +public class ClassPathDeployToAllRequest implements Message { /** Ignite class path id. */ final UUID icpId; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java index 0bab548ff6945..b47f386800cfb 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java @@ -17,11 +17,11 @@ package org.apache.ignite.internal.classpath; -import java.io.Serializable; import org.apache.ignite.internal.util.distributed.DistributedProcess; +import org.apache.ignite.plugin.extensions.communication.Message; /** * Class path deploy to all response for {@link DistributedProcess} initiate message. */ -public class ClassPathDeployToAllResponse implements Serializable { +public class ClassPathDeployToAllResponse implements Message { } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java index 364dbedc7258a..a04d092e03282 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -20,25 +20,18 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; -import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.internal.GridKernalContext; -import org.apache.ignite.internal.IgniteInternalFuture; import org.apache.ignite.internal.processors.GridProcessorAdapter; import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; -import org.apache.ignite.internal.util.distributed.DistributedProcess; -import org.apache.ignite.internal.util.future.GridFinishedFuture; -import org.apache.ignite.internal.util.future.GridFutureAdapter; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.A; import org.apache.ignite.internal.util.typedef.internal.U; import org.jetbrains.annotations.Nullable; import static org.apache.ignite.internal.classpath.IgniteClassPathState.CREATING; -import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.CLASSPATH_DEPLOY_TO_ALL; /** * TODO: @@ -52,11 +45,8 @@ public class ClassPathProcessor extends GridProcessorAdapter { /** Prefix for metastorage keys. */ public static final String METASTORE_PREFIX = "classpath."; - /** Distribute process that distributes new Ignite class path across all server nodes. */ - private final DistributedProcess deployToAllProc; - /** */ - private final Map> deployToAllFuts = new ConcurrentHashMap<>(); + public final DeployToAllProcess deployToAllProcess; /** * @param ctx Kernal context. @@ -64,12 +54,7 @@ public class ClassPathProcessor extends GridProcessorAdapter { public ClassPathProcessor(GridKernalContext ctx) { super(ctx); - deployToAllProc = new DistributedProcess<>( - ctx, - CLASSPATH_DEPLOY_TO_ALL, - this::startDeployToAllProcess, - this::processDeployToAllResult - ); + deployToAllProcess = new DeployToAllProcess(ctx); } /** @@ -88,7 +73,7 @@ public UUID startCreation(String name, String[] files, long[] lengths) { IgniteClassPath icp = new IgniteClassPath(UUID.randomUUID(), name, files, lengths); - toMetastorage(icp, null); + casToMetastorage(icp, null); try { NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); @@ -128,7 +113,7 @@ public void uploadFilePart( byte[] batch ) throws IOException { try { - IgniteClassPath icp = fromMetastorage(icpID); + IgniteClassPath icp = fromMetastorage(icpID, ctx); if (F.indexOf(icp.files(), name) == -1) throw new IllegalArgumentException("Unknown lib [icp=" + icp.name() + ", unknown_lib=" + name + ']'); @@ -159,79 +144,22 @@ public void uploadFilePart( } } - /** - * @param icpId ClassPath ID. - * @return - */ - public IgniteInternalFuture distributeToAllNodes(UUID icpId) { - GridFutureAdapter fut = new GridFutureAdapter<>(); - - synchronized (this) { - IgniteClassPath icp = fromMetastorage(icpId); - - ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId, ctx.localNodeId()); - - if (deployToAllFuts.put(icpId, fut) != null) - return new GridFinishedFuture<>(new IllegalStateException("Distribute to all process started, already: " + icp)); - - deployToAllProc.start(icpId, req); - } - - return fut; - } - - /** - * @param req Request on snapshot creation. - * @return Future which will be completed when a snapshot has been started. - */ - private IgniteInternalFuture startDeployToAllProcess(ClassPathDeployToAllRequest req) { - IgniteClassPath icp = fromMetastorage(req.icpId); - - if (deployToAllFuts.containsKey(req.icpId)) { - log.info("Upload node skip download [icp=" + icp + ']'); - - return new GridFinishedFuture<>(new ClassPathDeployToAllResponse()); - } - - log.info("Starting download new classpath [icp=" + icp + ']'); - - return new DownloadClassPathTask(ctx, icp).call(); - } - - /** - * @param id Request id. - * @param res Results. - * @param err Errors. - */ - private void processDeployToAllResult(UUID id, Map res, Map err) { - GridFutureAdapter fut = deployToAllFuts.remove(id); - - // Only upload node manage the process. - if (fut == null) { - if (log.isDebugEnabled()) - log.debug("Unknown distribute process [id=" + id + ']'); - - return; + /** */ + private void casToMetastorage(IgniteClassPath icp, @Nullable IgniteClassPath prev) { + try { + if (!ctx.distributedMetastorage().compareAndSet(metastorageKey(icp), prev, icp)) + throw new IgniteException("Classpath alreay exists: " + icp.name()); } - - IgniteClassPath icp = fromMetastorage(id); - - // TODO: check this exception not failed all node. - if (!fut.onDone("OK")) { - throw new IllegalStateException("Distribute process in wrong state " + - "[canceled=" + fut.isCancelled() + ", failed=" + fut.isFailed() + ", done=" + fut.isDone() + ']'); + catch (IgniteCheckedException e) { + throw new IgniteException(e); } - - icp.state(IgniteClassPathState.READY); - - log.info("Deploy to all DONE!"); } /** * @param icpID ClassPath ID. * @return Class path. */ - private IgniteClassPath fromMetastorage(UUID icpID) { + static IgniteClassPath fromMetastorage(UUID icpID, GridKernalContext ctx) { try { IgniteClassPath[] icp = new IgniteClassPath[1]; @@ -253,17 +181,6 @@ private IgniteClassPath fromMetastorage(UUID icpID) { } } - /** */ - private void toMetastorage(IgniteClassPath icp, @Nullable IgniteClassPath prev) { - try { - if (!ctx.distributedMetastorage().compareAndSet(metastorageKey(icp), prev, icp)) - throw new IgniteException("Classpath alreay exists: " + icp.name()); - } - catch (IgniteCheckedException e) { - throw new IgniteException(e); - } - } - /** */ private static String metastorageKey(IgniteClassPath icp) { return METASTORE_PREFIX + icp.name(); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java new file mode 100644 index 0000000000000..dbc4746eb3948 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java @@ -0,0 +1,126 @@ +/* + * 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.classpath; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.ignite.IgniteLogger; +import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.util.distributed.DistributedProcess; +import org.apache.ignite.internal.util.future.GridFinishedFuture; +import org.apache.ignite.internal.util.future.GridFutureAdapter; + +import static org.apache.ignite.internal.classpath.ClassPathProcessor.fromMetastorage; +import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.CLASSPATH_DEPLOY_TO_ALL; + +public class DeployToAllProcess { + /** */ + private final IgniteLogger log; + + /** */ + private final GridKernalContext ctx; + + /** Distribute process that distributes new Ignite class path across all server nodes. */ + private final DistributedProcess deployToAllProc; + + /** */ + private final Map> deployToAllFuts = new ConcurrentHashMap<>(); + + public DeployToAllProcess(GridKernalContext ctx) { + log = ctx.log(DeployToAllProcess.class); + + this.ctx = ctx; + + deployToAllProc = new DistributedProcess<>( + ctx, + CLASSPATH_DEPLOY_TO_ALL, + this::startDeployToAllProcess, + this::processDeployToAllResult + ); + } + + /** + * @param icpId ClassPath ID. + * @return + */ + public IgniteInternalFuture start(UUID icpId) { + GridFutureAdapter fut = new GridFutureAdapter<>(); + + synchronized (this) { + IgniteClassPath icp = fromMetastorage(icpId, ctx); + + ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId, ctx.localNodeId()); + + if (deployToAllFuts.put(icpId, fut) != null) + return new GridFinishedFuture<>(new IllegalStateException("Distribute to all process started, already: " + icp)); + + deployToAllProc.start(icpId, req); + } + + return fut; + } + + /** + * @param req Request on snapshot creation. + * @return Future which will be completed when a snapshot has been started. + */ + private IgniteInternalFuture startDeployToAllProcess(ClassPathDeployToAllRequest req) { + IgniteClassPath icp = fromMetastorage(req.icpId, ctx); + + if (deployToAllFuts.containsKey(req.icpId)) { + log.info("Upload node skip download [icp=" + icp + ']'); + + return new GridFinishedFuture<>(new ClassPathDeployToAllResponse()); + } + + log.info("Starting download new classpath [icp=" + icp + ']'); + + return new DownloadClassPathTask(ctx, icp).call(); + } + + /** + * @param id Request id. + * @param res Results. + * @param err Errors. + */ + private void processDeployToAllResult(UUID id, Map res, Map err) { + GridFutureAdapter fut = deployToAllFuts.remove(id); + + // Only upload node manage the process. + if (fut == null) { + if (log.isDebugEnabled()) + log.debug("Unknown distribute process [id=" + id + ']'); + + return; + } + + IgniteClassPath icp = fromMetastorage(id, ctx); + + // TODO: check this exception not failed all node. + if (!fut.onDone("OK")) { + throw new IllegalStateException("Distribute process in wrong state " + + "[canceled=" + fut.isCancelled() + ", failed=" + fut.isFailed() + ", done=" + fut.isDone() + ']'); + } + + icp.state(IgniteClassPathState.READY); + + log.info("Deploy to all DONE!"); + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java index b8a8f19a182b1..61e13ea81378c 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java @@ -54,7 +54,6 @@ public DownloadClassPathTask(GridKernalContext ctx, IgniteClassPath icp) { /** {@inheritDoc} */ @Override public IgniteInternalFuture call() { - // TODO: rewrite to async code (listen for each future completion. Stream.of(icp.files()) .map(DownloadClassPathFileTask::new) .map(ctx.pools().getPeerClassLoadingExecutorService()::submit) @@ -74,6 +73,8 @@ public DownloadClassPathTask(GridKernalContext ctx, IgniteClassPath icp) { } }); + res.onDone(new ClassPathDeployToAllResponse()); + return res; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadInputChannel.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadInputChannel.java index e8f6938890d25..38d0e5d13416d 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadInputChannel.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/PayloadInputChannel.java @@ -35,8 +35,8 @@ class PayloadInputChannel { * Constructor. */ PayloadInputChannel(ClientChannel ch, ByteBuffer payload) { - in = BinaryStreams.inputStream(payload); this.ch = ch; + in = BinaryStreams.inputStream(payload); } /** diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java index ff9270581ca64..e0a5475e2292a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java @@ -61,6 +61,22 @@ public T service( List targetNodes ) throws ClientException, ClientError; + /** + * Send request to the passed node and handle response. + * + * @throws ClientException Thrown by {@code payloadWriter} or {@code payloadReader}. + * @throws ClientAuthenticationException When user name or password is invalid. + * @throws ClientAuthorizationException When user has no permission to perform operation. + * @throws ClientProtocolError When failed to handshake with server. + * @throws ClientServerError When failed to process request on server. + */ + public T serviceForNode( + ClientOperation op, + Consumer payloadWriter, + Function payloadReader, + UUID targetNode + ) throws ClientException, ClientError; + /** * Send request and handle response asynchronously. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java index ec6e96cb0f979..f012328a66cad 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java @@ -28,7 +28,9 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -121,6 +123,9 @@ final class ReliableChannelImpl implements ReliableChannelEx { /** Open channels counter. */ private final AtomicInteger channelsCnt = new AtomicInteger(); + /** Future for channel init task. */ + private volatile Future initAllChannelFut; + /** * Constructor. */ @@ -208,6 +213,16 @@ final class ReliableChannelImpl implements ReliableChannelEx { ); } + /** {@inheritDoc} */ + @Override public T serviceForNode( + ClientOperation op, + Consumer payloadWriter, + Function payloadReader, + UUID targetNode + ) throws ClientException, ClientError { + return null; + } + /** {@inheritDoc} */ @Override public IgniteClientFuture serviceAsync( ClientOperation op, @@ -574,8 +589,8 @@ else if (scheduledChannelsReinit.get() && !partitionAwarenessEnabled) { /** * Asynchronously try to establish a connection to all configured servers. */ - private void initAllChannelsAsync() { - ForkJoinPool.commonPool().submit( + private Future initAllChannelsAsync() { + return ForkJoinPool.commonPool().submit( () -> { List holders = channels; @@ -781,7 +796,7 @@ void channelsInit(@Nullable List failures) { } if (partitionAwarenessEnabled) - initAllChannelsAsync(); + initAllChannelFut = initAllChannelsAsync(); } /** @@ -942,7 +957,6 @@ private T applyOnNodeChannelWithFallback(UUID tryNodeId, Function(); @@ -1075,7 +1089,7 @@ private boolean applyReconnectionThrottling() { /** * Get or create channel. */ - private ClientChannel getOrCreateChannel() + public ClientChannel getOrCreateChannel() throws ClientConnectionException, ClientAuthenticationException, ClientProtocolError { return getOrCreateChannel(false); } @@ -1175,6 +1189,26 @@ void setConfiguration(ClientChannelConfiguration chCfg) { } } + /** + * @param id Node id. + * @return Client channel for node. + */ + public ClientChannel nodeClientChannel(UUID id) { + try { + initAllChannelFut.get(); + } + catch (InterruptedException | ExecutionException e) { + throw new ClientException(e); + } + + ClientChannelHolder cliCh = nodeChannels.get(id); + + if (cliCh == null) + throw new ClientConnectionException("Node can't be found [id=" + id + ']'); + + return cliCh.getOrCreateChannel(); + } + /** * Get holders reference. For test purposes. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java index 0099f096b4fbf..e8ddf484d4857 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java @@ -1816,6 +1816,21 @@ public ReliableChannelWrapper(ReliableChannelImpl delegate, String cacheName) { } } + /** {@inheritDoc} */ + @Override public T serviceForNode( + ClientOperation op, + Consumer payloadWriter, + Function payloadReader, + UUID targetNode + ) throws ClientException, ClientError { + try { + return delegate.serviceForNode(op, payloadWriter, payloadReader, targetNode); + } + catch (Exception e) { + throw convertException(e, cacheName); + } + } + /** {@inheritDoc} */ @Override public IgniteClientFuture serviceAsync( ClientOperation op, diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index 21e8b45149260..078477d53bec1 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -23,7 +23,6 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.EventListener; import java.util.HashSet; import java.util.List; @@ -637,7 +636,7 @@ private void retrieveBinaryConfiguration(ClientConfiguration cfg) { * @param file File to upload. */ public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws IOException { - List uploadNode = Collections.singletonList(node.id()); + ClientChannel cliCh = ch.nodeClientChannel(node.id()); String name = file.getFileName().toString(); byte[] batch = new byte[(int)(U.MB)]; @@ -647,10 +646,11 @@ public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws int[] bytesCnt = new int[1]; // We want to create empty file on the server side. + // So, even 0 bytes read, one request need to be sent. bytesCnt[0] = fis.read(batch); do { - ch.service( + cliCh.service( ClientOperation.FILE_UPLOAD, ch -> { try (BinaryWriterEx w = BinaryUtils.writer(marsh.context(), ch.out(), null)) { @@ -662,8 +662,7 @@ public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws w.writeByteArray(batch); } }, - in -> null, - uploadNode // Request can still be sent to a random (default) node. + in -> null ); offset[0] += bytesCnt[0]; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java index 06b117e480d70..3b78d9e91a966 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java @@ -47,7 +47,7 @@ protected ClassPathDistributeJob(@Nullable UUID arg, boolean debug) { /** {@inheritDoc} */ @Override protected Void run(@Nullable UUID arg) throws IgniteException { - IgniteInternalFuture fut = ignite.context().classPath().distributeToAllNodes(arg); + IgniteInternalFuture fut = ignite.context().classPath().deployToAllProcess.start(arg); try { fut.get(); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java index beb51c6a5d56e..505e615654d43 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/VisorOneNodeTask.java @@ -31,8 +31,6 @@ public abstract class VisorOneNodeTask extends VisorMultiNodeTask @Override protected Collection jobNodes(VisorTaskArgument arg) { Collection nodes = super.jobNodes(arg); - System.out.println("nodes = " + nodes); - assert nodes.size() == 1 : nodes; return nodes; From f0b6a5724b70ee8d234736f983ae87e465559f18 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Wed, 29 Apr 2026 12:42:37 +0300 Subject: [PATCH 06/15] IGNITE-28207 WIP --- .../util/GridCommandHandlerClassPathTest.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index ad142cd002972..7735b2fd1b4a7 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -62,16 +62,17 @@ public void testCreate() throws Exception { final TestCommandHandler hnd = newCommandHandler(createTestLogger()); - assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", "mysuperapp", "--files", jars)); + try { + assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", "mysuperapp", "--files", jars)); + } + finally { + String outStr = testOut.toString(); - String outStr = testOut.toString(); + stopAllGrids(); - stopAllGrids(); - - System.out.println(outStr); + System.out.println(outStr); + } } - - // TODO check empty file creation. // TODO add in production code checks of files integriy. Perform file integrity check on startup. } From 0cc48759c02cbea235e39e90cbabcd728ed983e8 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Wed, 29 Apr 2026 14:12:06 +0300 Subject: [PATCH 07/15] IGNITE-28207 WIP --- .../client/thin/ReliableChannelEx.java | 16 ---------- .../client/thin/ReliableChannelImpl.java | 30 +++---------------- .../internal/client/thin/TcpClientCache.java | 15 ---------- .../internal/client/thin/TcpIgniteClient.java | 10 +++++++ .../classpath/ClassPathCreateCommand.java | 17 ++++------- 5 files changed, 19 insertions(+), 69 deletions(-) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java index e0a5475e2292a..ff9270581ca64 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelEx.java @@ -61,22 +61,6 @@ public T service( List targetNodes ) throws ClientException, ClientError; - /** - * Send request to the passed node and handle response. - * - * @throws ClientException Thrown by {@code payloadWriter} or {@code payloadReader}. - * @throws ClientAuthenticationException When user name or password is invalid. - * @throws ClientAuthorizationException When user has no permission to perform operation. - * @throws ClientProtocolError When failed to handshake with server. - * @throws ClientServerError When failed to process request on server. - */ - public T serviceForNode( - ClientOperation op, - Consumer payloadWriter, - Function payloadReader, - UUID targetNode - ) throws ClientException, ClientError; - /** * Send request and handle response asynchronously. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java index f012328a66cad..454e29189317c 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/ReliableChannelImpl.java @@ -28,9 +28,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -123,9 +121,6 @@ final class ReliableChannelImpl implements ReliableChannelEx { /** Open channels counter. */ private final AtomicInteger channelsCnt = new AtomicInteger(); - /** Future for channel init task. */ - private volatile Future initAllChannelFut; - /** * Constructor. */ @@ -213,16 +208,6 @@ final class ReliableChannelImpl implements ReliableChannelEx { ); } - /** {@inheritDoc} */ - @Override public T serviceForNode( - ClientOperation op, - Consumer payloadWriter, - Function payloadReader, - UUID targetNode - ) throws ClientException, ClientError { - return null; - } - /** {@inheritDoc} */ @Override public IgniteClientFuture serviceAsync( ClientOperation op, @@ -589,8 +574,8 @@ else if (scheduledChannelsReinit.get() && !partitionAwarenessEnabled) { /** * Asynchronously try to establish a connection to all configured servers. */ - private Future initAllChannelsAsync() { - return ForkJoinPool.commonPool().submit( + private void initAllChannelsAsync() { + ForkJoinPool.commonPool().submit( () -> { List holders = channels; @@ -796,7 +781,7 @@ void channelsInit(@Nullable List failures) { } if (partitionAwarenessEnabled) - initAllChannelFut = initAllChannelsAsync(); + initAllChannelsAsync(); } /** @@ -1048,7 +1033,7 @@ class ClientChannelHolder { private volatile ClientChannel ch; /** ID of the last server node that {@link #ch} is or was connected to. */ - private volatile UUID serverNodeId; + volatile UUID serverNodeId; /** Address that holder is bind to (chCfg.addr) is not in use now. So close the holder. */ private volatile boolean close; @@ -1194,13 +1179,6 @@ void setConfiguration(ClientChannelConfiguration chCfg) { * @return Client channel for node. */ public ClientChannel nodeClientChannel(UUID id) { - try { - initAllChannelFut.get(); - } - catch (InterruptedException | ExecutionException e) { - throw new ClientException(e); - } - ClientChannelHolder cliCh = nodeChannels.get(id); if (cliCh == null) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java index e8ddf484d4857..0099f096b4fbf 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpClientCache.java @@ -1816,21 +1816,6 @@ public ReliableChannelWrapper(ReliableChannelImpl delegate, String cacheName) { } } - /** {@inheritDoc} */ - @Override public T serviceForNode( - ClientOperation op, - Consumer payloadWriter, - Function payloadReader, - UUID targetNode - ) throws ClientException, ClientError { - try { - return delegate.serviceForNode(op, payloadWriter, payloadReader, targetNode); - } - catch (Exception e) { - throw convertException(e, cacheName); - } - } - /** {@inheritDoc} */ @Override public IgniteClientFuture serviceAsync( ClientOperation op, diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index 078477d53bec1..47114d9d269c9 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -27,11 +27,13 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.ignite.IgniteBinary; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteLogger; @@ -676,6 +678,14 @@ public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws } } + /** @return Node IDs client connected to. */ + public List connectedToNodes() { + return ch.getChannelHolders().stream() + .map(hldr -> hldr.serverNodeId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + /** * Thin client implementation of {@link BinaryMetadataHandler}. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java index e6d6458e7af78..ec9aa84044d59 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java @@ -20,13 +20,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.function.Consumer; import org.apache.ignite.Ignite; -import org.apache.ignite.IgniteException; import org.apache.ignite.client.IgniteClient; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.internal.client.thin.TcpIgniteClient; @@ -88,7 +86,7 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg // Server nodes require files names, only. arg.files = fileNames(files); - ClusterNode uploadNode = uploadNode(client); + ClusterNode uploadNode = uploadNode(cli); printer.accept("Upload node: " + uploadNode.id()); @@ -108,18 +106,13 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg } /** */ - private static ClusterNode uploadNode(IgniteClient client) { - Collection nodes = CommandUtils.nodes(client, null); + private static ClusterNode uploadNode(TcpIgniteClient client) { + List nodes = client.connectedToNodes(); if (F.isEmpty(nodes)) - throw new IgniteException("Cluster empty"); + throw new IllegalStateException("Not connected to node"); - Collection servers = CommandUtils.servers(nodes); - - if (F.isEmpty(servers)) - throw new IgniteException("No server nodes"); - - return F.first(servers); + return client.cluster().node(F.first(nodes)); } /** */ From 0e02270119cc5a0300c143a368db8b94371925a7 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Wed, 29 Apr 2026 16:13:45 +0300 Subject: [PATCH 08/15] IGNITE-28207 WIP --- .../org/apache/ignite/util/GridCommandHandlerClassPathTest.java | 2 ++ .../apache/ignite/internal/classpath/DeployToAllProcess.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index 7735b2fd1b4a7..ea4bc7ff1c79f 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -40,6 +40,8 @@ public class GridCommandHandlerClassPathTest extends GridCommandHandlerAbstractT /** Tests --create command. */ @Test public void testCreate() throws Exception { + //grid(0).cluster().state(ClusterState.ACTIVE); + String jars = Files.list(Path.of(getClass().getClassLoader().getResource(".").getPath() + "../")) .map(Path::toAbsolutePath) .map(Path::toString) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java index dbc4746eb3948..0519a42c7515a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java @@ -30,6 +30,7 @@ import static org.apache.ignite.internal.classpath.ClassPathProcessor.fromMetastorage; import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.CLASSPATH_DEPLOY_TO_ALL; +/** */ public class DeployToAllProcess { /** */ private final IgniteLogger log; @@ -43,6 +44,7 @@ public class DeployToAllProcess { /** */ private final Map> deployToAllFuts = new ConcurrentHashMap<>(); + /** */ public DeployToAllProcess(GridKernalContext ctx) { log = ctx.log(DeployToAllProcess.class); From fc810dd4df08221335e22472a6ec4b4cf2386f4f Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Wed, 20 May 2026 15:27:47 +0300 Subject: [PATCH 09/15] WIP --- .../apache/ignite/internal/CoreMessagesProvider.java | 5 +++++ .../classpath/ClassPathDeployToAllRequest.java | 7 +++++-- .../classpath/ClassPathDeployToAllResponse.java | 11 +++++++++++ .../ignite/internal/classpath/DeployToAllProcess.java | 2 +- .../internal/classpath/DownloadClassPathTask.java | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java b/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java index 28777790e3d6f..2d599899b6f72 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java @@ -23,6 +23,8 @@ import org.apache.ignite.internal.cache.query.index.IndexQueryResultMeta; import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition; import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypeSettings; +import org.apache.ignite.internal.classpath.ClassPathDeployToAllRequest; +import org.apache.ignite.internal.classpath.ClassPathDeployToAllResponse; import org.apache.ignite.internal.management.cache.PartitionKey; import org.apache.ignite.internal.managers.checkpoint.GridCheckpointRequest; import org.apache.ignite.internal.managers.communication.CompressedMessage; @@ -669,6 +671,9 @@ public CoreMessagesProvider(Marshaller dfltMarsh, Marshaller schemaAwareMarsh, C withNoSchema(PartitionHashRecord.class); withNoSchema(TransactionsHashRecord.class); + withNoSchema(ClassPathDeployToAllRequest.class); + withNoSchema(ClassPathDeployToAllResponse.class); + assert msgIdx <= MAX_MESSAGE_ID; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java index 3d8adbf6b512b..c5b4c50b54b6b 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java @@ -18,6 +18,7 @@ package org.apache.ignite.internal.classpath; import java.util.UUID; +import org.apache.ignite.internal.Order; import org.apache.ignite.internal.util.distributed.DistributedProcess; import org.apache.ignite.plugin.extensions.communication.Message; @@ -26,10 +27,12 @@ */ public class ClassPathDeployToAllRequest implements Message { /** Ignite class path id. */ - final UUID icpId; + @Order(0) + UUID icpId; /** Node containing class path files received from client. */ - final UUID uploadNodeId; + @Order(1) + UUID uploadNodeId; /** */ public ClassPathDeployToAllRequest(UUID icpId, UUID uploadNodeId) { diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java index b47f386800cfb..e0eb8861fc3e0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java @@ -17,6 +17,8 @@ package org.apache.ignite.internal.classpath; +import java.util.UUID; +import org.apache.ignite.internal.Order; import org.apache.ignite.internal.util.distributed.DistributedProcess; import org.apache.ignite.plugin.extensions.communication.Message; @@ -24,4 +26,13 @@ * Class path deploy to all response for {@link DistributedProcess} initiate message. */ public class ClassPathDeployToAllResponse implements Message { + + /** Ignite class path id. */ + @Order(0) + UUID icpId; + + /** */ + public ClassPathDeployToAllResponse(UUID icpId) { + this.icpId = icpId; + } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java index 0519a42c7515a..5e7689e9d05f0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java @@ -89,7 +89,7 @@ private IgniteInternalFuture startDeployToAllProce if (deployToAllFuts.containsKey(req.icpId)) { log.info("Upload node skip download [icp=" + icp + ']'); - return new GridFinishedFuture<>(new ClassPathDeployToAllResponse()); + return new GridFinishedFuture<>(new ClassPathDeployToAllResponse(icp.id())); } log.info("Starting download new classpath [icp=" + icp + ']'); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java index 61e13ea81378c..79dd5a17259d1 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java @@ -73,7 +73,7 @@ public DownloadClassPathTask(GridKernalContext ctx, IgniteClassPath icp) { } }); - res.onDone(new ClassPathDeployToAllResponse()); + res.onDone(new ClassPathDeployToAllResponse(icp.id())); return res; } From 4eebcf3e3039f2c8d6f5daba82a016ee147c5904 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Fri, 22 May 2026 13:48:34 +0300 Subject: [PATCH 10/15] WIP --- .../ignite/internal/MessageProcessor.java | 33 +++++++++++++++++++ .../ClassPathDeployToAllRequest.java | 5 +++ .../ClassPathDeployToAllResponse.java | 5 +++ 3 files changed, 43 insertions(+) diff --git a/modules/codegen/src/main/java/org/apache/ignite/internal/MessageProcessor.java b/modules/codegen/src/main/java/org/apache/ignite/internal/MessageProcessor.java index 6659d3e2195ce..83b0ef54c0797 100644 --- a/modules/codegen/src/main/java/org/apache/ignite/internal/MessageProcessor.java +++ b/modules/codegen/src/main/java/org/apache/ignite/internal/MessageProcessor.java @@ -33,12 +33,14 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; +import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.lang.IgniteBiTuple; import static org.apache.ignite.internal.MessageSerializerGenerator.DLFT_ENUM_MAPPER_CLS; @@ -115,6 +117,9 @@ public class MessageProcessor extends AbstractProcessor { if (clazz.getModifiers().contains(Modifier.ABSTRACT)) continue; + if (!checkConstructors(clazz)) + continue; + List fields = orderedFields(clazz); if (fields.isEmpty() && emptyMsgs.stream().noneMatch(t -> isAssignable(t, clazz))) { @@ -146,6 +151,34 @@ public class MessageProcessor extends AbstractProcessor { return true; } + /** */ + private boolean checkConstructors(TypeElement clazz) { + boolean isMarshallableMsg = isAssignable( + clazz.asType(), + processingEnv.getElementUtils().getTypeElement("org.apache.ignite.internal.MarshallableMessage") + ); + + for (Element el : clazz.getEnclosedElements()) { + if (el.getKind() != ElementKind.CONSTRUCTOR) + continue; + + ExecutableElement c = (ExecutableElement)el; + + boolean isDfltConstructor = F.isEmpty(c.getParameters()); + + if (isDfltConstructor && !isMarshallableMsg) + return true; + } + + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Class must have default constructor: " + clazz.getQualifiedName(), + clazz + ); + + return false; + } + /** * Collects all fields annotated with {@link Order} from the given {@link TypeElement} and all its superclasses. *

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java index c5b4c50b54b6b..8fb365625d5e8 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java @@ -34,6 +34,11 @@ public class ClassPathDeployToAllRequest implements Message { @Order(1) UUID uploadNodeId; + /** */ + public ClassPathDeployToAllRequest() { + // No-op. + } + /** */ public ClassPathDeployToAllRequest(UUID icpId, UUID uploadNodeId) { this.icpId = icpId; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java index e0eb8861fc3e0..939bee9eb8ed5 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java @@ -31,6 +31,11 @@ public class ClassPathDeployToAllResponse implements Message { @Order(0) UUID icpId; + /** */ + public ClassPathDeployToAllResponse() { + // No-op. + } + /** */ public ClassPathDeployToAllResponse(UUID icpId) { this.icpId = icpId; From 448dffdd27ed6a85437f3c1183032a67c8288523 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Mon, 8 Jun 2026 16:55:25 +0300 Subject: [PATCH 11/15] IGNITE-28596 Code review fixes --- .../apache/ignite/util/GridCommandHandlerClassPathTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index ea4bc7ff1c79f..77d0bd57b15f5 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -20,9 +20,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Collectors; +import org.junit.Assume; import org.junit.Test; import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK; +import static org.hamcrest.Matchers.is; /** * Test for --classpath command. @@ -40,6 +42,8 @@ public class GridCommandHandlerClassPathTest extends GridCommandHandlerAbstractT /** Tests --create command. */ @Test public void testCreate() throws Exception { + Assume.assumeThat("Only cli mode supported", commandHandler, is(CLI_CMD_HND)); + //grid(0).cluster().state(ClusterState.ACTIVE); String jars = Files.list(Path.of(getClass().getClassLoader().getResource(".").getPath() + "../")) From 85ca0f0714bc3b7c43475c326148a32b6340e59a Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Mon, 8 Jun 2026 18:39:57 +0300 Subject: [PATCH 12/15] IGNITE-28207 Classpath creation process --- .../classpath/ClassPathProcessor.java | 34 ++--- .../classpath/DeployToAllProcess.java | 2 +- .../classpath/DownloadClassPathTask.java | 53 ++++---- .../internal/classpath/IgniteClassPath.java | 4 +- .../classpath/IgniteClassPathState.java | 2 +- .../internal/client/thin/TcpIgniteClient.java | 2 +- .../classpath/ClassPathCreateCommand.java | 6 +- .../classpath/ClientFileUploadRequest.java | 118 +++++------------- 8 files changed, 90 insertions(+), 131 deletions(-) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java index a04d092e03282..af00a213532c0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -31,7 +31,7 @@ import org.apache.ignite.internal.util.typedef.internal.U; import org.jetbrains.annotations.Nullable; -import static org.apache.ignite.internal.classpath.IgniteClassPathState.CREATING; +import static org.apache.ignite.internal.classpath.IgniteClassPathState.NEW; /** * TODO: @@ -43,7 +43,7 @@ */ public class ClassPathProcessor extends GridProcessorAdapter { /** Prefix for metastorage keys. */ - public static final String METASTORE_PREFIX = "classpath."; + public static final String METASTORE_PREFIX = "icp."; /** */ public final DeployToAllProcess deployToAllProcess; @@ -69,33 +69,36 @@ public ClassPathProcessor(GridKernalContext ctx) { public UUID startCreation(String name, String[] files, long[] lengths) { assert files.length == lengths.length : "wrong arrays lengths"; + // TODO: allow dot here. A.ensure(U.alphanumericUnderscore(name), "Classpath name must satisfy the following name pattern: a-zA-Z0-9_"); - IgniteClassPath icp = new IgniteClassPath(UUID.randomUUID(), name, files, lengths); + IgniteClassPath icp = new IgniteClassPath(UUID.randomUUID(), name, files, lengths, NEW); - casToMetastorage(icp, null); + NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); - try { - NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); + File root = ft.classPath(name); - File root = ft.classPath(name); + try { + casToMetastorage(null, icp); NodeFileTree.mkdir(root, "Ignite Class Path root: " + name); - - log.info("New classpath registered [root = " + root + ", icp=" + icp + ']'); - - return icp.id(); } catch (Exception e) { try { ctx.distributedMetastorage().remove(metastorageKey(icp)); + + U.delete(root); } catch (IgniteCheckedException ex) { - log.error("Can't remove metastorage key for IgniteClassPath: " + metastorageKey(icp), e); + log.error("Cleanup after IgniteClassPath creation failed [key=" + metastorageKey(icp) + ", root=" + root + ']', e); } throw e; } + + log.info("New classpath created [root = " + root + ", icp=" + icp + ']'); + + return icp.id(); } /** @@ -145,7 +148,7 @@ public void uploadFilePart( } /** */ - private void casToMetastorage(IgniteClassPath icp, @Nullable IgniteClassPath prev) { + private void casToMetastorage(@Nullable IgniteClassPath prev, IgniteClassPath icp) { try { if (!ctx.distributedMetastorage().compareAndSet(metastorageKey(icp), prev, icp)) throw new IgniteException("Classpath alreay exists: " + icp.name()); @@ -157,6 +160,7 @@ private void casToMetastorage(IgniteClassPath icp, @Nullable IgniteClassPath pre /** * @param icpID ClassPath ID. + * @param ctx Kernal context. * @return Class path. */ static IgniteClassPath fromMetastorage(UUID icpID, GridKernalContext ctx) { @@ -171,8 +175,8 @@ static IgniteClassPath fromMetastorage(UUID icpID, GridKernalContext ctx) { if (icp[0] == null) throw new IgniteException("ClassPath not found: " + icpID); - if (icp[0].state() != CREATING) - throw new IgniteException("ClassPath in wrong state [expected=" + CREATING + ", status=" + icp[0].state() + ']'); + if (icp[0].state() != NEW) + throw new IgniteException("ClassPath in wrong state [expected=" + NEW + ", status=" + icp[0].state() + ']'); return icp[0]; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java index 5e7689e9d05f0..a64ab0afe29a0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java @@ -86,7 +86,7 @@ public IgniteInternalFuture start(UUID icpId) { private IgniteInternalFuture startDeployToAllProcess(ClassPathDeployToAllRequest req) { IgniteClassPath icp = fromMetastorage(req.icpId, ctx); - if (deployToAllFuts.containsKey(req.icpId)) { + if (req.uploadNodeId.equals(ctx.localNodeId())) { log.info("Upload node skip download [icp=" + icp + ']'); return new GridFinishedFuture<>(new ClassPathDeployToAllResponse(icp.id())); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java index 79dd5a17259d1..b56a0768c3ba1 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java @@ -18,7 +18,7 @@ package org.apache.ignite.internal.classpath; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.apache.ignite.IgniteLogger; import org.apache.ignite.internal.GridKernalContext; @@ -38,6 +38,8 @@ public class DownloadClassPathTask implements Callable res; @@ -50,30 +52,20 @@ public DownloadClassPathTask(GridKernalContext ctx, IgniteClassPath icp) { this.log = ctx.log(DownloadClassPathTask.class); this.icp = icp; this.res = new GridFutureAdapter<>(); + this.cntr = new AtomicInteger(icp.files().length); } /** {@inheritDoc} */ @Override public IgniteInternalFuture call() { - Stream.of(icp.files()) - .map(DownloadClassPathFileTask::new) - .map(ctx.pools().getPeerClassLoadingExecutorService()::submit) - .forEach(fileFut -> { - if (res.isDone()) - fileFut.cancel(true); - - try { - fileFut.get(); - } - catch (ExecutionException | InterruptedException e) { - if (!res.isDone()) { - log.warning("Classpath download files error: " + icp, e); - - res.onDone(e); - } - } - }); - - res.onDone(new ClassPathDeployToAllResponse(icp.id())); + try { + // TODO: check correct executor used. + Stream.of(icp.files()) + .map(DownloadClassPathFileTask::new) + .map(ctx.pools().getPeerClassLoadingExecutorService()::submit); + } + catch (Throwable e) { + res.onDone(e); + } return res; } @@ -90,7 +82,26 @@ public DownloadClassPathFileTask(String name) { /** {@inheritDoc} */ @Override public void run() { + try { + run0(); + } + catch (Throwable e) { + res.onDone(e); + } + } + + /** */ + private void run0() { log.info("Downloading file: " + name); + + if (!res.isDone()) { + int filesToDownload = cntr.addAndGet(-1); + + log.info("File downloaded [name=" + name + ", filesToDownload=" + filesToDownload + ']'); + + if (filesToDownload == 0) + res.onDone(new ClassPathDeployToAllResponse(icp.id())); + } } } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java index 78c2127723976..570ba8c581b37 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPath.java @@ -48,12 +48,12 @@ public class IgniteClassPath implements Serializable { * @param name User provided name. * @param files Files to include to classpath. */ - public IgniteClassPath(UUID id, String name, String[] files, long[] lengths) { + public IgniteClassPath(UUID id, String name, String[] files, long[] lengths, IgniteClassPathState state) { this.id = id; this.name = name; this.files = files; this.lengths = lengths; - this.state = IgniteClassPathState.CREATING; + this.state = state; } /** */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java index 595bec09fc129..1d58b43dbaac3 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java @@ -20,7 +20,7 @@ /** */ public enum IgniteClassPathState { /** Creationg process in progress. */ - CREATING, + NEW, /** Ready for usage. */ READY, diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index 47114d9d269c9..b96a82954cb2f 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -644,7 +644,7 @@ public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws byte[] batch = new byte[(int)(U.MB)]; try (InputStream fis = Files.newInputStream(file)) { - long[] offset = new long[] {0}; + long[] offset = new long[]{0}; int[] bytesCnt = new int[1]; // We want to create empty file on the server side. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java index ec9aa84044d59..587d5b36c818a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java @@ -27,6 +27,7 @@ import org.apache.ignite.Ignite; import org.apache.ignite.client.IgniteClient; import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.internal.classpath.IgniteClassPath; import org.apache.ignite.internal.client.thin.TcpIgniteClient; import org.apache.ignite.internal.management.api.CommandUtils; import org.apache.ignite.internal.management.api.NativeCommand; @@ -34,7 +35,9 @@ import org.jetbrains.annotations.Nullable; /** + * Ignite classpath creation command. * + * @see IgniteClassPath */ public class ClassPathCreateCommand implements NativeCommand { /** {@inheritDoc} */ @@ -85,6 +88,7 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg // We don't want to send full path to server nodes. // Server nodes require files names, only. arg.files = fileNames(files); + // TODO: add CRC or other check of file integrity. ClusterNode uploadNode = uploadNode(cli); @@ -92,7 +96,7 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg UUID icpID = CommandUtils.execute(client, null, ClassPathStartCreationTask.class, arg, Collections.singletonList(uploadNode)); - printer.accept("New classpath registered [uploadNode=" + uploadNode.id() + ", name=" + arg.name + ", id=" + icpID.toString() + ']'); + printer.accept("New classpath created [uploadNode=" + uploadNode.id() + ", name=" + arg.name + ", id=" + icpID.toString() + ']'); printer.accept("Starting to upload files:"); // TODO: add pretty print here. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java index 2546be36b18c8..f1359abe27b6f 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java @@ -18,26 +18,31 @@ package org.apache.ignite.internal.processors.platform.client.classpath; import java.io.IOException; -import java.util.Collections; import java.util.UUID; -import org.apache.ignite.IgniteCheckedException; -import org.apache.ignite.IgniteException; import org.apache.ignite.binary.BinaryRawReader; -import org.apache.ignite.cluster.ClusterNode; -import org.apache.ignite.internal.GridClosureCallMode; -import org.apache.ignite.internal.IgniteEx; import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext; import org.apache.ignite.internal.processors.platform.client.ClientRequest; import org.apache.ignite.internal.processors.platform.client.ClientResponse; -import org.apache.ignite.lang.IgniteCallable; -import org.apache.ignite.resources.IgniteInstanceResource; - -import static org.apache.ignite.internal.processors.task.TaskExecutionOptions.options; /** */ public class ClientFileUploadRequest extends ClientRequest { - /** */ - private final FileUploadPartCallable data; + /** Upload node ID. */ + private final UUID uploadNodeId; + + /** ClassPath ID. */ + private final UUID icpId; + + /** File name. */ + private final String name; + + /** Offset to write data to. */ + private final long offset; + + /** Bytes count in batch to write. */ + private final int bytesCnt; + + /** Batch. */ + private final byte[] batch; /** * Creates the file upload request. @@ -47,95 +52,30 @@ public class ClientFileUploadRequest extends ClientRequest { public ClientFileUploadRequest(BinaryRawReader reader) { super(reader); - data = new FileUploadPartCallable( - reader.readUuid(), - reader.readUuid(), - reader.readString(), - reader.readLong(), - reader.readInt(), - reader.readByteArray() - ); + uploadNodeId = reader.readUuid(); + icpId = reader.readUuid(); + name = reader.readString(); + offset = reader.readLong(); + bytesCnt = reader.readInt(); + batch = reader.readByteArray(); } /** {@inheritDoc} */ @Override public ClientResponse process(ClientConnectionContext ctx) { // TODO: add backward compatibility support. - try { - // Forward request to upload node. - if (!ctx.kernalContext().localNodeId().equals(data.uploadNodeId)) { - ClusterNode uploadNode = ctx.kernalContext().cluster().get().node(data.uploadNodeId); + UUID locNodeId = ctx.kernalContext().localNodeId(); - if (uploadNode == null) { - throw new IgniteException("Upload node not found [localNode=" + ctx.kernalContext().localNodeId() + - ", uploadNode=" + data.uploadNodeId + ", icp=" + data.name + ']'); - } + if (!uploadNodeId.equals(locNodeId)) + throw new IllegalStateException("Wrong node [uploadNode=" + uploadNodeId + ", localNode=" + locNodeId + ']'); - ctx.kernalContext().closure().callAsync( - GridClosureCallMode.BALANCE, - data, - options(Collections.singletonList(uploadNode)).withFailoverDisabled() - ).get(); - - return new ClientResponse(requestId()); - } - - ctx.kernalContext().classPath().uploadFilePart(data.icpId, data.name, data.offset, data.bytesCnt, data.batch); + try { + ctx.kernalContext().classPath().uploadFilePart(icpId, name, offset, bytesCnt, batch); return new ClientResponse(requestId()); } - catch (IgniteCheckedException | IOException e) { + catch (IOException e) { return new ClientResponse(requestId(), e.getMessage()); } } - - /** */ - private static class FileUploadPartCallable implements IgniteCallable { - /** */ - private static final long serialVersionUID = 0L; - - /** Upload node ID. */ - private final UUID uploadNodeId; - - /** ClassPath ID. */ - private final UUID icpId; - - /** File name. */ - private final String name; - - /** Offset to write data to. */ - private final long offset; - - /** Bytes count in batch to write. */ - private final int bytesCnt; - - /** Batch. */ - private final byte[] batch; - - /** */ - public FileUploadPartCallable(UUID uploadNodeId, UUID icpId, String name, long offset, int bytesCnt, byte[] batch) { - this.uploadNodeId = uploadNodeId; - this.icpId = icpId; - this.name = name; - this.offset = offset; - this.bytesCnt = bytesCnt; - this.batch = batch; - } - - /** Auto-injected grid instance. */ - @IgniteInstanceResource - private transient IgniteEx ignite; - - /** {@inheritDoc} */ - @Override public Void call() throws Exception { - UUID locNodeId = ignite.localNode().id(); - - assert uploadNodeId.equals(locNodeId) - : "Forwarded to wrong node [uploadNode=" + uploadNodeId + ", localNode=" + locNodeId + ']'; - - ignite.context().classPath().uploadFilePart(icpId, name, offset, bytesCnt, batch); - - return null; - } - } } From bcfe771831511256ea43c4ea352ca49532a4d3d9 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Thu, 11 Jun 2026 16:22:55 +0300 Subject: [PATCH 13/15] IGNITE-28207 Classpath creation process --- .../apache/ignite/binary/BinaryRawWriter.java | 8 + .../internal/binary/BinaryWriterExImpl.java | 15 + .../util/GridCommandHandlerClassPathTest.java | 18 + .../ignite/internal/CoreMessagesProvider.java | 4 + .../ClassPathDeployToAllRequest.java | 8 +- .../ClassPathDeployToAllResponse.java | 7 +- .../ClassPathFilesTransmissionHandler.java | 373 ++++++++++++++++++ .../classpath/ClassPathProcessor.java | 81 +++- .../classpath/DeployToAllProcess.java | 47 +-- .../DownloadClassPathFailureMessage.java | 55 +++ .../classpath/DownloadClassPathMessage.java | 50 +++ .../classpath/DownloadClassPathTask.java | 107 ----- .../classpath/IgniteClassPathState.java | 2 +- .../internal/client/thin/TcpIgniteClient.java | 3 +- .../classpath/ClassPathDistributeTask.java | 5 +- .../persistence/filename/NodeFileTree.java | 2 +- .../odbc/ClientListenerInternalRequest.java | 31 ++ .../odbc/ClientListenerRequest.java | 5 + .../platform/client/ClientMessageParser.java | 28 +- ... => ClientClassPathFileUploadRequest.java} | 17 +- 20 files changed, 693 insertions(+), 173 deletions(-) create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathFailureMessage.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathMessage.java delete mode 100644 modules/core/src/main/java/org/apache/ignite/internal/classpath/DownloadClassPathTask.java create mode 100644 modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerInternalRequest.java rename modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/{ClientFileUploadRequest.java => ClientClassPathFileUploadRequest.java} (85%) diff --git a/modules/binary/api/src/main/java/org/apache/ignite/binary/BinaryRawWriter.java b/modules/binary/api/src/main/java/org/apache/ignite/binary/BinaryRawWriter.java index ffa89ce274ceb..a2c408af6c919 100644 --- a/modules/binary/api/src/main/java/org/apache/ignite/binary/BinaryRawWriter.java +++ b/modules/binary/api/src/main/java/org/apache/ignite/binary/BinaryRawWriter.java @@ -128,6 +128,14 @@ public interface BinaryRawWriter { */ public void writeByteArray(@Nullable byte[] val) throws BinaryObjectException; + /** + * @param val Value to write. + * @param off Offset to start write from. + * @param len Count of bytes to write. + * @throws BinaryObjectException In case of error. + */ + public void writeByteArray(@Nullable byte[] val, int off, int len) throws BinaryObjectException; + /** * @param val Value to write. * @throws BinaryObjectException In case of error. diff --git a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java index 033be969abcf7..b3e1042f6d380 100644 --- a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java +++ b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryWriterExImpl.java @@ -904,6 +904,21 @@ void writeBooleanField(@Nullable Boolean val) { } } + /** {@inheritDoc} */ + @Override public void writeByteArray(@Nullable byte[] val, int pos, int len) throws BinaryObjectException { + if (val == null) + out.writeByte(GridBinaryMarshaller.NULL); + else { + if (len > val.length) + throw new IllegalArgumentException("len must be less then val.length"); + + out.unsafeEnsure(1 + 4); + out.unsafeWriteByte(GridBinaryMarshaller.BYTE_ARR); + out.unsafeWriteInt(len); + out.writeByteArray(val, pos, len); + } + } + /** {@inheritDoc} */ @Override public void writeShortArray(String fieldName, @Nullable short[] val) throws BinaryObjectException { writeFieldId(fieldName); diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index 77d0bd57b15f5..600d1bd5af170 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -81,4 +81,22 @@ public void testCreate() throws Exception { } // TODO check empty file creation. // TODO add in production code checks of files integriy. Perform file integrity check on startup. + + /** Tests --create command. */ + @Test + public void testEmptyFilesArgument() { + final TestCommandHandler hnd = newCommandHandler(createTestLogger()); + + try { + assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", "mysuperapp", "--files")); + } + finally { + String outStr = testOut.toString(); + + stopAllGrids(); + + System.out.println(outStr); + } + + } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java b/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java index 87bd9884c0b60..8eabba605795d 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java @@ -25,6 +25,8 @@ import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypeSettings; import org.apache.ignite.internal.classpath.ClassPathDeployToAllRequest; import org.apache.ignite.internal.classpath.ClassPathDeployToAllResponse; +import org.apache.ignite.internal.classpath.DownloadClassPathFailureMessage; +import org.apache.ignite.internal.classpath.DownloadClassPathMessage; import org.apache.ignite.internal.management.cache.PartitionKey; import org.apache.ignite.internal.managers.checkpoint.GridCheckpointRequest; import org.apache.ignite.internal.managers.communication.CompressedMessage; @@ -670,6 +672,8 @@ public CoreMessagesProvider(Marshaller dfltMarsh, Marshaller schemaAwareMarsh, C withNoSchema(ClassPathDeployToAllRequest.class); withNoSchema(ClassPathDeployToAllResponse.class); + withNoSchema(DownloadClassPathMessage.class); + withNoSchema(DownloadClassPathFailureMessage.class); assert msgIdx <= MAX_MESSAGE_ID; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java index 8fb365625d5e8..ea4199b86c031 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllRequest.java @@ -26,11 +26,15 @@ * Class path deploy to all request for {@link DistributedProcess} initiate message. */ public class ClassPathDeployToAllRequest implements Message { - /** Ignite class path id. */ + /** + * Classpath ID. + * + * @see IgniteClassPath#id() + */ @Order(0) UUID icpId; - /** Node containing class path files received from client. */ + /** Node containing class path files. */ @Order(1) UUID uploadNodeId; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java index 939bee9eb8ed5..c05efc2014f10 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathDeployToAllResponse.java @@ -26,8 +26,11 @@ * Class path deploy to all response for {@link DistributedProcess} initiate message. */ public class ClassPathDeployToAllResponse implements Message { - - /** Ignite class path id. */ + /** + * Classpath ID. + * + * @see IgniteClassPath#id() + */ @Order(0) UUID icpId; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java new file mode 100644 index 0000000000000..c771ec95e9eac --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java @@ -0,0 +1,373 @@ +package org.apache.ignite.internal.classpath; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.IgniteException; +import org.apache.ignite.IgniteLogger; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.failure.FailureContext; +import org.apache.ignite.failure.FailureType; +import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.GridTopic; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException; +import org.apache.ignite.internal.managers.communication.GridIoManager.TransmissionSender; +import org.apache.ignite.internal.managers.communication.GridMessageListener; +import org.apache.ignite.internal.managers.communication.TransmissionCancelledException; +import org.apache.ignite.internal.managers.communication.TransmissionHandler; +import org.apache.ignite.internal.managers.communication.TransmissionMeta; +import org.apache.ignite.internal.managers.communication.TransmissionPolicy; +import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; +import org.apache.ignite.internal.util.future.GridFinishedFuture; +import org.apache.ignite.internal.util.future.GridFutureAdapter; +import org.apache.ignite.internal.util.typedef.internal.U; + +import static org.apache.ignite.internal.classpath.ClassPathProcessor.fromMetastorage; +import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYSTEM_POOL; +import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.SNP_NODE_STOPPING_ERR_MSG; + +/** + * This manager is responsible for requesting and handling files from a remote node. + */ +class ClassPathFilesTransmissionHandler implements TransmissionHandler, GridMessageListener { + /** ClassPath topic to receive files from remote node. */ + static final Object FILES_TOPIC = GridTopic.TOPIC_CLASSLOAD.topic("rmt_files"); + + /** Transmission parameter for {@link IgniteClassPath#id()}. */ + private static final String ICP_ID_PARAM = "icpId"; + + /** Transmission parameter for {@link IgniteClassPath} file name. */ + private static final String NAME_PARAM = "name"; + + /** */ + private volatile DownloadClassPathTask active; + + /** + * Queue of asynchronous tasks to execute. + * Head of queue is taks that currently executing. + */ + private final Queue queue = new ConcurrentLinkedDeque<>(); + + /** Kernal context. */ + private final GridKernalContext ctx; + + /** Logger. */ + private final IgniteLogger log; + + /** {@code true} if the node is stopping. */ + private volatile boolean stopping; + + /** */ + public ClassPathFilesTransmissionHandler(GridKernalContext ctx) { + this.ctx = ctx; + this.log = ctx.log(ClassPathFilesTransmissionHandler.class); + } + + /** + * Downloads {@link IgniteClassPath} files locally from the remote node specified by {@code rmtNodeId}. + * @param icpId ClassPath id. + * @param rmtNodeId Remote node id. + * @return Future for download operation. + */ + IgniteInternalFuture downloadLocally(UUID icpId, UUID rmtNodeId) { + if (rmtNodeId.equals(ctx.localNodeId())) { + log.info("Upload node skip download [icpId=" + icpId + ']'); + + return new GridFinishedFuture<>(); + } + + try { + IgniteClassPath icp = fromMetastorage(icpId, ctx); + + log.info("Start download ClassPath files [icp=" + icp.name() + ']'); + + DownloadClassPathTask task = new DownloadClassPathTask(rmtNodeId, icp); + + try { + start(task); + } + catch (Throwable t) { + task.res.onDone(t); + } + + return task.res; + } + catch (Throwable e) { + return new GridFinishedFuture<>(e); + } + } + + /** Stopping handler. */ + void stop() { + synchronized (this) { + stopping = true; + } + + cancelAll(new IgniteException(SNP_NODE_STOPPING_ERR_MSG), r -> true); + } + + /** + * @param nodeId A node left the cluster. + */ + void onNodeLeft(UUID nodeId) { + cancelAll( + new ClusterTopologyCheckedException("The node from which a snapshot has been requested left the grid"), + r -> r.rmtNodeId.equals(nodeId) + ); + } + + /** + * @param err Task result. + * @param filter Filter to select tasks for cancel. + */ + private synchronized void cancelAll(Throwable err, Predicate filter) { + queue.forEach(r -> { + if (filter.test(r)) { + r.res.onDone(err); + } + }); + + if (active != null && filter.test(active)) + active.res.onDone(err); + } + + /** {@inheritDoc} */ + @Override public Consumer chunkHandler(UUID nodeId, TransmissionMeta initMeta) { + throw new UnsupportedOperationException("Loading file by chunks is not supported: " + nodeId); + } + + /** {@inheritDoc} */ + @Override public void onMessage(UUID nodeId, Object msg0, byte plc) { + try { + if (msg0 instanceof DownloadClassPathMessage) { + DownloadClassPathMessage msg = (DownloadClassPathMessage)msg0; + + IgniteClassPath icp = null; + + try { + icp = fromMetastorage(msg.icpId, ctx); + + NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); + + File root = ft.classPathRoot(icp.name()); + + if (!root.exists()) + throw new IgniteException("Classpath root not exists: " + root); + + // TODO: make async execution. + try (TransmissionSender sndr = ctx.io().openTransmissionSender(nodeId, FILES_TOPIC)) { + for (String name : icp.files()) { + File f = new File(root, name); + + if (!f.exists()) + throw new IgniteException("Classpath file not exists: " + f); + + sndr.send(f, Map.of(ICP_ID_PARAM, icp.id(), NAME_PARAM, name), TransmissionPolicy.FILE); + } + } + } + catch (Throwable t) { + U.error(log, "Error processing classpath file request [request=" + msg + ", nodeId=" + nodeId + ']', t); + + if (icp != null) { + ctx.io().sendToCustomTopic(nodeId, + FILES_TOPIC, + new DownloadClassPathFailureMessage(icp, t.getMessage()), + SYSTEM_POOL + ); + } + } + } + } + catch (Throwable e) { + U.error(log, "Processing snapshot request from remote node fails with an error", e); + + ctx.failure().process(new FailureContext(FailureType.CRITICAL_ERROR, e)); + } + } + + /** {@inheritDoc} */ + @Override public void onEnd(UUID nodeId) { + DownloadClassPathTask task = active; + + if (!ensureTask(nodeId, task)) + return; + + assert task.filesLeft.get() == 0 : task; + + if (log.isInfoEnabled()) + log.info("Classpath files from remote node has been fully received [icp=" + task.icp.name() + ']'); + + task.res.onDone((Void)null); + } + + /** {@inheritDoc} */ + @Override public void onException(UUID nodeId, Throwable ex) { + DownloadClassPathTask task = active; + + if (!ensureTask(nodeId, task)) + return; + + task.res.onDone(ex); + } + + /** {@inheritDoc} */ + @Override public String filePath(UUID nodeId, TransmissionMeta fileMeta) { + UUID icpId = (UUID)fileMeta.params().get(ICP_ID_PARAM); + String name = (String)fileMeta.params().get(NAME_PARAM); + + IgniteClassPath icp = fromMetastorage(icpId, ctx); + + DownloadClassPathTask task = active; + + if (!ensureTask(nodeId, task)) { + throw new TransmissionCancelledException("Stale snapshot transmission will be ignored " + + "[icpId=" + icp.id() + ", file=" + name + ']'); + } + + NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); + + File root = ft.classPathRoot(icp.name()); + + NodeFileTree.mkdir(root, "classpath root"); + + return new File(root, name).getAbsolutePath(); + } + + /** {@inheritDoc} */ + @Override public Consumer fileHandler(UUID nodeId, TransmissionMeta initMeta) { + UUID icpId = (UUID)initMeta.params().get(ICP_ID_PARAM); + String name = (String)initMeta.params().get(NAME_PARAM); + + IgniteClassPath icp = fromMetastorage(icpId, ctx); + + return file -> { + DownloadClassPathTask task = active; + + if (!ensureTask(nodeId, task)) { + throw new TransmissionCancelledException("Stale snapshot transmission will be ignored " + + "[icpId=" + icp.id() + ", file=" + name + ']'); + } + + int filesLeft = task.filesLeft.decrementAndGet(); + + if (log.isInfoEnabled()) { + log.info("Classpath file from remote node has been received " + + "[icp=" + task.icp.name() + ", file=" + name + ", filesLeft=" + filesLeft + ']'); + } + }; + } + + /** + * Starts {@code task} or adds it to queue. + * + * @param next Task to execute. + */ + private void start(DownloadClassPathTask next) { + ClusterNode rmtNode; + + synchronized (this) { + if (stopping) { + next.res.onDone(new IgniteException(SNP_NODE_STOPPING_ERR_MSG)); + + return; + } + + if (active != null && !active.res.isDone()) { + if (!queue.offer(next)) + next.res.onDone(new IgniteException("Can't put task in queue: " + next.icp)); + + return; + } + + rmtNode = ctx.discovery().node(next.rmtNodeId); + + if (rmtNode == null) { + next.res.onDone(new IgniteException("Can't download classpath files. " + + "Remote node left the grid [rmtNodeId=" + rmtNode + ']')); + + return; + } + + active = next; + + next.res.listen(this::onActiveDone); + } + + try { + ctx.cache().context().gridIO().sendOrderedMessage( + rmtNode, + FILES_TOPIC, + new DownloadClassPathMessage(next.icp), + SYSTEM_POOL, + Long.MAX_VALUE, + true + ); + } + catch (IgniteCheckedException e) { + next.res.onDone(new IgniteException("Can't download classpath files. " + + "Remote node left the grid [rmtNodeId=" + next.rmtNodeId + ']')); + + } + } + + /** Starts next task if exists. */ + private void onActiveDone(IgniteInternalFuture doneFut) { + DownloadClassPathTask next; + + synchronized (this) { + if (active == null || doneFut != active.res) + return; + + active = null; + + next = queue.poll(); + + while (next != null && next.res.isDone()) + next = queue.poll(); + } + + if (next != null) + start(next); + } + + /** */ + private static boolean ensureTask(UUID nodeId, DownloadClassPathTask task) { + return task != null + && !task.res.isDone() + && task.rmtNodeId.equals(nodeId); + } + + /** + * Task responsible for downloading {@link IgniteClassPath} files from remote node. + */ + private static class DownloadClassPathTask { + /** Node to download files from. */ + final UUID rmtNodeId; + + /** ClassPath to download files for. */ + final IgniteClassPath icp; + + /** Result of download. */ + final GridFutureAdapter res; + + /** Files counter. */ + final AtomicInteger filesLeft; + + /** */ + public DownloadClassPathTask(UUID rmtNodeId, IgniteClassPath icp) { + this.rmtNodeId = rmtNodeId; + this.icp = icp; + this.res = new GridFutureAdapter<>(); + this.filesLeft = new AtomicInteger(icp.files().length); + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java index af00a213532c0..6d1ec186606bd 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -24,6 +24,8 @@ import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.managers.eventstorage.DiscoveryEventListener; import org.apache.ignite.internal.processors.GridProcessorAdapter; import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; import org.apache.ignite.internal.util.typedef.F; @@ -31,6 +33,8 @@ import org.apache.ignite.internal.util.typedef.internal.U; import org.jetbrains.annotations.Nullable; +import static org.apache.ignite.events.EventType.EVT_NODE_FAILED; +import static org.apache.ignite.events.EventType.EVT_NODE_LEFT; import static org.apache.ignite.internal.classpath.IgniteClassPathState.NEW; /** @@ -45,8 +49,14 @@ public class ClassPathProcessor extends GridProcessorAdapter { /** Prefix for metastorage keys. */ public static final String METASTORE_PREFIX = "icp."; - /** */ - public final DeployToAllProcess deployToAllProcess; + /** Handles download requests for {@link IgniteClassPath} files. */ + private final ClassPathFilesTransmissionHandler icpFilesHnd; + + /** Distributed process that deploys classpath files to all nodes. */ + private final DeployToAllProcess deployToAllProc; + + /** System discovery message listener. */ + private DiscoveryEventListener discoLsnr; /** * @param ctx Kernal context. @@ -54,7 +64,27 @@ public class ClassPathProcessor extends GridProcessorAdapter { public ClassPathProcessor(GridKernalContext ctx) { super(ctx); - deployToAllProcess = new DeployToAllProcess(ctx); + icpFilesHnd = new ClassPathFilesTransmissionHandler(ctx); + deployToAllProc = new DeployToAllProcess(ctx, icpFilesHnd); + } + + /** {@inheritDoc} */ + @Override public void start() throws IgniteCheckedException { + ctx.event().addDiscoveryEventListener(discoLsnr = (evt, discoCache) -> { + // TODO: check busyLock required. See IgniteSnapshotManager. + UUID leftNodeId = evt.eventNode().id(); + + if (evt.type() == EVT_NODE_LEFT || evt.type() == EVT_NODE_FAILED) + icpFilesHnd.onNodeLeft(leftNodeId); + }, EVT_NODE_LEFT, EVT_NODE_FAILED); + } + + /** {@inheritDoc} */ + @Override public void onKernalStop(boolean cancel) { + icpFilesHnd.stop(); + + if (discoLsnr != null) + ctx.event().removeDiscoveryEventListener(discoLsnr); } /** @@ -76,7 +106,7 @@ public UUID startCreation(String name, String[] files, long[] lengths) { NodeFileTree ft = ctx.pdsFolderResolver().fileTree(); - File root = ft.classPath(name); + File root = ft.classPathRoot(name); try { casToMetastorage(null, icp); @@ -102,51 +132,60 @@ public UUID startCreation(String name, String[] files, long[] lengths) { } /** - * @param icpID ClassPath ID. + * Writes {@code batch} to the Ignite Class Path file. + * + * @param icpId ClassPath id. * @param name File name. * @param offset Offset to write data to. - * @param bytesCnt Bytes count in batch to write. * @param batch Batch. */ - public void uploadFilePart( - UUID icpID, + public void writeFilePartFromClient( + UUID icpId, String name, long offset, - int bytesCnt, byte[] batch ) throws IOException { try { - IgniteClassPath icp = fromMetastorage(icpID, ctx); + IgniteClassPath icp = fromMetastorage(icpId, ctx); if (F.indexOf(icp.files(), name) == -1) throw new IllegalArgumentException("Unknown lib [icp=" + icp.name() + ", unknown_lib=" + name + ']'); - File lib = new File(ctx.pdsFolderResolver().fileTree().classPath(icp.name()), name); + File f = new File(ctx.pdsFolderResolver().fileTree().classPathRoot(icp.name()), name); if (offset == 0) { - log.info("Creating new classpath file: " + lib); + log.info("Creating new classpath file: " + f); - if (!lib.createNewFile()) - throw new IgniteException("File exists: " + lib); + if (!f.createNewFile()) + throw new IgniteException("File exists: " + f); } - try (RandomAccessFile raf = new RandomAccessFile(lib, "rw")) { + try (RandomAccessFile raf = new RandomAccessFile(f, "rw")) { if (raf.length() < offset) { throw new IgniteException("Wrong offset [icp=" + icp.name() + ", lib=" + name + ", " + "fileLength=" + raf.length() + ", offset=" + offset + ']'); } raf.seek(offset); - raf.write(batch, 0, bytesCnt); + raf.write(batch); } } catch (Exception e) { - log.error("UploadFilePart:", e); + log.error("Upload file part error :", e); throw e; } } + /** + * Deploy {@link IgniteClassPath} to all nodes. + * @param icpId ClassPath id. + * @return Future for process result. + */ + public IgniteInternalFuture deployToAll(@Nullable UUID icpId) { + return deployToAllProc.start(icpId); + } + /** */ private void casToMetastorage(@Nullable IgniteClassPath prev, IgniteClassPath icp) { try { @@ -159,21 +198,21 @@ private void casToMetastorage(@Nullable IgniteClassPath prev, IgniteClassPath ic } /** - * @param icpID ClassPath ID. + * @param icpId ClassPath id. * @param ctx Kernal context. * @return Class path. */ - static IgniteClassPath fromMetastorage(UUID icpID, GridKernalContext ctx) { + static IgniteClassPath fromMetastorage(UUID icpId, GridKernalContext ctx) { try { IgniteClassPath[] icp = new IgniteClassPath[1]; ctx.distributedMetastorage().iterate(METASTORE_PREFIX, (key, icp0) -> { - if (icpID.equals(((IgniteClassPath)icp0).id())) + if (icpId.equals(((IgniteClassPath)icp0).id())) icp[0] = (IgniteClassPath)icp0; }); if (icp[0] == null) - throw new IgniteException("ClassPath not found: " + icpID); + throw new IgniteException("ClassPath not found: " + icpId); if (icp[0].state() != NEW) throw new IgniteException("ClassPath in wrong state [expected=" + NEW + ", status=" + icp[0].state() + ']'); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java index a64ab0afe29a0..532cf05e82650 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java @@ -26,29 +26,34 @@ import org.apache.ignite.internal.util.distributed.DistributedProcess; import org.apache.ignite.internal.util.future.GridFinishedFuture; import org.apache.ignite.internal.util.future.GridFutureAdapter; +import org.apache.ignite.internal.util.lang.GridClosureException; import static org.apache.ignite.internal.classpath.ClassPathProcessor.fromMetastorage; import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.CLASSPATH_DEPLOY_TO_ALL; -/** */ -public class DeployToAllProcess { - /** */ +/** Distributed process to spread {@link IgniteClassPath} files across cluster. */ +class DeployToAllProcess { + /** Logger. */ private final IgniteLogger log; /** */ private final GridKernalContext ctx; + /** */ + private final ClassPathFilesTransmissionHandler icpFilesHnd; + /** Distribute process that distributes new Ignite class path across all server nodes. */ private final DistributedProcess deployToAllProc; /** */ - private final Map> deployToAllFuts = new ConcurrentHashMap<>(); + private final Map> futs = new ConcurrentHashMap<>(); /** */ - public DeployToAllProcess(GridKernalContext ctx) { + public DeployToAllProcess(GridKernalContext ctx, ClassPathFilesTransmissionHandler icpFilesHnd) { log = ctx.log(DeployToAllProcess.class); this.ctx = ctx; + this.icpFilesHnd = icpFilesHnd; deployToAllProc = new DistributedProcess<>( ctx, @@ -60,23 +65,23 @@ public DeployToAllProcess(GridKernalContext ctx) { /** * @param icpId ClassPath ID. - * @return + * @return Future for deploy process result. */ public IgniteInternalFuture start(UUID icpId) { - GridFutureAdapter fut = new GridFutureAdapter<>(); + GridFutureAdapter deployRes = new GridFutureAdapter<>(); synchronized (this) { IgniteClassPath icp = fromMetastorage(icpId, ctx); ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId, ctx.localNodeId()); - if (deployToAllFuts.put(icpId, fut) != null) - return new GridFinishedFuture<>(new IllegalStateException("Distribute to all process started, already: " + icp)); + if (futs.put(icpId, deployRes) != null) + return new GridFinishedFuture<>(new IllegalStateException("Deploy to all process started, already: " + icp.name())); deployToAllProc.start(icpId, req); } - return fut; + return deployRes; } /** @@ -84,17 +89,12 @@ public IgniteInternalFuture start(UUID icpId) { * @return Future which will be completed when a snapshot has been started. */ private IgniteInternalFuture startDeployToAllProcess(ClassPathDeployToAllRequest req) { - IgniteClassPath icp = fromMetastorage(req.icpId, ctx); + return icpFilesHnd.downloadLocally(req.icpId, req.uploadNodeId).chain(f -> { + if (f.error() != null) + throw new GridClosureException(f.error()); - if (req.uploadNodeId.equals(ctx.localNodeId())) { - log.info("Upload node skip download [icp=" + icp + ']'); - - return new GridFinishedFuture<>(new ClassPathDeployToAllResponse(icp.id())); - } - - log.info("Starting download new classpath [icp=" + icp + ']'); - - return new DownloadClassPathTask(ctx, icp).call(); + return new ClassPathDeployToAllResponse(req.icpId); + }); } /** @@ -103,7 +103,7 @@ private IgniteInternalFuture startDeployToAllProce * @param err Errors. */ private void processDeployToAllResult(UUID id, Map res, Map err) { - GridFutureAdapter fut = deployToAllFuts.remove(id); + GridFutureAdapter fut = futs.remove(id); // Only upload node manage the process. if (fut == null) { @@ -115,10 +115,11 @@ private void processDeployToAllResult(UUID id, Map> { - /** Kernal context. */ - private final GridKernalContext ctx; - - /** Logger. */ - private final IgniteLogger log; - - /** Ignite class path. */ - private final IgniteClassPath icp; - - private final AtomicInteger cntr; - - /** Resulting future. */ - private final GridFutureAdapter res; - - /** - * @param ctx Kernal context. - * @param icp Ignite class path. - */ - public DownloadClassPathTask(GridKernalContext ctx, IgniteClassPath icp) { - this.ctx = ctx; - this.log = ctx.log(DownloadClassPathTask.class); - this.icp = icp; - this.res = new GridFutureAdapter<>(); - this.cntr = new AtomicInteger(icp.files().length); - } - - /** {@inheritDoc} */ - @Override public IgniteInternalFuture call() { - try { - // TODO: check correct executor used. - Stream.of(icp.files()) - .map(DownloadClassPathFileTask::new) - .map(ctx.pools().getPeerClassLoadingExecutorService()::submit); - } - catch (Throwable e) { - res.onDone(e); - } - - return res; - } - - /** */ - private class DownloadClassPathFileTask implements Runnable { - /** */ - private final String name; - - /** */ - public DownloadClassPathFileTask(String name) { - this.name = name; - } - - /** {@inheritDoc} */ - @Override public void run() { - try { - run0(); - } - catch (Throwable e) { - res.onDone(e); - } - } - - /** */ - private void run0() { - log.info("Downloading file: " + name); - - if (!res.isDone()) { - int filesToDownload = cntr.addAndGet(-1); - - log.info("File downloaded [name=" + name + ", filesToDownload=" + filesToDownload + ']'); - - if (filesToDownload == 0) - res.onDone(new ClassPathDeployToAllResponse(icp.id())); - } - } - } -} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java index 1d58b43dbaac3..2cf677a206dda 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/IgniteClassPathState.java @@ -17,7 +17,7 @@ package org.apache.ignite.internal.classpath; -/** */ +/** State of {@link IgniteClassPath}. */ public enum IgniteClassPathState { /** Creationg process in progress. */ NEW, diff --git a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java index b96a82954cb2f..01f1848f56358 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/client/thin/TcpIgniteClient.java @@ -660,8 +660,7 @@ public void uploadClasspathFile(ClusterNode node, UUID icpID, Path file) throws w.writeUuid(icpID); w.writeString(name); w.writeLong(offset[0]); - w.writeInt(bytesCnt[0]); - w.writeByteArray(batch); + w.writeByteArray(batch, 0, bytesCnt[0]); } }, in -> null diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java index 3b78d9e91a966..f9213dab6aa67 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathDistributeTask.java @@ -21,11 +21,12 @@ import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.classpath.IgniteClassPath; import org.apache.ignite.internal.visor.VisorJob; import org.apache.ignite.internal.visor.VisorOneNodeTask; import org.jetbrains.annotations.Nullable; -/** */ +/** Task to start deploy process of newly created {@link IgniteClassPath}. */ public class ClassPathDistributeTask extends VisorOneNodeTask { /** */ private static final long serialVersionUID = 0L; @@ -47,7 +48,7 @@ protected ClassPathDistributeJob(@Nullable UUID arg, boolean debug) { /** {@inheritDoc} */ @Override protected Void run(@Nullable UUID arg) throws IgniteException { - IgniteInternalFuture fut = ignite.context().classPath().deployToAllProcess.start(arg); + IgniteInternalFuture fut = ignite.context().classPath().deployToAll(arg); try { fut.get(); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java index 2100a9fe0d0f6..1ec5b0477e37f 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/filename/NodeFileTree.java @@ -1096,7 +1096,7 @@ public File maintenanceFile() { * @param name IgniteClassPath name. * @return IgniteClassPath directory. */ - public File classPath(String name) { + public File classPathRoot(String name) { return new File(icp, name); } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerInternalRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerInternalRequest.java new file mode 100644 index 0000000000000..733e737383ce5 --- /dev/null +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerInternalRequest.java @@ -0,0 +1,31 @@ +/* + * 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.processors.odbc; + +import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext; + +/** + * Marker interface for requests that can be executed by control.sh, only + * @see ClientConnectionContext#managementClient() + */ +public interface ClientListenerInternalRequest extends ClientListenerRequest { + /** {@inheritDoc} */ + @Override default boolean internal() { + return true; + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerRequest.java index d7006f70a617f..bb37216852cf5 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/ClientListenerRequest.java @@ -33,4 +33,9 @@ public interface ClientListenerRequest { default boolean beforeStartupRequest() { return false; } + + /** @return {@code True} if request can be executed only by control.sh client. */ + default boolean internal() { + return false; + } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java index 4d344fc3f1216..9a1365be4eef4 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.processors.platform.client; +import org.apache.ignite.client.ClientException; import org.apache.ignite.internal.binary.BinaryReaderEx; import org.apache.ignite.internal.binary.BinaryUtils; import org.apache.ignite.internal.binary.BinaryWriterEx; @@ -76,7 +77,7 @@ import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheScanQueryRequest; import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlFieldsQueryRequest; import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlQueryRequest; -import org.apache.ignite.internal.processors.platform.client.classpath.ClientFileUploadRequest; +import org.apache.ignite.internal.processors.platform.client.classpath.ClientClassPathFileUploadRequest; import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterChangeStateRequest; import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGetDataCenterNodesRequest; import org.apache.ignite.internal.processors.platform.client.cluster.ClientClusterGetStateRequest; @@ -116,6 +117,8 @@ import org.apache.ignite.internal.processors.platform.client.streamer.ClientDataStreamerStartRequest; import org.apache.ignite.internal.processors.platform.client.tx.ClientTxEndRequest; import org.apache.ignite.internal.processors.platform.client.tx.ClientTxStartRequest; +import org.apache.ignite.internal.thread.context.Scope; +import org.apache.ignite.plugin.security.SecurityPermission; /** * Thin client message parser. @@ -455,6 +458,9 @@ public class ClientMessageParser implements ClientListenerMessageParser { if (ctx.kernalContext().recoveryMode() && !req.beforeStartupRequest()) return new ClientRawRequest(req.requestId(), ClientStatus.FAILED, "Node in recovery mode."); + if (req.internal()) + checkPermissionsForInternalRequest(); + return req; } @@ -740,7 +746,7 @@ public ClientListenerRequest decode(BinaryReaderEx reader) { return new ClientServiceTopologyRequest(reader); case FILE_UPLOAD: - return new ClientFileUploadRequest(reader); + return new ClientClassPathFileUploadRequest(reader); case OP_STOP_WARMUP: return new ClientCacheStopWarmupRequest(reader); @@ -778,4 +784,22 @@ public ClientListenerRequest decode(BinaryReaderEx reader) { @Override public long decodeRequestId(ClientMessage msg) { return 0; } + + /** + * Check permissions to invoke internal requests. + */ + private void checkPermissionsForInternalRequest() { + if (!ctx.managementClient()) + throw new ClientException("Only management client are allowed to execute this"); + + // When security is enabled, only an administrator can connect and execute commands. + if (ctx.securityContext() != null) { + try (Scope ignored = ctx.kernalContext().security().withContext(ctx.securityContext())) { + ctx.kernalContext().security().authorize(SecurityPermission.ADMIN_OPS); + } + catch (SecurityException e) { + throw new ClientException("ADMIN_OPS permission required"); + } + } + } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientClassPathFileUploadRequest.java similarity index 85% rename from modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java rename to modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientClassPathFileUploadRequest.java index f1359abe27b6f..75e61fb33a439 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientFileUploadRequest.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/classpath/ClientClassPathFileUploadRequest.java @@ -20,12 +20,15 @@ import java.io.IOException; import java.util.UUID; import org.apache.ignite.binary.BinaryRawReader; +import org.apache.ignite.internal.processors.odbc.ClientListenerInternalRequest; import org.apache.ignite.internal.processors.platform.client.ClientConnectionContext; import org.apache.ignite.internal.processors.platform.client.ClientRequest; import org.apache.ignite.internal.processors.platform.client.ClientResponse; -/** */ -public class ClientFileUploadRequest extends ClientRequest { +/** + * TODO: only for admin client! + */ +public class ClientClassPathFileUploadRequest extends ClientRequest implements ClientListenerInternalRequest { /** Upload node ID. */ private final UUID uploadNodeId; @@ -38,9 +41,6 @@ public class ClientFileUploadRequest extends ClientRequest { /** Offset to write data to. */ private final long offset; - /** Bytes count in batch to write. */ - private final int bytesCnt; - /** Batch. */ private final byte[] batch; @@ -49,28 +49,25 @@ public class ClientFileUploadRequest extends ClientRequest { * * @param reader Reader. */ - public ClientFileUploadRequest(BinaryRawReader reader) { + public ClientClassPathFileUploadRequest(BinaryRawReader reader) { super(reader); uploadNodeId = reader.readUuid(); icpId = reader.readUuid(); name = reader.readString(); offset = reader.readLong(); - bytesCnt = reader.readInt(); batch = reader.readByteArray(); } /** {@inheritDoc} */ @Override public ClientResponse process(ClientConnectionContext ctx) { - // TODO: add backward compatibility support. - UUID locNodeId = ctx.kernalContext().localNodeId(); if (!uploadNodeId.equals(locNodeId)) throw new IllegalStateException("Wrong node [uploadNode=" + uploadNodeId + ", localNode=" + locNodeId + ']'); try { - ctx.kernalContext().classPath().uploadFilePart(icpId, name, offset, bytesCnt, batch); + ctx.kernalContext().classPath().writeFilePartFromClient(icpId, name, offset, batch); return new ClientResponse(requestId()); } From 28a3beba70d2e82459907d0a9d3f494dd1734e79 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Thu, 11 Jun 2026 22:53:10 +0300 Subject: [PATCH 14/15] IGNITE-28207 Classpath creation process --- .../util/GridCommandHandlerClassPathTest.java | 51 ++++++++++-------- .../ClassPathFilesTransmissionHandler.java | 52 ++++++++++++++----- .../classpath/ClassPathProcessor.java | 36 +++++-------- .../classpath/DeployToAllProcess.java | 45 ++++++++++------ .../internal/classpath/IgniteClassPath.java | 12 +++-- .../internal/client/thin/TcpIgniteClient.java | 2 +- .../platform/client/ClientMessageParser.java | 9 ++-- .../client/ThinClientPermissionCheckTest.java | 47 +++++++++++++++++ 8 files changed, 173 insertions(+), 81 deletions(-) diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index 600d1bd5af170..fdea25d21def0 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -20,11 +20,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.stream.Collectors; -import org.junit.Assume; import org.junit.Test; +import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_INVALID_ARGUMENTS; import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK; -import static org.hamcrest.Matchers.is; +import static org.apache.ignite.testframework.GridTestUtils.assertContains; /** * Test for --classpath command. @@ -39,11 +39,12 @@ public class GridCommandHandlerClassPathTest extends GridCommandHandlerAbstractT super.beforeTestsStarted(); } + // TODO check empty file creation. + // TODO add in production code checks of files integriy. Perform file integrity check on startup. + /** Tests --create command. */ @Test public void testCreate() throws Exception { - Assume.assumeThat("Only cli mode supported", commandHandler, is(CLI_CMD_HND)); - //grid(0).cluster().state(ClusterState.ACTIVE); String jars = Files.list(Path.of(getClass().getClassLoader().getResource(".").getPath() + "../")) @@ -74,29 +75,37 @@ public void testCreate() throws Exception { finally { String outStr = testOut.toString(); - stopAllGrids(); - System.out.println(outStr); } } - // TODO check empty file creation. - // TODO add in production code checks of files integriy. Perform file integrity check on startup. - /** Tests --create command. */ + /** Tests --create command arguments format. */ @Test public void testEmptyFilesArgument() { - final TestCommandHandler hnd = newCommandHandler(createTestLogger()); - - try { - assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", "mysuperapp", "--files")); - } - finally { - String outStr = testOut.toString(); - - stopAllGrids(); - - System.out.println(outStr); - } + injectTestSystemOut(); + assertContains( + log, + executeCommand(EXIT_CODE_INVALID_ARGUMENTS, "--class-path", "create", "--name", "mysuperapp", "--files"), + "Please specify a value for argument: --files" + ); + + assertContains( + log, + executeCommand(EXIT_CODE_INVALID_ARGUMENTS, "--class-path", "create", "--name", "mysuperapp"), + "Mandatory argument(s) missing: [--files]" + ); + + assertContains( + log, + executeCommand(EXIT_CODE_INVALID_ARGUMENTS, "--class-path", "create", "--name", "--files", "some_files"), + "Please specify a value for argument: --name" + ); + + assertContains( + log, + executeCommand(EXIT_CODE_INVALID_ARGUMENTS, "--class-path", "create", "--files", "some_files"), + "Mandatory argument(s) missing: [--name]" + ); } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java index c771ec95e9eac..170de4e5607a9 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathFilesTransmissionHandler.java @@ -25,12 +25,16 @@ import org.apache.ignite.internal.managers.communication.TransmissionHandler; import org.apache.ignite.internal.managers.communication.TransmissionMeta; import org.apache.ignite.internal.managers.communication.TransmissionPolicy; +import org.apache.ignite.internal.managers.eventstorage.DiscoveryEventListener; import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; import org.apache.ignite.internal.util.future.GridFinishedFuture; import org.apache.ignite.internal.util.future.GridFutureAdapter; import org.apache.ignite.internal.util.typedef.internal.U; +import static org.apache.ignite.events.EventType.EVT_NODE_FAILED; +import static org.apache.ignite.events.EventType.EVT_NODE_LEFT; import static org.apache.ignite.internal.classpath.ClassPathProcessor.fromMetastorage; +import static org.apache.ignite.internal.classpath.IgniteClassPathState.NEW; import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYSTEM_POOL; import static org.apache.ignite.internal.processors.cache.persistence.snapshot.IgniteSnapshotManager.SNP_NODE_STOPPING_ERR_MSG; @@ -47,6 +51,9 @@ class ClassPathFilesTransmissionHandler implements TransmissionHandler, GridMess /** Transmission parameter for {@link IgniteClassPath} file name. */ private static final String NAME_PARAM = "name"; + /** System discovery message listener. */ + private DiscoveryEventListener discoLsnr; + /** */ private volatile DownloadClassPathTask active; @@ -73,21 +80,21 @@ public ClassPathFilesTransmissionHandler(GridKernalContext ctx) { /** * Downloads {@link IgniteClassPath} files locally from the remote node specified by {@code rmtNodeId}. - * @param icpId ClassPath id. * @param rmtNodeId Remote node id. + * @param icpId ClassPath id. * @return Future for download operation. */ - IgniteInternalFuture downloadLocally(UUID icpId, UUID rmtNodeId) { - if (rmtNodeId.equals(ctx.localNodeId())) { - log.info("Upload node skip download [icpId=" + icpId + ']'); + IgniteInternalFuture downloadLocally(UUID rmtNodeId, UUID icpId) { + try { + IgniteClassPath icp = fromMetastorage(icpId, NEW, ctx); - return new GridFinishedFuture<>(); - } + if (rmtNodeId.equals(ctx.localNodeId())) { + log.info("Skip download ClassPath files for upload node [name=" + icp.name() + ", id=" + icp.id() + ']'); - try { - IgniteClassPath icp = fromMetastorage(icpId, ctx); + return new GridFinishedFuture<>(); + } - log.info("Start download ClassPath files [icp=" + icp.name() + ']'); + log.info("Start download ClassPath files [name=" + icp.name() + ", id=" + icp.id() + ']'); DownloadClassPathTask task = new DownloadClassPathTask(rmtNodeId, icp); @@ -105,9 +112,28 @@ IgniteInternalFuture downloadLocally(UUID icpId, UUID rmtNodeId) { } } + /** Starts handler. */ + synchronized void start() { + ctx.event().addDiscoveryEventListener(discoLsnr = (evt, discoCache) -> { + UUID leftNodeId = evt.eventNode().id(); + + if (evt.type() == EVT_NODE_LEFT || evt.type() == EVT_NODE_FAILED) + onNodeLeft(leftNodeId); + }, EVT_NODE_LEFT, EVT_NODE_FAILED); + + ctx.io().addMessageListener(FILES_TOPIC, this); + ctx.io().addTransmissionHandler(FILES_TOPIC, this); + } + /** Stopping handler. */ void stop() { synchronized (this) { + if (discoLsnr != null) + ctx.event().removeDiscoveryEventListener(discoLsnr); + + ctx.io().removeMessageListener(FILES_TOPIC); + ctx.io().removeTransmissionHandler(FILES_TOPIC); + stopping = true; } @@ -153,7 +179,7 @@ private synchronized void cancelAll(Throwable err, Predicate { DownloadClassPathTask task = active; @@ -313,6 +339,8 @@ private void start(DownloadClassPathTask next) { ); } catch (IgniteCheckedException e) { + log.warning("Can't start download ClassPath files:", e); + next.res.onDone(new IgniteException("Can't download classpath files. " + "Remote node left the grid [rmtNodeId=" + next.rmtNodeId + ']')); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java index 6d1ec186606bd..9afe797bc3f0a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/ClassPathProcessor.java @@ -25,7 +25,6 @@ import org.apache.ignite.IgniteException; import org.apache.ignite.internal.GridKernalContext; import org.apache.ignite.internal.IgniteInternalFuture; -import org.apache.ignite.internal.managers.eventstorage.DiscoveryEventListener; import org.apache.ignite.internal.processors.GridProcessorAdapter; import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; import org.apache.ignite.internal.util.typedef.F; @@ -33,8 +32,6 @@ import org.apache.ignite.internal.util.typedef.internal.U; import org.jetbrains.annotations.Nullable; -import static org.apache.ignite.events.EventType.EVT_NODE_FAILED; -import static org.apache.ignite.events.EventType.EVT_NODE_LEFT; import static org.apache.ignite.internal.classpath.IgniteClassPathState.NEW; /** @@ -55,9 +52,6 @@ public class ClassPathProcessor extends GridProcessorAdapter { /** Distributed process that deploys classpath files to all nodes. */ private final DeployToAllProcess deployToAllProc; - /** System discovery message listener. */ - private DiscoveryEventListener discoLsnr; - /** * @param ctx Kernal context. */ @@ -70,21 +64,12 @@ public ClassPathProcessor(GridKernalContext ctx) { /** {@inheritDoc} */ @Override public void start() throws IgniteCheckedException { - ctx.event().addDiscoveryEventListener(discoLsnr = (evt, discoCache) -> { - // TODO: check busyLock required. See IgniteSnapshotManager. - UUID leftNodeId = evt.eventNode().id(); - - if (evt.type() == EVT_NODE_LEFT || evt.type() == EVT_NODE_FAILED) - icpFilesHnd.onNodeLeft(leftNodeId); - }, EVT_NODE_LEFT, EVT_NODE_FAILED); + icpFilesHnd.start(); } /** {@inheritDoc} */ @Override public void onKernalStop(boolean cancel) { icpFilesHnd.stop(); - - if (discoLsnr != null) - ctx.event().removeDiscoveryEventListener(discoLsnr); } /** @@ -146,7 +131,7 @@ public void writeFilePartFromClient( byte[] batch ) throws IOException { try { - IgniteClassPath icp = fromMetastorage(icpId, ctx); + IgniteClassPath icp = fromMetastorage(icpId, NEW, ctx); if (F.indexOf(icp.files(), name) == -1) throw new IllegalArgumentException("Unknown lib [icp=" + icp.name() + ", unknown_lib=" + name + ']'); @@ -187,10 +172,15 @@ public IgniteInternalFuture deployToAll(@Nullable UUID icpId) { } /** */ - private void casToMetastorage(@Nullable IgniteClassPath prev, IgniteClassPath icp) { + void casToMetastorage(@Nullable IgniteClassPath prev, IgniteClassPath icp) { try { - if (!ctx.distributedMetastorage().compareAndSet(metastorageKey(icp), prev, icp)) - throw new IgniteException("Classpath alreay exists: " + icp.name()); + String key = metastorageKey(icp); + + if (!ctx.distributedMetastorage().compareAndSet(key, prev, icp)) { + IgniteClassPath val = ctx.distributedMetastorage().read(key); + + throw new IgniteException("Fail to write new ClassPath state[exp=" + icp + ", actual=" + val + ", new=" + icp); + } } catch (IgniteCheckedException e) { throw new IgniteException(e); @@ -202,7 +192,7 @@ private void casToMetastorage(@Nullable IgniteClassPath prev, IgniteClassPath ic * @param ctx Kernal context. * @return Class path. */ - static IgniteClassPath fromMetastorage(UUID icpId, GridKernalContext ctx) { + static IgniteClassPath fromMetastorage(UUID icpId, IgniteClassPathState expState, GridKernalContext ctx) { try { IgniteClassPath[] icp = new IgniteClassPath[1]; @@ -214,8 +204,8 @@ static IgniteClassPath fromMetastorage(UUID icpId, GridKernalContext ctx) { if (icp[0] == null) throw new IgniteException("ClassPath not found: " + icpId); - if (icp[0].state() != NEW) - throw new IgniteException("ClassPath in wrong state [expected=" + NEW + ", status=" + icp[0].state() + ']'); + if (icp[0].state() != expState) + throw new IgniteException("ClassPath in wrong state [expected=" + expState + ", status=" + icp[0].state() + ']'); return icp[0]; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java index 532cf05e82650..8fa318bb51aa3 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/classpath/DeployToAllProcess.java @@ -29,6 +29,7 @@ import org.apache.ignite.internal.util.lang.GridClosureException; import static org.apache.ignite.internal.classpath.ClassPathProcessor.fromMetastorage; +import static org.apache.ignite.internal.classpath.IgniteClassPathState.NEW; import static org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.CLASSPATH_DEPLOY_TO_ALL; /** Distributed process to spread {@link IgniteClassPath} files across cluster. */ @@ -36,16 +37,16 @@ class DeployToAllProcess { /** Logger. */ private final IgniteLogger log; - /** */ + /** Kernal context. */ private final GridKernalContext ctx; - /** */ + /** Files handler. */ private final ClassPathFilesTransmissionHandler icpFilesHnd; /** Distribute process that distributes new Ignite class path across all server nodes. */ private final DistributedProcess deployToAllProc; - /** */ + /** Future results of started distributed process. */ private final Map> futs = new ConcurrentHashMap<>(); /** */ @@ -58,7 +59,7 @@ public DeployToAllProcess(GridKernalContext ctx, ClassPathFilesTransmissionHandl deployToAllProc = new DistributedProcess<>( ctx, CLASSPATH_DEPLOY_TO_ALL, - this::startDeployToAllProcess, + this::downloadLocally, this::processDeployToAllResult ); } @@ -68,28 +69,40 @@ public DeployToAllProcess(GridKernalContext ctx, ClassPathFilesTransmissionHandl * @return Future for deploy process result. */ public IgniteInternalFuture start(UUID icpId) { - GridFutureAdapter deployRes = new GridFutureAdapter<>(); + boolean added = false; + + try { + GridFutureAdapter deployRes = new GridFutureAdapter<>(); - synchronized (this) { - IgniteClassPath icp = fromMetastorage(icpId, ctx); + synchronized (this) { + IgniteClassPath icp = fromMetastorage(icpId, NEW, ctx); - ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId, ctx.localNodeId()); + ClassPathDeployToAllRequest req = new ClassPathDeployToAllRequest(icpId, ctx.localNodeId()); - if (futs.put(icpId, deployRes) != null) - return new GridFinishedFuture<>(new IllegalStateException("Deploy to all process started, already: " + icp.name())); + added = futs.putIfAbsent(icpId, deployRes) == null; - deployToAllProc.start(icpId, req); + if (!added) + return new GridFinishedFuture<>(new IllegalStateException("Deploy to all process started, already: " + icp.name())); + + deployToAllProc.start(icpId, req); + } + + return deployRes; } + catch (Exception e) { + if (added) + futs.remove(icpId); - return deployRes; + return new GridFinishedFuture<>(e); + } } /** * @param req Request on snapshot creation. * @return Future which will be completed when a snapshot has been started. */ - private IgniteInternalFuture startDeployToAllProcess(ClassPathDeployToAllRequest req) { - return icpFilesHnd.downloadLocally(req.icpId, req.uploadNodeId).chain(f -> { + private IgniteInternalFuture downloadLocally(ClassPathDeployToAllRequest req) { + return icpFilesHnd.downloadLocally(req.uploadNodeId, req.icpId).chain(f -> { if (f.error() != null) throw new GridClosureException(f.error()); @@ -113,7 +126,7 @@ private void processDeployToAllResult(UUID id, Map null diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java index 9a1365be4eef4..a1bfe607c5e4f 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/platform/client/ClientMessageParser.java @@ -421,6 +421,9 @@ public class ClientMessageParser implements ClientListenerMessageParser { /** Stop warmup. */ private static final short OP_STOP_WARMUP = 10000; + /** */ + public static final String INTERNAL_REQ_ERR_MSG = "Only management client are allowed to execute this"; + /** Marshaller. */ private final GridBinaryMarshaller marsh; @@ -459,7 +462,7 @@ public class ClientMessageParser implements ClientListenerMessageParser { return new ClientRawRequest(req.requestId(), ClientStatus.FAILED, "Node in recovery mode."); if (req.internal()) - checkPermissionsForInternalRequest(); + checkInternalRequestAllowed(); return req; } @@ -788,9 +791,9 @@ public ClientListenerRequest decode(BinaryReaderEx reader) { /** * Check permissions to invoke internal requests. */ - private void checkPermissionsForInternalRequest() { + private void checkInternalRequestAllowed() { if (!ctx.managementClient()) - throw new ClientException("Only management client are allowed to execute this"); + throw new ClientException(INTERNAL_REQ_ERR_MSG); // When security is enabled, only an administrator can connect and execute commands. if (ctx.securityContext() != null) { diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java index 401edd85015dd..f0cf1ee674b01 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/client/ThinClientPermissionCheckTest.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.processors.security.client; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -36,6 +38,7 @@ import org.apache.ignite.client.ClientAuthenticationException; import org.apache.ignite.client.ClientAuthorizationException; import org.apache.ignite.client.ClientCache; +import org.apache.ignite.client.ClientConnectionException; import org.apache.ignite.client.ClientException; import org.apache.ignite.client.Config; import org.apache.ignite.client.IgniteClient; @@ -49,6 +52,7 @@ import org.apache.ignite.configuration.ThinClientConfiguration; import org.apache.ignite.events.CacheEvent; import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.client.thin.TcpIgniteClient; import org.apache.ignite.internal.processors.cache.eviction.paged.TestObject; import org.apache.ignite.internal.processors.platform.cache.expiry.PlatformExpiryPolicyFactory; import org.apache.ignite.internal.processors.security.AbstractSecurityTest; @@ -440,6 +444,49 @@ public void testConnectAsManagementClient() throws Exception { checkDflt.run(); } + /** */ + @Test + public void testClassPathUploadFilePart() throws Exception { + File tmpFile = File.createTempFile("file", "temp"); + + tmpFile.deleteOnExit(); + + // Client has no permission to invoke internal methods. + // Trying to invoke without specifying "management client" flag must fail. + for (String name: new String[] {CLIENT, ADMIN}) { + assertThrows(log, () -> { + try (IgniteClient cli = startClient(name)) { + ((TcpIgniteClient)cli).uploadClasspathFile(F.first(cli.cluster().nodes()), UUID.randomUUID(), tmpFile.toPath()); + return null; + } + }, ClientConnectionException.class, "Channel is closed"); + } + + userAttrs = F.asMap(MANAGEMENT_CLIENT_ATTR, "true"); + + try { + // Trying to invoke as CLIENT with "management client" flag must fail, because of security. + // CLIENT has no ADMIN_OPS permission. + assertThrows(log, () -> { + try (IgniteClient cli = startClient(CLIENT)) { + ((TcpIgniteClient)cli).uploadClasspathFile(F.first(cli.cluster().nodes()), UUID.randomUUID(), tmpFile.toPath()); + return null; + } + }, ClientConnectionException.class, "Channel is closed"); + + // Check that request actually invoked and failed due to unknown ClassPath. + assertThrows(log, () -> { + try (IgniteClient cli = startClient(ADMIN)) { + ((TcpIgniteClient)cli).uploadClasspathFile(F.first(cli.cluster().nodes()), UUID.randomUUID(), tmpFile.toPath()); + return null; + } + }, ClientException.class, "ClassPath not found"); + } + finally { + userAttrs = null; + } + } + /** * Gets all operations. * From 1796281f746bcfa40d4b2c1658d49a9e4af176b2 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Fri, 12 Jun 2026 16:37:08 +0300 Subject: [PATCH 15/15] IGNITE-28207 Classpath creation process --- .../util/GridCommandHandlerClassPathTest.java | 74 ++++++++++++++----- .../classpath/ClassPathCreateCommand.java | 3 + 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java index fdea25d21def0..20ca06227cf4f 100644 --- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java +++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerClassPathTest.java @@ -17,9 +17,14 @@ package org.apache.ignite.util; +import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.ignite.internal.processors.cache.persistence.filename.NodeFileTree; import org.junit.Test; import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_INVALID_ARGUMENTS; @@ -30,53 +35,59 @@ * Test for --classpath command. */ public class GridCommandHandlerClassPathTest extends GridCommandHandlerAbstractTest { + /** */ + public static final int GRID_CNT = 3; + /** {@inheritDoc} */ @Override protected void beforeTestsStarted() throws Exception { cleanPersistenceDir(); - startGrids(2); + startGrids(GRID_CNT); super.beforeTestsStarted(); } // TODO check empty file creation. // TODO add in production code checks of files integriy. Perform file integrity check on startup. + // Support JXM handler. + // Support pretty print for command. + // Test for different failure types: node fail, file write fail, etc. + // Concurrent creation of CP with the same name. /** Tests --create command. */ @Test - public void testCreate() throws Exception { + public void testCreate() { //grid(0).cluster().state(ClusterState.ACTIVE); - String jars = Files.list(Path.of(getClass().getClassLoader().getResource(".").getPath() + "../")) - .map(Path::toAbsolutePath) - .map(Path::toString) - .filter(f -> f.endsWith("jar")) - .collect(Collectors.joining(",")); - - Path dir = Path.of(getClass().getClassLoader().getResource(".").getPath() + "../../../core/target"); - - System.out.println("dir = " + dir); - - String coreJars = Files.list(dir) - .map(Path::toAbsolutePath) - .map(Path::toString) - .filter(f -> f.endsWith("jar")) - .collect(Collectors.joining(",")); - - jars += "," + coreJars; + Set jars = jars( + Path.of(getClass().getClassLoader().getResource(".").getPath() + "../"), + Path.of(getClass().getClassLoader().getResource(".").getPath() + "../../../core/target") + ); injectTestSystemOut(); final TestCommandHandler hnd = newCommandHandler(createTestLogger()); + String cpName = "mysuperapp"; + try { - assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", "mysuperapp", "--files", jars)); + String files = jars.stream().map(Path::toFile).map(File::getAbsolutePath).collect(Collectors.joining(",")); + + assertEquals(EXIT_CODE_OK, execute(hnd, "--class-path", "create", "--name", cpName, "--files", files)); } finally { String outStr = testOut.toString(); System.out.println(outStr); } + + Set cpFilesNames = fileNames(jars); + + for (int i = 0; i < GRID_CNT; i++) { + NodeFileTree ft = grid(i).context().pdsFolderResolver().fileTree(); + + assertEquals("Files must be deployed on each node", cpFilesNames, fileNames(jars(ft.classPathRoot(cpName).toPath()))); + } } /** Tests --create command arguments format. */ @@ -107,5 +118,28 @@ public void testEmptyFilesArgument() { executeCommand(EXIT_CODE_INVALID_ARGUMENTS, "--class-path", "create", "--files", "some_files"), "Mandatory argument(s) missing: [--name]" ); + + assertContains( + log, + executeCommand(EXIT_CODE_INVALID_ARGUMENTS, "--class-path", "create", "--name", "mysuperapp", "--files", ""), + cliCommandHandler() ? "File name must not be empty" : "Argument --files required" + ); + } + + /** */ + private Set fileNames(Set dirs) { + return dirs.stream().map(Path::getFileName).map(Path::toString).collect(Collectors.toSet()); + } + + /** */ + private Set jars(Path... dirs) { + return Stream.of(dirs).flatMap(dir -> { + try { + return Files.list(dir).filter(p -> p.getFileName().toString().endsWith("jar")); + } + catch (IOException e) { + throw new RuntimeException(e); + } + }).map(Path::toAbsolutePath).collect(Collectors.toSet()); } } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java index 587d5b36c818a..a97726814d945 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/management/classpath/ClassPathCreateCommand.java @@ -32,6 +32,7 @@ import org.apache.ignite.internal.management.api.CommandUtils; import org.apache.ignite.internal.management.api.NativeCommand; import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.util.typedef.internal.A; import org.jetbrains.annotations.Nullable; /** @@ -74,6 +75,8 @@ private void create(@Nullable IgniteClient client, ClassPathCreateCommandArg arg long[] lengths = new long[arg.files.length]; for (int i = 0; i < arg.files.length; i++) { + A.notEmpty(arg.files[i], "File name"); + Path f = Path.of(arg.files[i]); if (!Files.exists(f))