Skip to content

Commit a24b776

Browse files
YamlDotNot no longer appears to be thread-safe
1 parent f78a516 commit a24b776

File tree

3 files changed

+91
-7
lines changed

3 files changed

+91
-7
lines changed

src/KubernetesClient/KubernetesClient.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
<PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" />
1515
<PackageReference Include="Fractions" Version="7.3.0" />
1616
<PackageReference Include="YamlDotNet" Version="15.1.0" />
17+
<!--Same issue with the latest version of YamlDotNet-->
18+
<!--<PackageReference Include="YamlDotNet" Version="15.1.2" />-->
1719
</ItemGroup>
1820

1921
<ItemGroup>

src/KubernetesClient/KubernetesYaml.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ namespace k8s
1212
/// </summary>
1313
public static class KubernetesYaml
1414
{
15+
//private static DeserializerBuilder GetCommonDeserializerBuilder() =>
16+
// new DeserializerBuilder()
17+
// .WithNamingConvention(CamelCaseNamingConvention.Instance)
18+
// .WithTypeConverter(new IntOrStringYamlConverter())
19+
// .WithTypeConverter(new ByteArrayStringYamlConverter())
20+
// .WithTypeConverter(new ResourceQuantityYamlConverter())
21+
// .WithAttemptingUnquotedStringTypeDeserialization()
22+
// .WithOverridesFromJsonPropertyAttributes();
23+
24+
//private static IDeserializer GetStrictDeserializer() =>
25+
// GetCommonDeserializerBuilder()
26+
// .WithDuplicateKeyChecking()
27+
// .Build();
28+
29+
//private static IDeserializer GetDeserializer() =>
30+
// GetCommonDeserializerBuilder()
31+
// .IgnoreUnmatchedProperties()
32+
// .Build();
33+
34+
//private static IDeserializer GetDeserializer(bool strict) => strict ? GetStrictDeserializer() : GetDeserializer();
35+
1536
private static DeserializerBuilder CommonDeserializerBuilder =>
1637
new DeserializerBuilder()
1738
.WithNamingConvention(CamelCaseNamingConvention.Instance)
@@ -23,12 +44,12 @@ public static class KubernetesYaml
2344

2445
private static readonly IDeserializer StrictDeserializer =
2546
CommonDeserializerBuilder
26-
.WithDuplicateKeyChecking()
27-
.Build();
47+
.WithDuplicateKeyChecking()
48+
.Build();
2849
private static readonly IDeserializer Deserializer =
2950
CommonDeserializerBuilder
30-
.IgnoreUnmatchedProperties()
31-
.Build();
51+
.IgnoreUnmatchedProperties()
52+
.Build();
3253
private static IDeserializer GetDeserializer(bool strict) => strict ? StrictDeserializer : Deserializer;
3354

3455
private static readonly IValueSerializer Serializer =

tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
using k8s.Authentication;
2-
using k8s.Exceptions;
3-
using k8s.KubeConfigModels;
41
using System;
2+
using System.Collections.Concurrent;
53
using System.Collections.Generic;
64
using System.IO;
75
using System.IO.Abstractions;
@@ -10,6 +8,10 @@
108
using System.Runtime.InteropServices;
119
using System.Threading;
1210
using System.Threading.Tasks;
11+
using FluentAssertions;
12+
using k8s.Authentication;
13+
using k8s.Exceptions;
14+
using k8s.KubeConfigModels;
1315
using Xunit;
1416

1517
namespace k8s.Tests
@@ -638,6 +640,65 @@ public void ContextPreferencesExtensionsMergeWithDuplicates()
638640
Assert.Single(cfg.Preferences);
639641
}
640642

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+
641702
/// <summary>
642703
/// Ensures Kube config file can be loaded from within a non-default <see cref="SynchronizationContext"/>.
643704
/// The use of <see cref="UIFactAttribute"/> ensures the test is run from within a UI-like <see cref="SynchronizationContext"/>.

0 commit comments

Comments
 (0)