Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public MutationHistoryEntry HistoryEntry_FromGovernedExecution()
StateId = _fixture.Result.Request.StateId,
Intent = _fixture.Mutation.Intent,
Context = _fixture.Mutation.Context,
Changes = _fixture.Result.MutationResult!.Changes,
Changes = _fixture.Result.MutationResult!.Value.Changes,
SideEffects = _fixture.Result.Request.SideEffects.ToList(),
Timestamp = _fixture.Result.Request.Versioning.ExecutedAt ?? DateTimeOffset.UtcNow,
ExecutionTime = TimeSpan.FromMilliseconds(2)
Expand All @@ -50,17 +50,18 @@ public MutationHistoryEntry HistoryEntry_FromGovernedExecution()
[Benchmark]
public MutationAuditEntry AuditEntry_FromGovernedExecution()
{
var mr = _fixture.Result.MutationResult!.Value;
return new MutationAuditEntry
{
ExecutionId = _fixture.Result.Request.RequestId,
StateId = _fixture.Result.Request.StateId,
StateType = _fixture.Result.Request.StateType,
MutationIntent = _fixture.Mutation.Intent,
Context = _fixture.Mutation.Context,
Changes = _fixture.Result.MutationResult!.Changes,
IsSuccess = _fixture.Result.MutationResult.IsSuccess,
Changes = mr.Changes,
IsSuccess = mr.IsSuccess,
ErrorMessage = null,
PolicyDecisions = _fixture.Result.MutationResult.PolicyDecisions,
PolicyDecisions = mr.PolicyDecisions,
SideEffects = _fixture.Result.Request.SideEffects.ToList(),
Timestamp = _fixture.Result.Request.Versioning.ExecutedAt ?? DateTimeOffset.UtcNow,
Duration = TimeSpan.FromMilliseconds(2),
Expand Down
47 changes: 24 additions & 23 deletions Benchmarks/Results/MutationOutputMaterializationBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,70 +1,71 @@
using BenchmarkDotNet.Attributes;
using ModularityKit.Mutator.Abstractions.Audit;
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Effects;
using ModularityKit.Mutator.Abstractions.History;
using ModularityKit.Mutator.Abstractions.Intent;
using ModularityKit.Mutator.Abstractions.Results;
using ModularityKit.Mutator.Benchmarks.Results.Support;

namespace ModularityKit.Mutator.Benchmarks.Results;

/// <summary>
/// Benchmarks materialization of history and audit output from an executed mutation result.
/// </summary>
[BenchmarkCategory("Results")]
[MemoryDiagnoser]
[InProcess]
public class MutationOutputMaterializationBenchmarks
{
private MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> _result = null!;
private MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> _result = default!;
private string _executionId = string.Empty;
private TimeSpan _duration;
private IReadOnlyList<SideEffect> _sideEffectList = null!;
private MutationIntent _historyIntent = null!;
private MutationIntent _auditIntent = null!;
private MutationContext _historyContext = null!;
private MutationContext _auditContext = null!;

/// <summary>
/// Prepares a representative executed mutation result for output materialization benchmarks.
/// </summary>
[GlobalSetup]
public void Setup()
{
_result = ResultsBenchmarkSupport.CreateExecutedResult(sideEffectCount: 3, changeCount: 4);
_executionId = "results-benchmark-execution";
_duration = TimeSpan.FromMilliseconds(2);
_sideEffectList = _result.SideEffects.ToList();
_historyIntent = ResultsBenchmarkSupport.CreateIntent(
"ResultHistoryMaterialization",
"Materialize history output for benchmark results.");
_auditIntent = ResultsBenchmarkSupport.CreateIntent(
"ResultAuditMaterialization",
"Materialize audit output for benchmark results.");
_historyContext = ResultsBenchmarkSupport.CreateContext("history");
_auditContext = ResultsBenchmarkSupport.CreateContext("audit");
}

/// <summary>
/// Measures materialization of the mutation history entry, including change and side effect copying.
/// </summary>
[Benchmark(Baseline = true)]
public MutationHistoryEntry HistoryEntry_Materialization()
{
return new MutationHistoryEntry
{
ExecutionId = _executionId,
StateId = ResultsBenchmarkSupport.StateId,
Intent = ResultsBenchmarkSupport.CreateIntent(
"ResultHistoryMaterialization",
"Materialize history output for benchmark results."),
Context = ResultsBenchmarkSupport.CreateContext("history"),
Intent = _historyIntent,
Context = _historyContext,
Changes = _result.Changes,
SideEffects = _result.SideEffects.ToList(),
SideEffects = _sideEffectList,
Timestamp = DateTimeOffset.UtcNow,
ExecutionTime = _duration
};
}

/// <summary>
/// Measures materialization of the audit entry produced from the same executed mutation result.
/// </summary>
[Benchmark]
public MutationAuditEntry AuditEntry_Materialization()
{
return new MutationAuditEntry
{
ExecutionId = _executionId,
StateId = ResultsBenchmarkSupport.StateId,
StateType = nameof(ResultsBenchmarkSupport.ResultBenchmarkState),
MutationIntent = ResultsBenchmarkSupport.CreateIntent(
"ResultAuditMaterialization",
"Materialize audit output for benchmark results."),
Context = ResultsBenchmarkSupport.CreateContext("audit"),
StateType = "ResultBenchmarkState",
MutationIntent = _auditIntent,
Context = _auditContext,
Changes = _result.Changes,
IsSuccess = _result.IsSuccess,
ErrorMessage = null,
Expand Down
22 changes: 9 additions & 13 deletions Benchmarks/Results/MutationResultCreationBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ namespace ModularityKit.Mutator.Benchmarks.Results;
[InProcess]
public class MutationResultCreationBenchmarks
{
private ResultsBenchmarkSupport.ResultBenchmarkState _state = null!;
private ResultsBenchmarkSupport.ResultBenchmarkState _state = default!;
private ChangeSet _changes = null!;
private IReadOnlyList<SideEffect> _singleSideEffect = null!;
private IReadOnlyList<SideEffect> _multipleSideEffects = null!;

/// <summary>
/// Prepares the shared state and change set used by the result creation cases.
/// Prepares the shared state, change set, and side effect lists used by the result creation cases.
/// </summary>
[GlobalSetup]
public void Setup()
{
_state = new ResultsBenchmarkSupport.ResultBenchmarkState(0, 42);
_changes = ResultsBenchmarkSupport.CreateChangeSet(_state.Revision, 2);
_singleSideEffect = ResultsBenchmarkSupport.CreateSideEffects(1);
_multipleSideEffects = ResultsBenchmarkSupport.CreateSideEffects(4);
}

/// <summary>
Expand All @@ -45,22 +49,14 @@ _state with
/// </summary>
[Benchmark]
public MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> Success_SingleSideEffect()
{
var sideEffect = SideEffect.Create(
"ResultMaterialization",
"Single side effect",
new ResultsBenchmarkSupport.SideEffectPayload(1, "single"),
SideEffectSeverity.Info);

return MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
=> MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
_state with
{
Revision = _state.Revision + 1,
Value = _state.Value + 1
},
_changes,
[sideEffect]);
}
_singleSideEffect);

/// <summary>
/// Measures creation of a successful mutation result with several side effects.
Expand All @@ -74,5 +70,5 @@ _state with
Value = _state.Value + 1
},
_changes,
ResultsBenchmarkSupport.CreateSideEffects(4));
_multipleSideEffects);
}
4 changes: 2 additions & 2 deletions Benchmarks/Results/Support/ResultsBenchmarkSupport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ public static MutationResult<ResultBenchmarkState> CreateExecutedResult(
/// </summary>
/// <param name="Revision">The revision counter advanced on each benchmark mutation.</param>
/// <param name="Value">The mutable numeric value exercised by the benchmark mutation.</param>
public sealed record ResultBenchmarkState(int Revision, int Value);
public readonly record struct ResultBenchmarkState(int Revision, int Value);

/// <summary>
/// Typed payload used to give side effects realistic materialization shape.
/// </summary>
/// <param name="Index">The ordinal of the side effect.</param>
/// <param name="Token">A stable payload token.</param>
public sealed record SideEffectPayload(int Index, string Token);
public readonly record struct SideEffectPayload(int Index, string Token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public async Task ExecuteApproved_executes_operator_rollback_compensation_and_li
var compensationResult = await executionManager.ExecuteApproved(
compensationRequest.RequestId,
compensationMutation,
originalResult.MutationResult!.NewState!,
originalResult.MutationResult!.Value.NewState!,
governanceContext: MutationContext.Service("governance-runtime", "Execute operator rollback"),
strategy: VersionedRequestResolutionStrategy.RejectStale);

Expand Down
16 changes: 8 additions & 8 deletions src/Abstractions/Audit/MutationAuditEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@
namespace ModularityKit.Mutator.Abstractions.Audit;

/// <summary>
/// Represents a single audit record for a mutation operation.
/// Represents single audit record for a mutation operation.
/// Captures the intent, context, changes, policy decisions, and metadata of the mutation.
/// </summary>
/// <remarks>
/// Mutation audit entries are used for compliance, traceability, debugging, and monitoring.
/// Each entry is immutable once created. Typical consumers include auditors, logging systems,
/// or analytics pipelines.
/// </remarks>
public sealed class MutationAuditEntry
public readonly record struct MutationAuditEntry
{
/// <summary>
/// Unique execution identifier for this mutation.
/// </summary>
public string ExecutionId { get; init; } = string.Empty;
public string ExecutionId { get; init; }

/// <summary>
/// Identifier of the state object that was mutated.
Expand All @@ -35,17 +35,17 @@ public sealed class MutationAuditEntry
/// <summary>
/// The intent describing what the mutation is trying to achieve.
/// </summary>
public MutationIntent MutationIntent { get; init; } = null!;
public MutationIntent MutationIntent { get; init; }

/// <summary>
/// Context of the mutation (e.g., correlation data, user context).
/// </summary>
public MutationContext Context { get; init; } = null!;
public MutationContext Context { get; init; }

/// <summary>
/// Changes applied by the mutation.
/// </summary>
public ChangeSet Changes { get; init; } = ChangeSet.Empty;
public ChangeSet Changes { get; init; }

/// <summary>
/// Indicates whether the mutation was successful.
Expand All @@ -60,12 +60,12 @@ public sealed class MutationAuditEntry
/// <summary>
/// Decisions made by policies during the mutation evaluation.
/// </summary>
public IReadOnlyList<PolicyDecision> PolicyDecisions { get; init; } = [];
public IReadOnlyList<PolicyDecision> PolicyDecisions { get; init; }

/// <summary>
/// Side effects produced during the mutation.
/// </summary>
public IReadOnlyList<SideEffect> SideEffects { get; init; } = [];
public IReadOnlyList<SideEffect> SideEffects { get; init; }

/// <summary>
/// Timestamp when the mutation started.
Expand Down
13 changes: 7 additions & 6 deletions src/Abstractions/Changes/ChangeSet.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
namespace ModularityKit.Mutator.Abstractions.Changes;

/// <summary>
/// Represents a collection of state changes introduced by a mutation.
/// This is a **primary feature** of a mutation and not an optional addition.
/// Represents collection of state changes introduced by a mutation.
/// This is primary feature of mutation and not an optional addition.
/// </summary>
public sealed class ChangeSet
{
private static readonly ChangeSet _empty = new();
private readonly List<StateChange> _changes = [];

/// <summary>
Expand Down Expand Up @@ -34,7 +35,7 @@ public sealed class ChangeSet
public string? Checksum { get; init; }

/// <summary>
/// Adds a new state change to the changeset.
/// Adds new state change to the changeset.
/// </summary>
/// <param name="change">The state change to add.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="change"/> is null.</exception>
Expand All @@ -45,15 +46,15 @@ public void Add(StateChange change)
}

/// <summary>
/// Retrieves all changes corresponding to a specific path.
/// Retrieves all changes corresponding to specific path.
/// </summary>
/// <param name="path">The path to filter changes.</param>
/// <returns>Enumerable of <see cref="StateChange"/> matching the path.</returns>
public IEnumerable<StateChange> GetChanges(string path)
=> _changes.Where(c => c.Path == path);

/// <summary>
/// Determines whether a specific path has been changed.
/// Determines whether specific path has been changed.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>True if the path has been changed; otherwise, false.</returns>
Expand All @@ -70,7 +71,7 @@ public IEnumerable<string> GetChangedPaths()
/// <summary>
/// Returns an empty changeset.
/// </summary>
public static ChangeSet Empty => new();
public static ChangeSet Empty => _empty;

/// <summary>
/// Creates changeset containing single state change.
Expand Down
20 changes: 17 additions & 3 deletions src/Abstractions/Effects/SideEffect.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Text.Json.Serialization;

namespace ModularityKit.Mutator.Abstractions.Effects;
Expand Down Expand Up @@ -189,20 +190,33 @@ private static SideEffect CreateCore(
};
}

private static readonly ConcurrentDictionary<Type, (string? ContractType, int? ContractVersion)> _contractCache = new();

private static (string? ContractType, int? ContractVersion) ResolveContract(object? data)
{
if (data is null)
return (null, null);

var dataType = data.GetType();
if (_contractCache.TryGetValue(dataType, out var cached))
return cached;

var contract = dataType.GetCustomAttributes(typeof(SideEffectDataContractAttribute), inherit: false)
.OfType<SideEffectDataContractAttribute>()
.SingleOrDefault();

(string?, int?) result;
if (contract is null)
return (null, null);
{
result = (null, null);
}
else
{
SideEffectDataContractRegistry.Register(dataType);
result = (contract.ContractType, contract.ContractVersion);
}

SideEffectDataContractRegistry.Register(dataType);
return (contract.ContractType, contract.ContractVersion);
_contractCache.TryAdd(dataType, result);
return result;
}
}
8 changes: 4 additions & 4 deletions src/Abstractions/History/MutationHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
namespace ModularityKit.Mutator.Abstractions.History;

/// <summary>
/// Represents the full mutation history of a specific state object.
/// Represents the full mutation history of specific state object.
/// </summary>
/// <remarks>
/// MutationHistory stores a chronological sequence of <see cref="MutationHistoryEntry"/> entries.
/// MutationHistory stores chronological sequence of <see cref="MutationHistoryEntry"/> entries.
/// It allows replaying state changes, querying timelines for specific paths, and computing statistics.
/// This is typically used in combination with <see cref="IMutationHistoryStore"/> to persist and retrieve histories.
/// </remarks>
Expand All @@ -32,13 +32,13 @@ public sealed class MutationHistory
/// Timestamp of the first mutation in the history.
/// </summary>
public DateTimeOffset? FirstMutationAt
=> Entries.FirstOrDefault()?.Timestamp;
=> Entries.Count > 0 ? Entries[0].Timestamp : null;

/// <summary>
/// Timestamp of the last mutation in the history.
/// </summary>
public DateTimeOffset? LastMutationAt
=> Entries.LastOrDefault()?.Timestamp;
=> Entries.Count > 0 ? Entries[^1].Timestamp : null;

/// <summary>
/// Total number of mutations recorded.
Expand Down
Loading
Loading