2
0
mirror of https://github.com/esiur/esiur-dotnet.git synced 2026-04-29 06:48:41 +00:00
This commit is contained in:
2026-04-05 12:35:27 +03:00
parent 44983d7784
commit c7d095ea96
17 changed files with 546 additions and 732 deletions
+7 -20
View File
@@ -58,22 +58,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConcurrentAttach", "Concurr
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ResourceCount", "ResourceCount", "{058F6BE3-A684-45F9-B61A-25839C64F503}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ResourceCount", "ResourceCount", "{058F6BE3-A684-45F9-B61A-25839C64F503}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{17796DCC-760D-4AD7-BCA9-EFE4801B9044}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{FFF7D07F-BA9F-4129-B7AD-99861E11F05E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{DA2EC9AF-E2D9-4B8D-8EC3-CC65CFD3B974}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{DA2EC9AF-E2D9-4B8D-8EC3-CC65CFD3B974}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{413A4292-C2B3-4096-94CF-D6F607C20939}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{413A4292-C2B3-4096-94CF-D6F607C20939}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttach.Client", "Tests\Distribution\ConcurrentAttach\Client\Esiur.Tests.ConcurrentAttach.Client.csproj", "{CD889154-4EA5-61D3-9FF4-E15F7B3D3573}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttach.Server", "Tests\Distribution\ConcurrentAttach\Server\Esiur.Tests.ConcurrentAttach.Server.csproj", "{9A468603-1310-7434-3A2B-4528DA8221C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ResourceCount.Client", "Tests\Distribution\ResourceCount\Client\Esiur.Tests.ResourceCount.Client.csproj", "{69A075E7-D924-59C6-0BF2-17A09201DDF3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ResourceCount.Client", "Tests\Distribution\ResourceCount\Client\Esiur.Tests.ResourceCount.Client.csproj", "{69A075E7-D924-59C6-0BF2-17A09201DDF3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ResourceCount.Server", "Tests\Distribution\ResourceCount\Server\Esiur.Tests.ResourceCount.Server.csproj", "{D1DF309F-40DE-9C0E-A78B-2648544B77D2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ResourceCount.Server", "Tests\Distribution\ResourceCount\Server\Esiur.Tests.ResourceCount.Server.csproj", "{D1DF309F-40DE-9C0E-A78B-2648544B77D2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttach", "Tests\Distribution\ConcurrentAttach\Esiur.Tests.ConcurrentAttach.csproj", "{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -132,14 +126,6 @@ Global
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.Build.0 = Release|Any CPU {D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.Build.0 = Release|Any CPU
{CD889154-4EA5-61D3-9FF4-E15F7B3D3573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD889154-4EA5-61D3-9FF4-E15F7B3D3573}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD889154-4EA5-61D3-9FF4-E15F7B3D3573}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD889154-4EA5-61D3-9FF4-E15F7B3D3573}.Release|Any CPU.Build.0 = Release|Any CPU
{9A468603-1310-7434-3A2B-4528DA8221C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A468603-1310-7434-3A2B-4528DA8221C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A468603-1310-7434-3A2B-4528DA8221C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A468603-1310-7434-3A2B-4528DA8221C6}.Release|Any CPU.Build.0 = Release|Any CPU
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -148,6 +134,10 @@ Global
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.Build.0 = Release|Any CPU {D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.Build.0 = Release|Any CPU
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -175,14 +165,11 @@ Global
{D8340DC7-5D27-2A71-74CC-634493847FF0} = {8AC1C925-068F-4E78-ADE4-4DA3CF996662} {D8340DC7-5D27-2A71-74CC-634493847FF0} = {8AC1C925-068F-4E78-ADE4-4DA3CF996662}
{336B5CE1-95DA-4FDD-A876-0919E3C446CA} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917} {336B5CE1-95DA-4FDD-A876-0919E3C446CA} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
{058F6BE3-A684-45F9-B61A-25839C64F503} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917} {058F6BE3-A684-45F9-B61A-25839C64F503} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
{17796DCC-760D-4AD7-BCA9-EFE4801B9044} = {336B5CE1-95DA-4FDD-A876-0919E3C446CA}
{FFF7D07F-BA9F-4129-B7AD-99861E11F05E} = {336B5CE1-95DA-4FDD-A876-0919E3C446CA}
{DA2EC9AF-E2D9-4B8D-8EC3-CC65CFD3B974} = {058F6BE3-A684-45F9-B61A-25839C64F503} {DA2EC9AF-E2D9-4B8D-8EC3-CC65CFD3B974} = {058F6BE3-A684-45F9-B61A-25839C64F503}
{413A4292-C2B3-4096-94CF-D6F607C20939} = {058F6BE3-A684-45F9-B61A-25839C64F503} {413A4292-C2B3-4096-94CF-D6F607C20939} = {058F6BE3-A684-45F9-B61A-25839C64F503}
{CD889154-4EA5-61D3-9FF4-E15F7B3D3573} = {FFF7D07F-BA9F-4129-B7AD-99861E11F05E}
{9A468603-1310-7434-3A2B-4528DA8221C6} = {17796DCC-760D-4AD7-BCA9-EFE4801B9044}
{69A075E7-D924-59C6-0BF2-17A09201DDF3} = {413A4292-C2B3-4096-94CF-D6F607C20939} {69A075E7-D924-59C6-0BF2-17A09201DDF3} = {413A4292-C2B3-4096-94CF-D6F607C20939}
{D1DF309F-40DE-9C0E-A78B-2648544B77D2} = {DA2EC9AF-E2D9-4B8D-8EC3-CC65CFD3B974} {D1DF309F-40DE-9C0E-A78B-2648544B77D2} = {DA2EC9AF-E2D9-4B8D-8EC3-CC65CFD3B974}
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9} = {336B5CE1-95DA-4FDD-A876-0919E3C446CA}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2} SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2}
+8 -5
View File
@@ -267,15 +267,18 @@ namespace Esiur.Data
// case TRUIdentifier. } // case TRUIdentifier. }
//} //}
private static Dictionary<Type, Tru> _cache = new Dictionary<Type, Tru>(); private static Dictionary<Type, Tru> cache = new Dictionary<Type, Tru>();
private static object cacheLook = new object();
public static Tru? FromType(Type type) public static Tru? FromType(Type type)
{ {
if (type == null) if (type == null)
return new Tru(TruIdentifier.Void, true); return new Tru(TruIdentifier.Void, true);
if (_cache.ContainsKey(type)) lock (cacheLook)
return _cache[type]; {
if (cache.ContainsKey(type))
return cache[type];
var nullable = false; var nullable = false;
@@ -496,7 +499,7 @@ namespace Esiur.Data
if (tru != null) if (tru != null)
{ {
_cache.Add(type, tru); cache.Add(type, tru);
return tru; return tru;
} }
@@ -523,7 +526,7 @@ namespace Esiur.Data
_ when type == typeof(ResourceLink) => new Tru(TruIdentifier.Resource, nullable), _ when type == typeof(ResourceLink) => new Tru(TruIdentifier.Resource, nullable),
_ => null _ => null
}; };
}
} }
public Tru(TruIdentifier identifier, bool nullable, Uuid? uuid = null, Tru[]? subTypes = null) public Tru(TruIdentifier identifier, bool nullable, Uuid? uuid = null, Tru[]? subTypes = null)
+5 -5
View File
@@ -12,12 +12,12 @@ namespace Esiur.Net.Packets
Stream = 0x2, Stream = 0x2,
// Error // Error
PermissionError = 0x81, PermissionError = 0x4,
ExecutionError = 0x82, ExecutionError = 0x5,
// Partial // Partial
Progress = 0x10, Progress = 0x8,
Chunk = 0x11, Chunk = 0x9,
Warning = 0x12 Warning = 0xA
} }
} }
@@ -57,9 +57,11 @@ partial class EpConnection
Dictionary<Uuid, TypeDef> typeDefs = new Dictionary<Uuid, TypeDef>(); Dictionary<Uuid, TypeDef> typeDefs = new Dictionary<Uuid, TypeDef>();
object typeDefsLock = new object();
KeyList<uint, AsyncReply> requests = new KeyList<uint, AsyncReply>(); KeyList<uint, AsyncReply> requests = new KeyList<uint, AsyncReply>();
volatile uint callbackCounter = 0; volatile int callbackCounter = 0;
Dictionary<IResource, List<byte>> subscriptions = new Dictionary<IResource, List<byte>>(); Dictionary<IResource, List<byte>> subscriptions = new Dictionary<IResource, List<byte>>();
@@ -73,15 +75,18 @@ partial class EpConnection
/// <summary> /// <summary>
/// Send IIP request. /// Send EP request.
/// </summary> /// </summary>
/// <param name="action">Packet action.</param> /// <param name="action">Packet action.</param>
/// <param name="args">Arguments to send.</param> /// <param name="args">Arguments to send.</param>
/// <returns></returns> /// <returns></returns>
///
AsyncReply SendRequest(EpPacketRequest action, params object[] args) AsyncReply SendRequest(EpPacketRequest action, params object[] args)
{ {
var reply = new AsyncReply(); var reply = new AsyncReply();
var c = callbackCounter++; // avoid thread racing var c = (uint)Interlocked.Increment(ref callbackCounter);
//callbackCounter++; // avoid thread racing
requests.Add(c, reply); requests.Add(c, reply);
if (args.Length == 0) if (args.Length == 0)
@@ -112,7 +117,7 @@ partial class EpConnection
} }
/// <summary> /// <summary>
/// Send IIP notification. /// Send EP notification.
/// </summary> /// </summary>
/// <param name="action">Packet action.</param> /// <param name="action">Packet action.</param>
/// <param name="args">Arguments to send.</param> /// <param name="args">Arguments to send.</param>
@@ -343,7 +348,7 @@ partial class EpConnection
var args = DataDeserializer.ListParser(dataType, Instance.Warehouse) var args = DataDeserializer.ListParser(dataType, Instance.Warehouse)
as object[]; as object[];
var errorCode = (ushort)args[0]; var errorCode =Convert.ToUInt16( args[0]);
var errorMsg = (string)args[1]; var errorMsg = (string)args[1];
req.TriggerError(new AsyncException(type, errorCode, errorMsg)); req.TriggerError(new AsyncException(type, errorCode, errorMsg));
@@ -1720,6 +1725,8 @@ partial class EpConnection
/// <param name="typeId">Type UUID.</param> /// <param name="typeId">Type UUID.</param>
/// <returns>TypeSchema.</returns> /// <returns>TypeSchema.</returns>
public AsyncReply<TypeDef> GetTypeDefById(Uuid typeId) public AsyncReply<TypeDef> GetTypeDefById(Uuid typeId)
{
lock (typeDefsLock)
{ {
if (typeDefs.ContainsKey(typeId)) if (typeDefs.ContainsKey(typeId))
return new AsyncReply<TypeDef>(typeDefs[typeId]); return new AsyncReply<TypeDef>(typeDefs[typeId]);
@@ -1745,9 +1752,12 @@ partial class EpConnection
return reply; return reply;
} }
}
public AsyncReply<TypeDef> GetTypeDefByName(string typeName) public AsyncReply<TypeDef> GetTypeDefByName(string typeName)
{
lock (typeDefsLock)
{ {
var typeDef = typeDefs.Values.FirstOrDefault(x => x.Name == typeName); var typeDef = typeDefs.Values.FirstOrDefault(x => x.Name == typeName);
if (typeDef != null) if (typeDef != null)
@@ -1776,6 +1786,7 @@ partial class EpConnection
return reply; return reply;
} }
}
// IStore interface // IStore interface
/// <summary> /// <summary>
@@ -1846,9 +1857,12 @@ partial class EpConnection
/// </summary> /// </summary>
/// <param name="id">Resource Id</param> /// <param name="id">Resource Id</param>
/// <returns>DistributedResource</returns> /// <returns>DistributedResource</returns>
///
object fetchLock = new object();
public AsyncReply<EpResource> Fetch(uint id, uint[] requestSequence) public AsyncReply<EpResource> Fetch(uint id, uint[] requestSequence)
{ {
//lock (fetchLock)
//{
EpResource resource = null; EpResource resource = null;
attachedResources[id]?.TryGetTarget(out resource); attachedResources[id]?.TryGetTarget(out resource);
@@ -1979,6 +1993,8 @@ partial class EpConnection
{ {
if (resource == null) if (resource == null)
{ {
dr.ResourceDefinition = typeDef;
Instance.Warehouse.Put(this.Instance.Link + "/" + id.ToString(), dr) Instance.Warehouse.Put(this.Instance.Link + "/" + id.ToString(), dr)
.Then(initResource).Error((ex) => reply.TriggerError(ex)); .Then(initResource).Error((ex) => reply.TriggerError(ex));
} }
@@ -1996,6 +2012,7 @@ partial class EpConnection
return reply; return reply;
//}
} }
+8 -27
View File
@@ -117,30 +117,6 @@ namespace Esiur.Proxy
} }
} }
// var code = @$"using Esiur.Resource;
//using Esiur.Core;
//#nullable enable
//namespace {ci.ClassSymbol.ContainingNamespace.ToDisplayString()} {{
//";
// if (IsInterfaceImplemented(ci, classes))
// code += $"public partial class {ci.Name} {{\r\n";
// else
// {
// code +=
//$@" public partial class {ci.Name} : IResource {{
// public virtual Instance? Instance {{ get; set; }}
// public virtual event DestroyedEvent? OnDestroy;
// public virtual void Destroy() {{ OnDestroy?.Invoke(this); }}
//";
// if (!ci.HasTrigger)
// code += "\tpublic virtual AsyncReply<bool> Trigger(ResourceTrigger trigger) => new AsyncReply<bool>(true);\r\n\r\n";
// }
foreach (var f in ci.Fields) foreach (var f in ci.Fields)
{ {
var givenName = f.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "ExportAttribute")?.ConstructorArguments.FirstOrDefault().Value as string; var givenName = f.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "ExportAttribute")?.ConstructorArguments.FirstOrDefault().Value as string;
@@ -156,12 +132,17 @@ namespace Esiur.Proxy
if (f.Type.Name.StartsWith("ResourceEventHandler") || f.Type.Name.StartsWith("CustomResourceEventHandler")) if (f.Type.Name.StartsWith("ResourceEventHandler") || f.Type.Name.StartsWith("CustomResourceEventHandler"))
{ {
code.AppendLine($"public event {f.Type} {pn};"); code.AppendLine($"\tpublic event {f.Type} {pn};");
} }
else else
{ {
code.AppendLine($"\t{attrs}\r\n\t public {f.Type} {pn} {{ \r\n\t\t get => {fn}; \r\n\t\t set {{ \r\n\t\t this.{fn} = value; \r\n\t\t Instance?.Modified(); \r\n\t\t}}\r\n\t}}\r\n"); code.AppendLine($"\tpublic {f.Type} {pn} {{");
code.AppendLine($"\t\t get => {fn};");
code.AppendLine($"\t\t set {{");
code.AppendLine($"\t\t this.{fn} = value;");
code.AppendLine($"\t\t Instance?.Modified();");
code.AppendLine($"\t\t}}");
code.AppendLine($"\t}}");
} }
} }
+4 -1
View File
@@ -11,6 +11,7 @@ namespace Esiur.Proxy;
public static class ResourceProxy public static class ResourceProxy
{ {
static Dictionary<Type, Type> cache = new Dictionary<Type, Type>(); static Dictionary<Type, Type> cache = new Dictionary<Type, Type>();
static object cacheLock = new object();
#if NETSTANDARD #if NETSTANDARD
static MethodInfo modifyMethod = typeof(Instance).GetTypeInfo().GetMethod("Modified"); static MethodInfo modifyMethod = typeof(Instance).GetTypeInfo().GetMethod("Modified");
@@ -48,7 +49,8 @@ public static class ResourceProxy
public static Type GetProxy(Type type) public static Type GetProxy(Type type)
{ {
lock (cacheLock)
{
if (cache.ContainsKey(type)) if (cache.ContainsKey(type))
return cache[type]; return cache[type];
@@ -114,6 +116,7 @@ public static class ResourceProxy
return t; return t;
#endif #endif
} }
}
public static Type GetProxy<T>() public static Type GetProxy<T>()
where T : IResource where T : IResource
+1 -1
View File
@@ -753,7 +753,7 @@ public class Instance
} }
else else
{ {
this.definition = Warehouse.GetTypeDefByType(resource.GetType()); this.definition = warehouse.GetTypeDefByType(resource.GetType());
} }
// set ages // set ages
+10 -1
View File
@@ -67,6 +67,8 @@ public class Warehouse
[TypeDefKind.Enum] = new KeyList<Uuid, TypeDef>(), [TypeDefKind.Enum] = new KeyList<Uuid, TypeDef>(),
}; };
object typeDefsLock = new object();
bool warehouseIsOpen = false; bool warehouseIsOpen = false;
public delegate void StoreEvent(IStore store); public delegate void StoreEvent(IStore store);
@@ -507,12 +509,15 @@ public class Warehouse
/// </summary> /// </summary>
/// <param name="typeDef">Resource type definition.</param> /// <param name="typeDef">Resource type definition.</param>
public void RegisterTypeDef(TypeDef typeDef) public void RegisterTypeDef(TypeDef typeDef)
{
lock (typeDefsLock)
{ {
if (typeDefs[typeDef.Kind].ContainsKey(typeDef.Id)) if (typeDefs[typeDef.Kind].ContainsKey(typeDef.Id))
throw new Exception($"TypeDef with same class Id already exists. {typeDefs[typeDef.Kind][typeDef.Id].Name} -> {typeDef.Name}"); throw new Exception($"TypeDef with same class Id already exists. {typeDefs[typeDef.Kind][typeDef.Id].Name} -> {typeDef.Name}");
typeDefs[typeDef.Kind][typeDef.Id] = typeDef; typeDefs[typeDef.Kind][typeDef.Id] = typeDef;
} }
}
/// <summary> /// <summary>
@@ -522,9 +527,11 @@ public class Warehouse
/// <returns>Resource TypeDef.</returns> /// <returns>Resource TypeDef.</returns>
public TypeDef GetTypeDefByType(Type type) public TypeDef GetTypeDefByType(Type type)
{ {
if (!(type.IsClass || type.IsEnum)) if (!(type.IsClass || type.IsEnum))
return null; return null;
var baseType = ResourceProxy.GetBaseType(type); var baseType = ResourceProxy.GetBaseType(type);
if (baseType == typeof(IResource) if (baseType == typeof(IResource)
@@ -541,6 +548,8 @@ public class Warehouse
else else
return null; return null;
lock (typeDefsLock)
{
var typeDef = typeDefs[typeDefKind].Values.FirstOrDefault(x => x.DefinedType == baseType); var typeDef = typeDefs[typeDefKind].Values.FirstOrDefault(x => x.DefinedType == baseType);
if (typeDef != null) if (typeDef != null)
return typeDef; return typeDef;
@@ -548,9 +557,9 @@ public class Warehouse
// create new TypeDef for type // create new TypeDef for type
typeDef = new TypeDef(baseType, this); typeDef = new TypeDef(baseType, this);
TypeDef.GetDependencies(typeDef, this); TypeDef.GetDependencies(typeDef, this);
return typeDef; return typeDef;
} }
}
/// <summary> /// <summary>
/// Get a TypeDef by TypeId from the warehouse. If not in the warehouse, a new TypeDef is created and added to the warehouse. /// Get a TypeDef by TypeId from the warehouse. If not in the warehouse, a new TypeDef is created and added to the warehouse.
@@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" /> <ProjectReference Include="..\..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -17,9 +17,9 @@
// Usage (client only): dotnet run -- --mode client --host 127.0.0.1 --concurrent 50 --resources 200 // Usage (client only): dotnet run -- --mode client --host 127.0.0.1 --concurrent 50 --resources 200
// ============================================================ // ============================================================
using Esiur.Protocol;
using Esiur.Resource; using Esiur.Resource;
using Esiur.Stores; using Esiur.Stores;
using Esiur.Protocol;
using System.Diagnostics; using System.Diagnostics;
var mode = GetArg(args, "--mode", "both"); var mode = GetArg(args, "--mode", "both");
@@ -29,30 +29,30 @@ var concurrent = int.Parse(GetArg(args, "--concurrent", "50"));
var resources = int.Parse(GetArg(args, "--resources", "200")); var resources = int.Parse(GetArg(args, "--resources", "200"));
var timeoutMs = int.Parse(GetArg(args, "--timeout", "10000")); var timeoutMs = int.Parse(GetArg(args, "--timeout", "10000"));
var rounds = int.Parse(GetArg(args, "--rounds", "5")); var rounds = int.Parse(GetArg(args, "--rounds", "5"));
var wh = new Warehouse();
var clientWh = new Warehouse();
var serverWh = new Warehouse();
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// SERVER SIDE // SERVER SIDE
// ---------------------------------------------------------------- // ----------------------------------------------------------------
if (mode == "server" || mode == "both") if (mode == "server" || mode == "both")
{ {
await serverWh.Put("sys", new MemoryStore());
await wh.Put("sys", new MemoryStore()); await serverWh.Put("sys/server", new EpServer() { Port = (ushort)port });
await wh.Put("sys/server", new EpServer() { Port = (ushort)port });
for (int i = 0; i < resources; i++) for (int i = 0; i < resources; i++)
{ {
await wh.Put($"sys/sensor_{i}", new SensorResource { SensorId = i, Value = i }); await serverWh.Put($"sys/sensor_{i}", new SensorResource { SensorId = i, Value = i });
} }
await wh.Open(); await serverWh.Open();
Console.WriteLine($"[Server-T3] Ready: {resources} resources on port {port}"); Console.WriteLine($"[Server-T3] Ready: {resources} resources on port {port}");
if (mode == "server") if (mode == "server")
{ {
Console.WriteLine("Press ENTER to stop."); Console.WriteLine("Press ENTER to stop.");
Console.ReadLine(); Console.ReadLine();
await wh.Close(); await serverWh.Close();
return; return;
} }
@@ -90,8 +90,8 @@ for (int round = 0; round < rounds; round++)
using var cts = new CancellationTokenSource(timeoutMs); using var cts = new CancellationTokenSource(timeoutMs);
try try
{ {
var proxy = await wh.Get<IResource>( var proxy = await clientWh.Get<IResource>(
$"iip://{host}:{port}/sys/sensor_{resourceIdx}"); $"ep://{host}:{port}/sys/sensor_{resourceIdx}");
sw.Stop(); sw.Stop();
latencies[taskIdx] = sw.Elapsed.TotalMilliseconds; latencies[taskIdx] = sw.Elapsed.TotalMilliseconds;
@@ -161,8 +161,11 @@ var csv = "round,concurrent,succeeded,failed,timed_out,total_wall_ms,min_ms,p50_
await File.WriteAllTextAsync("test3_concurrent_attach.csv", csv); await File.WriteAllTextAsync("test3_concurrent_attach.csv", csv);
Console.WriteLine("\n[Client-T3] Results written to test3_concurrent_attach.csv"); Console.WriteLine("\n[Client-T3] Results written to test3_concurrent_attach.csv");
if (mode == "both") if (mode == "server" || mode == "both")
await wh.Close(); await serverWh.Close();
if (mode == "client" || mode == "both")
await clientWh.Close();
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project>
@@ -1,182 +0,0 @@
// ============================================================
// Test 3: Concurrent Attachments — COMBINED (server + clients
// in the same process for local stress testing, or run the
// server section separately for multi-machine testing).
//
// Fires N concurrent Warehouse.Get calls simultaneously and
// measures:
// - Time for all proxies to reach Ready state
// - Whether any attachments fail or deadlock (timeout)
// - Distribution of per-attachment latency
//
// This directly stress-tests Algorithm 1 (FETCH-RESOURCE) and
// the parallel deadlock detection mechanism from Section V.D.
//
// Usage (single process): dotnet run -- --mode both --concurrent 50 --resources 200
// Usage (server only): dotnet run -- --mode server --resources 200 --port 10902
// Usage (client only): dotnet run -- --mode client --host 127.0.0.1 --concurrent 50 --resources 200
// ============================================================
using Esiur.Protocol;
using Esiur.Resource;
using Esiur.Stores;
using System.Diagnostics;
var mode = GetArg(args, "--mode", "both");
var host = GetArg(args, "--host", "127.0.0.1");
var port = int.Parse(GetArg(args, "--port", "10902"));
var concurrent = int.Parse(GetArg(args, "--concurrent", "50"));
var resources = int.Parse(GetArg(args, "--resources", "200"));
var timeoutMs = int.Parse(GetArg(args, "--timeout", "10000"));
var rounds = int.Parse(GetArg(args, "--rounds", "5"));
var wh = new Warehouse();
// ----------------------------------------------------------------
// SERVER SIDE
// ----------------------------------------------------------------
if (mode == "server" || mode == "both")
{
await wh.Put("sys", new MemoryStore());
await wh.Put("sys/server", new EpServer() { Port = (ushort)port });
for (int i = 0; i < resources; i++)
{
await wh.Put($"sys/sensor_{i}", new SensorResource { SensorId = i, Value = i });
}
await wh.Open();
Console.WriteLine($"[Server-T3] Ready: {resources} resources on port {port}");
if (mode == "server")
{
Console.WriteLine("Press ENTER to stop.");
Console.ReadLine();
await wh.Close();
return;
}
// Give server a moment to fully initialise before client fires
await Task.Delay(500);
}
// ----------------------------------------------------------------
// CLIENT SIDE
// ----------------------------------------------------------------
Console.WriteLine($"[Client-T3] concurrent={concurrent} resources={resources} rounds={rounds}");
var roundResults = new List<RoundResult>();
for (int round = 0; round < rounds; round++)
{
Console.WriteLine($"\n[Client-T3] Round {round + 1}/{rounds}");
// Pick `concurrent` random resource indices (may overlap — intentional,
// because overlapping triggers the "already in progress" path of Algorithm 1)
var rng = new Random(round);
var targets = Enumerable.Range(0, concurrent)
.Select(_ => rng.Next(resources))
.ToArray();
long succeeded = 0, failed = 0, timedOut = 0;
var latencies = new double[concurrent];
var roundSw = Stopwatch.StartNew();
// Fire all attachments simultaneously
var tasks = targets.Select((resourceIdx, taskIdx) => Task.Run(async () =>
{
var sw = Stopwatch.StartNew();
using var cts = new CancellationTokenSource(timeoutMs);
try
{
var proxy = await wh.Get<IResource>(
$"iip://{host}:{port}/sys/sensor_{resourceIdx}");
sw.Stop();
latencies[taskIdx] = sw.Elapsed.TotalMilliseconds;
if (proxy != null)
Interlocked.Increment(ref succeeded);
else
Interlocked.Increment(ref failed);
}
catch (OperationCanceledException)
{
sw.Stop();
latencies[taskIdx] = timeoutMs;
Interlocked.Increment(ref timedOut);
Console.WriteLine($" [!] Timeout on sensor_{resourceIdx} after {timeoutMs}ms");
}
catch (Exception ex)
{
sw.Stop();
latencies[taskIdx] = sw.Elapsed.TotalMilliseconds;
Interlocked.Increment(ref failed);
Console.WriteLine($" [!] Error on sensor_{resourceIdx}: {ex.Message}");
}
})).ToArray();
await Task.WhenAll(tasks);
roundSw.Stop();
var sorted = latencies.OrderBy(x => x).ToArray();
int n = sorted.Length;
var result = new RoundResult
{
Round = round + 1,
Concurrent = concurrent,
Succeeded = succeeded,
Failed = failed,
TimedOut = timedOut,
TotalMs = roundSw.Elapsed.TotalMilliseconds,
MinMs = sorted[0],
P50Ms = sorted[(int)(n * 0.50)],
P95Ms = sorted[(int)(n * 0.95)],
P99Ms = sorted[(int)(n * 0.99)],
MaxMs = sorted[n - 1],
MeanMs = sorted.Average()
};
roundResults.Add(result);
Console.WriteLine($" succeeded={succeeded}/{concurrent} failed={failed} timedOut={timedOut}");
Console.WriteLine($" total_wall={result.TotalMs:F0}ms");
Console.WriteLine($" latency: min={result.MinMs:F1} p50={result.P50Ms:F1} p95={result.P95Ms:F1} " +
$"p99={result.P99Ms:F1} max={result.MaxMs:F1} mean={result.MeanMs:F1} (ms)");
// Release all proxies before next round to reset attachment state
GC.Collect();
await Task.Delay(1000);
}
// ----------------------------------------------------------------
// CSV output
// ----------------------------------------------------------------
var csv = "round,concurrent,succeeded,failed,timed_out,total_wall_ms,min_ms,p50_ms,p95_ms,p99_ms,max_ms,mean_ms\n" +
string.Join("\n", roundResults.Select(r =>
$"{r.Round},{r.Concurrent},{r.Succeeded},{r.Failed},{r.TimedOut}," +
$"{r.TotalMs:F1},{r.MinMs:F2},{r.P50Ms:F2},{r.P95Ms:F2},{r.P99Ms:F2},{r.MaxMs:F2},{r.MeanMs:F2}"));
await File.WriteAllTextAsync("test3_concurrent_attach.csv", csv);
Console.WriteLine("\n[Client-T3] Results written to test3_concurrent_attach.csv");
if (mode == "both")
await wh.Close();
// ----------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------
static string GetArg(string[] args, string key, string def)
{
int i = Array.IndexOf(args, key);
return (i >= 0 && i + 1 < args.Length) ? args[i + 1] : def;
}
record RoundResult
{
public int Round;
public int Concurrent;
public long Succeeded, Failed, TimedOut;
public double TotalMs, MinMs, P50Ms, P95Ms, P99Ms, MaxMs, MeanMs;
}
@@ -1,15 +0,0 @@
using Esiur.Resource;
/// <summary>
/// Shared observable sensor resource used across all scalability tests.
/// Property changes via Value setter are automatically propagated
/// to all attached remote peers by the Esiur runtime.
/// </summary>
[Resource]
public partial class SensorResource : Resource
{
public int SensorId { get; set; }
[Export]
public double value;
}
@@ -29,7 +29,7 @@ long latencySamples = 0;
var latencyLock = new object(); var latencyLock = new object();
// --- Attach all resources ------------------------------------------- // --- Attach all resources -------------------------------------------
var proxies = new dynamic[resourceCount]; var proxies = new IResource[resourceCount];
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var wh = new Warehouse(); var wh = new Warehouse();
@@ -38,16 +38,18 @@ try
{ {
for (int i = 0; i < resourceCount; i++) for (int i = 0; i < resourceCount; i++)
{ {
proxies[i] = await wh.Get<IResource>($"iip://{host}:{port}/sys/sensor_{i}"); proxies[i] = await wh.Get<IResource>($"ep://{host}:{port}/sys/sensor_{i}");
dynamic resource = proxies[i];
// Subscribe to property change notifications via the Esiur event model // Subscribe to property change notifications via the Esiur event model
double lastValue = (double)proxies[i].Value; double lastValue = (double)resource.Value;
long lastTick = Stopwatch.GetTimestamp(); long lastTick = Stopwatch.GetTimestamp();
int capturedI = i; int capturedI = i;
proxies[i].OnPropertyModified += (string propName, object oldVal, object newVal) => proxies[i].Instance.PropertyModified += (PropertyModificationInfo data) =>
{ {
if (propName != "Value") return; if (data.Name != "Value") return;
long nowTick = Stopwatch.GetTimestamp(); long nowTick = Stopwatch.GetTimestamp();
double elapsedMs = (nowTick - lastTick) * 1000.0 / Stopwatch.Frequency; double elapsedMs = (nowTick - lastTick) * 1000.0 / Stopwatch.Frequency;
@@ -73,6 +75,7 @@ catch (Exception ex)
return; return;
} }
// --- Measurement window --------------------------------------------- // --- Measurement window ---------------------------------------------
sw.Restart(); sw.Restart();
long lastReceived = 0; long lastReceived = 0;
@@ -8,6 +8,7 @@
// Usage: dotnet run -- --host 127.0.0.1 --port 10901 --resources 10000 // Usage: dotnet run -- --host 127.0.0.1 --port 10901 --resources 10000
// ============================================================ // ============================================================
using Esiur.Protocol;
using Esiur.Resource; using Esiur.Resource;
using System.Diagnostics; using System.Diagnostics;
@@ -18,14 +19,19 @@ var batchSize = int.Parse(GetArg(args, "--batch", "100"));
Console.WriteLine($"[Client-T2] Connecting to {host}:{port}, resources={resourceCount}"); Console.WriteLine($"[Client-T2] Connecting to {host}:{port}, resources={resourceCount}");
var wh = new Warehouse();
var connnection = await wh.Get<EpConnection>(
$"ep://{host}:{port}");
var attachLatencies = new List<double>(resourceCount); var attachLatencies = new List<double>(resourceCount);
var proxies = new dynamic[resourceCount]; var proxies = new IResource[resourceCount];
// --- Attach in batches to avoid overwhelming the runtime ------------- // --- Attach in batches to avoid overwhelming the runtime -------------
var totalSw = Stopwatch.StartNew(); var totalSw = Stopwatch.StartNew();
var wh = new Warehouse();
for (int batch = 0; batch < resourceCount; batch += batchSize) for (int batch = 0; batch < resourceCount; batch += batchSize)
{ {
int end = Math.Min(batch + batchSize, resourceCount); int end = Math.Min(batch + batchSize, resourceCount);
var batchTasks = new Task[end - batch]; var batchTasks = new Task[end - batch];
@@ -35,8 +41,8 @@ for (int batch = 0; batch < resourceCount; batch += batchSize)
batchTasks[i - batch] = Task.Run(async () => batchTasks[i - batch] = Task.Run(async () =>
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
proxies[capturedI] = await wh.Get<IResource>( proxies[capturedI] = await connnection.Get($"sys/sensor_{capturedI}");
$"iip://{host}:{port}/sys/sensor_{capturedI}");
sw.Stop(); sw.Stop();
lock (attachLatencies) lock (attachLatencies)
@@ -45,7 +51,7 @@ for (int batch = 0; batch < resourceCount; batch += batchSize)
} }
await Task.WhenAll(batchTasks); await Task.WhenAll(batchTasks);
//Console.WriteLine("D");
if (batch % 1000 == 0) if (batch % 1000 == 0)
Console.WriteLine($"[Client-T2] Attached {Math.Min(batch + batchSize, resourceCount)}/{resourceCount} " + Console.WriteLine($"[Client-T2] Attached {Math.Min(batch + batchSize, resourceCount)}/{resourceCount} " +
$"elapsed={totalSw.Elapsed.TotalSeconds:F1}s"); $"elapsed={totalSw.Elapsed.TotalSeconds:F1}s");
@@ -60,10 +66,10 @@ int n = attachLatencies.Count;
Console.WriteLine($"[Client-T2] Attach latency (ms):"); Console.WriteLine($"[Client-T2] Attach latency (ms):");
Console.WriteLine($" min={attachLatencies[0]:F2}"); Console.WriteLine($" min={attachLatencies[0]:F2}");
Console.WriteLine($" p50={attachLatencies[(int)(n*0.50)]:F2}"); Console.WriteLine($" p50={attachLatencies[(int)(n * 0.50)]:F2}");
Console.WriteLine($" p95={attachLatencies[(int)(n*0.95)]:F2}"); Console.WriteLine($" p95={attachLatencies[(int)(n * 0.95)]:F2}");
Console.WriteLine($" p99={attachLatencies[(int)(n*0.99)]:F2}"); Console.WriteLine($" p99={attachLatencies[(int)(n * 0.99)]:F2}");
Console.WriteLine($" max={attachLatencies[n-1]:F2}"); Console.WriteLine($" max={attachLatencies[n - 1]:F2}");
Console.WriteLine($" mean={attachLatencies.Average():F2}"); Console.WriteLine($" mean={attachLatencies.Average():F2}");
// --- Notification round-trip after full load ------------------------ // --- Notification round-trip after full load ------------------------
@@ -74,13 +80,15 @@ double sumLatencyMs = 0;
for (int i = 0; i < Math.Min(500, resourceCount); i++) for (int i = 0; i < Math.Min(500, resourceCount); i++)
{ {
int capturedI = i; int capturedI = i;
proxies[capturedI].OnPropertyModified += (string propName, object oldVal, object newVal) => proxies[capturedI].Instance.PropertyModified += (PropertyModificationInfo data) =>
{ {
if (propName == "Value") if (data.Name == "Value")
Interlocked.Increment(ref received); Interlocked.Increment(ref received);
}; };
} }
await connnection.Call("UpdateValues");
await Task.Delay(10000); // observe for 10s await Task.Delay(10000); // observe for 10s
Console.WriteLine($"[Client-T2] Received {received} notifications in 10s from first 500 resources"); Console.WriteLine($"[Client-T2] Received {received} notifications in 10s from first 500 resources");
@@ -18,18 +18,29 @@ Console.WriteLine($"[Server-T2] Creating {resourceCount} resources on port {port
var wh = new Warehouse(); var wh = new Warehouse();
await wh.Put("sys", new MemoryStore()); await wh.Put("sys", new MemoryStore());
await wh.Put("sys/server", new EpServer() { Port = (ushort)port }); var server = await wh.Put("sys/server", new EpServer() { Port = (ushort)port });
long memBefore = GC.GetTotalMemory(forceFullCollection: true); long memBefore = GC.GetTotalMemory(forceFullCollection: true);
List<SensorResource> sensors = new List<SensorResource>();
for (int i = 0; i < resourceCount; i++) for (int i = 0; i < resourceCount; i++)
{ {
var s = new SensorResource { SensorId = i, Value = i * 0.1 }; var sensor = await wh.Put($"sys/sensor_{i}",
await wh.Put($"sys/sensor_{i}", s); new SensorResource { SensorId = i, Value = i * 0.1 });
sensors.Add(sensor);
} }
await wh.Open(); await wh.Open();
server.MapCall("UpdateValues", () =>
{
foreach(var sensor in sensors)
{
sensor.Value += 0.1;
}
});
long memAfter = GC.GetTotalMemory(forceFullCollection: true); long memAfter = GC.GetTotalMemory(forceFullCollection: true);
double memMB = (memAfter - memBefore) / (1024.0 * 1024.0); double memMB = (memAfter - memBefore) / (1024.0 * 1024.0);