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) ??

Reply via email to