Skip to content
Open
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
42 changes: 36 additions & 6 deletions src/SnapDB/BitConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
//
//******************************************************************************************************

using System.Diagnostics;

namespace SnapDB;

/// <summary>
Expand All @@ -47,7 +45,7 @@ public static class BitConvert
/// This method performs an unsafe conversion by treating the input float as an uint and
/// then casting it to ulong.
/// </remarks>
/// <seealso cref="ToUInt64(float)"/>
/// <seealso cref="ToSingle(ulong)"/>
public static unsafe ulong ToUInt64(float value)
{
return *(uint*)&value;
Expand All @@ -72,6 +70,38 @@ public static unsafe float ToSingle(ulong value)
return *(float*)&tmpValue;
}

/// <summary>
/// Converts a double-precision floating-point number to an unsigned 64-bit integer representation.
/// </summary>
/// <param name="value">The double-precision floating-point number to convert.</param>
/// <returns>
/// An unsigned 64-bit integer representation of the input double-precision floating-point number.
/// </returns>
/// <remarks>
/// This method performs an unsafe conversion by casting double to ulong.
/// </remarks>
/// <seealso cref="ToDouble(ulong)"/>
public static unsafe ulong ToUInt64(double value)
{
return *(ulong*)&value;
}

/// <summary>
/// Converts an unsigned 64-bit integer to a double-precision floating-point number.
/// </summary>
/// <param name="value">The unsigned 64-bit integer to convert.</param>
/// <returns>
/// A double-precision floating-point number representing the input unsigned 64-bit integer.
/// </returns>
/// <remarks>
/// This method performs an unsafe conversion by casting the input ulong as a double.
/// </remarks>
/// <seealso cref="ToUInt64(double)"/>
public static unsafe double ToDouble(ulong value)
{
return *(double*)&value;
}

/// <summary>
/// Converts a Guid value into two unsigned 64-bit integers.
/// </summary>
Expand All @@ -86,15 +116,15 @@ public static unsafe (ulong value1, ulong value2) ToUInt64Pair(Guid value)

ulong value1;
ulong value2;

fixed (byte* ptr = bytes)
{
ulong* lptr = (ulong*)ptr;
value1 = *lptr;
lptr++;
value2 = *lptr;
}

return (value1, value2);
}

Expand All @@ -120,6 +150,6 @@ public static unsafe Guid ToGuid(ulong value1, ulong value2)

return new Guid(bytes);
}

#endregion
}
2 changes: 0 additions & 2 deletions src/SnapDB/Snap/Services/ArchiveDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
// Converted code to .NET core.
//
//******************************************************************************************************
using System.Linq;


namespace SnapDB.Snap.Services;

Expand Down
43 changes: 43 additions & 0 deletions src/SnapDB/Snap/Services/ArchiveDirectoryFillMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//******************************************************************************************************
// ArchiveDirectoryFillMethod.cs - Gbtc
//
// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 06/23/2026 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************

namespace SnapDB.Snap.Services;

/// <summary>
/// Specifies how a write directory is selected from the set of configured archive directories when more than one
/// is available.
/// </summary>
public enum ArchiveDirectoryFillMethod
{
/// <summary>
/// Fill each configured write path, in order, advancing to the next only when the current path's drive lacks
/// sufficient free space. This is the default and matches the historical archive write behavior.
/// </summary>
Sequential,

/// <summary>
/// Distribute new files across the configured write paths in round-robin order, skipping any path whose drive
/// lacks sufficient free space.
/// </summary>
RoundRobin
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public AdvancedServerDatabaseConfig(string databaseName, string mainPath, bool s
DesiredRemainingSpace = 5L * SI2.Giga;
StagingCount = 3;
DirectoryMethod = ArchiveDirectoryMethod.TopDirectoryOnly;
FillMethod = ArchiveDirectoryFillMethod.Sequential;
DiskFlushInterval = 10000;
CacheFlushInterval = 100;
}
Expand Down Expand Up @@ -120,6 +121,12 @@ public AdvancedServerDatabaseConfig(string databaseName, string mainPath, bool s
/// </summary>
public ArchiveDirectoryMethod DirectoryMethod { get; set; }

/// <summary>
/// Gets or sets how a write path is selected from the final write paths when more than one is configured.
/// Defaults to <see cref="ArchiveDirectoryFillMethod.Sequential"/>.
/// </summary>
public ArchiveDirectoryFillMethod FillMethod { get; set; }

/// <summary>
/// The number of milliseconds before data is automatically flushed to the disk.
/// </summary>
Expand Down Expand Up @@ -234,8 +241,12 @@ private void ToWriteProcessorSettings(WriteProcessorSettings settings)
if (remainingStages > 0)
rollover.ArchiveSettings.ConfigureOnDisk([m_mainPath], SI2.Giga, ArchiveDirectoryMethod.TopDirectoryOnly, ArchiveEncodingMethod, "stage" + stage, intermediateFilePendingExtension, intermediateFileFinalExtension, FileFlags.GetStage(stage), FileFlags.IntermediateFile);
else
// Final staging file
{
// Final staging file - this is the only stage that writes across the configured final paths, so the
// fill method (sequential vs round-robin) applies here
rollover.ArchiveSettings.ConfigureOnDisk(finalPaths, DesiredRemainingSpace, DirectoryMethod, ArchiveEncodingMethod, DatabaseName.ToNonNullNorEmptyString("stage" + stage).RemoveInvalidFileNameCharacters(), finalFilePendingExtension, finalFileFinalExtension, FileFlags.GetStage(stage));
rollover.ArchiveSettings.FillMethod = FillMethod;
}

rollover.ArchiveSettings.Metadata = Metadata;

Expand Down
50 changes: 50 additions & 0 deletions src/SnapDB/Snap/Services/Net/CustomCommandNotSupportedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//******************************************************************************************************
// CustomCommandNotSupportedException.cs - Gbtc
//
// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 06/23/2026 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************

namespace SnapDB.Snap.Services.Net;

/// <summary>
/// Exception thrown when a client invokes a custom command that the server does not support, either because no
/// handler is registered for the command name or because the server is an older version that does not recognize
/// the custom-command protocol.
/// </summary>
/// <remarks>
/// Callers can catch this exception to degrade gracefully when connecting to servers that lack support for a
/// given custom command.
/// </remarks>
public class CustomCommandNotSupportedException : Exception
{
/// <summary>
/// Gets the name of the custom command that was not supported, when known.
/// </summary>
public string? CommandName { get; }

/// <summary>
/// Creates a new <see cref="CustomCommandNotSupportedException"/>.
/// </summary>
/// <param name="commandName">The name of the unsupported custom command, when known.</param>
public CustomCommandNotSupportedException(string? commandName) : base($"Custom command '{commandName}' is not supported by the server.")
{
CommandName = commandName;
}
}
28 changes: 26 additions & 2 deletions src/SnapDB/Snap/Services/Net/ServerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ public enum ServerCommand : byte

/// <summary>
/// </summary>
GetAllDatabases = 7
GetAllDatabases = 7,

/// <summary>
/// Invokes a named, server-registered custom command with an opaque request payload, returning an opaque
/// response payload. Provides a generic extension point without coupling the protocol to specific features.
/// </summary>
RunCustomCommand = 8
}

/// <summary>
Expand Down Expand Up @@ -184,7 +190,25 @@ public enum ServerResponse : byte

/// <summary>
/// </summary>
AuthenticationFailed
AuthenticationFailed,

/// <summary>
/// Returned by <see cref="ServerCommand.RunCustomCommand"/> when a registered handler produced a result. The
/// length-prefixed response payload follows.
/// </summary>
CustomCommandResult = 26,

/// <summary>
/// Returned by <see cref="ServerCommand.RunCustomCommand"/> when no handler is registered for the requested
/// command name. The connection remains usable.
/// </summary>
CustomCommandNotSupported = 27,

/// <summary>
/// Returned by <see cref="ServerCommand.RunCustomCommand"/> when a registered handler threw an exception. The
/// error message string follows. The connection remains usable.
/// </summary>
CustomCommandError = 28
}

#endregion
48 changes: 48 additions & 0 deletions src/SnapDB/Snap/Services/Net/SnapStreamingClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,54 @@ public override bool Contains(string databaseName)
return m_databaseInfos.ContainsKey(databaseName.ToUpper());
}

