IGNITE-13592 .NET: Respect cgroup limits in DataRegionConfiguration.DefaultMaxSize

* Read cgroup limits to determine available memory for the current process
* Cache retrieved values in static fields
* Tweak `IgniteConfigurationTest.TestSpringXml` to account for the fact that older JDK versions ignore cgroup limits
* Skip `MaxSize` property check in `IgnitionStartTest.TestIgniteStartsFromSpringXml`
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
index d3e8f46..0f67ed4 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
@@ -209,6 +209,7 @@
     <Compile Include="Client\Services\TestServiceGenericMethods.cs" />
     <Compile Include="Client\Services\TestServiceOverloads.cs" />
     <Compile Include="Common\IgniteProductVersionTests.cs" />
+    <Compile Include="Common\MemoryInfoTest.cs" />
     <Compile Include="Compute\ComputeApiTest.JavaTask.cs" />
     <Compile Include="Compute\ComputeWithExecutorTest.cs" />
     <Compile Include="Deployment\CacheGetFunc.cs" />
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Common/MemoryInfoTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Common/MemoryInfoTest.cs
new file mode 100644
index 0000000..3f2052b
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Common/MemoryInfoTest.cs
@@ -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.
+ */
+
+namespace Apache.Ignite.Core.Tests.Common
+{
+    using Apache.Ignite.Core.Impl;
+    using Apache.Ignite.Core.Impl.Common;
+    using Apache.Ignite.Core.Impl.Unmanaged;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests for <see cref="MemoryInfo"/>.
+    /// </summary>
+    public class MemoryInfoTest
+    {
+        /// <summary>
+        /// Tests that cgroup limit can be always determined on Linux.
+        /// </summary>
+        [Test]
+        public void TestMemoryInfoReturnsNonNullLimitOnLinux()
+        {
+            if (Os.IsWindows)
+            {
+                return;
+            }
+
+            Assert.IsNotNull(MemoryInfo.TotalPhysicalMemory);
+            Assert.Greater(MemoryInfo.TotalPhysicalMemory, 655360);
+
+            Assert.IsNotNull(MemoryInfo.MemoryLimit);
+            Assert.Greater(MemoryInfo.MemoryLimit, 655360);
+
+            Assert.IsNotNull(CGroup.MemoryLimitInBytes);
+            Assert.Greater(CGroup.MemoryLimitInBytes, 655360);
+
+            if (CGroup.MemoryLimitInBytes > MemoryInfo.TotalPhysicalMemory)
+            {
+                Assert.AreEqual(MemoryInfo.TotalPhysicalMemory, MemoryInfo.MemoryLimit,
+                    "When cgroup limit is not set, memory limit is equal to physical memory amount.");
+            }
+            else
+            {
+                Assert.AreEqual(CGroup.MemoryLimitInBytes, MemoryInfo.MemoryLimit,
+                    "When cgroup limit is set, memory limit is equal to cgroup limit.");
+            }
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs
index b4bc0fe..e043b7e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgniteConfigurationTest.cs
@@ -40,6 +40,7 @@
     using Apache.Ignite.Core.Discovery.Tcp.Static;
     using Apache.Ignite.Core.Encryption.Keystore;
     using Apache.Ignite.Core.Events;
+    using Apache.Ignite.Core.Impl;
     using Apache.Ignite.Core.Impl.Common;
     using Apache.Ignite.Core.PersistentStore;
     using Apache.Ignite.Core.Tests.Plugin;
@@ -611,11 +612,24 @@
             Assert.AreEqual(DataRegionConfiguration.DefaultEmptyPagesPoolSize, cfg.EmptyPagesPoolSize);
             Assert.AreEqual(DataRegionConfiguration.DefaultEvictionThreshold, cfg.EvictionThreshold);
             Assert.AreEqual(DataRegionConfiguration.DefaultInitialSize, cfg.InitialSize);
-            Assert.AreEqual(DataRegionConfiguration.DefaultMaxSize, cfg.MaxSize);
             Assert.AreEqual(DataRegionConfiguration.DefaultPersistenceEnabled, cfg.PersistenceEnabled);
             Assert.AreEqual(DataRegionConfiguration.DefaultMetricsRateTimeInterval, cfg.MetricsRateTimeInterval);
             Assert.AreEqual(DataRegionConfiguration.DefaultMetricsSubIntervalCount, cfg.MetricsSubIntervalCount);
             Assert.AreEqual(default(long), cfg.CheckpointPageBufferSize);
+
+            if (DataRegionConfiguration.DefaultMaxSize != cfg.MaxSize)
+            {
+                // Java respects cgroup limits only in recent JDK versions.
+                // We don't know which version is used for tests, so we should expect both variants
+                var physMem = MemoryInfo.TotalPhysicalMemory;
+                Assert.IsNotNull(physMem);
+
+                var expected = (long) physMem / 5;
+
+                Assert.AreEqual(expected, cfg.MaxSize,
+                    string.Format("Expected max size with cgroup limit: '{0}', without: '{1}'",
+                        DataRegionConfiguration.DefaultMaxSize, expected));
+            }
         }
 
         /// <summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs
index 7bcf32c..a0dfef1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/IgnitionStartTest.cs
@@ -18,6 +18,7 @@
 namespace Apache.Ignite.Core.Tests
 {
     using System;
+    using System.Collections.Generic;
     using System.IO;
     using System.Linq;
     using Apache.Ignite.Core.Configuration;
@@ -115,7 +116,9 @@
                         Name = "default"
                     }
                 };
-                AssertExtensions.ReflectionEqual(dsCfg, resCfg.DataStorageConfiguration);
+
+                AssertExtensions.ReflectionEqual(dsCfg, resCfg.DataStorageConfiguration,
+                    ignoredProperties: new HashSet<string> {"MaxSize"});
             }
         }
     }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj b/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
index 76b77fe..99bf6fd 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Apache.Ignite.Core.csproj
@@ -132,6 +132,7 @@
     <Compile Include="Impl\Client\Services\ServicesClient.cs" />
     <Compile Include="Impl\Client\SocketEndpoint.cs" />
     <Compile Include="Impl\Client\Transactions\ClientCacheTransactionManager.cs" />
+    <Compile Include="Impl\Common\Cgroup.cs" />
     <Compile Include="Impl\Common\PlatformType.cs" />
     <Compile Include="Impl\Client\Transactions\TransactionClient.cs" />
     <Compile Include="Impl\Client\Transactions\TransactionsClient.cs" />
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/MemoryPolicyConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/MemoryPolicyConfiguration.cs
index 0eedd48..00e6647 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/MemoryPolicyConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Cache/Configuration/MemoryPolicyConfiguration.cs
@@ -51,7 +51,7 @@
         /// The default maximum size, equals to 20% of total RAM.
         /// </summary>
         public static readonly long DefaultMaxSize =
-            (long) ((long) MemoryInfo.GetTotalPhysicalMemory(2048L * 1024 * 1024) * 0.2);
+            (long) ((long) (MemoryInfo.MemoryLimit ?? 2048L * 1024 * 1024) * 0.2);
 
         /// <summary>
         /// The default sub intervals.
@@ -187,14 +187,14 @@
         public TimeSpan RateTimeInterval { get; set; }
 
         /// <summary>
-        /// Gets or sets the number of sub intervals to split <see cref="RateTimeInterval"/> into to calculate 
+        /// Gets or sets the number of sub intervals to split <see cref="RateTimeInterval"/> into to calculate
         /// <see cref="IMemoryMetrics.AllocationRate"/> and <see cref="IMemoryMetrics.EvictionRate"/>.
         /// <para />
         /// Bigger value results in more accurate metrics.
         /// </summary>
         [DefaultValue(DefaultSubIntervals)]
-        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", 
+        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly",
             Justification = "Consistency with Java config")]
         public int SubIntervals { get; set; }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataRegionConfiguration.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataRegionConfiguration.cs
index 35417a7..c50e8a1 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataRegionConfiguration.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Configuration/DataRegionConfiguration.cs
@@ -27,7 +27,7 @@
 
     /// <summary>
     /// Defines custom data region configuration for Apache Ignite page memory
-    /// (see <see cref="DataStorageConfiguration"/>). 
+    /// (see <see cref="DataStorageConfiguration"/>).
     /// <para />
     /// For each configured data region Apache Ignite instantiates respective memory regions with different
     /// parameters like maximum size, eviction policy, swapping options, etc.
@@ -60,7 +60,7 @@
         /// The default maximum size, equals to 20% of total RAM.
         /// </summary>
         public static readonly long DefaultMaxSize =
-            (long)((long)MemoryInfo.GetTotalPhysicalMemory(2048L * 1024 * 1024) * 0.2);
+            (long)((long) (MemoryInfo.MemoryLimit ?? 2048L * 1024 * 1024) * 0.2);
 
         /// <summary>
         /// The default sub intervals.
@@ -171,7 +171,7 @@
 
         /// <summary>
         /// Gets or sets the page eviction mode. If <see cref="DataPageEvictionMode.Disabled"/> is used (default)
-        /// then an out of memory exception will be thrown if the memory region usage 
+        /// then an out of memory exception will be thrown if the memory region usage
         /// goes beyond <see cref="MaxSize"/>.
         /// </summary>
         public DataPageEvictionMode PageEvictionMode { get; set; }
@@ -212,7 +212,7 @@
         public TimeSpan MetricsRateTimeInterval { get; set; }
 
         /// <summary>
