mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-09-08 10:10:49 +00:00
Revisions
This commit is contained in:
@@ -13,15 +13,27 @@ public class CustomEventOccurredInfo
|
||||
public readonly object Value;
|
||||
public readonly object Issuer;
|
||||
public readonly Func<Session, bool> Receivers;
|
||||
public readonly ResourceCursor Cursor;
|
||||
public ulong Revision => Cursor.Revision;
|
||||
public readonly DateTime RecordedAt;
|
||||
|
||||
public string Name => EventDef.Name;
|
||||
|
||||
public CustomEventOccurredInfo(IResource resource, EventDef eventDef, Func<Session, bool> receivers, object issuer, object value)
|
||||
public CustomEventOccurredInfo(
|
||||
IResource resource,
|
||||
EventDef eventDef,
|
||||
Func<Session, bool> receivers,
|
||||
object issuer,
|
||||
object value,
|
||||
ResourceCursor cursor,
|
||||
DateTime recordedAt)
|
||||
{
|
||||
Resource = resource;
|
||||
EventDef = eventDef;
|
||||
Receivers = receivers;
|
||||
Issuer = issuer;
|
||||
Value = value;
|
||||
Cursor = cursor;
|
||||
RecordedAt = recordedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,27 @@ namespace Esiur.Resource
|
||||
|
||||
public readonly IResource Resource;
|
||||
public readonly object Value;
|
||||
public readonly ResourceCursor Cursor;
|
||||
public ulong Revision => Cursor.Revision;
|
||||
public readonly DateTime RecordedAt;
|
||||
|
||||
public EventOccurredInfo(IResource resource, EventDef eventDef, object value)
|
||||
public EventOccurredInfo(
|
||||
IResource resource,
|
||||
EventDef eventDef,
|
||||
object value,
|
||||
ResourceCursor cursor,
|
||||
DateTime recordedAt)
|
||||
{
|
||||
Resource = resource;
|
||||
Value = value;
|
||||
Definition = eventDef;
|
||||
Cursor = cursor;
|
||||
RecordedAt = recordedAt;
|
||||
}
|
||||
|
||||
public EventOccurredInfo(IResource resource, EventDef eventDef, object value)
|
||||
: this(resource, eventDef, value, new ResourceCursor(Guid.Empty, 0), DateTime.UtcNow)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ public class Instance
|
||||
List<ulong?> ages = new();
|
||||
List<DateTime?> modificationDates = new();
|
||||
private ulong instanceAge;
|
||||
private Guid streamGeneration;
|
||||
private readonly object journalSync = new();
|
||||
private byte hops;
|
||||
private DateTime instanceModificationDate;
|
||||
|
||||
@@ -331,12 +333,34 @@ public class Instance
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Age of the instance, incremented by 1 in every modification.
|
||||
/// Legacy alias for the resource-wide revision. It advances for every
|
||||
/// property modification and event occurrence.
|
||||
/// </summary>
|
||||
public ulong Age
|
||||
{
|
||||
get { return instanceAge; }
|
||||
internal set { instanceAge = value; }
|
||||
get { lock (journalSync) return instanceAge; }
|
||||
internal set { lock (journalSync) instanceAge = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current resource revision. Unlike the legacy property-only age, this
|
||||
/// advances for every property modification and event occurrence.
|
||||
/// </summary>
|
||||
public ulong Revision
|
||||
{
|
||||
get { lock (journalSync) return instanceAge; }
|
||||
}
|
||||
|
||||
/// <summary>Generation of the current resource change stream.</summary>
|
||||
public Guid Generation
|
||||
{
|
||||
get { lock (journalSync) return streamGeneration; }
|
||||
}
|
||||
|
||||
/// <summary>Current replay cursor for the resource.</summary>
|
||||
public ResourceCursor Cursor
|
||||
{
|
||||
get { lock (journalSync) return new ResourceCursor(streamGeneration, instanceAge); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -454,6 +478,16 @@ public class Instance
|
||||
return props;
|
||||
}
|
||||
|
||||
/// <summary>Exports every property keyed by its member index.</summary>
|
||||
public Map<byte, PropertyValue> SerializeMap()
|
||||
{
|
||||
var values = Serialize();
|
||||
var map = new Map<byte, PropertyValue>();
|
||||
for (byte index = 0; index < values.Length; index++)
|
||||
map.Add(index, values[index]);
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// If True, the instance can be stored to disk.
|
||||
@@ -477,24 +511,25 @@ public class Instance
|
||||
IResource res;
|
||||
if (this.resource.TryGetTarget(out res))
|
||||
{
|
||||
instanceAge++;
|
||||
var now = DateTime.UtcNow;
|
||||
lock (journalSync)
|
||||
{
|
||||
var cursor = NextCursor();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
ages[pt.Index] = instanceAge;
|
||||
modificationDates[pt.Index] = now;
|
||||
ages[pt.Index] = cursor.Revision;
|
||||
modificationDates[pt.Index] = now;
|
||||
instanceModificationDate = now;
|
||||
|
||||
//if (pt.HasHistory)
|
||||
//{
|
||||
// store.Record(res, pt.Name, value, ages[pt.Index], now);
|
||||
//}
|
||||
//else //if (pt.Storage == StorageMode.Recordable)
|
||||
//{
|
||||
store.Modify(res, pt, value, ages[pt.Index], now);
|
||||
//}
|
||||
store.Modify(res, pt, value, cursor.Revision, now);
|
||||
CommitJournal(res, new ResourceJournalEntry(
|
||||
cursor,
|
||||
now,
|
||||
ResourceJournalEntryKind.PropertyModified,
|
||||
pt.Index,
|
||||
value), pt.Historical);
|
||||
|
||||
//ResourceModified?.Invoke(res, pt.Name, value);
|
||||
|
||||
PropertyModified?.Invoke(new PropertyModificationInfo(res, pt, value, instanceAge));
|
||||
PropertyModified?.Invoke(new PropertyModificationInfo(res, pt, value, cursor, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +561,21 @@ public class Instance
|
||||
IResource res;
|
||||
if (this.resource.TryGetTarget(out res))
|
||||
{
|
||||
CustomEventOccurred?.Invoke(new CustomEventOccurredInfo(res, eventDef, receivers, issuer, value));
|
||||
lock (journalSync)
|
||||
{
|
||||
var cursor = NextCursor();
|
||||
var now = DateTime.UtcNow;
|
||||
instanceModificationDate = now;
|
||||
// Receiver predicates are session-specific and cannot be replayed safely.
|
||||
CommitJournal(res, new ResourceJournalEntry(
|
||||
cursor,
|
||||
now,
|
||||
ResourceJournalEntryKind.EventOccurred,
|
||||
eventDef.Index,
|
||||
value), false);
|
||||
CustomEventOccurred?.Invoke(new CustomEventOccurredInfo(
|
||||
res, eventDef, receivers, issuer, value, cursor, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +584,7 @@ public class Instance
|
||||
IResource res;
|
||||
if (this.resource.TryGetTarget(out res))
|
||||
{
|
||||
EventOccurred?.Invoke(new EventOccurredInfo(res, eventDef, value));
|
||||
EmitResourceEventCore(res, eventDef, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,7 +594,7 @@ public class Instance
|
||||
if (this.resource.TryGetTarget(out res))
|
||||
{
|
||||
var eventDef = definition.GetEventDefByIndex(eventIndex);
|
||||
EventOccurred?.Invoke(new EventOccurredInfo(res, eventDef, value));
|
||||
EmitResourceEventCore(res, eventDef, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +604,141 @@ public class Instance
|
||||
if (this.resource.TryGetTarget(out res))
|
||||
{
|
||||
var eventDef = definition.GetEventDefByIndex(eventIndex);
|
||||
CustomEventOccurred?.Invoke(new CustomEventOccurredInfo(res, eventDef, receivers, issuer, value));
|
||||
EmitCustomResourceEvent(issuer, receivers, eventDef, value);
|
||||
}
|
||||
}
|
||||
|
||||
void EmitResourceEventCore(IResource res, EventDef eventDef, object value)
|
||||
{
|
||||
if (eventDef == null)
|
||||
return;
|
||||
|
||||
lock (journalSync)
|
||||
{
|
||||
var cursor = NextCursor();
|
||||
var now = DateTime.UtcNow;
|
||||
instanceModificationDate = now;
|
||||
CommitJournal(res, new ResourceJournalEntry(
|
||||
cursor,
|
||||
now,
|
||||
ResourceJournalEntryKind.EventOccurred,
|
||||
eventDef.Index,
|
||||
value), eventDef.Historical);
|
||||
EventOccurred?.Invoke(new EventOccurredInfo(res, eventDef, value, cursor, now));
|
||||
}
|
||||
}
|
||||
|
||||
ResourceCursor NextCursor()
|
||||
{
|
||||
instanceAge++;
|
||||
return new ResourceCursor(streamGeneration, instanceAge);
|
||||
}
|
||||
|
||||
void CommitJournal(IResource res, ResourceJournalEntry entry, bool retain)
|
||||
{
|
||||
if (store is IResourceJournalStore journal &&
|
||||
!journal.AppendJournalEntry(res, entry, retain))
|
||||
throw new InvalidOperationException(
|
||||
$"The store rejected resource journal revision {entry.Cursor} for `{Link}`.");
|
||||
}
|
||||
|
||||
/// <summary>Reads retained changes from this resource's owning store.</summary>
|
||||
public ResourceJournalPage QueryJournal(ResourceJournalQuery query)
|
||||
{
|
||||
lock (journalSync)
|
||||
{
|
||||
if (resource.TryGetTarget(out var res) && store is IResourceJournalStore journal)
|
||||
{
|
||||
var page = journal.QueryJournal(res, query ?? new ResourceJournalQuery());
|
||||
var cursor = new ResourceCursor(streamGeneration, instanceAge);
|
||||
if (page.HighWatermark == cursor)
|
||||
return page;
|
||||
|
||||
return new ResourceJournalPage(
|
||||
page.OldestAvailable,
|
||||
cursor,
|
||||
page.Next,
|
||||
page.CursorExpired,
|
||||
page.HasMore,
|
||||
page.Entries);
|
||||
}
|
||||
|
||||
var currentCursor = new ResourceCursor(streamGeneration, instanceAge);
|
||||
return new ResourceJournalPage(currentCursor, currentCursor, query?.After ?? currentCursor,
|
||||
false, false, Array.Empty<ResourceJournalEntry>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs an attach/replay operation against an atomic resource high-watermark.
|
||||
/// Emission uses the same lock, so notifications after the callback are live
|
||||
/// changes strictly newer than the captured cursor.
|
||||
/// </summary>
|
||||
internal T SynchronizeJournal<T>(Func<ResourceCursor, T> action)
|
||||
{
|
||||
lock (journalSync)
|
||||
return action(new ResourceCursor(streamGeneration, instanceAge));
|
||||
}
|
||||
|
||||
internal void ObserveRemoteCursor(ResourceCursor cursor, DateTime recordedAt)
|
||||
{
|
||||
lock (journalSync)
|
||||
{
|
||||
if (streamGeneration != cursor.Generation)
|
||||
{
|
||||
streamGeneration = cursor.Generation;
|
||||
instanceAge = cursor.Revision;
|
||||
}
|
||||
else if (cursor.Revision > instanceAge)
|
||||
instanceAge = cursor.Revision;
|
||||
|
||||
if (recordedAt > instanceModificationDate)
|
||||
instanceModificationDate = recordedAt;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsNewerPropertyRevision(byte index, ResourceCursor cursor)
|
||||
{
|
||||
lock (journalSync)
|
||||
{
|
||||
if (streamGeneration != cursor.Generation)
|
||||
return true;
|
||||
return index >= ages.Count || !ages[index].HasValue || ages[index].Value < cursor.Revision;
|
||||
}
|
||||
}
|
||||
|
||||
internal void ApplyRemotePropertyModification(
|
||||
PropertyDef propertyDef,
|
||||
object value,
|
||||
ResourceCursor cursor,
|
||||
DateTime recordedAt)
|
||||
{
|
||||
if (!resource.TryGetTarget(out var res))
|
||||
return;
|
||||
|
||||
lock (journalSync)
|
||||
{
|
||||
ObserveRemoteCursor(cursor, recordedAt);
|
||||
ages[propertyDef.Index] = cursor.Revision;
|
||||
modificationDates[propertyDef.Index] = recordedAt;
|
||||
PropertyModified?.Invoke(new PropertyModificationInfo(
|
||||
res, propertyDef, value, cursor, recordedAt));
|
||||
}
|
||||
}
|
||||
|
||||
internal void ApplyRemoteEvent(
|
||||
EventDef eventDef,
|
||||
object value,
|
||||
ResourceCursor cursor,
|
||||
DateTime recordedAt)
|
||||
{
|
||||
if (!resource.TryGetTarget(out var res))
|
||||
return;
|
||||
|
||||
lock (journalSync)
|
||||
{
|
||||
ObserveRemoteCursor(cursor, recordedAt);
|
||||
EventOccurred?.Invoke(new EventOccurredInfo(res, eventDef, value, cursor, recordedAt));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,7 +938,14 @@ public class Instance
|
||||
/// <param name="name">Name of the instance.</param>
|
||||
/// <param name="resource">Resource to manage.</param>
|
||||
/// <param name="store">Store responsible for the resource.</param>
|
||||
public Instance(Warehouse warehouse, uint id, string name, IResource resource, IStore store, ulong age = 0)
|
||||
public Instance(
|
||||
Warehouse warehouse,
|
||||
uint id,
|
||||
string name,
|
||||
IResource resource,
|
||||
IStore store,
|
||||
ulong age = 0,
|
||||
string resourceKey = null)
|
||||
{
|
||||
this.Warehouse = warehouse;
|
||||
this.store = store;
|
||||
@@ -763,6 +953,17 @@ public class Instance
|
||||
this.id = id;
|
||||
this.name = name ?? "";
|
||||
this.instanceAge = age;
|
||||
this.streamGeneration = Guid.NewGuid();
|
||||
|
||||
if (store is IResourceJournalStore journal)
|
||||
{
|
||||
var cursor = journal.OpenJournal(
|
||||
resource,
|
||||
resourceKey ?? name ?? string.Empty,
|
||||
new ResourceCursor(streamGeneration, instanceAge));
|
||||
streamGeneration = cursor.Generation;
|
||||
instanceAge = Math.Max(instanceAge, cursor.Revision);
|
||||
}
|
||||
|
||||
//this.attributes = new KeyList<string, object>(this);
|
||||
//children = new AutoList<IResource, Instance>(this);
|
||||
@@ -779,6 +980,7 @@ public class Instance
|
||||
if (resource is IDynamicResource dynamicResource)
|
||||
{
|
||||
this.definition = dynamicResource.ResourceDefinition;
|
||||
warehouse.RegisterDynamicTypeDef(this.definition);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -796,6 +998,8 @@ public class Instance
|
||||
// connect events
|
||||
if (!(resource is EpResource))
|
||||
{
|
||||
if (resource is IDynamicResourceEventSource dynamicEventSource)
|
||||
dynamicEventSource.ResourceEventOccurred += EmitResourceEventByIndex;
|
||||
|
||||
Type t = ResourceProxy.GetBaseType(resource);
|
||||
|
||||
|
||||
@@ -12,15 +12,30 @@ public struct PropertyModificationInfo
|
||||
public readonly PropertyDef PropertyDef;
|
||||
public string Name => PropertyDef.Name;
|
||||
public readonly ulong Age;
|
||||
public ulong Revision => Cursor.Revision;
|
||||
public readonly ResourceCursor Cursor;
|
||||
public readonly DateTime RecordedAt;
|
||||
public object Value;
|
||||
|
||||
public PropertyModificationInfo(IResource resource, PropertyDef propertyDef, object value, ulong age)
|
||||
public PropertyModificationInfo(
|
||||
IResource resource,
|
||||
PropertyDef propertyDef,
|
||||
object value,
|
||||
ResourceCursor cursor,
|
||||
DateTime recordedAt)
|
||||
{
|
||||
Resource = resource;
|
||||
PropertyDef = propertyDef;
|
||||
Age = age;
|
||||
Cursor = cursor;
|
||||
Age = cursor.Revision;
|
||||
RecordedAt = recordedAt;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public PropertyModificationInfo(IResource resource, PropertyDef propertyDef, object value, ulong age)
|
||||
: this(resource, propertyDef, value, new ResourceCursor(Guid.Empty, age), DateTime.UtcNow)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace Esiur.Resource;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies an exact position in a resource's ordered change stream.
|
||||
/// Generation changes when the stream is recreated; revision increases for
|
||||
/// every observable property modification or event occurrence.
|
||||
/// </summary>
|
||||
public readonly struct ResourceCursor : IEquatable<ResourceCursor>
|
||||
{
|
||||
public Guid Generation { get; }
|
||||
public ulong Revision { get; }
|
||||
|
||||
public ResourceCursor(Guid generation, ulong revision)
|
||||
{
|
||||
Generation = generation;
|
||||
Revision = revision;
|
||||
}
|
||||
|
||||
public bool IsEmpty => Generation == Guid.Empty && Revision == 0;
|
||||
|
||||
public bool Equals(ResourceCursor other) =>
|
||||
Generation == other.Generation && Revision == other.Revision;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is ResourceCursor other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
(Generation.GetHashCode() * 397) ^ Revision.GetHashCode();
|
||||
|
||||
public override string ToString() => $"{Generation:N}:{Revision}";
|
||||
|
||||
public static bool operator ==(ResourceCursor left, ResourceCursor right) => left.Equals(right);
|
||||
public static bool operator !=(ResourceCursor left, ResourceCursor right) => !left.Equals(right);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Esiur.Resource;
|
||||
|
||||
public enum ResourceJournalEntryKind : byte
|
||||
{
|
||||
PropertyModified = 0,
|
||||
EventOccurred = 1,
|
||||
}
|
||||
|
||||
/// <summary>An ordered, optionally retained resource change.</summary>
|
||||
public sealed class ResourceJournalEntry
|
||||
{
|
||||
public ResourceCursor Cursor { get; }
|
||||
public DateTime RecordedAt { get; }
|
||||
public ResourceJournalEntryKind Kind { get; }
|
||||
public byte MemberIndex { get; }
|
||||
public object Value { get; }
|
||||
|
||||
public ResourceJournalEntry(
|
||||
ResourceCursor cursor,
|
||||
DateTime recordedAt,
|
||||
ResourceJournalEntryKind kind,
|
||||
byte memberIndex,
|
||||
object value)
|
||||
{
|
||||
Cursor = cursor;
|
||||
RecordedAt = recordedAt.Kind == DateTimeKind.Utc
|
||||
? recordedAt
|
||||
: recordedAt.ToUniversalTime();
|
||||
Kind = kind;
|
||||
MemberIndex = memberIndex;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Filters a bounded resource-journal query.</summary>
|
||||
public sealed class ResourceJournalQuery
|
||||
{
|
||||
public ResourceCursor After { get; set; }
|
||||
public ulong? ThroughRevision { get; set; }
|
||||
public DateTime? FromTime { get; set; }
|
||||
public DateTime? ToTime { get; set; }
|
||||
public ResourceJournalEntryKind? Kind { get; set; }
|
||||
public byte? MemberIndex { get; set; }
|
||||
public int Limit { get; set; } = 1000;
|
||||
}
|
||||
|
||||
/// <summary>A journal page and the stream boundaries used to read it.</summary>
|
||||
public sealed class ResourceJournalPage
|
||||
{
|
||||
public ResourceCursor OldestAvailable { get; }
|
||||
public ResourceCursor HighWatermark { get; }
|
||||
public ResourceCursor Next { get; }
|
||||
public bool CursorExpired { get; }
|
||||
public bool HasMore { get; }
|
||||
public ResourceJournalEntry[] Entries { get; }
|
||||
|
||||
public ResourceJournalPage(
|
||||
ResourceCursor oldestAvailable,
|
||||
ResourceCursor highWatermark,
|
||||
ResourceCursor next,
|
||||
bool cursorExpired,
|
||||
bool hasMore,
|
||||
ResourceJournalEntry[] entries)
|
||||
{
|
||||
OldestAvailable = oldestAvailable;
|
||||
HighWatermark = highWatermark;
|
||||
Next = next;
|
||||
CursorExpired = cursorExpired;
|
||||
HasMore = hasMore;
|
||||
Entries = entries ?? Array.Empty<ResourceJournalEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional store capability for persisting historical property changes and
|
||||
/// event occurrences. The resource instance owns ordering; the store owns
|
||||
/// retention and persistence.
|
||||
/// </summary>
|
||||
public interface IResourceJournalStore
|
||||
{
|
||||
ResourceCursor OpenJournal(
|
||||
IResource resource,
|
||||
string resourceKey,
|
||||
ResourceCursor proposedCursor);
|
||||
bool AppendJournalEntry(IResource resource, ResourceJournalEntry entry, bool retain);
|
||||
ResourceJournalPage QueryJournal(IResource resource, ResourceJournalQuery query);
|
||||
void RemoveJournal(IResource resource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounded in-memory implementation for volatile stores and tests. Persistent
|
||||
/// stores can implement <see cref="IResourceJournalStore"/> with their own
|
||||
/// transactional backend while preserving the same protocol contract.
|
||||
/// </summary>
|
||||
public sealed class ResourceJournalBuffer : IResourceJournalStore
|
||||
{
|
||||
sealed class State
|
||||
{
|
||||
public ResourceCursor Head;
|
||||
public ulong DiscardedThroughRevision;
|
||||
public readonly LinkedList<ResourceJournalEntry> Entries = new();
|
||||
}
|
||||
|
||||
sealed class ResourceReferenceComparer : IEqualityComparer<IResource>
|
||||
{
|
||||
public bool Equals(IResource x, IResource y) => ReferenceEquals(x, y);
|
||||
public int GetHashCode(IResource obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
|
||||
readonly object sync = new();
|
||||
readonly Dictionary<IResource, State> states =
|
||||
new(new ResourceReferenceComparer());
|
||||
readonly int maximumEntriesPerResource;
|
||||
|
||||
public ResourceJournalBuffer(int maximumEntriesPerResource = 10000)
|
||||
{
|
||||
if (maximumEntriesPerResource < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(maximumEntriesPerResource));
|
||||
this.maximumEntriesPerResource = maximumEntriesPerResource;
|
||||
}
|
||||
|
||||
public ResourceCursor OpenJournal(
|
||||
IResource resource,
|
||||
string resourceKey,
|
||||
ResourceCursor proposedCursor)
|
||||
{
|
||||
if (resource == null) throw new ArgumentNullException(nameof(resource));
|
||||
lock (sync)
|
||||
{
|
||||
if (states.TryGetValue(resource, out var existing))
|
||||
return existing.Head;
|
||||
|
||||
var generation = proposedCursor.Generation == Guid.Empty
|
||||
? Guid.NewGuid()
|
||||
: proposedCursor.Generation;
|
||||
var cursor = new ResourceCursor(generation, proposedCursor.Revision);
|
||||
states.Add(resource, new State { Head = cursor });
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AppendJournalEntry(IResource resource, ResourceJournalEntry entry, bool retain)
|
||||
{
|
||||
if (resource == null) throw new ArgumentNullException(nameof(resource));
|
||||
if (entry == null) throw new ArgumentNullException(nameof(entry));
|
||||
|
||||
lock (sync)
|
||||
{
|
||||
if (!states.TryGetValue(resource, out var state))
|
||||
{
|
||||
state = new State { Head = new ResourceCursor(entry.Cursor.Generation, 0) };
|
||||
states.Add(resource, state);
|
||||
}
|
||||
|
||||
if (state.Head.Generation != entry.Cursor.Generation ||
|
||||
entry.Cursor.Revision <= state.Head.Revision)
|
||||
return false;
|
||||
|
||||
state.Head = entry.Cursor;
|
||||
if (retain)
|
||||
state.Entries.AddLast(entry);
|
||||
|
||||
while (state.Entries.Count > maximumEntriesPerResource)
|
||||
{
|
||||
state.DiscardedThroughRevision = Math.Max(
|
||||
state.DiscardedThroughRevision,
|
||||
state.Entries.First.Value.Cursor.Revision);
|
||||
state.Entries.RemoveFirst();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceJournalPage QueryJournal(IResource resource, ResourceJournalQuery query)
|
||||
{
|
||||
if (resource == null) throw new ArgumentNullException(nameof(resource));
|
||||
query ??= new ResourceJournalQuery();
|
||||
var limit = Math.Max(1, Math.Min(query.Limit, 10000));
|
||||
|
||||
lock (sync)
|
||||
{
|
||||
var proposed = new ResourceCursor(Guid.NewGuid(), 0);
|
||||
if (!states.TryGetValue(resource, out var state))
|
||||
{
|
||||
state = new State { Head = proposed };
|
||||
states.Add(resource, state);
|
||||
}
|
||||
|
||||
var after = query.After;
|
||||
var generationMismatch = after.Generation != Guid.Empty &&
|
||||
after.Generation != state.Head.Generation;
|
||||
var expired = generationMismatch ||
|
||||
(!generationMismatch &&
|
||||
after.Revision < state.DiscardedThroughRevision);
|
||||
|
||||
if (generationMismatch)
|
||||
after = new ResourceCursor(state.Head.Generation, 0);
|
||||
|
||||
IEnumerable<ResourceJournalEntry> filtered = state.Entries
|
||||
.Where(entry => entry.Cursor.Revision > after.Revision);
|
||||
|
||||
if (query.ThroughRevision.HasValue)
|
||||
filtered = filtered.Where(entry => entry.Cursor.Revision <= query.ThroughRevision.Value);
|
||||
if (query.FromTime.HasValue)
|
||||
filtered = filtered.Where(entry => entry.RecordedAt >= query.FromTime.Value.ToUniversalTime());
|
||||
if (query.ToTime.HasValue)
|
||||
filtered = filtered.Where(entry => entry.RecordedAt <= query.ToTime.Value.ToUniversalTime());
|
||||
if (query.Kind.HasValue)
|
||||
filtered = filtered.Where(entry => entry.Kind == query.Kind.Value);
|
||||
if (query.MemberIndex.HasValue)
|
||||
filtered = filtered.Where(entry => entry.MemberIndex == query.MemberIndex.Value);
|
||||
|
||||
var selected = filtered.Take(limit + 1).ToArray();
|
||||
var hasMore = selected.Length > limit;
|
||||
var entries = hasMore ? selected.Take(limit).ToArray() : selected;
|
||||
var next = entries.Length == 0 ? after : entries[entries.Length - 1].Cursor;
|
||||
var oldest = state.Entries.First?.Value.Cursor ?? state.Head;
|
||||
|
||||
return new ResourceJournalPage(
|
||||
oldest,
|
||||
state.Head,
|
||||
next,
|
||||
expired,
|
||||
hasMore,
|
||||
entries);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveJournal(IResource resource)
|
||||
{
|
||||
if (resource == null) return;
|
||||
lock (sync) states.Remove(resource);
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1036,14 @@ public class Warehouse
|
||||
|
||||
var resourceId = (uint)Interlocked.Increment(ref _resourceCounter);
|
||||
|
||||
resource.Instance = new Instance(this, resourceId, instanceName, resource, store, resourceContext?.Age ?? 0);
|
||||
resource.Instance = new Instance(
|
||||
this,
|
||||
resourceId,
|
||||
instanceName,
|
||||
resource,
|
||||
store,
|
||||
resourceContext?.Age ?? 0,
|
||||
string.Join("/", location));
|
||||
|
||||
resource.Instance.Managers.AddRange(resourceManagers);
|
||||
|
||||
@@ -1310,6 +1317,48 @@ public class Warehouse
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a definition supplied by an <see cref="IDynamicResource"/>
|
||||
/// without replacing its stable identifier. Dynamic definitions are not
|
||||
/// backed by a CLR <see cref="LocalTypeDef"/>, so they cannot use the
|
||||
/// incremental local registration path, but remote peers must still be
|
||||
/// able to resolve them through TypeDefById.
|
||||
/// </summary>
|
||||
internal void RegisterDynamicTypeDef(TypeDef typeDef)
|
||||
{
|
||||
if (typeDef == null)
|
||||
throw new ArgumentNullException(nameof(typeDef));
|
||||
|
||||
lock (_typeDefsLock)
|
||||
{
|
||||
// A remote definition keeps the producer's id for wire lookups, but
|
||||
// TryRegisterRemoteTypeDef gives it a Warehouse-local id because remote
|
||||
// resource/record/enum id spaces can overlap. Dynamic proxy instances
|
||||
// must reuse that local registration instead of indexing by the wire id.
|
||||
var registrationId = typeDef is RemoteTypeDef remoteTypeDef
|
||||
&& remoteTypeDef.LocalTypeDefId != 0
|
||||
? remoteTypeDef.LocalTypeDefId
|
||||
: typeDef.Id;
|
||||
var existing = _localTypeDefs[registrationId];
|
||||
if (existing == null)
|
||||
{
|
||||
_localTypeDefs[registrationId] = typeDef;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(existing, typeDef) ||
|
||||
(existing.Kind == typeDef.Kind &&
|
||||
existing.Version == typeDef.Version &&
|
||||
string.Equals(existing.Name, typeDef.Name, StringComparison.Ordinal)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Dynamic TypeDef id {registrationId} is already registered by '{existing.Name}', not '{typeDef.Name}'.");
|
||||
}
|
||||
}
|
||||
|
||||
internal KeyList<TypeDefKind, KeyList<string , Type>> GetProxyTypesByDomain(string domain)
|
||||
{
|
||||
return _proxyTypeDefs[domain];
|
||||
@@ -1317,6 +1366,7 @@ public class Warehouse
|
||||
|
||||
public bool TryRegisterRemoteTypeDef(string domain, RemoteTypeDef typeDef)
|
||||
{
|
||||
domain = string.IsNullOrWhiteSpace(domain) ? "anonymous" : domain;
|
||||
lock (_typeDefsLock)
|
||||
{
|
||||
if (!_remoteTypeDefs.ContainsKey(domain))
|
||||
|
||||
Reference in New Issue
Block a user