소스 검색

Made DatabaseSchema properly concurrent.

Kenric Nugteren 1 년 전
부모
커밋
435781d9fc
3개의 변경된 파일157개의 추가작업 그리고 139개의 파일을 삭제
  1. 1 1
      InABox.Core/CoreTable/CoreRow.cs
  2. 153 133
      InABox.Core/DatabaseSchema/DatabaseSchema.cs
  3. 3 5
      InABox.Core/Editors/Utils/EditorTypeUtils.cs

+ 1 - 1
InABox.Core/CoreTable/CoreRow.cs

@@ -92,7 +92,7 @@ namespace InABox.Core
             for (var i = 0; i < Table.Columns.Count; i++)
             {
                 var column = Table.Columns[i].ColumnName;
-                var value =  i > -1 && i < Values.Count
+                var value =  i < Values.Count
                     ? Values[i]
                     : CoreUtils.GetDefault(Table.Columns[i].DataType);
                 try

+ 153 - 133
InABox.Core/DatabaseSchema/DatabaseSchema.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Reflection;
@@ -10,8 +11,8 @@ namespace InABox.Core
     public static class DatabaseSchema
     {
         // {className: {propertyName: property}}
-        private static Dictionary<string, Dictionary<string, IProperty>> _properties
-            = new Dictionary<string, Dictionary<string, IProperty>>();
+        private static ConcurrentDictionary<Type, ImmutableDictionary<string, IProperty>> _properties
+            = new ConcurrentDictionary<Type, ImmutableDictionary<string, IProperty>>();
 
         private struct SubObject
         {
@@ -32,9 +33,9 @@ namespace InABox.Core
             }
         }
 
-        private static Dictionary<Type, List<SubObject>> _subObjects { get; } = new Dictionary<Type, List<SubObject>>();
+        private static ConcurrentDictionary<Type, ImmutableList<SubObject>> _subObjects { get; } = new ConcurrentDictionary<Type, ImmutableList<SubObject>>();
 
-        private static List<SubObject>? GetSubObjectDefs(Type t)
+        private static IReadOnlyCollection<SubObject>? GetSubObjectDefs(Type t)
         {
             CheckProperties(t);
             return _subObjects.GetValueOrDefault(t);
@@ -59,7 +60,7 @@ namespace InABox.Core
 
         public static void InitializeSubObjects(BaseObject obj)
         {
-            var objs = GetSubObjectDefs(obj.GetType())?.ToArray();
+            var objs = GetSubObjectDefs(obj.GetType());
             if(objs is null)
             {
                 return;
@@ -73,20 +74,18 @@ namespace InABox.Core
             }
         }
 
-        private static void RegisterSubObject(Type objectType, Type propertyType, string name)
+        // For synchronisation purposes, we register sub objects in bulk, removing the need for nested concurrent dictionaries.
+        private static void RegisterSubObjects(Type objectType, IEnumerable<Tuple<Type, string>> objects)
         {
-            lock (_updatelock)
+            if (!_subObjects.TryGetValue(objectType, out var subObjects))
             {
-                if (!_subObjects.TryGetValue(objectType, out var properties))
-                {
-                    properties = new List<SubObject>();
-                    _subObjects[objectType] = properties;
-                }
-                if (!properties.Any(x => x.Name == name && x.PropertyType == propertyType))
-                {
-                    properties.Add(new SubObject(objectType, propertyType, name));
-                }
+                subObjects = ImmutableList<SubObject>.Empty;
             }
+
+            // No synchronisation issues, since the original collection is not being modified, just the entry in the concurrent dictionary is updated.
+            _subObjects[objectType] = subObjects.AddRange(
+                objects.Where(x => !subObjects.Any(y => x.Item1 == y.PropertyType && x.Item2 == y.Name))
+                    .Select(x => new SubObject(objectType, x.Item1, x.Item2)));
         }
 
         private static void RegisterProperties(Type master, Type type, string prefix, StandardProperty? parent)
@@ -101,129 +100,131 @@ namespace InABox.Core
                         (x.DeclaringType.IsSubclassOf(typeof(BaseObject)) 
                         || x.DeclaringType.IsSubclassOf(typeof(BaseEditor)))
                 );
-                var classProps = _properties.GetValueOrDefault(classname);
+                var classProps = _properties.GetValueOrDefault(master);
+
+                var subObjects = new List<Tuple<Type, string>>();
+                var newProperties = new List<IProperty>();
 
                 foreach (var prop in properties)
                 {
-                    var name = string.IsNullOrWhiteSpace(prefix) ? prop.Name : string.Format("{0}.{1}", prefix, prop.Name);
+                    var name = prefix.IsNullOrWhiteSpace() ? prop.Name : $"{prefix}.{prop.Name}";
                     var p = classProps?.GetValueOrDefault(name);
-                    if (p == null)
+                    if (p == null && !prop.GetAccessors(true)[0].IsStatic)
                     {
-                        var isstatic = prop.GetAccessors(true)[0].IsStatic;
-                        if (!isstatic)
+                        BaseEditor? editor;
+                        if (parent != null && parent.HasEditor && parent.Editor is NullEditor)
+                        {
+                            editor = parent.Editor;
+                        }
+                        else
                         {
-                            BaseEditor? editor;
-                            if (parent != null && parent.HasEditor && parent.Editor is NullEditor)
+                            editor = prop.GetEditor();
+                        }
+
+                        var captionAttr = prop.GetCustomAttribute<Caption>();
+                        var subCaption = captionAttr != null ? captionAttr.Text : prop.Name;
+                        var path = captionAttr == null || captionAttr.IncludePath; // If no caption attribute, we should always include the path
+                        var caption = parent?.Caption ?? ""; // We default to the parent caption if subCaption doesn't exist
+                        if (!string.IsNullOrWhiteSpace(subCaption))
+                        {
+                            if (!string.IsNullOrWhiteSpace(caption) && path)
                             {
-                                editor = parent.Editor;
+                                caption = $"{caption} {subCaption}";
                             }
                             else
                             {
-                                editor = prop.GetEditor();
-                            }
-
-                            var captionAttr = prop.GetCustomAttribute<Caption>();
-                            var subCaption = captionAttr != null ? captionAttr.Text : prop.Name;
-                            var path = captionAttr == null || captionAttr.IncludePath; // If no caption attribute, we should always include the path
-                            var caption = parent?.Caption ?? ""; // We default to the parent caption if subCaption doesn't exist
-                            if (!string.IsNullOrWhiteSpace(subCaption))
-                            {
-                                if (!string.IsNullOrWhiteSpace(caption) && path)
-                                {
-                                    caption = $"{caption} {subCaption}";
-                                }
-                                else
-                                {
-                                    caption = subCaption;
-                                }
+                                caption = subCaption;
                             }
+                        }
 
-                            // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent
-                            // The same goes for sequence
-                            var page = parent?.Page;
-                            var sequence = parent?.Sequence;
-                            if (string.IsNullOrWhiteSpace(page))
+                        // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent
+                        // The same goes for sequence
+                        var page = parent?.Page;
+                        var sequence = parent?.Sequence;
+                        if (string.IsNullOrWhiteSpace(page))
+                        {
+                            var sequenceAttribute = prop.GetCustomAttribute<EditorSequence>();
+                            if (sequenceAttribute != null)
                             {
-                                var sequenceAttribute = prop.GetCustomAttribute<EditorSequence>();
-                                if (sequenceAttribute != null)
-                                {
-                                    page = sequenceAttribute.Page;
-                                    sequence = sequenceAttribute.Sequence;
-                                }
+                                page = sequenceAttribute.Page;
+                                sequence = sequenceAttribute.Sequence;
                             }
-                            editor = editor?.Clone() as BaseEditor;
-                            if (editor != null)
-                            {
-                                editor.Page = page;
-                                editor.Caption = caption;
-                                editor.EditorSequence = (int)(sequence ?? 999);
+                        }
+                        editor = editor?.Clone() as BaseEditor;
+                        if (editor != null)
+                        {
+                            editor.Page = page;
+                            editor.Caption = caption;
+                            editor.EditorSequence = (int)(sequence ?? 999);
 
-                                editor.Security = prop.GetCustomAttributes<SecurityAttribute>().ToArray();
-                            }
+                            editor.Security = prop.GetCustomAttributes<SecurityAttribute>().ToArray();
+                        }
 
-                            bool required = false;
-                            if (parent == null || parent.Required)
-                            {
-                                required = prop.GetCustomAttribute<RequiredColumnAttribute>() != null;
-                            }
+                        bool required = false;
+                        if (parent == null || parent.Required)
+                        {
+                            required = prop.GetCustomAttribute<RequiredColumnAttribute>() != null;
+                        }
 
-                            LoggablePropertyAttribute? loggable = null;
-                            if (parent == null || parent.Loggable != null)
-                            {
-                                loggable = prop.GetCustomAttribute<LoggablePropertyAttribute>();
-                            }
+                        LoggablePropertyAttribute? loggable = null;
+                        if (parent == null || parent.Loggable != null)
+                        {
+                            loggable = prop.GetCustomAttribute<LoggablePropertyAttribute>();
+                        }
 
-                            var newProperty = new StandardProperty
-                            {
-                                _class = master,
-                                //Class = classname,
-                                Name = name,
-                                PropertyType = prop.PropertyType,
-                                Editor = editor ?? new NullEditor(),
-                                HasEditor = editor != null,
-                                Caption = caption,
-                                Sequence = sequence ?? 999,
-                                Page = page ?? "",
-                                Required = required,
-                                Loggable = loggable,
-                                Parent = parent,
-                                Property = prop
-                            };
-
-                            var parentWithEditable = newProperty.GetOuterParent(x =>
-                                x is StandardProperty st
-                                && st.Property.GetCustomAttribute<EditableAttribute>() != null);
-                            if(parentWithEditable != null)
-                            {
-                                var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute<EditableAttribute>()!;
-                                newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
-                            }
-                            else if(prop.GetCustomAttribute<EditableAttribute>() is EditableAttribute attr)
-                            {
-                                newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
-                            }
+                        var newProperty = new StandardProperty
+                        {
+                            _class = master,
+                            //Class = classname,
+                            Name = name,
+                            PropertyType = prop.PropertyType,
+                            Editor = editor ?? new NullEditor(),
+                            HasEditor = editor != null,
+                            Caption = caption,
+                            Sequence = sequence ?? 999,
+                            Page = page ?? "",
+                            Required = required,
+                            Loggable = loggable,
+                            Parent = parent,
+                            Property = prop
+                        };
+
+                        var parentWithEditable = newProperty.GetOuterParent(x =>
+                            x is StandardProperty st
+                            && st.Property.GetCustomAttribute<EditableAttribute>() != null);
+                        if(parentWithEditable != null)
+                        {
+                            var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute<EditableAttribute>()!;
+                            newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
+                        }
+                        else if(prop.GetCustomAttribute<EditableAttribute>() is EditableAttribute attr)
+                        {
+                            newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
+                        }
 
-                            var isLink = prop.PropertyType.GetInterfaces().Contains(typeof(IEntityLink));
-                            var isEnclosedEntity = prop.PropertyType.GetInterfaces().Contains(typeof(IEnclosedEntity));
-                            var isBaseEditor = prop.PropertyType.Equals(typeof(BaseEditor)) ||
-                                prop.PropertyType.IsSubclassOf(typeof(BaseEditor));
-                            if ((isLink || isEnclosedEntity) && !isBaseEditor)
-                            {
-                                RegisterSubObject(type, prop.PropertyType, prop.Name);
-                            }
+                        var isLink = typeof(IEntityLink).IsAssignableFrom(prop.PropertyType);
+                        var isEnclosedEntity = typeof(IEnclosedEntity).IsAssignableFrom(prop.PropertyType);
+                        var isBaseEditor = prop.PropertyType.Equals(typeof(BaseEditor)) ||
+                            prop.PropertyType.IsSubclassOf(typeof(BaseEditor));
+                        if ((isLink || isEnclosedEntity) && !isBaseEditor)
+                        {
+                            subObjects.Add(new Tuple<Type, string>(prop.PropertyType, prop.Name));
+                        }
 
-                            if (isLink || isEnclosedEntity || isBaseEditor)
-                            {
-                                RegisterProperties(master, prop.PropertyType, name, newProperty);
-                            }
-                            else
-                            {
-                                RegisterProperty(newProperty);
-                            }
+                        if (isLink || isEnclosedEntity || isBaseEditor)
+                        {
+                            RegisterProperties(master, prop.PropertyType, name, newProperty);
+                        }
+                        else
+                        {
+                            newProperties.Add(newProperty);
                         }
                     }
                 }
 
+                RegisterProperties(master, newProperties);
+                RegisterSubObjects(type, subObjects);
+
                 if (type.IsSubclassOf(typeof(BaseObject)))
                     RegisterProperties(master, type.BaseType, prefix, parent);
             }
