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 00e6e7337be IGNITE-25437 .NET: Improve exception when job assembly
requires higher runtime version (#6979)
00e6e7337be is described below
commit 00e6e7337be74334456d655c985fc6b9d4e9d3ef
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon Nov 17 08:13:18 2025 +0200
IGNITE-25437 .NET: Improve exception when job assembly requires higher
runtime version (#6979)
---
.../Apache.Ignite.Tests/Apache.Ignite.Tests.csproj | 10 +++++
.../Apache.Ignite.Tests/Compute/DotNetJobs.cs | 18 +++++++++
.../Compute/Executor/DeploymentUnitLoaderTests.cs | 23 +++++++++++
.../Compute/Executor/NewerDotnetJobs/EchoJob.cs | 24 +++++++++++
.../NewerDotnetJobs/NewerDotnetJobs.csproj | 13 ++++++
.../Executor/NewerDotnetJobs/NewerDotnetJobs.dll | Bin 0 -> 6144 bytes
.../Compute/Executor/NewerDotnetJobs/global.json | 6 +++
.../Compute/PlatformComputeTests.cs | 13 ++++++
.../TestHelpers/ManagementApi.cs | 5 ++-
.../Internal/Compute/Executor/JobLoadContext.cs | 44 +++++++++++++++++++++
10 files changed, 155 insertions(+), 1 deletion(-)
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj
b/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj
index c5d41ea8428..7160fda0c38 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Apache.Ignite.Tests.csproj
@@ -59,4 +59,14 @@
</AssemblyAttribute>
</ItemGroup>
+ <ItemGroup>
+ <Compile Remove="Compute\Executor\NewerDotnetJobs\EchoJob.cs" />
+ <Content Include="Compute\Executor\NewerDotnetJobs\EchoJob.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Remove="Compute\Executor\NewerDotnetJobs\NewerDotnetJobs.dll" />
+ <EmbeddedResource
Include="Compute\Executor\NewerDotnetJobs\NewerDotnetJobs.dll" />
+ </ItemGroup>
+
</Project>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/DotNetJobs.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/DotNetJobs.cs
index 7e35dca2573..435debadc3b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/DotNetJobs.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/DotNetJobs.cs
@@ -19,7 +19,9 @@ namespace Apache.Ignite.Tests.Compute;
using System;
using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Linq;
+using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
@@ -37,6 +39,22 @@ public static class DotNetJobs
public static readonly JobDescriptor<object?, object?> ProcessExit =
JobDescriptor.Of(new ProcessExitJob());
public static readonly JobDescriptor<string, string> ApiTest =
new(typeof(ApiTestJob));
public static readonly JobDescriptor<object?, int>
AssemblyLoadContextCount = JobDescriptor.Of(new AssemblyLoadContextCountJob());
+ public static readonly JobDescriptor<string, string> NewerDotNetJob = new(
+ JobClassName: "NewerDotnetJobs.EchoJob, NewerDotnetJobs",
+ Options: new JobExecutionOptions(ExecutorType:
JobExecutorType.DotNetSidecar));
+
+ public static async Task<string> WriteNewerDotnetJobsAssembly(string
tempDirPath, string asmName)
+ {
+ var targetFile = Path.Combine(tempDirPath, asmName + ".dll");
+
+ await using var fileStream = File.Create(targetFile);
+
+ await Assembly.GetExecutingAssembly()
+
.GetManifestResourceStream("Apache.Ignite.Tests.Compute.Executor.NewerDotnetJobs.NewerDotnetJobs.dll")!
+ .CopyToAsync(fileStream);
+
+ return targetFile;
+ }
public class AddOneJob : IComputeJob<int, int>
{
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/DeploymentUnitLoaderTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/DeploymentUnitLoaderTests.cs
index cbbd82801ed..672997810a0 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/DeploymentUnitLoaderTests.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/DeploymentUnitLoaderTests.cs
@@ -17,6 +17,9 @@
namespace Apache.Ignite.Tests.Compute.Executor;
+using System;
+using System.IO;
+using System.Reflection;
using System.Threading.Tasks;
using Internal.Compute.Executor;
using NUnit.Framework;
@@ -119,4 +122,24 @@ public class DeploymentUnitLoaderTests
Assert.AreEqual("Res1", res1);
Assert.AreEqual("Res2", res2);
}
+
+ [Test]
+ public async Task TestNewerDotnetVersionAssembly()
+ {
+ using var tempDir = new TempDir();
+ var asmName = "NewerDotnetJobs";
+ await DotNetJobs.WriteNewerDotnetJobsAssembly(tempDir.Path, asmName);
+
+ using JobLoadContext jobCtx =
DeploymentUnitLoader.GetJobLoadContext(new DeploymentUnitPaths([tempDir.Path]));
+
+ var ex = Assert.Throws<InvalidOperationException>(() =>
jobCtx.CreateJobWrapper($"NewerDotnetJobs.EchoJob, {asmName}"));
+
+ var expectedMessage =
+ "Failed to load type 'NewerDotnetJobs.EchoJob, NewerDotnetJobs'
because it depends on a newer .NET runtime version " +
+ "(required: 10, current: 8, missing assembly: " +
+ "System.Runtime, Version=10.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a). " +
+ "Either target .NET 8 when building the job assembly, or use .NET
10 on servers to run the job executor.";
+
+ Assert.AreEqual(expectedMessage, ex!.Message);
+ }
}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/EchoJob.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/EchoJob.cs
new file mode 100644
index 00000000000..f99ade0a26f
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/EchoJob.cs
@@ -0,0 +1,24 @@
+// 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 NewerDotnetJobs;
+
+using Apache.Ignite.Compute;
+
+public class EchoJob : IComputeJob<string, string>
+{
+ public ValueTask<string> ExecuteAsync(IJobExecutionContext context, string
arg, CancellationToken cancellationToken)
+ => ValueTask.FromResult(arg);
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/NewerDotnetJobs.csproj
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/NewerDotnetJobs.csproj
new file mode 100644
index 00000000000..1019484736a
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/NewerDotnetJobs.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Apache.Ignite" Version="3.1.0" />
+ </ItemGroup>
+
+</Project>
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/NewerDotnetJobs.dll
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/NewerDotnetJobs.dll
new file mode 100644
index 00000000000..1864e328116
Binary files /dev/null and
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/NewerDotnetJobs.dll
differ
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/global.json
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/global.json
new file mode 100644
index 00000000000..68d4fbea75d
--- /dev/null
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/Executor/NewerDotnetJobs/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestMinor"
+ }
+}
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
index 5626b36e0e3..90091a9da72 100644
---
a/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
+++
b/modules/platforms/dotnet/Apache.Ignite.Tests/Compute/PlatformComputeTests.cs
@@ -287,6 +287,19 @@ public class PlatformComputeTests : IgniteTestsBase
Assert.AreEqual("Job executor type 'DotNetSidecar' is not supported by
the server.", ex.Message);
}
+ [Test]
+ public void TestNewerDotnetVersionAssembly()
+ {
+ var ex = Assert.ThrowsAsync<IgniteException>(async() => await
ExecJobAsync(DotNetJobs.NewerDotNetJob, "test"));
+
+ StringAssert.StartsWith(
+ ".NET job failed: Failed to load type 'NewerDotnetJobs.EchoJob,
NewerDotnetJobs' " +
+ "because it depends on a newer .NET runtime version (required: 10,
current: 8",
+ ex.Message);
+
+ Assert.AreEqual("IGN-COMPUTE-9", ex.CodeAsString);
+ }
+
private async Task<IClusterNode> GetClusterNodeAsync(string? suffix = null)
{
var nodeName = ComputeTests.PlatformTestNodeRunner + suffix;
diff --git
a/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
b/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
index da584fb2758..05e052f7a0b 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/TestHelpers/ManagementApi.cs
@@ -26,6 +26,7 @@ using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Apache.Ignite.Compute;
+using Compute;
using Internal.Common;
using NUnit.Framework;
@@ -103,7 +104,9 @@ public static class ManagementApi
public static async Task<DeploymentUnit> DeployTestsAssembly(string?
unitId = null, string? unitVersion = null)
{
+ using var tempDir = new TempDir();
var testsDll = typeof(ManagementApi).Assembly.Location;
+ var newerDotNetDll = await
DotNetJobs.WriteNewerDotnetJobsAssembly(tempDir.Path, "NewerDotnetJobs");
var unitId0 = unitId ?? TestContext.CurrentContext.Test.FullName;
var unitVersion0 = unitVersion ?? GetRandomUnitVersion();
@@ -111,7 +114,7 @@ public static class ManagementApi
return await UnitDeploy(
unitId: unitId0,
unitVersion: unitVersion0,
- unitContent: [testsDll]);
+ unitContent: [testsDll, newerDotNetDll]);
}
public static string GetRandomUnitVersion() =>
DateTime.Now.TimeOfDay.ToString(@"m\.s\.f");
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 bfd625a5ba9..65ae7c79a2d 100644
---
a/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/JobLoadContext.cs
+++
b/modules/platforms/dotnet/Apache.Ignite/Internal/Compute/Executor/JobLoadContext.cs
@@ -19,6 +19,7 @@ namespace Apache.Ignite.Internal.Compute.Executor;
using System;
using System.Collections.Concurrent;
+using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Ignite.Compute;
@@ -110,10 +111,53 @@ internal readonly record struct
JobLoadContext(AssemblyLoadContext AssemblyLoadC
}
catch (Exception e)
{
+ if (e is FileNotFoundException fe)
+ {
+ CheckRuntimeVersions(typeName, fe.FileName);
+ }
+
throw new InvalidOperationException($"Failed to load type
'{typeName}' from the specified deployment units: {e.Message}", e);
}
}
+ private static void CheckRuntimeVersions(string typeName, string? fileName)
+ {
+ if (fileName == null || !fileName.StartsWith("System.",
StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ // System assembly failed to load - potentially due to runtime version
mismatch.
+ if (TryParseAssemblyName(fileName) is not { } assemblyName)
+ {
+ return;
+ }
+
+ int? requestedRuntimeVersion = assemblyName.Version?.Major;
+ int? currentRuntimeVersion =
typeof(object).Assembly.GetName().Version?.Major;
+
+ if (requestedRuntimeVersion > currentRuntimeVersion)
+ {
+ throw new InvalidOperationException(
+ $"Failed to load type '{typeName}' because it depends on a
newer .NET runtime version " +
+ $"(required: {requestedRuntimeVersion}, current:
{currentRuntimeVersion}, missing assembly: {assemblyName}). " +
+ $"Either target .NET {currentRuntimeVersion} when building the
job assembly, " +
+ $"or use .NET {requestedRuntimeVersion} on servers to run the
job executor.");
+ }
+ }
+
+ private static AssemblyName? TryParseAssemblyName(string assemblyName)
+ {
+ try
+ {
+ return new AssemblyName(assemblyName);
+ }
+ catch (FileLoadException)
+ {
+ return null;
+ }
+ }
+
// Simple lookup by name. Will throw in a case of ambiguity.
private static Type FindInterface(Type type, Type interfaceType) =>
type.GetInterface(interfaceType.Name, ignoreCase: false) ??