Cyclicity
An object graph that has circular chain of references is cyclical.
Service library can process cyclic request object graphs and produce cyclic response graphs. It breaks down complicated cyclic requests into format that is processable by simple handlers.
However, there are a few considerations that handler implementations must adhere to:
- Result objects must be referable types.
- Result objects must be constructed in two phases. Initial reference must be assigned right after construction.
- Cache must be used for the request type.
- Other handlers must get initial reference with .GetInitialized().
- One handler must set the entry into Completed state e.g. with .SetValue().
Handler cycle may occur if rules are not followed. The most common cause is that request is non-cached.
Handlers must be carefully designed to avoid cycles, as the library does not always detect cyclic processing loop. Loop detection is kept simple to to conserve CPU and memory cycles in other cases.
Example
In the following example node count of an object graph is calculated. This example uses so called dynamic programming and utilizes cached results.
// Create service
IService service = Services.Create(new NodeHandler());
// Get count service
IService<NodeCount, int> countService = service.Cast<NodeCount, int>();
// Create nodes
Node root = new("root"), a = new("a"), b = new("b"), c = new("c");
// Cyclic graph: root->a, a->b, b->c, c->a
root.Edges.Add(a); a.Edges.Add(b); b.Edges.Add(c); c.Edges.Add(a);
// Count number of nodes for each start node
WriteLine(countService.GetRequired(root)); // "4"
WriteLine(countService.GetRequired(a)); // "3"
WriteLine(countService.GetRequired(b)); // "3"
WriteLine(countService.GetRequired(c)); // "3"
Node class has name and forward edges.
public record Node(string name)
{
/// <summary>Forward reference edge</summary>
public List<Node> Edges = new();
}
NodeCount is request to calculate node count.
/// <summary>Request to calculate node count in <paramref name="node"/>.</summary>
public record struct NodeCount(Node node) : IRequestFor<int>, IRequestToBeCached
{
public static implicit operator NodeCount(Node node) => new NodeCount(node);
}
NodeTraverse is request to traverse graph on forward edges.
/// <summary>Request to traverse forward edges</summary>
public record struct NodeTraverse(Node node) : IRequestFor<HashSet<Node>>, IRequestToBeCached
{
public static implicit operator NodeTraverse(Node node) => new NodeTraverse(node);
}
NodeHandler processes node requests
/// <summary>Calculates nodes</summary>
public class NodeHandler : IHandler<NodeCount, int>, IHandler<NodeTraverse, HashSet<Node>>
{
/// <summary>Count nodes</summary>
public void Handle(IQuery<NodeCount, int> query)
{
// Already handled
if (query.Handled()) return;
// Traverse node graph
HashSet<Node> traverse = query.Service.GetRequired<NodeTraverse, HashSet<Node>>(query.Request.node, query.CancellationToken, query.Context);
// Assign count
query.Response.SetValue(traverse.Count);
}
/// <summary>Traverse node graph</summary>
public void Handle(IQuery<NodeTraverse, HashSet<Node>> query)
{
// Already handled
if (query.Handled()) return;
// Get node
Node node = query.Request.node;
// Create traverse set
HashSet<Node> traverse = new(node.Edges.Count + 1);
// Add self and direct forward edges
traverse.Add(node);
traverse.AddRange(node.Edges);
// Assign initial value (required)
query.Response.SetValue(traverse);
// Remember the count that was in the initial assignment
int initialCount = traverse.Count;
// Add children
foreach (Node child in node.Edges)
{
// Get entry to initial value, same as service.GetInitialized(), but returns entry instead
IEntry<HashSet<Node>> entry = ((IEntryProvider)query.Service).GetEntry<NodeTraverse, HashSet<Node>>(child, query.CancellationToken, query.Context)!;
// Add initial values
traverse.AddRange(entry.Value<HashSet<Node>>()!);
// Add state change listener
((IEntryObservable)entry).Subscribe(@event =>
{
// Process on Value update, but not on dispose
if ((@event.NewState.Status & (EntryStatus.Value | EntryStatus.ValueDisposed | EntryStatus.Disposed)) != EntryStatus.Value) return;
// Get nodes
HashSet<Node>? nodes = @event.NewState.Value as HashSet<Node>;
// No nodes
if (nodes == null) return;
// Get count
int prevCount = traverse.Count;
// Add nodes from listened object
traverse.AddRange(nodes);
// No change
if (traverse.Count == prevCount) return;
// Invoke listeners of change in result
query.Response.SetValue(traverse);
});
}
// Invoke listeners of change in result
if (traverse.Count != initialCount) query.Response.SetValue(traverse);
}
}
If intention is not to keep the cache, then a transient scope is needed.
// Create service
IService service = Services.Create((ServiceHandlers.Instance, ServiceScopeHandler.Instance, new NodeHandler()));
// Create nodes
Node root = new("root"), a = new("a"), b = new("b"), c = new("c");
// Cyclic graph: root->a, a->b, b->c, c->a
root.Edges.Add(a); a.Edges.Add(b); b.Edges.Add(c); c.Edges.Add(a);
// Create transient scope
using IServiceScope scope = ((IServiceScopeFactory)service.GetService(typeof(IServiceScopeFactory))!).CreateScope();
// Get transient scoped count service
IService<NodeCount, int> countService = ((IService)scope.ServiceProvider).Cast<NodeCount, int>();
// Count number of nodes for each start node
WriteLine(countService.GetRequired(root)); // "4"
WriteLine(countService.GetRequired(a)); // "3"
WriteLine(countService.GetRequired(b)); // "3"
WriteLine(countService.GetRequired(c)); // "3"
scope.Dispose();
Full Example
Full example
using System.Collections.Generic;
using Avalanche.Service;
using Avalanche.Utilities;
using Microsoft.Extensions.DependencyInjection;
using static System.Console;
public class handler_cyclic
{
public static void Run()
{
{
// <01>
// Create service
IService service = Services.Create(new NodeHandler());
// Get count service
IService<NodeCount, int> countService = service.Cast<NodeCount, int>();
// Create nodes
Node root = new("root"), a = new("a"), b = new("b"), c = new("c");
// Cyclic graph: root->a, a->b, b->c, c->a
root.Edges.Add(a); a.Edges.Add(b); b.Edges.Add(c); c.Edges.Add(a);
// Count number of nodes for each start node
WriteLine(countService.GetRequired(root)); // "4"
WriteLine(countService.GetRequired(a)); // "3"
WriteLine(countService.GetRequired(b)); // "3"
WriteLine(countService.GetRequired(c)); // "3"
// </01>
}
{
// <02>
// Create service
IService service = Services.Create((ServiceHandlers.Instance, ServiceScopeHandler.Instance, new NodeHandler()));
// Create nodes
Node root = new("root"), a = new("a"), b = new("b"), c = new("c");
// Cyclic graph: root->a, a->b, b->c, c->a
root.Edges.Add(a); a.Edges.Add(b); b.Edges.Add(c); c.Edges.Add(a);
// Create transient scope
using IServiceScope scope = ((IServiceScopeFactory)service.GetService(typeof(IServiceScopeFactory))!).CreateScope();
// Get transient scoped count service
IService<NodeCount, int> countService = ((IService)scope.ServiceProvider).Cast<NodeCount, int>();
// Count number of nodes for each start node
WriteLine(countService.GetRequired(root)); // "4"
WriteLine(countService.GetRequired(a)); // "3"
WriteLine(countService.GetRequired(b)); // "3"
WriteLine(countService.GetRequired(c)); // "3"
scope.Dispose();
// </02>
}
}
// <91>
public record Node(string name)
{
/// <summary>Forward reference edge</summary>
public List<Node> Edges = new();
}
// </91>
// <92>
/// <summary>Request to calculate node count in <paramref name="node"/>.</summary>
public record struct NodeCount(Node node) : IRequestFor<int>, IRequestToBeCached
{
public static implicit operator NodeCount(Node node) => new NodeCount(node);
}
// </92>
// <93>
/// <summary>Request to traverse forward edges</summary>
public record struct NodeTraverse(Node node) : IRequestFor<HashSet<Node>>, IRequestToBeCached
{
public static implicit operator NodeTraverse(Node node) => new NodeTraverse(node);
}
// </93>
// <94>
/// <summary>Calculates nodes</summary>
public class NodeHandler : IHandler<NodeCount, int>, IHandler<NodeTraverse, HashSet<Node>>
{
/// <summary>Count nodes</summary>
public void Handle(IQuery<NodeCount, int> query)
{
// Already handled
if (query.Handled()) return;
// Traverse node graph
HashSet<Node> traverse = query.Service.GetRequired<NodeTraverse, HashSet<Node>>(query.Request.node, query.CancellationToken, query.Context);
// Assign count
query.Response.SetValue(traverse.Count);
}
/// <summary>Traverse node graph</summary>
public void Handle(IQuery<NodeTraverse, HashSet<Node>> query)
{
// Already handled
if (query.Handled()) return;
// Get node
Node node = query.Request.node;
// Create traverse set
HashSet<Node> traverse = new(node.Edges.Count + 1);
// Add self and direct forward edges
traverse.Add(node);
traverse.AddRange(node.Edges);
// Assign initial value (required)
query.Response.SetValue(traverse);
// Remember the count that was in the initial assignment
int initialCount = traverse.Count;
// Add children
foreach (Node child in node.Edges)
{
// Get entry to initial value, same as service.GetInitialized(), but returns entry instead
IEntry<HashSet<Node>> entry = ((IEntryProvider)query.Service).GetEntry<NodeTraverse, HashSet<Node>>(child, query.CancellationToken, query.Context)!;
// Add initial values
traverse.AddRange(entry.Value<HashSet<Node>>()!);
// Add state change listener
((IEntryObservable)entry).Subscribe(@event =>
{
// Process on Value update, but not on dispose
if ((@event.NewState.Status & (EntryStatus.Value | EntryStatus.ValueDisposed | EntryStatus.Disposed)) != EntryStatus.Value) return;
// Get nodes
HashSet<Node>? nodes = @event.NewState.Value as HashSet<Node>;
// No nodes
if (nodes == null) return;
// Get count
int prevCount = traverse.Count;
// Add nodes from listened object
traverse.AddRange(nodes);
// No change
if (traverse.Count == prevCount) return;
// Invoke listeners of change in result
query.Response.SetValue(traverse);
});
}
// Invoke listeners of change in result
if (traverse.Count != initialCount) query.Response.SetValue(traverse);
}
}
// </94>
}