@@ -248,55 +249,74 @@ namespace InABox.Core
         }
         
         private static readonly object _updatelock =  new object();
+
+        private static void RegisterProperties(Type master, IEnumerable<IProperty> toAdd)
+        {
+            if (!_properties.TryGetValue(master, out var properties))
+            {
+                properties = ImmutableDictionary<string, IProperty>.Empty;
+            }
+            _properties[master] = properties.AddRange(
+                toAdd.Select(x => new KeyValuePair<string, IProperty>(x.Name, x)));
+        }
         
         public static void RegisterProperty(IProperty entry)
         {
-            lock (_updatelock)
+            var type = entry.ClassType;
+            if (type is null) return;
+
+            if (!_properties.TryGetValue(type, out var properties))
             {
-                if (!_properties.ContainsKey(entry.Class))
-                    _properties[entry.Class] = new Dictionary<string, IProperty>();
-                _properties[entry.Class][entry.Name] = entry;
+                properties = ImmutableDictionary<string, IProperty>.Empty;
             }
+            _properties[type] = properties.Add(entry.Name, entry);
         }
 
         public static void Load(CustomProperty[] customproperties)
         {
-            foreach (var prop in customproperties)
-                RegisterProperty(prop);
+            var perType = customproperties.GroupBy(x => x.ClassType);
+            foreach(var group in perType)
+            {
+                if (group.Key is null) continue;
+
+                RegisterProperties(group.Key, group);
+            }
         }
 
