This is an automated email from the ASF dual-hosted git repository.
ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new a4d630779db IGNITE-25473 .NET: Fix compute executor type resolver
compatibility (#5898)
a4d630779db is described below
commit a4d630779dbe635baebf88e49cf3842ebebd71aa
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon May 26 19:45:33 2025 +0300
IGNITE-25473 .NET: Fix compute executor type resolver compatibility (#5898)
* Fix `DeploymentUnitLoader` to substitute current `Apache.Ignite` assembly
as a job dependency, regardless of requested version. **This allows the users
to run .NET jobs compiled against different Ignite versions.**
* Improve error handling in `JobLoadContext`.
* Add compatibility test that compiles Ignite with version 11.22.33,
compiles a Compute job against that, and runs it on current cluster.
---
.../platform/dotnet/DotNetComputeExecutor.java | 21 ++-
.../Compute/Executor/JobGenerator.cs | 30 +++-
.../Compute/PlatformComputeCompatibilityTests.cs | 161 +++++++++++++++++++++
.../Compute/PlatformComputeTests.cs | 6 +-
.../Table/DataStreamerPlatformReceiverTests.cs | 10 +-
.../TestHelpers/AssemblyGenerator.cs | 4 +-
.../TestHelpers/ManagementApi.cs | 15 +-
.../Compute/Executor/DeploymentUnitLoader.cs | 11 ++
.../Internal/Compute/Executor/JobLoadContext.cs | 15 +-
modules/platforms/dotnet/Directory.Build.props | 2 +
10 files changed, 248 insertions(+), 27 deletions(-)
diff --git
a/modules/compute/src/main/java/org/apache/ignite/internal/compute/executor/platform/dotnet/DotNetComputeExecutor.java
b/modules/compute/src/main/java/org/apache/ignite/internal/compute/executor/platform/dotnet/DotNetComputeExecutor.java
index d8903bb5381..a6761cfca66 100644
---
a/modules/compute/src/main/java/org/apache/ignite/internal/compute/executor/platform/dotnet/DotNetComputeExecutor.java
+++
b/modules/compute/src/main/java/org/apache/ignite/internal/compute/executor/platform/dotnet/DotNetComputeExecutor.java
@@ -196,9 +196,7 @@ public class DotNetComputeExecutor {
return fut;
}
- private static Throwable handleTransportError(Process proc, Throwable
cause) {
- Throwable cause0 = unwrapCause(cause);
-
+ private static Throwable handleTransportError(Process proc, @Nullable
Throwable cause) {
String output = getProcessOutputTail(proc, 10_000);
if (proc.isAlive()) {
@@ -206,11 +204,14 @@ public class DotNetComputeExecutor {
proc.destroyForcibly();
}
- if (cause0 instanceof TraceableException) {
- TraceableException te = (TraceableException) cause;
+ if (cause != null) {
+ Throwable cause0 = unwrapCause(cause);
+ if (cause0 instanceof TraceableException) {
+ TraceableException te = (TraceableException) cause;
- if (te.code() == Client.PROTOCOL_COMPATIBILITY_ERR) {
- return cause;
+ if (te.code() == Client.PROTOCOL_COMPATIBILITY_ERR) {
+ return cause;
+ }
}
}
@@ -246,7 +247,11 @@ public class DotNetComputeExecutor {
// 2. Start the process. It connects to the server, passes the id,
and the server knows it is the right one.
String dotnetBinaryPath = DOTNET_BINARY_PATH;
- LOG.debug("Starting .NET executor process [executorId={},
binaryPath={}]", executorId, dotnetBinaryPath);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Starting .NET executor process [executorId={},
binaryPath={}]", executorId, dotnetBinaryPath);
+ }
+
Process proc = startDotNetProcess(transport.serverAddress(),
transport.sslEnabled(), executorId, dotnetBinaryPath);
proc.onExit().thenRun(() -> {
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/JobGenerator.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/JobGenerator.cs
index ba0981171a1..55e4ad921b0 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/JobGenerator.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/JobGenerator.cs
@@ -72,14 +72,39 @@ public static class JobGenerator
}
""");
- public static string EmitJob(TempDir tempDir, string asmName,
[StringSyntax("C#")] string jobCode)
+ public static string EmitGetReferencedIgniteAssemblyJob(TempDir tempDir,
string asmName, string? igniteDllPath = null) =>
+ EmitJob(
+ tempDir,
+ asmName,
+ """
+ public class GetReferencedIgniteAssemblyJob : IComputeJob<string,
string>
+ {
+ public ValueTask<string> ExecuteAsync(IJobExecutionContext
context, string arg, CancellationToken cancellationToken)
+ {
+ foreach (var asm in
Assembly.GetExecutingAssembly().GetReferencedAssemblies())
+ {
+ if (asm.FullName.Contains("Apache.Ignite",
StringComparison.Ordinal))
+ {
+ return ValueTask.FromResult(asm.FullName);
+ }
+ }
+
+ return ValueTask.FromResult(string.Empty);
+ }
+ }
+ """,
+ igniteDllPath);
+
+ public static string EmitJob(TempDir tempDir, string asmName,
[StringSyntax("C#")] string jobCode, string? igniteDllPath = null)
{
var targetFile = Path.Combine(tempDir.Path, $"{asmName}.dll");
+ igniteDllPath ??= typeof(IgniteClient).Assembly.Location;
AssemblyGenerator.EmitClassLib(
targetFile,
$$"""
using System;
+ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Apache.Ignite.Compute;
@@ -88,7 +113,8 @@ public static class JobGenerator
{
{{jobCode}}
}
- """);
+ """,
+ igniteDllPath);
return targetFile;
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeCompatibilityTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeCompatibilityTests.cs
new file mode 100644
index 00000000000..65b8e0161d9
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeCompatibilityTests.cs
@@ -0,0 +1,161 @@
+/*
+ * 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.
+ */
+
+namespace Apache.Ignite.Tests.Compute;
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Executor;
+using Ignite.Compute;
+using NUnit.Framework;
+using TestHelpers;
+
+[Platform("Linux", Reason = "File locking on Windows prevents build.")]
+public class PlatformComputeCompatibilityTests : IgniteTestsBase
+{
+ private const string JobAssemblyName =
nameof(PlatformComputeCompatibilityTests);
+
+ private const string FutureIgniteVersion = "11.22.33";
+
+ private DeploymentUnit _unit;
+
+ [OneTimeSetUp]
+ public async Task UnitDeploy()
+ {
+ using var igniteBuildDir = new TempDir();
+ using var jobBuildDir = new TempDir();
+
+ // Build Ignite with some unlikely future version.
+ BuildIgniteWithVersion(igniteBuildDir.Path, FutureIgniteVersion);
+
+ var jobDllPath = JobGenerator.EmitGetReferencedIgniteAssemblyJob(
+ jobBuildDir,
+ asmName: JobAssemblyName,
+ igniteDllPath: Path.Combine(igniteBuildDir.Path,
"Apache.Ignite.dll"));
+
+ _unit = await
ManagementApi.UnitDeploy($"unit-{JobAssemblyName}-{Guid.NewGuid()}", "1.0.0",
[jobDllPath]);
+ }
+
+ [OneTimeTearDown]
+ public async Task UnitUndeploy() => await
ManagementApi.UnitUndeploy(_unit);
+
+ [Test]
+ public async Task TestDotNetJobCompiledAgainstNewIgniteVersion()
+ {
+ var jobDesc = new JobDescriptor<string, string>(
+ JobClassName: $"TestNamespace.GetReferencedIgniteAssemblyJob,
{JobAssemblyName}",
+ DeploymentUnits: [_unit],
+ Options: new JobExecutionOptions(ExecutorType:
JobExecutorType.DotNetSidecar));
+
+ var nodes = await Client.GetClusterNodesAsync();
+ var target = JobTarget.Node(nodes.Single(x => x.Name ==
ComputeTests.PlatformTestNodeRunner));
+
+ var jobExec = await Client.Compute.SubmitAsync(target, jobDesc,
"test1");
+ var result = await jobExec.GetResultAsync();
+
+ // Verify that the job references a future Ignite version but still
works.
+ StringAssert.StartsWith($"Apache.Ignite,
Version={FutureIgniteVersion}", result);
+ }
+
+ private static void BuildIgniteWithVersion(string targetPath, string
version)
+ {
+ // Copy the Ignite solution and override build props to skip
GitVersioning.
+ var slnDirCopy =
Path.Combine(Directory.GetParent(TestUtils.SolutionDir)!.FullName,
Guid.NewGuid().ToString());
+ using var disposeSln = new DisposeAction(() =>
Directory.Delete(slnDirCopy, true));
+
+ CopyFilesAndDirectories(sourcePath: TestUtils.SolutionDir, targetPath:
slnDirCopy);
+
+ var buildPropsOverride = """
+ <Project>
+ <PropertyGroup>
+ <LangVersion>12</LangVersion>
+
<EnableNETAnalyzers>true</EnableNETAnalyzers>
+ <Nullable>enable</Nullable>
+ <AnalysisMode>None</AnalysisMode>
+ </PropertyGroup>
+ </Project>
+ """;
+
+ File.WriteAllText(Path.Combine(slnDirCopy, "Directory.Build.props"),
buildPropsOverride);
+
+ // Build the solution with the specified version.
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ ArgumentList =
+ {
+ "publish",
+ "-c", "Release",
+ "-o", targetPath,
+ "/p:Version=" + version,
+ "/p:VersionSuffix=" + version
+ },
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WorkingDirectory = Path.Combine(slnDirCopy, "Apache.Ignite"),
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ RedirectStandardInput = true
+ }
+ };
+
+ if (!process.Start())
+ {
+ throw new InvalidOperationException("Failed to start process: " +
process.StartInfo.FileName);
+ }
+
+ var output = GetOutput();
+ Console.WriteLine(output);
+
+ if (!process.WaitForExit(TimeSpan.FromSeconds(120)))
+ {
+ throw new TimeoutException($"Process did not complete in time:
{GetOutput()}");
+ }
+
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException($"Process failed with exit
code {process.ExitCode}: {output}");
+ }
+
+ string GetOutput() => process.StandardOutput.ReadToEnd() +
process.StandardError.ReadToEnd();
+ }
+
+ private static void CopyFilesAndDirectories(string sourcePath, string
targetPath)
+ {
+ foreach (var dir in Directory.GetDirectories(sourcePath, "*",
SearchOption.AllDirectories))
+ {
+ Directory.CreateDirectory(GetTargetPath(dir));
+ }
+
+ foreach (var file in Directory.GetFiles(sourcePath, "*",
SearchOption.AllDirectories))
+ {
+ File.Copy(file, GetTargetPath(file));
+ }
+
+ string GetTargetPath(string path)
+ {
+ var relative = Path.GetRelativePath(sourcePath, path);
+
+ return Path.Combine(targetPath, relative);
+ }
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
index ecc12c6b73f..5626b36e0e3 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
@@ -119,7 +119,8 @@ public class PlatformComputeTests : IgniteTestsBase
var jobExec = await Client.Compute.SubmitAsync(target, desc, "arg");
var ex = Assert.ThrowsAsync<IgniteException>(async () => await
jobExec.GetResultAsync());
- Assert.AreEqual(".NET job failed: Type 'MyNamespace.MyJob' not found
in the specified deployment units.", ex.Message);
+ StringAssert.StartsWith(".NET job failed: Failed to load type
'MyNamespace.MyJob'", ex.Message);
+ StringAssert.Contains("Could not resolve type 'MyNamespace.MyJob' in
assembly 'Apache.Ignite", ex.Message);
Assert.AreEqual("IGN-COMPUTE-9", ex.CodeAsString);
}
@@ -131,7 +132,8 @@ public class PlatformComputeTests : IgniteTestsBase
var jobExec = await Client.Compute.SubmitAsync(target,
DotNetJobs.Echo, "Hello world!");
var ex = Assert.ThrowsAsync<IgniteException>(async () => await
jobExec.GetResultAsync());
- StringAssert.StartsWith(".NET job failed: Could not load file or
assembly 'Apache.Ignite.Tests", ex.Message);
+ StringAssert.StartsWith(".NET job failed: Failed to load type
'Apache.Ignite.Tests.Compute.DotNetJobs+EchoJob", ex.Message);
+ StringAssert.Contains("Could not load file or assembly
'Apache.Ignite.Tests", ex.Message);
Assert.AreEqual("IGN-COMPUTE-9", ex.CodeAsString);
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerPlatformReceiverTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerPlatformReceiverTests.cs
index bfcaa7a090d..2c04fd0435e 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerPlatformReceiverTests.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/DataStreamerPlatformReceiverTests.cs
@@ -110,7 +110,8 @@ public class DataStreamerPlatformReceiverTests :
IgniteTestsBase
receiverArg: "arg");
var ex = Assert.ThrowsAsync<DataStreamerException>(async () => await
resStream.SingleAsync());
- Assert.AreEqual(".NET job failed: Type 'BadClass' not found in the
specified deployment units.", ex.Message);
+ StringAssert.StartsWith(".NET job failed: Failed to load type
'BadClass'", ex.Message);
+ StringAssert.Contains("Could not resolve type 'BadClass' in assembly
'Apache.Ignite", ex.Message);
Assert.AreEqual(1, ex.FailedItems.Count);
}
@@ -134,10 +135,9 @@ public class DataStreamerPlatformReceiverTests :
IgniteTestsBase
var ex = Assert.ThrowsAsync<DataStreamerException>(async () => await
task);
- Assert.AreEqual(
- ".NET job failed: Could not load file or assembly 'BadAssembly,
Culture=neutral, PublicKeyToken=null'. " +
- "The system cannot find the file specified.",
- ex.Message.Trim());
+ StringAssert.Contains(".NET job failed: Failed to load type 'MyClass,
BadAssembly'", ex.Message);
+ StringAssert.Contains("Could not load file or assembly 'BadAssembly",
ex.Message);
+ StringAssert.Contains("The system cannot find the file specified.",
ex.Message);
Assert.AreEqual(1, ex.FailedItems.Count);
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/AssemblyGenerator.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/AssemblyGenerator.cs
index a6c6237556e..6d4b9b177d7 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/AssemblyGenerator.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/AssemblyGenerator.cs
@@ -26,7 +26,7 @@ using Microsoft.CodeAnalysis.CSharp;
internal static class AssemblyGenerator
{
- internal static void EmitClassLib(string targetFile, [StringSyntax("C#")]
string code)
+ internal static void EmitClassLib(string targetFile, [StringSyntax("C#")]
string code, string referencePath)
{
var assemblyName = Path.GetFileNameWithoutExtension(targetFile);
var syntaxTree = CSharpSyntaxTree.ParseText(code);
@@ -38,7 +38,7 @@ internal static class AssemblyGenerator
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(refDir,
"System.dll")),
MetadataReference.CreateFromFile(Path.Combine(refDir,
"System.Runtime.dll")),
-
MetadataReference.CreateFromFile(typeof(IgniteClient).Assembly.Location),
+ MetadataReference.CreateFromFile(referencePath)
};
var compilation = CSharpCompilation.Create(
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
index 905bac05e0c..4c4d3a51782 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
@@ -41,7 +41,7 @@ public static class ManagementApi
PropertyNameCaseInsensitive = true
};
- public static async Task UnitDeploy(string unitId, string unitVersion,
IList<string> unitContent)
+ public static async Task<DeploymentUnit> UnitDeploy(string unitId, string
unitVersion, IList<string> unitContent)
{
// See DeployUnitClient.java
var url = GetUnitUrl(unitId, unitVersion);
@@ -82,14 +82,21 @@ public static class ManagementApi
timeoutMs: 5000,
() => $"Failed to deploy unit {unitId} version {unitVersion}:
{GetUnitStatusString()}");
+ return new DeploymentUnit(unitId, unitVersion);
+
string? GetUnitStatusString() =>
GetUnitStatus(unitId).GetAwaiter().GetResult()?
.SelectMany(x => x.VersionToStatus)
.StringJoin();
}
- public static async Task UnitUndeploy(DeploymentUnit unit)
+ public static async Task UnitUndeploy(DeploymentUnit? unit)
{
+ if (unit == null)
+ {
+ return;
+ }
+
using var client = new HttpClient();
await client.DeleteAsync(GetUnitUrl(unit.Name, unit.Version).Uri);
}
@@ -101,12 +108,10 @@ public static class ManagementApi
var unitId0 = unitId ?? TestContext.CurrentContext.Test.FullName;
var unitVersion0 = unitVersion ??
DateTime.Now.TimeOfDay.ToString(@"m\.s\.f");
- await UnitDeploy(
+ return await UnitDeploy(
unitId: unitId0,
unitVersion: unitVersion0,
unitContent: [testsDll]);
-
- return new DeploymentUnit(unitId0, unitVersion0);
}
private static async Task<DeploymentUnitStatus[]?> GetUnitStatus(string
unitId)
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/DeploymentUnitLoader.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/DeploymentUnitLoader.cs
index 25a6016055f..a2fc69d1bd6 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/DeploymentUnitLoader.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/DeploymentUnitLoader.cs
@@ -26,6 +26,10 @@ using System.Runtime.Loader;
/// </summary>
internal static class DeploymentUnitLoader
{
+ private static readonly Assembly IgniteAssembly =
typeof(DeploymentUnitLoader).Assembly;
+
+ private static readonly string IgniteAssemblyName =
IgniteAssembly.GetName().Name!;
+
/// <summary>
/// Creates a new job load context for the specified deployment unit paths.
/// </summary>
@@ -42,6 +46,13 @@ internal static class DeploymentUnitLoader
private static Assembly? ResolveAssembly(IReadOnlyList<string> paths,
AssemblyName name, AssemblyLoadContext ctx)
{
+ if (name.Name == IgniteAssemblyName)
+ {
+ // Compute job might be built against a different version of
Ignite, so we end up here.
+ // Redirect to the current Ignite assembly.
+ return IgniteAssembly;
+ }
+
foreach (var path in paths)
{
var dllName = $"{name.Name}.dll";
diff --git
a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/JobLoadContext.cs
b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/JobLoadContext.cs
index 786ba9dac00..13ed4c60b6b 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/JobLoadContext.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/JobLoadContext.cs
@@ -74,9 +74,18 @@ internal readonly record struct
JobLoadContext(AssemblyLoadContext AssemblyLoadC
}
}
- private static Type LoadType(string typeName, AssemblyLoadContext ctx) =>
- Type.GetType(typeName, ctx.LoadFromAssemblyName, null)
- ?? throw new InvalidOperationException($"Type '{typeName}' not found
in the specified deployment units.");
+ private static Type LoadType(string typeName, AssemblyLoadContext ctx)
+ {
+ try
+ {
+ return Type.GetType(typeName, ctx.LoadFromAssemblyName, null,
throwOnError: true)
+ ?? throw new InvalidOperationException($"Type '{typeName}'
not found in the specified deployment units.");
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException($"Failed to load type
'{typeName}' from the specified deployment units: {e.Message}", e);
+ }
+ }
// Simple lookup by name. Will throw in a case of ambiguity.
private static Type FindInterface(Type type, Type interfaceType) =>
diff --git a/modules/platforms/dotnet/Directory.Build.props
b/modules/platforms/dotnet/Directory.Build.props
index 3c01c4eb53d..26edc212f56 100644
--- a/modules/platforms/dotnet/Directory.Build.props
+++ b/modules/platforms/dotnet/Directory.Build.props
@@ -41,6 +41,8 @@
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
+
+
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
<ItemGroup>