Compare commits
9 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
3f53222b57 | |
|
|
574cadc0a1 | |
|
|
c2360c2e9e | |
|
|
d32dc19f6a | |
|
|
a5243e4cef | |
|
|
05f8b3e887 | |
|
|
c8803c6096 | |
|
|
871bf728a7 | |
|
|
80f78dceeb |
|
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## 1.0.1
|
||||||
|
|
||||||
|
- `NexusTimeSession` catalog/ops extras (ISP: not on `ITimeSession`): `ListSeries`,
|
||||||
|
`GetCardinality`, `ApplyRetention` over Wire.V1 RPCs with DTOs
|
||||||
|
`TimeSeriesCatalogEntry`, `TimeCardinalitySnapshot`, `TimeMetricCardinality`.
|
||||||
|
- Suite-layout ProjectReference resolution for `Mishima.Nexus.Wire.V1` (alongside sibling monorepo path).
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
- Initial `NexusClient`: connects via `nexus+graph|time|document|relational://` URIs (explicit port
|
- Initial `NexusClient`: connects via `nexus+graph|time|document|relational://` URIs (explicit port
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.0.1</Version>
|
||||||
<Product>Robotico.Nexus.Client</Product>
|
<Product>Robotico.Nexus.Client</Product>
|
||||||
<PackageId>Robotico.Nexus.Client</PackageId>
|
<PackageId>Robotico.Nexus.Client</PackageId>
|
||||||
<Title>Robotico.Nexus.Client</Title>
|
<Title>Robotico.Nexus.Client</Title>
|
||||||
|
|
@ -39,6 +39,11 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<RoboticoGraphHubProjectPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\robotico-graph-csharp\src\Robotico.Graph\Robotico.Graph.csproj'))</RoboticoGraphHubProjectPath>
|
<RoboticoGraphHubProjectPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\robotico-graph-csharp\src\Robotico.Graph\Robotico.Graph.csproj'))</RoboticoGraphHubProjectPath>
|
||||||
<RoboticoTimeHubProjectPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\robotico-time-csharp\src\Robotico.Time\Robotico.Time.csproj'))</RoboticoTimeHubProjectPath>
|
<RoboticoTimeHubProjectPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\robotico-time-csharp\src\Robotico.Time\Robotico.Time.csproj'))</RoboticoTimeHubProjectPath>
|
||||||
<MishimaNexusWireV1ProjectPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\mishima-suite\mishima-nexus\src\Mishima.Nexus.Wire.V1\Mishima.Nexus.Wire.V1.csproj'))</MishimaNexusWireV1ProjectPath>
|
<!-- Suite layout: robotico-patterns lives under mishima-suite/ -->
|
||||||
|
<_MishimaNexusWireV1Suite>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\mishima-nexus\src\Mishima.Nexus.Wire.V1\Mishima.Nexus.Wire.V1.csproj'))</_MishimaNexusWireV1Suite>
|
||||||
|
<!-- Sibling layout: robotico-patterns next to mishima-suite/ -->
|
||||||
|
<_MishimaNexusWireV1Sibling>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\mishima-suite\mishima-nexus\src\Mishima.Nexus.Wire.V1\Mishima.Nexus.Wire.V1.csproj'))</_MishimaNexusWireV1Sibling>
|
||||||
|
<MishimaNexusWireV1ProjectPath Condition="Exists('$(_MishimaNexusWireV1Suite)')">$(_MishimaNexusWireV1Suite)</MishimaNexusWireV1ProjectPath>
|
||||||
|
<MishimaNexusWireV1ProjectPath Condition="'$(MishimaNexusWireV1ProjectPath)' == '' and Exists('$(_MishimaNexusWireV1Sibling)')">$(_MishimaNexusWireV1Sibling)</MishimaNexusWireV1ProjectPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
|
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
|
||||||
<PackageVersion Include="Robotico.Result" Version="1.0.3" />
|
<PackageVersion Include="Robotico.Result" Version="1.0.4" />
|
||||||
<PackageVersion Include="Robotico.Graph" Version="1.0.0" />
|
<PackageVersion Include="Robotico.Graph" Version="1.0.0" />
|
||||||
<PackageVersion Include="Robotico.Time" Version="1.0.0" />
|
<PackageVersion Include="Robotico.Time" Version="1.0.0" />
|
||||||
<PackageVersion Include="Mishima.Nexus.Wire.V1" Version="0.4.9" />
|
<PackageVersion Include="Mishima.Nexus.Wire.V1" Version="0.4.9" />
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -1,16 +1,54 @@
|
||||||
# Robotico.Nexus.Client
|
[](https://dvalin.robotico.dev/mishima/mishima-nexus-client-csharp)
|
||||||
|
|
||||||
Unified client SDK for a remote [Mishima Nexus](https://brokkr.robotico.dev/robotico/mishima-suite)
|
[](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||||
|
[](https://learn.microsoft.com/dotnet/csharp/)
|
||||||
|
[](https://grpc.io/)
|
||||||
|
[](https://download.robotico.dev/nuget/)
|
||||||
|
[](https://brokkr.robotico.dev/mishima/mishima-nexus-client-csharp)
|
||||||
|
|
||||||
|
# mishima-nexus-client-csharp
|
||||||
|
|
||||||
|
C# client SDK for a remote [Mishima Nexus](https://brokkr.robotico.dev/mishima/mishima-nexus)
|
||||||
server: connect via a `nexus+*://` URI and get `Robotico.Graph` / `Robotico.Time` ports bound to a
|
server: connect via a `nexus+*://` URI and get `Robotico.Graph` / `Robotico.Time` ports bound to a
|
||||||
logical database over gRPC, plus thin Document/Relational connect helpers.
|
logical database over gRPC, plus thin Document/Relational connect helpers.
|
||||||
|
|
||||||
- NuGet: `Robotico.Nexus.Client`
|
- NuGet: `Robotico.Nexus.Client`
|
||||||
- Feed: `https://download.robotico.dev/nuget/v3/index.json`
|
- Feed: `https://download.robotico.dev/nuget/v3/index.json`
|
||||||
- Brokkr: `https://brokkr.robotico.dev/robotico/robotico-nexus-client-csharp`
|
- Brokkr: `https://brokkr.robotico.dev/mishima/mishima-nexus-client-csharp`
|
||||||
|
|
||||||
This package does **not** reference `Mishima.Nexus.Node.*` (the server host assemblies) — it only
|
This package does **not** reference `Mishima.Nexus.Node.*` (the server host assemblies) — it only
|
||||||
depends on the generated `Mishima.Nexus.Wire.V1` gRPC stubs, `Robotico.Graph`, and `Robotico.Time`.
|
depends on the generated `Mishima.Nexus.Wire.V1` gRPC stubs, `Robotico.Graph`, and `Robotico.Time`.
|
||||||
|
|
||||||
|
<!-- repo-uses:start -->
|
||||||
|
## Uses
|
||||||
|
|
||||||
|
This repository uses:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
mishima_nexus_client_csharp["mishima-nexus-client-csharp"]
|
||||||
|
mishima_nexus["mishima-nexus"]
|
||||||
|
mishima_nexus_client_csharp --> mishima_nexus
|
||||||
|
```
|
||||||
|
<!-- repo-uses:end -->
|
||||||
|
|
||||||
|
<!-- repo-used-by:start -->
|
||||||
|
## Used by
|
||||||
|
|
||||||
|
This repository is used by:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
mishima_nexus_client_csharp["mishima-nexus-client-csharp"]
|
||||||
|
mishima_nexus["mishima-nexus"]
|
||||||
|
mishima_nexus_wpf["mishima-nexus-wpf"]
|
||||||
|
tanukiarmy_bot_ng["tanukiarmy-bot-ng"]
|
||||||
|
mishima_nexus_client_csharp --> mishima_nexus
|
||||||
|
mishima_nexus_client_csharp --> mishima_nexus_wpf
|
||||||
|
mishima_nexus_client_csharp --> tanukiarmy_bot_ng
|
||||||
|
```
|
||||||
|
<!-- repo-used-by:end -->
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<WarningLevel>8</WarningLevel>
|
<WarningLevel>8</WarningLevel>
|
||||||
|
<WarningLevel Condition="'$(MSBuildProjectExtension)' == '.fsproj'">5</WarningLevel>
|
||||||
<AnalysisLevel>latest-all</AnalysisLevel>
|
<AnalysisLevel>latest-all</AnalysisLevel>
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,29 @@ public sealed class NexusDocumentClient
|
||||||
return NexusWireStatus.ToResult(response.Status, body);
|
return NexusWireStatus.ToResult(response.Status, body);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads many documents in one RPC. Missing ids are omitted from the list.
|
||||||
|
/// </summary>
|
||||||
|
public Robotico.Result.Result<IReadOnlyList<NexusRetrievedDocument>> GetDocuments(
|
||||||
|
string collectionName,
|
||||||
|
IReadOnlyList<string> documentIds) => InvokeGeneric(() =>
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(documentIds);
|
||||||
|
DocumentCollectionGetManyRequest request = new() { CollectionName = collectionName };
|
||||||
|
request.DocumentIds.AddRange(documentIds);
|
||||||
|
DocumentCollectionGetManyResponse response = _client.CollectionGetMany(request, Metadata());
|
||||||
|
List<NexusRetrievedDocument> found = new(response.Documents.Count);
|
||||||
|
foreach (DocumentCollectionDocumentWire doc in response.Documents)
|
||||||
|
{
|
||||||
|
string body = doc.JsonUtf8.Length > 0
|
||||||
|
? System.Text.Encoding.UTF8.GetString(doc.JsonUtf8.Span)
|
||||||
|
: doc.JsonBody;
|
||||||
|
found.Add(new NexusRetrievedDocument(doc.DocumentId, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NexusWireStatus.ToResult(response.Status, (IReadOnlyList<NexusRetrievedDocument>)found);
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>Deletes a document by id when present (idempotent).</summary>
|
/// <summary>Deletes a document by id when present (idempotent).</summary>
|
||||||
public Robotico.Result.Result DeleteDocument(string collectionName, string documentId) => Invoke(() =>
|
public Robotico.Result.Result DeleteDocument(string collectionName, string documentId) => Invoke(() =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace Robotico.Nexus.Client.Document;
|
||||||
|
|
||||||
|
/// <summary>One document returned by <see cref="NexusDocumentClient.GetDocuments"/>.</summary>
|
||||||
|
public readonly record struct NexusRetrievedDocument(string DocumentId, string JsonBody);
|
||||||
|
|
@ -322,6 +322,108 @@ public sealed class NexusTimeSession : ITimeSession
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists registered series for explorers (Nexus wire extra; not on <see cref="ITimeSession"/>).
|
||||||
|
/// </summary>
|
||||||
|
public Robotico.Result.Result<IReadOnlyList<TimeSeriesCatalogEntry>> ListSeries(int limit = 1000)
|
||||||
|
{
|
||||||
|
Robotico.Result.Result disposed = EnsureNotDisposed();
|
||||||
|
if (disposed.IsError(out IError? disposedError))
|
||||||
|
{
|
||||||
|
return Robotico.Result.Result.Error<IReadOnlyList<TimeSeriesCatalogEntry>>(disposedError!);
|
||||||
|
}
|
||||||
|
|
||||||
|
int normalizedLimit = limit < 1 ? 1000 : limit;
|
||||||
|
return InvokeGeneric(() =>
|
||||||
|
{
|
||||||
|
TimeListSeriesResponse response = _client.ListSeries(
|
||||||
|
new TimeListSeriesRequest { Limit = normalizedLimit },
|
||||||
|
Metadata());
|
||||||
|
TimeSeriesCatalogEntry[] entries = new TimeSeriesCatalogEntry[response.Series.Count];
|
||||||
|
for (int i = 0; i < response.Series.Count; i++)
|
||||||
|
{
|
||||||
|
TimeSeriesCatalogEntryWire wire = response.Series[i];
|
||||||
|
Dictionary<string, string> labels = new(StringComparer.Ordinal);
|
||||||
|
foreach (TimeLabelPair pair in wire.Labels)
|
||||||
|
{
|
||||||
|
labels[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries[i] = new TimeSeriesCatalogEntry(
|
||||||
|
wire.SeriesId,
|
||||||
|
wire.MetricName,
|
||||||
|
labels,
|
||||||
|
wire.SampleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NexusWireStatus.ToResult(response.Status, (IReadOnlyList<TimeSeriesCatalogEntry>)entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cardinality inspection (Nexus wire extra; not on <see cref="ITimeSession"/>).
|
||||||
|
/// </summary>
|
||||||
|
public Robotico.Result.Result<TimeCardinalitySnapshot> GetCardinality(int topN = 20)
|
||||||
|
{
|
||||||
|
Robotico.Result.Result disposed = EnsureNotDisposed();
|
||||||
|
if (disposed.IsError(out IError? disposedError))
|
||||||
|
{
|
||||||
|
return Robotico.Result.Result.Error<TimeCardinalitySnapshot>(disposedError!);
|
||||||
|
}
|
||||||
|
|
||||||
|
int normalizedTopN = topN < 1 ? 20 : topN;
|
||||||
|
return InvokeGeneric(() =>
|
||||||
|
{
|
||||||
|
TimeGetCardinalityResponse response = _client.GetCardinality(
|
||||||
|
new TimeGetCardinalityRequest { TopN = normalizedTopN },
|
||||||
|
Metadata());
|
||||||
|
TimeMetricCardinality[] top =
|
||||||
|
[
|
||||||
|
.. response.TopMetricsBySeriesCount.Select(static m =>
|
||||||
|
new TimeMetricCardinality(m.MetricName, m.SeriesCount)),
|
||||||
|
];
|
||||||
|
TimeCardinalitySnapshot snap = new(
|
||||||
|
response.ActiveSeriesCount,
|
||||||
|
response.TotalSampleCount,
|
||||||
|
top);
|
||||||
|
return NexusWireStatus.ToResult(response.Status, snap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies retention cutoff (Nexus wire extra; not on <see cref="ITimeSession"/>).
|
||||||
|
/// Drops samples/events with timestamp strictly before
|
||||||
|
/// <paramref name="retainAfterExclusiveUnixNanos"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Removed sample/event count reported by the engine.</returns>
|
||||||
|
public Robotico.Result.Result<long> ApplyRetention(long retainAfterExclusiveUnixNanos)
|
||||||
|
{
|
||||||
|
Robotico.Result.Result disposed = EnsureNotDisposed();
|
||||||
|
if (disposed.IsError(out IError? disposedError))
|
||||||
|
{
|
||||||
|
return Robotico.Result.Result.Error<long>(disposedError!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retainAfterExclusiveUnixNanos <= 0)
|
||||||
|
{
|
||||||
|
return Robotico.Result.Result.Error<long>(
|
||||||
|
TimeErrors.Error(
|
||||||
|
"retainAfterExclusiveUnixNanos must be positive.",
|
||||||
|
TimeErrors.InvalidArgument));
|
||||||
|
}
|
||||||
|
|
||||||
|
return InvokeGeneric(() =>
|
||||||
|
{
|
||||||
|
TimeApplyRetentionResponse response = _client.ApplyRetention(
|
||||||
|
new TimeApplyRetentionRequest
|
||||||
|
{
|
||||||
|
RetainAfterExclusiveUnixNanos = retainAfterExclusiveUnixNanos,
|
||||||
|
},
|
||||||
|
Metadata());
|
||||||
|
return NexusWireStatus.ToResult(response.Status, response.RemovedCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose() => _disposed = true;
|
public void Dispose() => _disposed = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Robotico.Nexus.Client.Time;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cardinality snapshot from <see cref="NexusTimeSession.GetCardinality"/>.
|
||||||
|
/// Not on <c>ITimeSession</c> (ISP): ops inspection surface for explorers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ActiveSeriesCount">Active series in the logical database.</param>
|
||||||
|
/// <param name="TotalSampleCount">Total samples across all series.</param>
|
||||||
|
/// <param name="TopMetricsBySeriesCount">Top metrics by series fan-out.</param>
|
||||||
|
public sealed record TimeCardinalitySnapshot(
|
||||||
|
long ActiveSeriesCount,
|
||||||
|
long TotalSampleCount,
|
||||||
|
IReadOnlyList<TimeMetricCardinality> TopMetricsBySeriesCount);
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Robotico.Nexus.Client.Time;
|
||||||
|
|
||||||
|
/// <summary>Per-metric series count for cardinality inspection.</summary>
|
||||||
|
/// <param name="MetricName">Metric name.</param>
|
||||||
|
/// <param name="SeriesCount">Number of series with this metric name.</param>
|
||||||
|
public sealed record TimeMetricCardinality(string MetricName, long SeriesCount);
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace Robotico.Nexus.Client.Time;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One registered series from <see cref="NexusTimeSession.ListSeries"/>.
|
||||||
|
/// Not on <c>ITimeSession</c> (ISP): catalog ops are Nexus wire extras for explorers/ops.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SeriesId">Engine-assigned series id.</param>
|
||||||
|
/// <param name="MetricName">Metric name (Prometheus-style).</param>
|
||||||
|
/// <param name="Labels">Series labels (sorted as returned by the engine).</param>
|
||||||
|
/// <param name="SampleCount">Current sample count for the series.</param>
|
||||||
|
public sealed record TimeSeriesCatalogEntry(
|
||||||
|
ulong SeriesId,
|
||||||
|
string MetricName,
|
||||||
|
IReadOnlyDictionary<string, string> Labels,
|
||||||
|
int SampleCount);
|
||||||
|
|
@ -48,6 +48,22 @@ public class NexusDocumentClientTests : IAsyncLifetime
|
||||||
Assert.Equal("""{"name":"Ada"}""", body);
|
Assert.Equal("""{"name":"Ada"}""", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDocuments_ReturnsFound_AndOmitsMissing()
|
||||||
|
{
|
||||||
|
_documentClient.UpsertDocument("users", "u1", """{"name":"Ada"}""");
|
||||||
|
_documentClient.UpsertDocument("users", "u3", """{"name":"Grace"}""");
|
||||||
|
|
||||||
|
Robotico.Result.Result<IReadOnlyList<NexusRetrievedDocument>> fetched =
|
||||||
|
_documentClient.GetDocuments("users", ["u3", "missing", "u1"]);
|
||||||
|
|
||||||
|
Assert.True(fetched.IsSuccess(out IReadOnlyList<NexusRetrievedDocument>? docs, out _));
|
||||||
|
Assert.Equal(2, docs!.Count);
|
||||||
|
Assert.Equal("u3", docs[0].DocumentId);
|
||||||
|
Assert.Equal("""{"name":"Grace"}""", docs[0].JsonBody);
|
||||||
|
Assert.Equal("u1", docs[1].DocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetDocument_ReturnsNull_WhenNotFound()
|
public void GetDocument_ReturnsNull_WhenNotFound()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ using Mishima.Nexus.Wire.V1;
|
||||||
namespace Robotico.Nexus.Client.Tests.TestSupport;
|
namespace Robotico.Nexus.Client.Tests.TestSupport;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// In-memory fake standing in for a real Nexus document engine host — only the RPCs used by
|
/// In-memory fake standing in for a real Nexus document engine host — the RPCs used by
|
||||||
/// <see cref="Robotico.Nexus.Client.Document.NexusDocumentClient"/> (Ping + single-document CRUD).
|
/// <see cref="Robotico.Nexus.Client.Document.NexusDocumentClient"/> (Ping, CRUD, GetMany).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class FakeDocumentEngineService : NexusDocumentEngineService.NexusDocumentEngineServiceBase
|
public sealed class FakeDocumentEngineService : NexusDocumentEngineService.NexusDocumentEngineServiceBase
|
||||||
{
|
{
|
||||||
|
|
@ -36,6 +36,29 @@ public sealed class FakeDocumentEngineService : NexusDocumentEngineService.Nexus
|
||||||
return Task.FromResult(new DocumentGetResponse { Status = NexusStatuses.Ok, Found = false });
|
return Task.FromResult(new DocumentGetResponse { Status = NexusStatuses.Ok, Found = false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<DocumentCollectionGetManyResponse> CollectionGetMany(
|
||||||
|
DocumentCollectionGetManyRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
DocumentCollectionGetManyResponse response = new() { Status = NexusStatuses.Ok };
|
||||||
|
foreach (string id in request.DocumentIds)
|
||||||
|
{
|
||||||
|
if (!_documents.TryGetValue((request.CollectionName, id), out string? body))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Documents.Add(new DocumentCollectionDocumentWire
|
||||||
|
{
|
||||||
|
DocumentId = id,
|
||||||
|
JsonBody = body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<DocumentDeleteResponse> DeleteDocument(DocumentDeleteRequest request, ServerCallContext context)
|
public override Task<DocumentDeleteResponse> DeleteDocument(DocumentDeleteRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ namespace Robotico.Nexus.Client.Tests.TestSupport;
|
||||||
public sealed class FakeTimeEngineService : NexusTimeEngineService.NexusTimeEngineServiceBase
|
public sealed class FakeTimeEngineService : NexusTimeEngineService.NexusTimeEngineServiceBase
|
||||||
{
|
{
|
||||||
private readonly Dictionary<ulong, List<TimeSampleWire>> _samplesBySeries = [];
|
private readonly Dictionary<ulong, List<TimeSampleWire>> _samplesBySeries = [];
|
||||||
|
private readonly Dictionary<ulong, (string MetricName, IReadOnlyList<TimeLabelPair> Labels)> _seriesMeta = [];
|
||||||
private readonly Dictionary<ulong, List<TimeWideEventWire>> _eventsByTable = [];
|
private readonly Dictionary<ulong, List<TimeWideEventWire>> _eventsByTable = [];
|
||||||
private readonly Dictionary<ulong, List<TimeEventFieldWire>> _fieldsByTable = [];
|
private readonly Dictionary<ulong, List<TimeEventFieldWire>> _fieldsByTable = [];
|
||||||
private ulong _nextSeriesId = 1;
|
private ulong _nextSeriesId = 1;
|
||||||
private ulong _nextTableId = 1;
|
private ulong _nextTableId = 1;
|
||||||
private ulong _nextEventId = 1;
|
private ulong _nextEventId = 1;
|
||||||
|
private long _retentionRemoved;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<TimePingResponse> Ping(TimePingRequest request, ServerCallContext context)
|
public override Task<TimePingResponse> Ping(TimePingRequest request, ServerCallContext context)
|
||||||
|
|
@ -40,9 +42,76 @@ public sealed class FakeTimeEngineService : NexusTimeEngineService.NexusTimeEngi
|
||||||
{
|
{
|
||||||
ulong id = _nextSeriesId++;
|
ulong id = _nextSeriesId++;
|
||||||
_samplesBySeries[id] = [];
|
_samplesBySeries[id] = [];
|
||||||
|
_seriesMeta[id] = (request.MetricName, [.. request.Labels]);
|
||||||
return Task.FromResult(new TimeRegisterSeriesResponse { Status = NexusStatuses.Ok, SeriesId = id });
|
return Task.FromResult(new TimeRegisterSeriesResponse { Status = NexusStatuses.Ok, SeriesId = id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<TimeListSeriesResponse> ListSeries(TimeListSeriesRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
TimeListSeriesResponse response = new() { Status = NexusStatuses.Ok };
|
||||||
|
int limit = request.Limit is 0 ? int.MaxValue : request.Limit;
|
||||||
|
foreach ((ulong id, (string metric, IReadOnlyList<TimeLabelPair> labels)) in _seriesMeta.Take(limit))
|
||||||
|
{
|
||||||
|
TimeSeriesCatalogEntryWire entry = new()
|
||||||
|
{
|
||||||
|
SeriesId = id,
|
||||||
|
MetricName = metric,
|
||||||
|
SampleCount = _samplesBySeries.TryGetValue(id, out List<TimeSampleWire>? samples) ? samples.Count : 0,
|
||||||
|
};
|
||||||
|
entry.Labels.AddRange(labels);
|
||||||
|
response.Series.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<TimeGetCardinalityResponse> GetCardinality(TimeGetCardinalityRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
Dictionary<string, int> byMetric = new(StringComparer.Ordinal);
|
||||||
|
foreach ((string metric, _) in _seriesMeta.Values)
|
||||||
|
{
|
||||||
|
byMetric[metric] = byMetric.GetValueOrDefault(metric) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int topN = request.TopN is 0 ? 20 : request.TopN;
|
||||||
|
TimeGetCardinalityResponse response = new()
|
||||||
|
{
|
||||||
|
Status = NexusStatuses.Ok,
|
||||||
|
ActiveSeriesCount = _seriesMeta.Count,
|
||||||
|
TotalSampleCount = _samplesBySeries.Values.Sum(list => list.Count),
|
||||||
|
};
|
||||||
|
foreach ((string metric, int count) in byMetric.OrderByDescending(static p => p.Value).ThenBy(static p => p.Key, StringComparer.Ordinal).Take(topN))
|
||||||
|
{
|
||||||
|
response.TopMetricsBySeriesCount.Add(new TimeMetricCardinalityWire { MetricName = metric, SeriesCount = count });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<TimeApplyRetentionResponse> ApplyRetention(TimeApplyRetentionRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
long removed = 0;
|
||||||
|
foreach (List<TimeSampleWire> samples in _samplesBySeries.Values)
|
||||||
|
{
|
||||||
|
int before = samples.Count;
|
||||||
|
samples.RemoveAll(s => s.TimestampUnixNanos < request.RetainAfterExclusiveUnixNanos);
|
||||||
|
removed += before - samples.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (List<TimeWideEventWire> events in _eventsByTable.Values)
|
||||||
|
{
|
||||||
|
int before = events.Count;
|
||||||
|
events.RemoveAll(e => e.TimestampUnixNanos < request.RetainAfterExclusiveUnixNanos);
|
||||||
|
removed += before - events.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
_retentionRemoved = removed;
|
||||||
|
return Task.FromResult(new TimeApplyRetentionResponse { Status = NexusStatuses.Ok, RemovedCount = removed });
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<TimeAppendSampleResponse> AppendSample(TimeAppendSampleRequest request, ServerCallContext context)
|
public override Task<TimeAppendSampleResponse> AppendSample(TimeAppendSampleRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Robotico.Nexus.Client.Tests.TestSupport;
|
using Robotico.Nexus.Client.Tests.TestSupport;
|
||||||
|
using Robotico.Nexus.Client.Time;
|
||||||
using Robotico.Result.Errors;
|
using Robotico.Result.Errors;
|
||||||
using Robotico.Time;
|
using Robotico.Time;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -230,9 +231,36 @@ public class NexusTimeSessionTests : IAsyncLifetime
|
||||||
Assert.Single(hits!);
|
Assert.Single(hits!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ListSeries_GetCardinality_ApplyRetention_CatalogOps()
|
||||||
|
{
|
||||||
|
NexusTimeSession nexus = Assert.IsType<NexusTimeSession>(_session);
|
||||||
|
|
||||||
|
ulong s1 = RegisterSeries("cpu.load");
|
||||||
|
_session.AppendSample(s1, 100, 1.0);
|
||||||
|
_session.AppendSample(s1, 200, 2.0);
|
||||||
|
ulong s2 = RegisterSeries("cpu.load");
|
||||||
|
RegisterSeries("mem.used");
|
||||||
|
|
||||||
|
Assert.True(nexus.ListSeries(10).IsSuccess(out IReadOnlyList<TimeSeriesCatalogEntry>? listed, out _));
|
||||||
|
Assert.Equal(3, listed!.Count);
|
||||||
|
Assert.Contains(listed, e => e is { SeriesId: var id, MetricName: "cpu.load", SampleCount: 2 } && id == s1);
|
||||||
|
|
||||||
|
Assert.True(nexus.GetCardinality(10).IsSuccess(out TimeCardinalitySnapshot? card, out _));
|
||||||
|
Assert.Equal(3, card!.ActiveSeriesCount);
|
||||||
|
Assert.Equal(2, card.TotalSampleCount);
|
||||||
|
Assert.Contains(card.TopMetricsBySeriesCount, m => m is { MetricName: "cpu.load", SeriesCount: 2 });
|
||||||
|
|
||||||
|
Assert.True(nexus.ApplyRetention(150).IsSuccess(out long removed, out _));
|
||||||
|
Assert.True(removed >= 1);
|
||||||
|
Assert.True(nexus.ApplyRetention(0).IsError(out IError? retErr));
|
||||||
|
Assert.Equal(TimeErrors.InvalidArgument, retErr!.Code);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AllOperations_Fail_AfterDispose()
|
public void AllOperations_Fail_AfterDispose()
|
||||||
{
|
{
|
||||||
|
NexusTimeSession nexus = Assert.IsType<NexusTimeSession>(_session);
|
||||||
_session.Dispose();
|
_session.Dispose();
|
||||||
|
|
||||||
Assert.True(_session.Doctor().IsError(out IError? e1));
|
Assert.True(_session.Doctor().IsError(out IError? e1));
|
||||||
|
|
@ -267,6 +295,13 @@ public class NexusTimeSessionTests : IAsyncLifetime
|
||||||
|
|
||||||
Assert.True(_session.SearchFullText(1, 0, "x", 10).IsError(out IError? e11));
|
Assert.True(_session.SearchFullText(1, 0, "x", 10).IsError(out IError? e11));
|
||||||
Assert.Equal(NexusErrors.Disposed, e11!.Code);
|
Assert.Equal(NexusErrors.Disposed, e11!.Code);
|
||||||
|
|
||||||
|
Assert.True(nexus.ListSeries().IsError(out IError? e12));
|
||||||
|
Assert.Equal(NexusErrors.Disposed, e12!.Code);
|
||||||
|
Assert.True(nexus.GetCardinality().IsError(out IError? e13));
|
||||||
|
Assert.Equal(NexusErrors.Disposed, e13!.Code);
|
||||||
|
Assert.True(nexus.ApplyRetention(1).IsError(out IError? e14));
|
||||||
|
Assert.Equal(NexusErrors.Disposed, e14!.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ulong RegisterSeries(string metricName)
|
private ulong RegisterSeries(string metricName)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue