|
1 |
| -using k8s.Authentication; |
2 |
| -using k8s.Exceptions; |
3 |
| -using k8s.KubeConfigModels; |
4 | 1 | using System;
|
| 2 | +using System.Collections.Concurrent; |
5 | 3 | using System.Collections.Generic;
|
6 | 4 | using System.IO;
|
7 | 5 | using System.IO.Abstractions;
|
|
10 | 8 | using System.Runtime.InteropServices;
|
11 | 9 | using System.Threading;
|
12 | 10 | using System.Threading.Tasks;
|
| 11 | +using FluentAssertions; |
| 12 | +using k8s.Authentication; |
| 13 | +using k8s.Exceptions; |
| 14 | +using k8s.KubeConfigModels; |
13 | 15 | using Xunit;
|
14 | 16 |
|
15 | 17 | namespace k8s.Tests
|
@@ -638,6 +640,65 @@ public void ContextPreferencesExtensionsMergeWithDuplicates()
|
638 | 640 | Assert.Single(cfg.Preferences);
|
639 | 641 | }
|
640 | 642 |
|
| 643 | + [Fact] |
| 644 | + public void LoadKubeConfigShouldBeThreadSafe() |
| 645 | + { |
| 646 | + // This is not guaranteed to fail but it usual throws the follow exception |
| 647 | + // - System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. |
| 648 | + // A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct. |
| 649 | + // at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) |
| 650 | + // at System.Collections.Generic.Dictionary`2.set_Item(TKey key, TValue value) |
| 651 | + // at YamlDotNet.Serialization.ObjectFactories.DefaultObjectFactory.GetStateMethods(Type attributeType, Type valueType) |
| 652 | + // at YamlDotNet.Serialization.ObjectFactories.DefaultObjectFactory.ExecuteState(Type attributeType, Object value) |
| 653 | + // at YamlDotNet.Serialization.ObjectFactories.DefaultObjectFactory.ExecuteOnDeserializing(Object value) |
| 654 | + // at YamlDotNet.Serialization.NodeDeserializers.ObjectNodeDeserializer.Deserialize(IParser parser, Type expectedType, Func`3 nestedObjectDeserializer, Object& value) |
| 655 | + // at YamlDotNet.Serialization.ValueDeserializers.NodeValueDeserializer.DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer)}. |
| 656 | + |
| 657 | + // YamlDotNet Deserializer no longer seems to be thread safe, changing KubernetesYaml to not use a static serializer makes this test pass. |
| 658 | + |
| 659 | + var exceptions = new ConcurrentStack<Exception>(); |
| 660 | + |
| 661 | + // Run it many times for a better failure rate |
| 662 | + Run(exceptions); |
| 663 | + Run(exceptions); |
| 664 | + Run(exceptions); |
| 665 | + Run(exceptions); |
| 666 | + Run(exceptions); |
| 667 | + |
| 668 | + exceptions.Should().HaveCount(0, "No exceptions should have been recorded"); |
| 669 | + |
| 670 | + static void Run(ConcurrentStack<Exception> exceptions) |
| 671 | + { |
| 672 | + var threadCount = 100; |
| 673 | + var threads = new List<Thread>(); |
| 674 | + var control = new SemaphoreSlim(0, threadCount); |
| 675 | + |
| 676 | + for (var i = 0; i < threadCount; i++) |
| 677 | + { |
| 678 | + threads.Add(new Thread(LoadKubeConfig)); |
| 679 | + } |
| 680 | + |
| 681 | + threads.ForEach(t => t.Start()); |
| 682 | + control.Release(threadCount); |
| 683 | + threads.ForEach(t => t.Join()); |
| 684 | + |
| 685 | + void LoadKubeConfig() |
| 686 | + { |
| 687 | + control.Wait(); |
| 688 | + |
| 689 | + try |
| 690 | + { |
| 691 | + var fileInfo = new FileInfo(Path.GetFullPath("assets/kubeconfig.yml")); |
| 692 | + KubernetesClientConfiguration.LoadKubeConfig(new [] { fileInfo }); |
| 693 | + } |
| 694 | + catch (Exception e) |
| 695 | + { |
| 696 | + exceptions.Push(e.InnerException ?? e); |
| 697 | + } |
| 698 | + } |
| 699 | + } |
| 700 | + } |
| 701 | + |
641 | 702 | /// <summary>
|
642 | 703 | /// Ensures Kube config file can be loaded from within a non-default <see cref="SynchronizationContext"/>.
|
643 | 704 | /// The use of <see cref="UIFactAttribute"/> ensures the test is run from within a UI-like <see cref="SynchronizationContext"/>.
|
|
0 commit comments