/// <summary>
/// Invokes a named, server-registered custom command at the connection root level.
/// </summary>
/// <param name="command">The case-insensitive name of the custom command to invoke.</param>
/// <param name="request">The opaque request payload; may be empty.</param>
/// <returns>The opaque response payload produced by the server-side handler.</returns>
/// <exception cref="CustomCommandNotSupportedException">
/// The server is an older version that does not recognize the custom-command protocol, or no handler is
/// registered for <paramref name="command"/>.
/// </exception>
public override byte[] RunCustomCommand(string command, byte[] request)
{
if (m_sortedTreeEngine is not null)
throw new InvalidOperationException("Cannot run a custom command while connected to a database; use a dedicated connection for custom commands.");

request ??= [];

m_stream.Write((byte)ServerCommand.RunCustomCommand);
m_stream.Write(command);
m_stream.Write(request.Length);

if (request.Length > 0)
m_stream.Write(request, 0, request.Length);

m_stream.Flush();

ServerResponse response = (ServerResponse)m_stream.ReadUInt8();

switch (response)
{
case ServerResponse.UnhandledException:
throw new Exception($"Server UnhandledException: \n{m_stream.ReadString()}");
case ServerResponse.CustomCommandResult:
int length = m_stream.ReadInt32();
return length > 0 ? m_stream.ReadBytes(length) : [];
case ServerResponse.CustomCommandNotSupported:
throw new CustomCommandNotSupportedException(m_stream.ReadString());
case ServerResponse.CustomCommandError:
throw new Exception($"Server custom command error: {m_stream.ReadString()}");
case ServerResponse.UnknownCommand:
// Older server that does not recognize the custom-command protocol; it echoes the command byte and drops the connection
m_stream.ReadUInt8();
throw new CustomCommandNotSupportedException(command);
default:
throw new Exception($"Unknown server response: {response}");
}
}

/// <summary>
/// Creates a <see cref="SnapStreamingClient"/>
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions src/SnapDB/Snap/Services/Net/SnapStreamingServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,37 @@ private void ProcessRootLevelCommands()
if (!success)
return;

break;
case ServerCommand.RunCustomCommand:
string customCommand = m_stream.ReadString();
int requestLength = m_stream.ReadInt32();
byte[] request = requestLength > 0 ? m_stream.ReadBytes(requestLength) : [];

if (!m_server.TryGetCustomCommand(customCommand, out Func<byte[], byte[]>? handler) || handler is null)
{
m_stream.Write((byte)ServerResponse.CustomCommandNotSupported);
m_stream.Write(customCommand);
m_stream.Flush();
break;
}

try
{
byte[] response = handler(request) ?? [];
m_stream.Write((byte)ServerResponse.CustomCommandResult);
m_stream.Write(response.Length);

if (response.Length > 0)
m_stream.Write(response, 0, response.Length);
}
catch (Exception ex)
{
Log.Publish(MessageLevel.Warning, "Custom Command Error", $"Custom command '{customCommand}' handler threw an exception.", null, ex);
m_stream.Write((byte)ServerResponse.CustomCommandError);
m_stream.Write(ex.Message);
}

m_stream.Flush();
break;
case ServerCommand.Disconnect:
m_stream.Write((byte)ServerResponse.GoodBye);
Expand Down
20 changes: 20 additions & 0 deletions src/SnapDB/Snap/Services/SnapClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ protected SnapClient() : base(MessageClass.Framework)
/// </remarks>
public abstract bool Contains(string databaseName);

/// <summary>
/// Invokes a named, server-registered custom command, passing an opaque request payload and returning an
/// opaque response payload.
/// </summary>
/// <param name="command">The case-insensitive name of the custom command to invoke.</param>
/// <param name="request">The opaque request payload; may be empty.</param>
/// <returns>The opaque response payload produced by the server-side handler.</returns>
/// <exception cref="Net.CustomCommandNotSupportedException">
/// The server does not support the custom-command protocol, or no handler is registered for
/// <paramref name="command"/>.
/// </exception>
/// <remarks>
/// Custom commands operate at the connection root level. When using a network client, do not invoke a custom
/// command while connected to a database; use a dedicated connection for custom commands.
/// </remarks>
public virtual byte[] RunCustomCommand(string command, byte[] request)
{
throw new NotSupportedException("This client does not support custom commands.");
}

#endregion

#region [ Static ]
Expand Down
Loading