-        private static void CheckProperties(Type type)
+        private static ImmutableDictionary<string, IProperty>? CheckProperties(Type type)
         {
-            var entityName = type.EntityName();
             try
             {
-                var props = _properties.GetValueOrDefault(entityName);
+                var props = _properties.GetValueOrDefault(type);
                 var hasprops = props?.Any(x => x.Value is StandardProperty) == true;
                 if (!hasprops && type.IsSubclassOf(typeof(BaseObject)))
+                {
                     RegisterProperties(type);
+                    return _properties.GetValueOrDefault(type);
+                }
+                else
+                {
+                    return props;
+                }
             }
             catch (Exception e)
             {
                 // This seems to be an intermittent error "Collection has been modified" when checking if the Dictionary has been populated already
                 // I've added a .ToArray() to concretise the list, but who knows?
-                Logger.Send(LogType.Error,"",$"Error Checking Properties for Type: {entityName}\n{e.Message}\n{e.StackTrace}");
+                Logger.Send(LogType.Error,"",$"Error Checking Properties for Type: {type.EntityName()}\n{e.Message}\n{e.StackTrace}");
+                return null;
             }
 
         }
 
         public static IEnumerable<IProperty> Properties(Type type)
         {
-            CheckProperties(type);
-            var entityName = type.EntityName();
-            return _properties.GetValueOrDefault(entityName)?.Select(x => x.Value) ?? Array.Empty<IProperty>();
+            return CheckProperties(type)?.Select(x => x.Value) ?? Enumerable.Empty<IProperty>();
         }
         
         public static IProperty? Property(Type type, string name)
         {
-            CheckProperties(type);
-            var entityName = type.EntityName();
-
-            var prop = _properties.GetValueOrDefault(entityName)?.GetValueOrDefault(name);
+            var prop = CheckProperties(type)?.GetValueOrDefault(name);
 
             // Walk up the inheritance tree, see if an ancestor has this property
             if (prop == null && type.BaseType != null)
@@ -309,7 +329,7 @@ namespace InABox.Core
         {
             entity.UserProperties.Load(Properties(entity.GetType())
                 .Where(x => x is CustomProperty)
-                .Select(x => new KeyValuePair<string, object?>(x.Name, DefaultValue(x.PropertyType))));
+                .Select(x => new KeyValuePair<string, object?>(x.Name, x.PropertyType)));
         }
     }
 }

+ 3 - 5
InABox.Core/Editors/Utils/EditorTypeUtils.cs

@@ -31,7 +31,7 @@ namespace InABox.Core
             else if (type.IsOrdinal())
                 editor = new IntegerEditor();
 
-            else if (type.GetInterfaces().Contains(typeof(IPackable)))
+            else if (typeof(IPackable).IsAssignableFrom(type))
                 editor = new NullEditor();
 
             return editor;
@@ -52,10 +52,8 @@ namespace InABox.Core
         /// <returns></returns>
         public static BaseEditor? GetEditor(this PropertyInfo prop)
         {
-            var attribute =
-                prop.GetCustomAttributes(true)
-                    .FirstOrDefault(x => x.GetType().IsSubclassOf(typeof(BaseEditor))); // typeof(EditorType), true).FirstOrDefault();
-            var editor = attribute as BaseEditor ?? GetEditor(prop.PropertyType);
+            var attribute = prop.GetCustomAttributes<BaseEditor>(true).FirstOrDefault();
+            var editor = attribute ?? GetEditor(prop.PropertyType);
             if (editor != null && !prop.CanWrite)
                 editor.Editable = Editable.Hidden;
             return editor;