-        /// Gets or sets the number of sub intervals to split <see cref="MetricsRateTimeInterval"/> into to calculate 
+        /// Gets or sets the number of sub intervals to split <see cref="MetricsRateTimeInterval"/> into to calculate
         /// <see cref="IDataRegionMetrics.AllocationRate"/> and <see cref="IDataRegionMetrics.EvictionRate"/>.
         /// <para />
         /// Bigger value results in more accurate metrics.
@@ -228,7 +228,7 @@
         /// Default is <c>0</c>: Ignite will choose buffer size automatically.
         /// </summary>
         public long CheckpointPageBufferSize { get; set; }
-        
+
         /// <summary>
         /// Gets or sets the lazy memory allocation flag.
         /// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/Cgroup.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/Cgroup.cs
new file mode 100644
index 0000000..b797b09
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/Common/Cgroup.cs
@@ -0,0 +1,174 @@
+/*
+ * 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.Core.Impl.Common
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Diagnostics.CodeAnalysis;
+    using System.IO;
+    using System.Linq;
+    using Apache.Ignite.Core.Impl.Unmanaged;
+
+    /// <summary>
+    /// Reads cgroup limits for the current process.
+    /// <para />
+    /// Based on cgroup handling in CLR:
+    /// https://github.com/dotnet/runtime/blob/master/src/coreclr/src/gc/unix/cgroup.cpp
+    /// </summary>
+    internal static class CGroup
+    {
+        /// <summary>
+        /// Gets cgroup memory limit in bytes.
+        /// </summary>
+        public static readonly ulong? MemoryLimitInBytes = GetMemoryLimitInBytes();
+
+        /** */
+        private const string MemorySubsystem = "memory";
+
+        /** */
+        private const string MemoryLimitFileName = "memory.limit_in_bytes";
+
+        /** */
+        private const string ProcMountInfoFileName = "/proc/self/mountinfo";
+
+        /** */
+        private const string ProcCGroupFileName = "/proc/self/cgroup";
+
+        /// <summary>
+        /// Gets memory limit in bytes.
+        /// </summary>
+        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
+        private static ulong? GetMemoryLimitInBytes()
+        {
+            if (Os.IsWindows)
+            {
+                return null;
+            }
+
+            try
+            {
+                var memMount = FindHierarchyMount(MemorySubsystem);
+                if (memMount == null)
+                {
+                    return null;
+                }
+
+                var cgroupPathRelativeToMount = FindCGroupPath(MemorySubsystem);
+                if (cgroupPathRelativeToMount == null)
+                {
+                    return null;
+                }
+
+                var hierarchyMount = memMount.Value.Key;
+                var hierarchyRoot = memMount.Value.Value;
+
+                // Host CGroup: append the relative path
+                // In Docker: root and relative path are the same
+                var groupPath =
+                    string.Equals(hierarchyRoot, cgroupPathRelativeToMount, StringComparison.Ordinal)
+                        ? hierarchyMount
+                        : hierarchyMount + cgroupPathRelativeToMount;
+
+                var memLimitFilePath = Path.Combine(groupPath, MemoryLimitFileName);
+
+                var memLimitText = File.ReadAllText(memLimitFilePath);
+
+                ulong memLimit;
+                if (ulong.TryParse(memLimitText, out memLimit))
+                {
+                    return memLimit;
+                }
+            }
+            catch (Exception e)
+            {
+                Console.Error.WriteLine("Failed to determine cgroup memory limit: " + e);
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Finds the hierarchy mount and root for the current process.
+        /// </summary>
+        private static KeyValuePair<string, string>? FindHierarchyMount(string subsystem)
+        {
+            foreach (var line in File.ReadAllLines(ProcMountInfoFileName))
+            {
+                var mount = GetHierarchyMount(line, subsystem);
+
+                if (mount != null)
+                {
+                    return mount;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Get the hierarchy mount and root.
+        /// </summary>
+        private static KeyValuePair<string, string>? GetHierarchyMount(string mountInfo, string subsystem)
+        {
+            // Example: 41 34 0:35 / /sys/fs/cgroup/memory rw,nosuid,nodev shared:17 - cgroup cgroup rw,memory
+            const string cGroup = " - cgroup ";
+
+            var cgroupIdx = mountInfo.IndexOf(cGroup, StringComparison.Ordinal);
+
+            if (cgroupIdx < 0)
+                return null;
+
+            var optionsIdx = mountInfo.LastIndexOf(" ", cgroupIdx + cGroup.Length, StringComparison.Ordinal);
+
+            if (optionsIdx < 0)
+                return null;
+
+            var memIdx = mountInfo.IndexOf(subsystem, optionsIdx + 1, StringComparison.Ordinal);
+
+            if (memIdx < 0)
+                return null;
+
+            var parts = mountInfo.Split(' ');
+
+            if (parts.Length < 5)
+                return null;
+
+            return new KeyValuePair<string, string>(parts[4], parts[3]);
+        }
+
+        /// <summary>
+        /// Finds the cgroup path for the current process.
+        /// </summary>
+        private static string FindCGroupPath(string subsystem)
+        {
+            var lines = File.ReadAllLines(ProcCGroupFileName);
+
+            foreach (var line in lines)
+            {
+                var parts = line.Split(new[] {':'}, 3);
+
+                if (parts.Length == 3 && parts[1].Split(',').Contains(subsystem, StringComparer.Ordinal))
+                {
+                    return parts[2];
+                }
+            }
+
+            return null;
+        }
+    }
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/MemoryInfo.cs b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/MemoryInfo.cs
index 7cfd019..062065e 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core/Impl/MemoryInfo.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core/Impl/MemoryInfo.cs
@@ -17,11 +17,13 @@
 
 namespace Apache.Ignite.Core.Impl
 {
+    using System;
     using System.Diagnostics.CodeAnalysis;
     using System.IO;
     using System.Linq;
     using System.Runtime.InteropServices;
     using System.Text.RegularExpressions;
+    using Apache.Ignite.Core.Impl.Common;
     using Apache.Ignite.Core.Impl.Unmanaged;
 
     /// <summary>
@@ -30,19 +32,56 @@
     internal static class MemoryInfo
     {
         /// <summary>
-        /// Gets the total physical memory.
+        /// Gets total physical memory.
         /// </summary>
-        public static ulong GetTotalPhysicalMemory(ulong defaultValue)
+        public static readonly ulong? TotalPhysicalMemory = GetTotalPhysicalMemory();
+
+        /// <summary>
+        /// Gets memory limit (when set by cgroups) or the value of <see cref="TotalPhysicalMemory"/>.
+        /// </summary>
+        public static readonly ulong? MemoryLimit = GetMemoryLimit();
+
+        /// <summary>
+        /// Gets the memory limit.
+        /// <para />
+        /// When memory is limited with cgroups, returns that limit. Otherwise, returns total physical memory.
+        /// </summary>
+        private static ulong? GetMemoryLimit()
         {
             if (Os.IsWindows)
             {
-                return NativeMethodsWindows.GlobalMemoryStatusExTotalPhys();
+                return null;
             }
 
-            const string memInfo = "/proc/meminfo";
+            var physical = TotalPhysicalMemory;
 
-            if (File.Exists(memInfo))
+            if (physical == null)
             {
+                return null;
+            }
+
+            var limit = CGroup.MemoryLimitInBytes;
+
+            return limit != null && limit < physical
+                ? limit.Value
+                : physical.Value;
+        }
+
+        /// <summary>
+        /// Gets total physical memory.
+        /// </summary>
+        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
+        private static ulong? GetTotalPhysicalMemory()
+        {
+            try
+            {
+                if (Os.IsWindows)
+                {
+                    return NativeMethodsWindows.GlobalMemoryStatusExTotalPhys();
+                }
+
+                const string memInfo = "/proc/meminfo";
+
                 var kbytes = File.ReadAllLines(memInfo).Select(x => Regex.Match(x, @"MemTotal:\s+([0-9]+) kB"))
                     .Where(x => x.Success)
                     .Select(x => x.Groups[1].Value).FirstOrDefault();
@@ -52,10 +91,15 @@
                     return ulong.Parse(kbytes) * 1024;
                 }
             }
+            catch (Exception e)
+            {
+                Console.Error.WriteLine("Failed to determine physical memory size: " + e);
+            }
 
-            return defaultValue;
+            return null;
         }
 
+
         /// <summary>
         /// Native methods.
         /// </summary>
@@ -107,4 +151,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/modules/platforms/dotnet/Apache.Ignite.DotNetCore.sln.DotSettings b/modules/platforms/dotnet/Apache.Ignite.DotNetCore.sln.DotSettings
index 3bbc7bd..59bf2e5 100644
--- a/modules/platforms/dotnet/Apache.Ignite.DotNetCore.sln.DotSettings
+++ b/modules/platforms/dotnet/Apache.Ignite.DotNetCore.sln.DotSettings
@@ -8,6 +8,7 @@
 	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertClosureToMethodGroup/@EntryIndexedValue">DO_NOT_SHOW</s:String>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EXml_002ECodeStyle_002EFormatSettingsUpgrade_002EXmlMoveToCommonFormatterSettingsUpgrade/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/UnitTesting/ShadowCopy/@EntryValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=cgroup/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=failover/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Multithreaded/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Reentrancy/@EntryIndexedValue">True</s:Boolean>