Threading tests to help enforce #1519 .
Includes a bunch of little fixes and reducing failure list.

Tests don't aggressively assert yes, too many failures. But we can at
least run and drive to 0.

---------

Co-authored-by: Mike Stall <jmstall@microsoft.com>
This commit is contained in:
Mike Stall 2023-05-19 16:48:21 -07:00 коммит произвёл GitHub
Родитель e405f64a8e
Коммит 024e6bcc36
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 217 добавлений и 30 удалений

Просмотреть файл

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using System;
namespace Microsoft.PowerFx
{
/// <summary>
/// This type is Not thread safe - even if the base class is marked as thread safe.
/// This can be used to mark a known type as single-threaded.
/// Or cases when the derived type is not thread safe, but the base type is.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)]
internal class NotThreadSafeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="NotThreadSafeAttribute"/> class.
/// </summary>
public NotThreadSafeAttribute()
{
}
}
}

Просмотреть файл

@ -8,6 +8,7 @@ using Microsoft.PowerFx.Syntax;
namespace Microsoft.PowerFx.Core.Entities
{
[ThreadSafeImmutable]
internal interface IExternalEntityScope
{
bool TryGetNamedEnum(DName identName, out DType enumType);

Просмотреть файл

@ -289,7 +289,7 @@ namespace Microsoft.PowerFx.Core.Functions
return _signatureConstraint;
}
protected set => _signatureConstraint = value;
protected init => _signatureConstraint = value;
}
/// <summary>

Просмотреть файл

@ -45,6 +45,7 @@ namespace Microsoft.PowerFx
// We can have multiple threads reading; which means they may be populating the cache.
private readonly object _lock = new object();
[ThreadSafeProtectedByLock(nameof(_lock))]
private readonly SlotMap<NameLookupInfo?> _slots = new SlotMap<NameLookupInfo?>();
// Override lookup.

Просмотреть файл

@ -22,6 +22,7 @@ namespace Microsoft.PowerFx
/// This is a publicly facing class around a <see cref="INameResolver"/>.
/// </summary>
[DebuggerDisplay("{DebugName}")]
[NotThreadSafe]
public class SymbolTable : ReadOnlySymbolTable, IGlobalSymbolNameResolver
{
private readonly SlotMap<NameLookupInfo?> _slots = new SlotMap<NameLookupInfo?>();

Просмотреть файл

@ -23,6 +23,7 @@ namespace Microsoft.PowerFx
// Mapping between slots and logical names on RecordType.
// name --> Slot; used at design time to ensure same slot per field.
[ThreadSafeProtectedByLock("_map")]
private readonly Dictionary<string, NameSymbol> _map = new Dictionary<string, NameSymbol>();
internal RecordType Type => _type;

Просмотреть файл

@ -11,6 +11,7 @@ using Microsoft.PowerFx.Core.Utils;
namespace Microsoft.PowerFx.Core
{
[ThreadSafeImmutable]
public abstract class DisplayNameProvider
{
public abstract bool TryGetLogicalName(DName displayName, out DName logicalName);

Просмотреть файл

@ -17,6 +17,7 @@ namespace Microsoft.PowerFx.Core.Types
/// Wrapper class, provides access to derived TryGetFieldType, as well as identity of lazy types via AggregateType.
/// Also provides faciility to fully expand a level of the type when needed by operations such as Union/AddColumns/...
/// </summary>
[ThreadSafeImmutable]
internal sealed class LazyTypeProvider
{
private readonly ConcurrentDictionary<DName, DType> _expandedFields = new ();

Просмотреть файл

@ -14,6 +14,7 @@ using Microsoft.PowerFx.Types;
namespace Microsoft.PowerFx.Core
{
[NotThreadSafe]
internal class DefinedTypeSymbolTable : TypeSymbolTable, IGlobalSymbolNameResolver
{
private readonly BidirectionalDictionary<string, FormulaType> _definedTypes = new ();

Просмотреть файл

@ -9,6 +9,7 @@ namespace Microsoft.PowerFx.Core.Types
{
// Represents a (simple) name together with an DType.
// TASK: 67008 - Make this public, or expose a public shim in Document.
[ThreadSafeImmutable]
internal struct TypedName : IEquatable<TypedName>, ICheckable
{
public readonly DName Name;

Просмотреть файл

@ -2,10 +2,14 @@
// Licensed under the MIT license.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.PowerFx.Core.Texl.Builtins;
using Xunit;
namespace Microsoft.PowerFx.Core.Tests
@ -15,6 +19,84 @@ namespace Microsoft.PowerFx.Core.Tests
/// </summary>
public class AnalyzeThreadSafety
{
// Return true if safe, false on error.
public static bool VerifyThreadSafeImmutable(Type t)
{
int errors = 0;
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
// Check out all fields and properties.
foreach (var prop in t.GetProperties(flags))
{
var name = prop.Name;
if (prop.CanWrite)
{
var isInitKeyword = prop.SetMethod.ReturnParameter.GetRequiredCustomModifiers().Contains(typeof(System.Runtime.CompilerServices.IsExternalInit));
if (!isInitKeyword)
{
// No mutable properties allowed. Init only ok.
Debugger.Log(0, string.Empty, $"{t.Name}.{name} has setter\r\n");
errors++;
}
}
Assert.True(prop.CanRead);
var propType = prop.PropertyType;
if (!IsTypeImmutable(propType))
{
// valuetypes are copies, so no contention
if (!prop.PropertyType.IsValueType)
{
// Fail.
Debugger.Log(0, string.Empty, $"{t.Name}.{name} returns mutable value\r\n");
errors++;
}
}
}
foreach (var field in t.GetFields(flags))
{
var name = field.Name;
if (name.StartsWith("<"))
{
// Ignore compile generated fields.
continue;
}
// ReadOnly
if (!field.IsInitOnly)
{
Debugger.Log(0, string.Empty, $"{t.Name}.{name} is not readonly\r\n");
errors++;
}
if (field.GetCustomAttributes<ThreadSafeProtectedByLockAttribute>().Any() ||
IsTypeConcurrent(field.FieldType))
{
continue;
}
if (!IsTypeImmutable(field.FieldType))
{
// Fail.
Debugger.Log(0, string.Empty, $"{t.Name}.{name} returns mutable value\r\n");
errors++;
}
}
if (errors > 0)
{
Debugger.Log(0, string.Empty, $"\r\n");
return false;
}
return true;
}
// Verify there are no "unsafe" static fields that could be threading issues.
// Bugs - list of field types types that don't work. This should be driven to 0.
// BugNames - list of "Type.Field" that don't work. This should be driven to 0.
@ -106,6 +188,20 @@ namespace Microsoft.PowerFx.Core.Tests
Assert.Empty(errors);
}
private static bool IsTypeConcurrent(Type type)
{
if (type.IsGenericType)
{
var genericDef = type.GetGenericTypeDefinition();
if (genericDef == typeof(ConcurrentDictionary<,>))
{
return true;
}
}
return false;
}
private static bool IsFieldVolatile(FieldInfo field)
{
var isVolatile = field
@ -120,10 +216,20 @@ namespace Microsoft.PowerFx.Core.Tests
// Primitives
typeof(object),
typeof(string),
typeof(System.Type),
typeof(Random),
typeof(DateTime),
typeof(System.Text.RegularExpressions.Regex),
typeof(System.Numerics.BigInteger),
typeof(NumberFormatInfo),
// Generics
typeof(IReadOnlyDictionary<,>),
typeof(IReadOnlyCollection<>),
typeof(IReadOnlyList<>),
typeof(Nullable<>),
typeof(IEnumerable<>),
typeof(KeyValuePair<,>)
};
// If the instance is readonly, is the type itself immutable ?
@ -150,27 +256,19 @@ namespace Microsoft.PowerFx.Core.Tests
if (t.IsGenericType)
{
var genericDef = t.GetGenericTypeDefinition();
if (genericDef == typeof(IReadOnlyDictionary<,>))
if (_knownImmutableTypes.Contains(genericDef))
{
// For a Dict<key,value>, need to make sure the values are also safe.
var valueArg = t.GetGenericArguments()[1];
var isValueArgSafe = IsTypeImmutable(valueArg);
return isValueArgSafe;
var typeArgs = t.GetGenericArguments();
foreach (var arg in typeArgs)
{
var isArgSafe = IsTypeImmutable(arg) || arg.IsValueType;
if (!isArgSafe)
{
return false;
}
}
if (genericDef == typeof(IReadOnlyCollection<>))
{
var keyArg = t.GetGenericArguments()[0];
var isKeyArgSafe = IsTypeImmutable(keyArg);
return isKeyArgSafe;
}
if (genericDef == typeof(IEnumerable<>)
|| genericDef == typeof(IReadOnlyList<>) || genericDef == typeof(Nullable<>))
{
var valueArg = t.GetGenericArguments()[0];
var isValueArgSafe = IsTypeImmutable(valueArg);
return isValueArgSafe;
return true;
}
}

Просмотреть файл

@ -16,6 +16,9 @@ namespace Microsoft.PowerFx.Core.Tests
/// </summary>
internal class ImmutabilityTests : PowerFxTest
{
// Include non-public types
// Include non-public properties!
// Are private-setters ok?
public static void CheckImmutability(Assembly asm)
{
var errors = new StringBuilder();

Просмотреть файл

@ -3,6 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.PowerFx.Core;
using Microsoft.PowerFx.Core.Tests;
using Xunit;
@ -20,5 +23,57 @@ namespace Microsoft.PowerFx.Interpreter.Tests
AnalyzeThreadSafety.CheckStatics(asm, bugsFieldType, bugNames);
}
// $$$ Supersedes ImmutabilityTests.
// This is more aggressive (includes private fields), but they don't all pass. So assert is disabled.
// Run this test under a debugger, and failure list is written to Debugger output window.
// Per https://github.com/microsoft/Power-Fx/issues/1519, enable assert here.
[Fact]
public void CheckImmutableType()
{
// Per https://github.com/microsoft/Power-Fx/issues/1519,
// Add ThreadSafeImmutable and get these to pass.
AnalyzeThreadSafety.VerifyThreadSafeImmutable(typeof(Core.IR.Nodes.IntermediateNode));
AnalyzeThreadSafety.VerifyThreadSafeImmutable(typeof(ReadOnlySymbolValues));
AnalyzeThreadSafety.VerifyThreadSafeImmutable(typeof(ComposedReadOnlySymbolValues));
AnalyzeThreadSafety.VerifyThreadSafeImmutable(typeof(ParsedExpression));
var assemblies = new Assembly[]
{
typeof(RecalcEngine).Assembly,
typeof(Types.FormulaType).Assembly
};
foreach (var assembly in assemblies)
{
foreach (Type type in assembly.GetTypes())
{
// includes base types
var attr = type.GetCustomAttribute<ThreadSafeImmutableAttribute>();
if (attr == null)
{
continue;
}
// Common pattern is a writeable derived type (like Dict vs. IReadOnlyDict).
var attrNotSafe = type.GetCustomAttribute<NotThreadSafeAttribute>(inherit: false);
if (attrNotSafe != null)
{
attr = type.GetCustomAttribute<ThreadSafeImmutableAttribute>(inherit: false);
if (attr != null)
{
Assert.True(false); // Class can't have both safe & unsafe together.
}
continue;
}
bool ok = AnalyzeThreadSafety.VerifyThreadSafeImmutable(type);
// Enable this, per https://github.com/microsoft/Power-Fx/issues/1519
// Assert.True(ok);
}
}
}
}
}