Initial Repo Setup
This commit is contained in:
commit
de14285314
48
.gitea/workflows/build.yml
Normal file
48
.gitea/workflows/build.yml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v3
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore Firefly.sln
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
chmod +x build-all.sh
|
||||||
|
./build-all.sh
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: firefly-artifacts
|
||||||
|
path: |
|
||||||
|
artifacts/exe/*
|
||||||
|
artifacts/native/*
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
artifacts/exe/*
|
||||||
|
artifacts/native/*
|
||||||
|
generate_release_notes: true
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.editorconfig
|
||||||
|
nuget.config
|
||||||
|
*.7z
|
||||||
|
backups
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
.venv
|
||||||
|
.vs
|
||||||
|
.vscode
|
62
Firefly.csproj
Normal file
62
Firefly.csproj
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Common settings -->
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<OptimizationPreference>Speed</OptimizationPreference>
|
||||||
|
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||||
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
|
||||||
|
<!-- Package information -->
|
||||||
|
<AssemblyName>Firefly</AssemblyName>
|
||||||
|
<RootNamespace>Firefly</RootNamespace>
|
||||||
|
<Title>Firefly</Title>
|
||||||
|
<Description>A Redis-compatible server</Description>
|
||||||
|
<PackageId>Firefly</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>IDSolutions</Authors>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
|
<IsPackable>true</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Executable configuration -->
|
||||||
|
<PropertyGroup Condition="'$(BuildType)' != 'lib'">
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishTrimmed>true</PublishTrimmed>
|
||||||
|
<StripSymbols>true</StripSymbols>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
|
||||||
|
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Library configuration -->
|
||||||
|
<PropertyGroup Condition="'$(BuildType)' == 'lib'">
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
|
<NativeLib>Shared</NativeLib>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<PublishTrimmed>true</PublishTrimmed>
|
||||||
|
<StripSymbols>true</StripSymbols>
|
||||||
|
<IlcOptimizationPreference>Speed</IlcOptimizationPreference>
|
||||||
|
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="icon.png" />
|
||||||
|
<Content Include="icon.ico" />
|
||||||
|
<None Include="resources\icon.png" Pack="true" Visible="false" PackagePath="\" />
|
||||||
|
<None Include="resources\icon.ico" Pack="true" Visible="false" PackagePath="\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
11
Firefly.csproj.user
Normal file
11
Firefly.csproj.user
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<_LastSelectedProfileId>G:\forge\firefly\extension\Firefly\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
30
Firefly.sln
Normal file
30
Firefly.sln
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.2.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Firefly", "Firefly.csproj", "{3DB35960-520A-BD42-07BD-4CAE9A4E697D}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{3DB35960-520A-BD42-07BD-4CAE9A4E697D}.Release|x64.Build.0 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {FD0A49B3-12BC-4356-BD54-4D73658BE95F}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
16
Properties/PublishProfiles/FolderProfile.pubxml
Normal file
16
Properties/PublishProfiles/FolderProfile.pubxml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>bin\Release\net9.0\publish\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
8
Properties/PublishProfiles/FolderProfile.pubxml.user
Normal file
8
Properties/PublishProfiles/FolderProfile.pubxml.user
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<History>True|2025-04-08T00:26:53.2010541Z||;True|2025-04-07T19:26:46.3598239-05:00||;True|2025-04-07T19:07:58.7012000-05:00||;True|2025-04-07T19:05:51.2227643-05:00||;True|2025-04-07T19:03:08.0269213-05:00||;True|2025-04-07T18:29:04.5003554-05:00||;True|2025-04-06T23:16:01.4293237-05:00||;False|2025-04-06T23:15:41.6244752-05:00||;True|2025-04-06T21:47:02.7035505-05:00||;True|2025-04-06T20:08:49.6142219-05:00||;True|2025-04-06T19:25:51.2602973-05:00||;True|2025-04-06T19:17:03.2181403-05:00||;True|2025-04-06T19:04:13.8498671-05:00||;True|2025-04-06T19:01:25.0268913-05:00||;True|2025-04-06T18:58:25.6890700-05:00||;True|2025-04-06T18:55:25.6367097-05:00||;True|2025-04-06T18:53:20.2451573-05:00||;True|2025-04-06T18:45:02.8482945-05:00||;False|2025-04-06T18:44:50.0356423-05:00||;True|2025-04-06T18:16:53.9309464-05:00||;True|2025-04-06T17:39:44.9315977-05:00||;False|2025-04-06T17:38:07.1833743-05:00||;True|2025-04-06T17:28:34.7769158-05:00||;True|2025-04-06T17:27:47.9261712-05:00||;True|2025-04-06T17:22:10.3979209-05:00||;True|2025-04-06T17:06:24.3332447-05:00||;True|2025-04-06T16:59:11.3671748-05:00||;True|2025-04-06T16:28:27.6102439-05:00||;True|2025-04-06T16:25:29.8493098-05:00||;True|2025-04-06T16:07:11.4149691-05:00||;True|2025-04-06T15:30:49.5703060-05:00||;True|2025-04-06T14:52:44.3134873-05:00||;True|2025-04-06T14:37:32.1843661-05:00||;True|2025-04-06T13:13:57.7572594-05:00||;True|2025-04-06T13:13:40.5523132-05:00||;False|2025-04-06T13:13:24.6183721-05:00||;True|2025-04-06T12:36:35.2352725-05:00||;True|2025-04-06T00:38:44.2848632-05:00||;False|2025-04-06T00:27:17.9337778-05:00||;False|2025-04-06T00:26:34.3407057-05:00||;False|2025-04-06T00:26:24.6819572-05:00||;True|2025-04-06T00:02:24.4950451-05:00||;True|2025-04-05T23:40:36.1415759-05:00||;True|2025-04-05T22:59:15.6812178-05:00||;True|2025-04-05T22:30:54.7814684-05:00||;True|2025-04-05T22:20:22.3907744-05:00||;True|2025-04-05T22:11:14.6770330-05:00||;True|2025-04-05T22:04:33.3236186-05:00||;True|2025-04-05T22:00:11.5893340-05:00||;True|2025-04-05T21:49:33.4826768-05:00||;True|2025-04-05T21:44:51.9154306-05:00||;True|2025-04-05T21:35:04.6071930-05:00||;True|2025-04-05T21:22:35.7252158-05:00||;True|2025-04-05T21:18:15.7082379-05:00||;True|2025-04-05T20:44:29.2052360-05:00||;True|2025-04-05T20:41:04.4189956-05:00||;True|2025-04-05T20:38:24.1151587-05:00||;True|2025-04-05T20:36:46.0623020-05:00||;True|2025-04-05T20:33:19.4003638-05:00||;True|2025-04-05T20:30:31.7236371-05:00||;</History>
|
||||||
|
<LastFailureDetails />
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
88
Properties/Resources.Designer.cs
generated
Normal file
88
Properties/Resources.Designer.cs
generated
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System.CodeDom.Compiler;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Resources;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Firefly.Properties {
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||||
|
[DebuggerNonUserCode()]
|
||||||
|
[CompilerGenerated()]
|
||||||
|
internal class Resources {
|
||||||
|
|
||||||
|
private static ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||||
|
internal static ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (ReferenceEquals(resourceMan, null)) {
|
||||||
|
ResourceManager temp = new ResourceManager("Firefly.Properties.Resources", typeof(Resources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)]
|
||||||
|
internal static CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized resource of type System.Byte[].
|
||||||
|
/// </summary>
|
||||||
|
internal static byte[] icon {
|
||||||
|
get {
|
||||||
|
object obj = ResourceManager.GetObject("icon", resourceCulture);
|
||||||
|
return ((byte[])(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized resource of type System.Byte[].
|
||||||
|
/// </summary>
|
||||||
|
internal static byte[] logo {
|
||||||
|
get {
|
||||||
|
object obj = ResourceManager.GetObject("logo", resourceCulture);
|
||||||
|
return ((byte[])(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
Properties/Resources.resx
Normal file
127
Properties/Resources.resx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||||
|
<data name="icon0" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||||
|
<value>..\resources\icon.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</data>
|
||||||
|
<data name="icon1" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||||
|
<value>..\resources\icon.ico;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
78
README.md
Normal file
78
README.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Firefly Database
|
||||||
|
|
||||||
|
Firefly is a high-performance, Redis-compatible key-value store optimized for gaming applications. Built with native AOT compilation support for maximum performance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Redis-Compatible**: Uses Redis Serialization Protocol (RESP) for compatibility with Redis clients
|
||||||
|
- **Concurrent Data Structures**: Uses parallel data structures for high throughput and low latency
|
||||||
|
- **Sharded Architecture**: Distributes data across multiple shards to reduce contention
|
||||||
|
- **Password Authentication**: Optional password protection for secure deployments
|
||||||
|
- **Native AOT Support**: Fully compatible with .NET Native AOT for maximum performance
|
||||||
|
- **Automatic Backups**: Configurable backup system with rotation
|
||||||
|
- **Multi-Datatype Support**: Strings, Lists and Hash Maps support
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server with default settings
|
||||||
|
firefly
|
||||||
|
|
||||||
|
# Start with password authentication
|
||||||
|
firefly --password yourSecretPassword
|
||||||
|
|
||||||
|
# Start with custom port
|
||||||
|
firefly --port 6380
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
When starting Firefly with password protection, clients must authenticate:
|
||||||
|
|
||||||
|
```
|
||||||
|
AUTH yourSecretPassword
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details on how to use authentication with different clients, see the [USAGE.md](USAGE.md) file.
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- .NET 9.0 SDK or later
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
```bash
|
||||||
|
# Standard build
|
||||||
|
dotnet build
|
||||||
|
|
||||||
|
# Release build
|
||||||
|
dotnet build -c Release
|
||||||
|
|
||||||
|
# Native AOT build (Windows x64)
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained -p:PublishAot=true
|
||||||
|
|
||||||
|
# Native AOT build (Linux x64)
|
||||||
|
dotnet publish -c Release -r linux-x64 --self-contained -p:PublishAot=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Benchmarks
|
||||||
|
|
||||||
|
The benchmark mode (`--benchmark`) runs a series of concurrent operations to test throughput:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firefly --benchmark
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical results on modern hardware:
|
||||||
|
- ~500,000 operations per second for simple key-value operations
|
||||||
|
- ~250,000 operations per second for list operations
|
||||||
|
- ~150,000 operations per second for hash map operations
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [USAGE.md](USAGE.md) - Detailed usage instructions
|
||||||
|
- [ArmaFireflyClient](https://gitea.innovativedevsolutions.org/IDSolutions/ArmaFireflyClient/) - Arma 3 extension client
|
167
USAGE.md
Normal file
167
USAGE.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Firefly Server Usage Guide
|
||||||
|
|
||||||
|
Firefly is key-value store optimized for performance with native AOT compilation support.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Starting the Server](#starting-the-server)
|
||||||
|
- [Password Authentication](#password-authentication)
|
||||||
|
- [Basic Operations](#basic-operations)
|
||||||
|
- [Client Integration](#client-integration)
|
||||||
|
- [ArmaFireflyClient Integration](#armafireflyclient-integration)
|
||||||
|
- [Performance Considerations](#performance-considerations)
|
||||||
|
|
||||||
|
## Starting the Server
|
||||||
|
|
||||||
|
### Basic Start
|
||||||
|
```bash
|
||||||
|
# Start with default settings (port 6379, backups enabled)
|
||||||
|
firefly
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Command Line Options
|
||||||
|
```bash
|
||||||
|
# Start with custom port
|
||||||
|
firefly --port 6380
|
||||||
|
|
||||||
|
# Start with authentication enabled
|
||||||
|
firefly --password yourSecretPassword
|
||||||
|
|
||||||
|
# Start with backups disabled
|
||||||
|
firefly --no-backup
|
||||||
|
|
||||||
|
# Start with custom backup interval (10 minutes)
|
||||||
|
firefly --backup-interval 10
|
||||||
|
|
||||||
|
# Start with limited backup files
|
||||||
|
firefly --max-backups 5
|
||||||
|
|
||||||
|
# Start with shorter connection timeout
|
||||||
|
firefly --timeout 60
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Command Reference
|
||||||
|
Run `firefly --help` to see all available options.
|
||||||
|
|
||||||
|
## Password Authentication
|
||||||
|
|
||||||
|
When starting Firefly with a password, clients must authenticate before running commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
AUTH yourSecretPassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Behaviors
|
||||||
|
|
||||||
|
- If no password is set, all commands work without authentication
|
||||||
|
- If a password is set, only PING and AUTH commands work without authentication
|
||||||
|
- After successful authentication, all commands work normally
|
||||||
|
- Failed authentication attempts return an error but allow retries
|
||||||
|
|
||||||
|
## Basic Operations
|
||||||
|
|
||||||
|
### String Operations
|
||||||
|
```
|
||||||
|
SET key value # Set a key-value pair
|
||||||
|
GET key # Get a value by key
|
||||||
|
DEL key # Delete a key (works for all data types)
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Operations
|
||||||
|
```
|
||||||
|
LPUSH key value1 value2 # Add values to the beginning of a list
|
||||||
|
RPUSH key value1 value2 # Add values to the end of a list
|
||||||
|
LPOP key # Remove and return the first value
|
||||||
|
RPOP key # Remove and return the last value
|
||||||
|
LRANGE key start stop # Get a range of values (inclusive)
|
||||||
|
LINDEX key index # Get a value at a specific index
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hash Table Operations
|
||||||
|
```
|
||||||
|
HSET key field value # Set a field in a hash
|
||||||
|
HGET key field # Get a field from a hash
|
||||||
|
HDEL key field # Delete a field from a hash
|
||||||
|
HEXISTS key field # Check if field exists in hash
|
||||||
|
HGETALL key # Get all fields and values
|
||||||
|
HMSET key field1 value1 field2 value2 # Set multiple fields at once
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Operations
|
||||||
|
```
|
||||||
|
SAVE # Save data immediately
|
||||||
|
BGSAVE # Save data in the background
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Integration
|
||||||
|
|
||||||
|
### Connecting with Redis Client
|
||||||
|
Firefly is compatible with Redis protocol. Use your preferred Redis client library:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// C# example using StackExchange.Redis
|
||||||
|
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379,password=yourpassword");
|
||||||
|
IDatabase db = redis.GetDatabase();
|
||||||
|
db.StringSet("mykey", "myvalue");
|
||||||
|
string value = db.StringGet("mykey");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connecting with Raw TCP
|
||||||
|
```csharp
|
||||||
|
// Basic TCP connection example
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
TcpClient client = new TcpClient("localhost", 6379);
|
||||||
|
NetworkStream stream = client.GetStream();
|
||||||
|
|
||||||
|
// Authenticate if needed
|
||||||
|
byte[] authCmd = Encoding.UTF8.GetBytes("AUTH yourpassword\r\n");
|
||||||
|
await stream.WriteAsync(authCmd);
|
||||||
|
// ... read response ...
|
||||||
|
|
||||||
|
// Send command
|
||||||
|
byte[] setCmd = Encoding.UTF8.GetBytes("SET mykey myvalue\r\n");
|
||||||
|
await stream.WriteAsync(setCmd);
|
||||||
|
// ... read response ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## ArmaFireflyClient Integration
|
||||||
|
|
||||||
|
When using ArmaFireflyClient with Arma 3, you need to ensure the client has the correct password configured.
|
||||||
|
|
||||||
|
### Setting Up Connection in Arma 3
|
||||||
|
```sqf
|
||||||
|
// Connect to a password-protected Firefly server
|
||||||
|
"setup" callExtension ["127.0.0.1", "6379", "yourSecretPassword"];
|
||||||
|
```
|
||||||
|
|
||||||
|
This updates the client configuration to use the specified password for authentication. The client will automatically authenticate when connecting to the server.
|
||||||
|
|
||||||
|
### Using Default XML Configuration
|
||||||
|
Create a config.xml file in your @firefly directory:
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<items>
|
||||||
|
<host>127.0.0.1</host>
|
||||||
|
<port>6379</port>
|
||||||
|
<password>yourSecretPassword</password>
|
||||||
|
<contextLog>false</contextLog>
|
||||||
|
<debug>false</debug>
|
||||||
|
</items>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warning About Empty Password
|
||||||
|
Setting up ArmaFireflyClient without a password when connecting to a password-protected Firefly server will result in authentication failures. Always make sure your password matches the server's password.
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Password authentication adds a small overhead to the initial connection
|
||||||
|
- For best performance with authenticated connections, reuse existing connections
|
||||||
|
- The server uses concurrent data structures to maintain performance even with authentication enabled
|
||||||
|
- Consider using a longer connection timeout (`--timeout`) to keep connections alive for longer periods
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
1. **Authentication Errors**: Make sure the client is using the correct password
|
||||||
|
2. **Connection Refused**: Verify the server is running and listening on the expected port
|
||||||
|
3. **Timeout Errors**: Increase the connection timeout or check for network issues
|
BIN
artifacts/exe/Firefly
Normal file
BIN
artifacts/exe/Firefly
Normal file
Binary file not shown.
BIN
artifacts/exe/Firefly.dbg
Normal file
BIN
artifacts/exe/Firefly.dbg
Normal file
Binary file not shown.
BIN
artifacts/exe/Firefly.exe
Normal file
BIN
artifacts/exe/Firefly.exe
Normal file
Binary file not shown.
BIN
artifacts/exe/Firefly.pdb
Normal file
BIN
artifacts/exe/Firefly.pdb
Normal file
Binary file not shown.
677
artifacts/exe/Firefly.xml
Normal file
677
artifacts/exe/Firefly.xml
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<doc>
|
||||||
|
<assembly>
|
||||||
|
<name>Firefly</name>
|
||||||
|
</assembly>
|
||||||
|
<members>
|
||||||
|
<member name="T:Firefly.Properties.Resources">
|
||||||
|
<summary>
|
||||||
|
A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.ResourceManager">
|
||||||
|
<summary>
|
||||||
|
Returns the cached ResourceManager instance used by this class.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.Culture">
|
||||||
|
<summary>
|
||||||
|
Overrides the current thread's CurrentUICulture property for all
|
||||||
|
resource lookups using this strongly typed resource class.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.icon">
|
||||||
|
<summary>
|
||||||
|
Looks up a localized resource of type System.Byte[].
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.logo">
|
||||||
|
<summary>
|
||||||
|
Looks up a localized resource of type System.Byte[].
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Firefly">
|
||||||
|
<summary>
|
||||||
|
Core class implementing the Firefly database server functionality
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HSET command which sets a field in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field value"</param>
|
||||||
|
<returns>1 if the field was added, 0 if it was updated</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHGetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HGET command which retrieves the value of a field in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field"</param>
|
||||||
|
<returns>The value of the field, or nil if the field doesn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHDelCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HDEL command which removes a field from a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field"</param>
|
||||||
|
<returns>1 if the field was removed, 0 if it didn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHExistsCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HEXISTS command which checks if a field exists in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field"</param>
|
||||||
|
<returns>1 if the field exists, 0 if it doesn't</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHGetAllCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HGETALL command which retrieves all fields and values in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key"</param>
|
||||||
|
<returns>All fields and values in the hash, or nil if the hash doesn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHMSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HMSET command which sets multiple fields in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field value [field value ...]"</param>
|
||||||
|
<returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetOrCreateHash(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets or creates a hash for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get or create the hash for</param>
|
||||||
|
<returns>The hash</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HashStoreExists(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a hash exists for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the hash for</param>
|
||||||
|
<returns>True if the hash exists, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HashStoreGet(System.String,System.Collections.Concurrent.ConcurrentDictionary{System.String,System.String}@)">
|
||||||
|
<summary>
|
||||||
|
Gets a hash for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the hash for</param>
|
||||||
|
<param name="value">The hash</param>
|
||||||
|
<returns>True if the hash was found, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HashStoreRemove(System.String)">
|
||||||
|
<summary>
|
||||||
|
Removes a hash for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to remove the hash for</param>
|
||||||
|
<returns>True if the hash was removed, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.SplitRespectingQuotes(System.String)">
|
||||||
|
<summary>
|
||||||
|
Splits a string respecting quoted sections.
|
||||||
|
</summary>
|
||||||
|
<param name="input">The input string to split</param>
|
||||||
|
<returns>An array of tokens from the input string</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.MyRegex">
|
||||||
|
<remarks>
|
||||||
|
Pattern:<br/>
|
||||||
|
<code>([^\\s"]+)|"([^"]*)"</code><br/>
|
||||||
|
Explanation:<br/>
|
||||||
|
<code>
|
||||||
|
○ Match with 2 alternative expressions, atomically.<br/>
|
||||||
|
○ 1st capture group.<br/>
|
||||||
|
○ Match a character in the set [^"\s] atomically at least once.<br/>
|
||||||
|
○ Match a sequence of expressions.<br/>
|
||||||
|
○ Match '"'.<br/>
|
||||||
|
○ 2nd capture group.<br/>
|
||||||
|
○ Match a character other than '"' atomically any number of times.<br/>
|
||||||
|
○ Match '"'.<br/>
|
||||||
|
</code>
|
||||||
|
</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.CreateShards``1">
|
||||||
|
<summary>
|
||||||
|
Creates an array of shards of type T.
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The type of the shards</typeparam>
|
||||||
|
<returns>An array of shards</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.CreateListLocks">
|
||||||
|
<summary>
|
||||||
|
Creates an array of ReaderWriterLockSlim instances for list operations.
|
||||||
|
</summary>
|
||||||
|
<returns>An array of locks</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetShardIndex(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets the shard index for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the shard index for</param>
|
||||||
|
<returns>The shard index</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.CreateLocks">
|
||||||
|
<summary>
|
||||||
|
Creates an array of locks.
|
||||||
|
</summary>
|
||||||
|
<returns>An array of locks</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.KeyExistsInAnyStore(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a key exists in any store (string, list, or hash).
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check</param>
|
||||||
|
<returns>True if the key exists in any store, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetKeyType(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets the type of a key if it exists in any store.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check</param>
|
||||||
|
<returns>The type of the key ("string", "list", "hash", or null if not found)</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.EnsureKeyDoesNotExist(System.String,System.String)">
|
||||||
|
<summary>
|
||||||
|
Ensures a key doesn't exist in any store before creating it in the target store.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check</param>
|
||||||
|
<param name="targetType">The type of store where the key will be created</param>
|
||||||
|
<returns>True if the key can be created, false if it already exists with a different type</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleDelCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the DEL command which removes a key from all stores (string, list, hash).
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to delete</param>
|
||||||
|
<returns>The number of keys that were removed</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleTypeCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the TYPE command which returns the type of a key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the type of</param>
|
||||||
|
<returns>The type of the key as a string response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleKeysCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the KEYS command which returns all keys matching a pattern.
|
||||||
|
</summary>
|
||||||
|
<param name="pattern">The pattern to match</param>
|
||||||
|
<returns>A list of keys matching the pattern</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLPushCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LPUSH command which adds an element to the left of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key value"</param>
|
||||||
|
<returns>The length of the list after the push operation</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleRPushCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the RPUSH command which adds values to the tail of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key value1 [value2 ...]"</param>
|
||||||
|
<returns>Response indicating the new length of the list</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLPopCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LPOP command which removes and returns the first element of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments containing the key</param>
|
||||||
|
<returns>The popped value or nil if the list is empty</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleRPopCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the RPOP command which removes and returns the last element of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments containing the key</param>
|
||||||
|
<returns>The popped value or nil if the list is empty</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLRangeCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LRANGE command which returns a range of elements from a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key start stop"</param>
|
||||||
|
<returns>Array of elements in the specified range</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLIndexCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LINDEX command which returns an element from a list by its index.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key index"</param>
|
||||||
|
<returns>The element at the specified index or nil if not found</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LSET command which sets the value of an element in a list by its index.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key index value"</param>
|
||||||
|
<returns>OK on success, error if index is out of range</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLPosCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LPOS command which returns the position of an element in a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key element [RANK rank] [MAXLEN len]"</param>
|
||||||
|
<returns>The position of the element or nil if not found</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLTrimCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LTRIM command which trims a list to the specified range.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key start stop"</param>
|
||||||
|
<returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLRemCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LREM command which removes elements equal to the given value from a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key count element"</param>
|
||||||
|
<returns>The number of removed elements</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetOrCreateList(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets or creates a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get or create the list for</param>
|
||||||
|
<returns>The list</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreExists(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a list exists for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the list for</param>
|
||||||
|
<returns>True if the list exists, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreGet(System.String,System.Collections.Generic.List{System.String}@)">
|
||||||
|
<summary>
|
||||||
|
Gets a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the list for</param>
|
||||||
|
<param name="value">The list</param>
|
||||||
|
<returns>True if the list was found, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreRemove(System.String)">
|
||||||
|
<summary>
|
||||||
|
Removes a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to remove the list for</param>
|
||||||
|
<returns>True if the list was removed, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreWithWriteLock(System.String,System.Action{System.Collections.Generic.List{System.String}})">
|
||||||
|
<summary>
|
||||||
|
Executes an action with a write lock on a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to execute the action on</param>
|
||||||
|
<param name="action">The action to execute</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreWithReadLock``1(System.String,System.Func{System.Collections.Generic.List{System.String},``0})">
|
||||||
|
<summary>
|
||||||
|
Executes an action with a read lock on a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The type of the result</typeparam>
|
||||||
|
<param name="key">The key to execute the action on</param>
|
||||||
|
<param name="action">The action to execute</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleAuthCommand(System.String,System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the AUTH command which authenticates a client.
|
||||||
|
</summary>
|
||||||
|
<param name="password">The password to authenticate with</param>
|
||||||
|
<param name="clientId">The client ID</param>
|
||||||
|
<returns>OK on success, error if authentication fails</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.IsAuthenticated(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a client is authenticated.
|
||||||
|
</summary>
|
||||||
|
<param name="clientId">The client ID</param>
|
||||||
|
<returns>True if the client is authenticated, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleSaveCommand">
|
||||||
|
<summary>
|
||||||
|
Handles the SAVE command which triggers a manual backup of the data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleBgSaveCommand">
|
||||||
|
<summary>
|
||||||
|
Handles the BGSAVE command which triggers an asynchronous backup of the data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ShutdownServer">
|
||||||
|
<summary>
|
||||||
|
Handles server shutdown operations
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the SET command which sets a key-value pair in the string store.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key value"</param>
|
||||||
|
<returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleGetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the GET command which retrieves a value from the string store.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments containing the key</param>
|
||||||
|
<returns>The value associated with the key, or nil if the key doesn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreExists(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a string exists for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the string for</param>
|
||||||
|
<returns>True if the string exists, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreSet(System.String,System.String)">
|
||||||
|
<summary>
|
||||||
|
Sets a string for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to set the string for</param>
|
||||||
|
<param name="value">The value to set</param>
|
||||||
|
<returns>True if the string was set, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreGet(System.String,System.String@)">
|
||||||
|
<summary>
|
||||||
|
Gets a string for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the string for</param>
|
||||||
|
<param name="value">The value</param>
|
||||||
|
<returns>True if the string was found, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreRemove(System.String)">
|
||||||
|
<summary>
|
||||||
|
Removes a string for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to remove the string for</param>
|
||||||
|
<returns>True if the string was removed, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.FireflyData">
|
||||||
|
<summary>
|
||||||
|
Container class for all Firefly database data used in serialization and backup operations.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.StringStore">
|
||||||
|
<summary>
|
||||||
|
Dictionary containing all string key-value pairs stored in the database.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.ListStore">
|
||||||
|
<summary>
|
||||||
|
Dictionary containing all lists stored in the database.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.HashStore">
|
||||||
|
<summary>
|
||||||
|
Dictionary containing all hash tables stored in the database.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.BackupTime">
|
||||||
|
<summary>
|
||||||
|
Timestamp when the backup was created.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.PipelineTest">
|
||||||
|
<summary>
|
||||||
|
Test client for verifying pipelining and batching functionality
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunTest(System.String[])">
|
||||||
|
<summary>
|
||||||
|
Runs the pipeline test
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.ReadResponses(System.Net.Sockets.NetworkStream,System.Byte[],System.Int32,System.String)">
|
||||||
|
<summary>
|
||||||
|
Read responses from the stream until we have the expected count
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunSequentialTest">
|
||||||
|
<summary>
|
||||||
|
Test with sequential commands (no pipelining)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunPipelinedTest">
|
||||||
|
<summary>
|
||||||
|
Test with pipelined commands
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunBatchedTest">
|
||||||
|
<summary>
|
||||||
|
Test with batched commands
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Protocol.FireflyProtocol">
|
||||||
|
<summary>
|
||||||
|
Defines the Firefly wire protocol specification for 3rd party clients
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.PROTOCOL_VERSION">
|
||||||
|
<summary>
|
||||||
|
The current version of the Firefly protocol
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.STRING_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for simple string responses (+)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.ERROR_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for error responses (-)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.INTEGER_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for integer responses (:)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.BULK_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for bulk string responses ($)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.ARRAY_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for array responses (*)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.NIL_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Special message indicating a nil value ($-1)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.MESSAGE_TERMINATOR">
|
||||||
|
<summary>
|
||||||
|
Standard message terminator sequence (\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.OK_RESPONSE">
|
||||||
|
<summary>
|
||||||
|
Standard OK response (+OK\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.QUEUED_RESPONSE">
|
||||||
|
<summary>
|
||||||
|
Response for queued pipeline commands (+QUEUED\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.NIL_RESPONSE">
|
||||||
|
<summary>
|
||||||
|
Standard nil response ($-1\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Protocol.FireflyProtocol.Commands">
|
||||||
|
<summary>
|
||||||
|
Command format specification for external clients
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.AUTH">
|
||||||
|
<summary>
|
||||||
|
Authenticate with the server using a password
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.PING">
|
||||||
|
<summary>
|
||||||
|
Test server connection
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.QUIT">
|
||||||
|
<summary>
|
||||||
|
Close the connection
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.SET">
|
||||||
|
<summary>
|
||||||
|
Set a key to hold a string value
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.GET">
|
||||||
|
<summary>
|
||||||
|
Get the value of a key
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.DEL">
|
||||||
|
<summary>
|
||||||
|
Delete a key
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.LPUSH">
|
||||||
|
<summary>
|
||||||
|
Insert elements at the head of a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.RPUSH">
|
||||||
|
<summary>
|
||||||
|
Insert elements at the tail of a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.LPOP">
|
||||||
|
<summary>
|
||||||
|
Remove and get the first element in a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.RPOP">
|
||||||
|
<summary>
|
||||||
|
Remove and get the last element in a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.LRANGE">
|
||||||
|
<summary>
|
||||||
|
Get a range of elements from a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HSET">
|
||||||
|
<summary>
|
||||||
|
Set the string value of a hash field
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HGET">
|
||||||
|
<summary>
|
||||||
|
Get the value of a hash field
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HDEL">
|
||||||
|
<summary>
|
||||||
|
Delete a hash field
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HGETALL">
|
||||||
|
<summary>
|
||||||
|
Get all fields and values in a hash
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.PIPELINE">
|
||||||
|
<summary>
|
||||||
|
Start a pipeline for batch processing
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.EXEC">
|
||||||
|
<summary>
|
||||||
|
Execute all commands in the pipeline
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Protocol.FireflyProtocol.Responses">
|
||||||
|
<summary>
|
||||||
|
Response format specification for external clients
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatString(System.String)">
|
||||||
|
<summary>
|
||||||
|
Format a simple string response
|
||||||
|
</summary>
|
||||||
|
<param name="value">The string value to format</param>
|
||||||
|
<returns>Formatted string response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatError(System.String)">
|
||||||
|
<summary>
|
||||||
|
Format an error response
|
||||||
|
</summary>
|
||||||
|
<param name="message">The error message</param>
|
||||||
|
<returns>Formatted error response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatInteger(System.Int32)">
|
||||||
|
<summary>
|
||||||
|
Format an integer response
|
||||||
|
</summary>
|
||||||
|
<param name="value">The integer value to format</param>
|
||||||
|
<returns>Formatted integer response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatBulkString(System.String)">
|
||||||
|
<summary>
|
||||||
|
Format a bulk string response
|
||||||
|
</summary>
|
||||||
|
<param name="value">The string value to format</param>
|
||||||
|
<returns>Formatted bulk string response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatArray(System.String[])">
|
||||||
|
<summary>
|
||||||
|
Format an array response
|
||||||
|
</summary>
|
||||||
|
<param name="values">Array of strings to format</param>
|
||||||
|
<returns>Formatted array response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.MyRegex_0">
|
||||||
|
<summary>Custom <see cref="T:System.Text.RegularExpressions.Regex"/>-derived type for the MyRegex method.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:System.Text.RegularExpressions.Generated.MyRegex_0.Instance">
|
||||||
|
<summary>Cached, thread-safe singleton instance.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.#ctor">
|
||||||
|
<summary>Initializes the instance.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory">
|
||||||
|
<summary>Provides a factory for creating <see cref="T:System.Text.RegularExpressions.RegexRunner"/> instances to be used by methods on <see cref="T:System.Text.RegularExpressions.Regex"/>.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.CreateInstance">
|
||||||
|
<summary>Creates an instance of a <see cref="T:System.Text.RegularExpressions.RegexRunner"/> used by methods on <see cref="T:System.Text.RegularExpressions.Regex"/>.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner">
|
||||||
|
<summary>Provides the runner that contains the custom logic implementing the specified regular expression.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner.Scan(System.ReadOnlySpan{System.Char})">
|
||||||
|
<summary>Scan the <paramref name="inputSpan"/> starting from base.runtextstart for the next match.</summary>
|
||||||
|
<param name="inputSpan">The text being scanned by the regular expression.</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner.TryFindNextPossibleStartingPosition(System.ReadOnlySpan{System.Char})">
|
||||||
|
<summary>Search <paramref name="inputSpan"/> starting from base.runtextpos for the next location a match could possibly start.</summary>
|
||||||
|
<param name="inputSpan">The text being scanned by the regular expression.</param>
|
||||||
|
<returns>true if a possible match was found; false if no more matches are possible.</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner.TryMatchAtCurrentPosition(System.ReadOnlySpan{System.Char})">
|
||||||
|
<summary>Determine whether <paramref name="inputSpan"/> at base.runtextpos is a match for the regular expression.</summary>
|
||||||
|
<param name="inputSpan">The text being scanned by the regular expression.</param>
|
||||||
|
<returns>true if the regular expression matches at the current position; otherwise, false.</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.Utilities">
|
||||||
|
<summary>Helper methods used by generated <see cref="T:System.Text.RegularExpressions.Regex"/>-derived implementations.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:System.Text.RegularExpressions.Generated.Utilities.s_defaultTimeout">
|
||||||
|
<summary>Default timeout value set in <see cref="T:System.AppContext"/>, or <see cref="F:System.Text.RegularExpressions.Regex.InfiniteMatchTimeout"/> if none was set.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:System.Text.RegularExpressions.Generated.Utilities.s_hasTimeout">
|
||||||
|
<summary>Whether <see cref="F:System.Text.RegularExpressions.Generated.Utilities.s_defaultTimeout"/> is non-infinite.</summary>
|
||||||
|
</member>
|
||||||
|
</members>
|
||||||
|
</doc>
|
BIN
artifacts/native/Firefly.dll
Normal file
BIN
artifacts/native/Firefly.dll
Normal file
Binary file not shown.
BIN
artifacts/native/Firefly.pdb
Normal file
BIN
artifacts/native/Firefly.pdb
Normal file
Binary file not shown.
BIN
artifacts/native/Firefly.so
Normal file
BIN
artifacts/native/Firefly.so
Normal file
Binary file not shown.
BIN
artifacts/native/Firefly.so.dbg
Normal file
BIN
artifacts/native/Firefly.so.dbg
Normal file
Binary file not shown.
677
artifacts/native/Firefly.xml
Normal file
677
artifacts/native/Firefly.xml
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<doc>
|
||||||
|
<assembly>
|
||||||
|
<name>Firefly</name>
|
||||||
|
</assembly>
|
||||||
|
<members>
|
||||||
|
<member name="T:Firefly.Properties.Resources">
|
||||||
|
<summary>
|
||||||
|
A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.ResourceManager">
|
||||||
|
<summary>
|
||||||
|
Returns the cached ResourceManager instance used by this class.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.Culture">
|
||||||
|
<summary>
|
||||||
|
Overrides the current thread's CurrentUICulture property for all
|
||||||
|
resource lookups using this strongly typed resource class.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.icon">
|
||||||
|
<summary>
|
||||||
|
Looks up a localized resource of type System.Byte[].
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.Properties.Resources.logo">
|
||||||
|
<summary>
|
||||||
|
Looks up a localized resource of type System.Byte[].
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Firefly">
|
||||||
|
<summary>
|
||||||
|
Core class implementing the Firefly database server functionality
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HSET command which sets a field in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field value"</param>
|
||||||
|
<returns>1 if the field was added, 0 if it was updated</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHGetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HGET command which retrieves the value of a field in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field"</param>
|
||||||
|
<returns>The value of the field, or nil if the field doesn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHDelCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HDEL command which removes a field from a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field"</param>
|
||||||
|
<returns>1 if the field was removed, 0 if it didn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHExistsCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HEXISTS command which checks if a field exists in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field"</param>
|
||||||
|
<returns>1 if the field exists, 0 if it doesn't</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHGetAllCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HGETALL command which retrieves all fields and values in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key"</param>
|
||||||
|
<returns>All fields and values in the hash, or nil if the hash doesn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleHMSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the HMSET command which sets multiple fields in a hash.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key field value [field value ...]"</param>
|
||||||
|
<returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetOrCreateHash(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets or creates a hash for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get or create the hash for</param>
|
||||||
|
<returns>The hash</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HashStoreExists(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a hash exists for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the hash for</param>
|
||||||
|
<returns>True if the hash exists, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HashStoreGet(System.String,System.Collections.Concurrent.ConcurrentDictionary{System.String,System.String}@)">
|
||||||
|
<summary>
|
||||||
|
Gets a hash for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the hash for</param>
|
||||||
|
<param name="value">The hash</param>
|
||||||
|
<returns>True if the hash was found, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HashStoreRemove(System.String)">
|
||||||
|
<summary>
|
||||||
|
Removes a hash for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to remove the hash for</param>
|
||||||
|
<returns>True if the hash was removed, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.SplitRespectingQuotes(System.String)">
|
||||||
|
<summary>
|
||||||
|
Splits a string respecting quoted sections.
|
||||||
|
</summary>
|
||||||
|
<param name="input">The input string to split</param>
|
||||||
|
<returns>An array of tokens from the input string</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.MyRegex">
|
||||||
|
<remarks>
|
||||||
|
Pattern:<br/>
|
||||||
|
<code>([^\\s"]+)|"([^"]*)"</code><br/>
|
||||||
|
Explanation:<br/>
|
||||||
|
<code>
|
||||||
|
○ Match with 2 alternative expressions, atomically.<br/>
|
||||||
|
○ 1st capture group.<br/>
|
||||||
|
○ Match a character in the set [^"\s] atomically at least once.<br/>
|
||||||
|
○ Match a sequence of expressions.<br/>
|
||||||
|
○ Match '"'.<br/>
|
||||||
|
○ 2nd capture group.<br/>
|
||||||
|
○ Match a character other than '"' atomically any number of times.<br/>
|
||||||
|
○ Match '"'.<br/>
|
||||||
|
</code>
|
||||||
|
</remarks>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.CreateShards``1">
|
||||||
|
<summary>
|
||||||
|
Creates an array of shards of type T.
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The type of the shards</typeparam>
|
||||||
|
<returns>An array of shards</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.CreateListLocks">
|
||||||
|
<summary>
|
||||||
|
Creates an array of ReaderWriterLockSlim instances for list operations.
|
||||||
|
</summary>
|
||||||
|
<returns>An array of locks</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetShardIndex(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets the shard index for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the shard index for</param>
|
||||||
|
<returns>The shard index</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.CreateLocks">
|
||||||
|
<summary>
|
||||||
|
Creates an array of locks.
|
||||||
|
</summary>
|
||||||
|
<returns>An array of locks</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.KeyExistsInAnyStore(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a key exists in any store (string, list, or hash).
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check</param>
|
||||||
|
<returns>True if the key exists in any store, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetKeyType(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets the type of a key if it exists in any store.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check</param>
|
||||||
|
<returns>The type of the key ("string", "list", "hash", or null if not found)</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.EnsureKeyDoesNotExist(System.String,System.String)">
|
||||||
|
<summary>
|
||||||
|
Ensures a key doesn't exist in any store before creating it in the target store.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check</param>
|
||||||
|
<param name="targetType">The type of store where the key will be created</param>
|
||||||
|
<returns>True if the key can be created, false if it already exists with a different type</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleDelCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the DEL command which removes a key from all stores (string, list, hash).
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to delete</param>
|
||||||
|
<returns>The number of keys that were removed</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleTypeCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the TYPE command which returns the type of a key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the type of</param>
|
||||||
|
<returns>The type of the key as a string response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleKeysCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the KEYS command which returns all keys matching a pattern.
|
||||||
|
</summary>
|
||||||
|
<param name="pattern">The pattern to match</param>
|
||||||
|
<returns>A list of keys matching the pattern</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLPushCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LPUSH command which adds an element to the left of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key value"</param>
|
||||||
|
<returns>The length of the list after the push operation</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleRPushCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the RPUSH command which adds values to the tail of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key value1 [value2 ...]"</param>
|
||||||
|
<returns>Response indicating the new length of the list</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLPopCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LPOP command which removes and returns the first element of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments containing the key</param>
|
||||||
|
<returns>The popped value or nil if the list is empty</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleRPopCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the RPOP command which removes and returns the last element of a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments containing the key</param>
|
||||||
|
<returns>The popped value or nil if the list is empty</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLRangeCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LRANGE command which returns a range of elements from a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key start stop"</param>
|
||||||
|
<returns>Array of elements in the specified range</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLIndexCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LINDEX command which returns an element from a list by its index.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key index"</param>
|
||||||
|
<returns>The element at the specified index or nil if not found</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LSET command which sets the value of an element in a list by its index.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key index value"</param>
|
||||||
|
<returns>OK on success, error if index is out of range</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLPosCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LPOS command which returns the position of an element in a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key element [RANK rank] [MAXLEN len]"</param>
|
||||||
|
<returns>The position of the element or nil if not found</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLTrimCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LTRIM command which trims a list to the specified range.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key start stop"</param>
|
||||||
|
<returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleLRemCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the LREM command which removes elements equal to the given value from a list.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key count element"</param>
|
||||||
|
<returns>The number of removed elements</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.GetOrCreateList(System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets or creates a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get or create the list for</param>
|
||||||
|
<returns>The list</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreExists(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a list exists for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the list for</param>
|
||||||
|
<returns>True if the list exists, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreGet(System.String,System.Collections.Generic.List{System.String}@)">
|
||||||
|
<summary>
|
||||||
|
Gets a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the list for</param>
|
||||||
|
<param name="value">The list</param>
|
||||||
|
<returns>True if the list was found, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreRemove(System.String)">
|
||||||
|
<summary>
|
||||||
|
Removes a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to remove the list for</param>
|
||||||
|
<returns>True if the list was removed, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreWithWriteLock(System.String,System.Action{System.Collections.Generic.List{System.String}})">
|
||||||
|
<summary>
|
||||||
|
Executes an action with a write lock on a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to execute the action on</param>
|
||||||
|
<param name="action">The action to execute</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ListStoreWithReadLock``1(System.String,System.Func{System.Collections.Generic.List{System.String},``0})">
|
||||||
|
<summary>
|
||||||
|
Executes an action with a read lock on a list for a given key.
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The type of the result</typeparam>
|
||||||
|
<param name="key">The key to execute the action on</param>
|
||||||
|
<param name="action">The action to execute</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleAuthCommand(System.String,System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the AUTH command which authenticates a client.
|
||||||
|
</summary>
|
||||||
|
<param name="password">The password to authenticate with</param>
|
||||||
|
<param name="clientId">The client ID</param>
|
||||||
|
<returns>OK on success, error if authentication fails</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.IsAuthenticated(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a client is authenticated.
|
||||||
|
</summary>
|
||||||
|
<param name="clientId">The client ID</param>
|
||||||
|
<returns>True if the client is authenticated, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleSaveCommand">
|
||||||
|
<summary>
|
||||||
|
Handles the SAVE command which triggers a manual backup of the data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleBgSaveCommand">
|
||||||
|
<summary>
|
||||||
|
Handles the BGSAVE command which triggers an asynchronous backup of the data.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.ShutdownServer">
|
||||||
|
<summary>
|
||||||
|
Handles server shutdown operations
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleSetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the SET command which sets a key-value pair in the string store.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments in format: "key value"</param>
|
||||||
|
<returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.HandleGetCommand(System.String)">
|
||||||
|
<summary>
|
||||||
|
Handles the GET command which retrieves a value from the string store.
|
||||||
|
</summary>
|
||||||
|
<param name="args">Command arguments containing the key</param>
|
||||||
|
<returns>The value associated with the key, or nil if the key doesn't exist</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreExists(System.String)">
|
||||||
|
<summary>
|
||||||
|
Checks if a string exists for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to check the string for</param>
|
||||||
|
<returns>True if the string exists, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreSet(System.String,System.String)">
|
||||||
|
<summary>
|
||||||
|
Sets a string for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to set the string for</param>
|
||||||
|
<param name="value">The value to set</param>
|
||||||
|
<returns>True if the string was set, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreGet(System.String,System.String@)">
|
||||||
|
<summary>
|
||||||
|
Gets a string for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to get the string for</param>
|
||||||
|
<param name="value">The value</param>
|
||||||
|
<returns>True if the string was found, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Firefly.StringStoreRemove(System.String)">
|
||||||
|
<summary>
|
||||||
|
Removes a string for a given key.
|
||||||
|
</summary>
|
||||||
|
<param name="key">The key to remove the string for</param>
|
||||||
|
<returns>True if the string was removed, false otherwise</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.FireflyData">
|
||||||
|
<summary>
|
||||||
|
Container class for all Firefly database data used in serialization and backup operations.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.StringStore">
|
||||||
|
<summary>
|
||||||
|
Dictionary containing all string key-value pairs stored in the database.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.ListStore">
|
||||||
|
<summary>
|
||||||
|
Dictionary containing all lists stored in the database.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.HashStore">
|
||||||
|
<summary>
|
||||||
|
Dictionary containing all hash tables stored in the database.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:Firefly.FireflyData.BackupTime">
|
||||||
|
<summary>
|
||||||
|
Timestamp when the backup was created.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.PipelineTest">
|
||||||
|
<summary>
|
||||||
|
Test client for verifying pipelining and batching functionality
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunTest(System.String[])">
|
||||||
|
<summary>
|
||||||
|
Runs the pipeline test
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.ReadResponses(System.Net.Sockets.NetworkStream,System.Byte[],System.Int32,System.String)">
|
||||||
|
<summary>
|
||||||
|
Read responses from the stream until we have the expected count
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunSequentialTest">
|
||||||
|
<summary>
|
||||||
|
Test with sequential commands (no pipelining)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunPipelinedTest">
|
||||||
|
<summary>
|
||||||
|
Test with pipelined commands
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.PipelineTest.RunBatchedTest">
|
||||||
|
<summary>
|
||||||
|
Test with batched commands
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Protocol.FireflyProtocol">
|
||||||
|
<summary>
|
||||||
|
Defines the Firefly wire protocol specification for 3rd party clients
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.PROTOCOL_VERSION">
|
||||||
|
<summary>
|
||||||
|
The current version of the Firefly protocol
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.STRING_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for simple string responses (+)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.ERROR_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for error responses (-)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.INTEGER_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for integer responses (:)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.BULK_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for bulk string responses ($)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.ARRAY_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Prefix for array responses (*)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.NIL_MESSAGE">
|
||||||
|
<summary>
|
||||||
|
Special message indicating a nil value ($-1)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.MESSAGE_TERMINATOR">
|
||||||
|
<summary>
|
||||||
|
Standard message terminator sequence (\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.OK_RESPONSE">
|
||||||
|
<summary>
|
||||||
|
Standard OK response (+OK\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.QUEUED_RESPONSE">
|
||||||
|
<summary>
|
||||||
|
Response for queued pipeline commands (+QUEUED\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.NIL_RESPONSE">
|
||||||
|
<summary>
|
||||||
|
Standard nil response ($-1\r\n)
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Protocol.FireflyProtocol.Commands">
|
||||||
|
<summary>
|
||||||
|
Command format specification for external clients
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.AUTH">
|
||||||
|
<summary>
|
||||||
|
Authenticate with the server using a password
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.PING">
|
||||||
|
<summary>
|
||||||
|
Test server connection
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.QUIT">
|
||||||
|
<summary>
|
||||||
|
Close the connection
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.SET">
|
||||||
|
<summary>
|
||||||
|
Set a key to hold a string value
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.GET">
|
||||||
|
<summary>
|
||||||
|
Get the value of a key
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.DEL">
|
||||||
|
<summary>
|
||||||
|
Delete a key
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.LPUSH">
|
||||||
|
<summary>
|
||||||
|
Insert elements at the head of a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.RPUSH">
|
||||||
|
<summary>
|
||||||
|
Insert elements at the tail of a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.LPOP">
|
||||||
|
<summary>
|
||||||
|
Remove and get the first element in a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.RPOP">
|
||||||
|
<summary>
|
||||||
|
Remove and get the last element in a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.LRANGE">
|
||||||
|
<summary>
|
||||||
|
Get a range of elements from a list
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HSET">
|
||||||
|
<summary>
|
||||||
|
Set the string value of a hash field
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HGET">
|
||||||
|
<summary>
|
||||||
|
Get the value of a hash field
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HDEL">
|
||||||
|
<summary>
|
||||||
|
Delete a hash field
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.HGETALL">
|
||||||
|
<summary>
|
||||||
|
Get all fields and values in a hash
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.PIPELINE">
|
||||||
|
<summary>
|
||||||
|
Start a pipeline for batch processing
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Firefly.Protocol.FireflyProtocol.Commands.EXEC">
|
||||||
|
<summary>
|
||||||
|
Execute all commands in the pipeline
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Firefly.Protocol.FireflyProtocol.Responses">
|
||||||
|
<summary>
|
||||||
|
Response format specification for external clients
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatString(System.String)">
|
||||||
|
<summary>
|
||||||
|
Format a simple string response
|
||||||
|
</summary>
|
||||||
|
<param name="value">The string value to format</param>
|
||||||
|
<returns>Formatted string response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatError(System.String)">
|
||||||
|
<summary>
|
||||||
|
Format an error response
|
||||||
|
</summary>
|
||||||
|
<param name="message">The error message</param>
|
||||||
|
<returns>Formatted error response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatInteger(System.Int32)">
|
||||||
|
<summary>
|
||||||
|
Format an integer response
|
||||||
|
</summary>
|
||||||
|
<param name="value">The integer value to format</param>
|
||||||
|
<returns>Formatted integer response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatBulkString(System.String)">
|
||||||
|
<summary>
|
||||||
|
Format a bulk string response
|
||||||
|
</summary>
|
||||||
|
<param name="value">The string value to format</param>
|
||||||
|
<returns>Formatted bulk string response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:Firefly.Protocol.FireflyProtocol.Responses.FormatArray(System.String[])">
|
||||||
|
<summary>
|
||||||
|
Format an array response
|
||||||
|
</summary>
|
||||||
|
<param name="values">Array of strings to format</param>
|
||||||
|
<returns>Formatted array response</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.MyRegex_0">
|
||||||
|
<summary>Custom <see cref="T:System.Text.RegularExpressions.Regex"/>-derived type for the MyRegex method.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:System.Text.RegularExpressions.Generated.MyRegex_0.Instance">
|
||||||
|
<summary>Cached, thread-safe singleton instance.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.#ctor">
|
||||||
|
<summary>Initializes the instance.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory">
|
||||||
|
<summary>Provides a factory for creating <see cref="T:System.Text.RegularExpressions.RegexRunner"/> instances to be used by methods on <see cref="T:System.Text.RegularExpressions.Regex"/>.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.CreateInstance">
|
||||||
|
<summary>Creates an instance of a <see cref="T:System.Text.RegularExpressions.RegexRunner"/> used by methods on <see cref="T:System.Text.RegularExpressions.Regex"/>.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner">
|
||||||
|
<summary>Provides the runner that contains the custom logic implementing the specified regular expression.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner.Scan(System.ReadOnlySpan{System.Char})">
|
||||||
|
<summary>Scan the <paramref name="inputSpan"/> starting from base.runtextstart for the next match.</summary>
|
||||||
|
<param name="inputSpan">The text being scanned by the regular expression.</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner.TryFindNextPossibleStartingPosition(System.ReadOnlySpan{System.Char})">
|
||||||
|
<summary>Search <paramref name="inputSpan"/> starting from base.runtextpos for the next location a match could possibly start.</summary>
|
||||||
|
<param name="inputSpan">The text being scanned by the regular expression.</param>
|
||||||
|
<returns>true if a possible match was found; false if no more matches are possible.</returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:System.Text.RegularExpressions.Generated.MyRegex_0.RunnerFactory.Runner.TryMatchAtCurrentPosition(System.ReadOnlySpan{System.Char})">
|
||||||
|
<summary>Determine whether <paramref name="inputSpan"/> at base.runtextpos is a match for the regular expression.</summary>
|
||||||
|
<param name="inputSpan">The text being scanned by the regular expression.</param>
|
||||||
|
<returns>true if the regular expression matches at the current position; otherwise, false.</returns>
|
||||||
|
</member>
|
||||||
|
<member name="T:System.Text.RegularExpressions.Generated.Utilities">
|
||||||
|
<summary>Helper methods used by generated <see cref="T:System.Text.RegularExpressions.Regex"/>-derived implementations.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:System.Text.RegularExpressions.Generated.Utilities.s_defaultTimeout">
|
||||||
|
<summary>Default timeout value set in <see cref="T:System.AppContext"/>, or <see cref="F:System.Text.RegularExpressions.Regex.InfiniteMatchTimeout"/> if none was set.</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:System.Text.RegularExpressions.Generated.Utilities.s_hasTimeout">
|
||||||
|
<summary>Whether <see cref="F:System.Text.RegularExpressions.Generated.Utilities.s_defaultTimeout"/> is non-infinite.</summary>
|
||||||
|
</member>
|
||||||
|
</members>
|
||||||
|
</doc>
|
44
build-all.ps1
Normal file
44
build-all.ps1
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Build configuration
|
||||||
|
$configuration = "Release"
|
||||||
|
$baseOutputPath = ".\artifacts"
|
||||||
|
|
||||||
|
# Determine current OS and platform
|
||||||
|
$currentPlatform = "win-x64" # Since we're running PowerShell, we're on Windows
|
||||||
|
Write-Host "Building for current platform: $currentPlatform"
|
||||||
|
|
||||||
|
# Create output directories
|
||||||
|
$exeOutputPath = "$baseOutputPath\exe"
|
||||||
|
$nativeOutputPath = "$baseOutputPath\native"
|
||||||
|
New-Item -ItemType Directory -Force -Path $exeOutputPath | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path $nativeOutputPath | Out-Null
|
||||||
|
|
||||||
|
# Build executable
|
||||||
|
Write-Host "Building executable..."
|
||||||
|
dotnet publish `
|
||||||
|
-c $configuration `
|
||||||
|
-r $currentPlatform `
|
||||||
|
--self-contained true `
|
||||||
|
-p:PublishDir=$exeOutputPath
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Executable build failed. Check the error messages above."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build native shared library
|
||||||
|
Write-Host "Building native shared library..."
|
||||||
|
dotnet publish `
|
||||||
|
-c $configuration `
|
||||||
|
-r $currentPlatform `
|
||||||
|
--self-contained true `
|
||||||
|
-p:BuildType=lib `
|
||||||
|
-p:PublishDir=$nativeOutputPath
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Library build failed. Check the error messages above."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Build complete. Outputs available at:"
|
||||||
|
Write-Host "Executable: $exeOutputPath"
|
||||||
|
Write-Host "Native library: $nativeOutputPath"
|
59
build-all.sh
Normal file
59
build-all.sh
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
configuration="Release"
|
||||||
|
baseOutputPath="./artifacts"
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
currentPlatform="osx-x64"
|
||||||
|
else
|
||||||
|
currentPlatform="linux-x64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building for current platform: $currentPlatform"
|
||||||
|
|
||||||
|
# Create output directories
|
||||||
|
exeOutputPath="$baseOutputPath/exe"
|
||||||
|
nativeOutputPath="$baseOutputPath/native"
|
||||||
|
mkdir -p "$exeOutputPath"
|
||||||
|
mkdir -p "$nativeOutputPath"
|
||||||
|
|
||||||
|
# Build executable
|
||||||
|
echo "Building executable..."
|
||||||
|
dotnet publish \
|
||||||
|
-c $configuration \
|
||||||
|
-r $currentPlatform \
|
||||||
|
--self-contained true \
|
||||||
|
-p:PublishDir=$exeOutputPath
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Executable build failed. Check the error messages above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build native shared library
|
||||||
|
echo "Building native shared library..."
|
||||||
|
dotnet publish \
|
||||||
|
-c $configuration \
|
||||||
|
-r $currentPlatform \
|
||||||
|
--self-contained true \
|
||||||
|
-p:BuildType=lib \
|
||||||
|
-p:PublishDir=$nativeOutputPath
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Library build failed. Check the error messages above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build complete. Outputs available at:"
|
||||||
|
echo "Executable: $exeOutputPath"
|
||||||
|
echo "Native library: $nativeOutputPath"
|
||||||
|
|
||||||
|
# Make the outputs executable
|
||||||
|
if [[ "$currentPlatform" == "linux-x64" ]]; then
|
||||||
|
chmod +x "$exeOutputPath/Firefly"
|
||||||
|
chmod +x "$nativeOutputPath/Firefly.so"
|
||||||
|
elif [[ "$currentPlatform" == "osx-x64" ]]; then
|
||||||
|
chmod +x "$exeOutputPath/Firefly"
|
||||||
|
chmod +x "$nativeOutputPath/Firefly.dylib"
|
||||||
|
fi
|
BIN
resources/icon.ico
Normal file
BIN
resources/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
BIN
resources/icon.png
Normal file
BIN
resources/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 KiB |
358
src/BackupSystem.cs
Normal file
358
src/BackupSystem.cs
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Core class implementing the Firefly database server functionality
|
||||||
|
/// </summary>
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region Backup System
|
||||||
|
static void InitializeBackupSystem()
|
||||||
|
{
|
||||||
|
// Create backup directory if it doesn't exist
|
||||||
|
if (!Directory.Exists(backupDirectory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(backupDirectory);
|
||||||
|
Console.WriteLine($"Created backup directory: {backupDirectory}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Warning: Failed to create backup directory: {ex.Message}");
|
||||||
|
Console.WriteLine("Backups will be disabled");
|
||||||
|
backupsEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up automatic backup timer
|
||||||
|
backupTimer = new System.Timers.Timer(autoBackupIntervalMinutes * 60 * 1000); // Convert minutes to milliseconds
|
||||||
|
backupTimer.Elapsed += (sender, e) => BackupData();
|
||||||
|
backupTimer.AutoReset = true;
|
||||||
|
backupTimer.Start();
|
||||||
|
|
||||||
|
Console.WriteLine($"Automatic backup system initialized (every {autoBackupIntervalMinutes} minutes)");
|
||||||
|
Console.WriteLine($"Keeping up to {maxBackupFiles} backup files");
|
||||||
|
|
||||||
|
// Register backup on exit
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
|
||||||
|
{
|
||||||
|
if (backupsEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server shutting down, performing final backup...");
|
||||||
|
BackupData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LoadDataFromBackup()
|
||||||
|
{
|
||||||
|
if (!backupsEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Backups are disabled, starting with empty database");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? mostRecentBackup = GetMostRecentBackupFile();
|
||||||
|
if (mostRecentBackup != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Loading data from backup: {Path.GetFileName(mostRecentBackup)}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var fileStream = new FileStream(mostRecentBackup, FileMode.Open, FileAccess.Read);
|
||||||
|
using var binaryReader = new BinaryReader(fileStream);
|
||||||
|
|
||||||
|
// Read backup time
|
||||||
|
long backupTimeBinary = binaryReader.ReadInt64();
|
||||||
|
DateTime backupTime = DateTime.FromBinary(backupTimeBinary);
|
||||||
|
Console.WriteLine($"Backup was created on: {backupTime:f}");
|
||||||
|
|
||||||
|
// Read string store
|
||||||
|
int stringCount = binaryReader.ReadInt32();
|
||||||
|
Console.WriteLine($"Loading {stringCount} string keys from backup");
|
||||||
|
for (int i = 0; i < stringCount; i++)
|
||||||
|
{
|
||||||
|
string key = binaryReader.ReadString();
|
||||||
|
string value = binaryReader.ReadString();
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
stringStoreShards[shardIndex][key] = value;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Loaded {stringCount} string keys from backup");
|
||||||
|
|
||||||
|
// Read list store
|
||||||
|
int listCount = binaryReader.ReadInt32();
|
||||||
|
Console.WriteLine($"Loading {listCount} list keys from backup");
|
||||||
|
for (int i = 0; i < listCount; i++)
|
||||||
|
{
|
||||||
|
string key = binaryReader.ReadString();
|
||||||
|
int itemCount = binaryReader.ReadInt32();
|
||||||
|
var list = new List<string>();
|
||||||
|
|
||||||
|
for (int j = 0; j < itemCount; j++)
|
||||||
|
{
|
||||||
|
string item = binaryReader.ReadString();
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
listStoreShards[shardIndex][key] = list;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Loaded {listCount} list keys from backup");
|
||||||
|
|
||||||
|
// Read hash store
|
||||||
|
int hashCount = binaryReader.ReadInt32();
|
||||||
|
Console.WriteLine($"Loading {hashCount} hash keys from backup");
|
||||||
|
for (int i = 0; i < hashCount; i++)
|
||||||
|
{
|
||||||
|
string key = binaryReader.ReadString();
|
||||||
|
int fieldCount = binaryReader.ReadInt32();
|
||||||
|
var concurrentHash = new ConcurrentDictionary<string, string>();
|
||||||
|
|
||||||
|
for (int j = 0; j < fieldCount; j++)
|
||||||
|
{
|
||||||
|
string fieldKey = binaryReader.ReadString();
|
||||||
|
string fieldValue = binaryReader.ReadString();
|
||||||
|
concurrentHash[fieldKey] = fieldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
hashStoreShards[shardIndex][key] = concurrentHash;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Loaded {hashCount} hash keys from backup");
|
||||||
|
|
||||||
|
Console.WriteLine($"Data successfully loaded from backup: {Path.GetFileName(mostRecentBackup)}");
|
||||||
|
Console.WriteLine($"Total keys loaded: {stringCount + listCount + hashCount}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error loading data from backup: {ex.Message}");
|
||||||
|
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||||
|
Console.WriteLine("The backup file may be corrupted or created with an incompatible version");
|
||||||
|
Console.WriteLine("Starting with empty database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("No backup files found, starting with empty database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent backup file
|
||||||
|
static string? GetMostRecentBackupFile()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(backupDirectory))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var backupFiles = Directory.GetFiles(backupDirectory, backupFilePrefix + "*" + backupFileExtension)
|
||||||
|
.OrderByDescending(file => File.GetLastWriteTime(file))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return backupFiles.Length > 0 ? backupFiles[0] : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error accessing backup files: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes oldest backup files if the number exceeds maxBackupFiles
|
||||||
|
static void RotateBackupFiles()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(backupDirectory))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var backupFiles = Directory.GetFiles(backupDirectory, backupFilePrefix + "*" + backupFileExtension)
|
||||||
|
.OrderByDescending(file => File.GetLastWriteTime(file))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Remove oldest files if we have more than maxBackupFiles
|
||||||
|
if (backupFiles.Length > maxBackupFiles)
|
||||||
|
{
|
||||||
|
for (int i = maxBackupFiles; i < backupFiles.Length; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(backupFiles[i]);
|
||||||
|
Console.WriteLine($"Deleted old backup file: {Path.GetFileName(backupFiles[i])}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error deleting old backup file {backupFiles[i]}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error during backup rotation: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BackupData()
|
||||||
|
{
|
||||||
|
// Skip if backups are disabled
|
||||||
|
if (!backupsEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Use a global lock to prevent backup conflicts
|
||||||
|
backupLock.Enter(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = new FireflyData
|
||||||
|
{
|
||||||
|
BackupTime = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy string store from all shards
|
||||||
|
for (int i = 0; i < ShardCount; i++)
|
||||||
|
{
|
||||||
|
foreach (var kvp in stringStoreShards[i])
|
||||||
|
{
|
||||||
|
data.StringStore[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy list store from all shards
|
||||||
|
for (int i = 0; i < ShardCount; i++)
|
||||||
|
{
|
||||||
|
listStoreLocks[i].EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var kvp in listStoreShards[i])
|
||||||
|
{
|
||||||
|
data.ListStore[kvp.Key] = [.. kvp.Value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[i].ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy hash store from all shards
|
||||||
|
for (int i = 0; i < ShardCount; i++)
|
||||||
|
{
|
||||||
|
foreach (var kvp in hashStoreShards[i])
|
||||||
|
{
|
||||||
|
// Convert ConcurrentDictionary to Dictionary for serialization
|
||||||
|
var regularDict = new Dictionary<string, string>();
|
||||||
|
foreach (var fieldKvp in kvp.Value)
|
||||||
|
{
|
||||||
|
regularDict[fieldKvp.Key] = fieldKvp.Value;
|
||||||
|
}
|
||||||
|
data.HashStore[kvp.Key] = regularDict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have any data to back up
|
||||||
|
if (data.StringStore.Count > 0 || data.ListStore.Count > 0 || data.HashStore.Count > 0)
|
||||||
|
{
|
||||||
|
// Generate timestamp for the filename
|
||||||
|
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
string backupFileName = $"{backupFilePrefix}_{timestamp}{backupFileExtension}";
|
||||||
|
string backupFilePath = Path.Combine(backupDirectory, backupFileName);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Serialize and save data - use a temporary file first to avoid corruption
|
||||||
|
string tempFilePath = backupFilePath + ".tmp";
|
||||||
|
|
||||||
|
// Replace JSON serialization with BinaryFormatter
|
||||||
|
using (var fileStream = new FileStream(tempFilePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
using var binaryWriter = new BinaryWriter(fileStream);
|
||||||
|
// Write backup time
|
||||||
|
binaryWriter.Write(data.BackupTime.ToBinary());
|
||||||
|
|
||||||
|
// Write string store
|
||||||
|
binaryWriter.Write(data.StringStore.Count);
|
||||||
|
foreach (var kvp in data.StringStore)
|
||||||
|
{
|
||||||
|
binaryWriter.Write(kvp.Key);
|
||||||
|
binaryWriter.Write(kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write list store
|
||||||
|
binaryWriter.Write(data.ListStore.Count);
|
||||||
|
foreach (var kvp in data.ListStore)
|
||||||
|
{
|
||||||
|
binaryWriter.Write(kvp.Key);
|
||||||
|
binaryWriter.Write(kvp.Value.Count);
|
||||||
|
foreach (var item in kvp.Value)
|
||||||
|
{
|
||||||
|
binaryWriter.Write(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write hash store
|
||||||
|
binaryWriter.Write(data.HashStore.Count);
|
||||||
|
foreach (var kvp in data.HashStore)
|
||||||
|
{
|
||||||
|
binaryWriter.Write(kvp.Key);
|
||||||
|
binaryWriter.Write(kvp.Value.Count);
|
||||||
|
foreach (var fieldKvp in kvp.Value)
|
||||||
|
{
|
||||||
|
binaryWriter.Write(fieldKvp.Key);
|
||||||
|
binaryWriter.Write(fieldKvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If successfully written, move to final location
|
||||||
|
if (File.Exists(tempFilePath))
|
||||||
|
{
|
||||||
|
// Delete the target file if it already exists
|
||||||
|
if (File.Exists(backupFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(backupFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename temp to final
|
||||||
|
File.Move(tempFilePath, backupFilePath);
|
||||||
|
|
||||||
|
Console.WriteLine($"Backup completed at {DateTime.Now}: " +
|
||||||
|
$"{data.StringStore.Count} string keys, " +
|
||||||
|
$"{data.ListStore.Count} list keys, " +
|
||||||
|
$"{data.HashStore.Count} hash keys");
|
||||||
|
Console.WriteLine($"Saved to: {backupFileName}");
|
||||||
|
|
||||||
|
// Rotate backup files if needed
|
||||||
|
RotateBackupFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error during backup file creation: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("No data to back up");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error during backup: {ex.Message}");
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
180
src/CommandLineParser.cs
Normal file
180
src/CommandLineParser.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region Command Line Parsing
|
||||||
|
static async Task ParseArguments(string[] args)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
string arg = args[i].ToLower();
|
||||||
|
|
||||||
|
switch (arg)
|
||||||
|
{
|
||||||
|
case "--port":
|
||||||
|
case "-p":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int port))
|
||||||
|
{
|
||||||
|
serverPort = port;
|
||||||
|
Console.WriteLine($"Server port set to: {serverPort}");
|
||||||
|
i++; // Skip the next arg (the port number)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Warning: Invalid port specified, using default port 6379");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--bind":
|
||||||
|
case "-b":
|
||||||
|
if (i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
bindAddress = args[i + 1];
|
||||||
|
if (bindAddress == "0.0.0.0")
|
||||||
|
{
|
||||||
|
Console.WriteLine("Server will listen on all network interfaces");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Server bind address set to: {bindAddress}");
|
||||||
|
}
|
||||||
|
i++; // Skip the next arg (the bind address)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Warning: Bind address argument specified but no address provided");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--password":
|
||||||
|
case "-pass":
|
||||||
|
if (i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
serverPassword = args[i + 1];
|
||||||
|
Console.WriteLine("Password authentication enabled");
|
||||||
|
i++; // Skip the next arg (the password)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Warning: Password argument specified but no password provided");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--no-backup":
|
||||||
|
case "-nb":
|
||||||
|
backupsEnabled = false;
|
||||||
|
Console.WriteLine("Backups disabled");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--backup-interval":
|
||||||
|
case "-bi":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int interval) && interval > 0)
|
||||||
|
{
|
||||||
|
autoBackupIntervalMinutes = interval;
|
||||||
|
Console.WriteLine($"Backup interval set to: {autoBackupIntervalMinutes} minutes");
|
||||||
|
i++; // Skip the next arg (the interval)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Warning: Invalid backup interval specified, using default ({autoBackupIntervalMinutes} minutes)");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--max-backups":
|
||||||
|
case "-mb":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int maxFiles) && maxFiles > 0)
|
||||||
|
{
|
||||||
|
maxBackupFiles = maxFiles;
|
||||||
|
Console.WriteLine($"Maximum backup files set to: {maxBackupFiles}");
|
||||||
|
i++; // Skip the next arg (the count)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Warning: Invalid maximum backup files specified, using default ({maxBackupFiles})");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--timeout":
|
||||||
|
case "-t":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int timeout) && timeout > 0)
|
||||||
|
{
|
||||||
|
connectionTimeoutSeconds = timeout;
|
||||||
|
Console.WriteLine($"Connection timeout set to: {connectionTimeoutSeconds} seconds");
|
||||||
|
i++; // Skip the next arg (the timeout)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Warning: Invalid timeout specified, using default ({connectionTimeoutSeconds} seconds)");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--pipeline-test":
|
||||||
|
case "-pt":
|
||||||
|
Console.WriteLine("Running in pipeline test mode...");
|
||||||
|
|
||||||
|
// Start the server in the background
|
||||||
|
var serverTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await StartServerAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give the server a moment to start up
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
// Run the pipeline test with all arguments
|
||||||
|
await PipelineTest.RunTest(args);
|
||||||
|
|
||||||
|
// After the test completes, exit the application
|
||||||
|
Environment.Exit(0);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
DisplayHelp();
|
||||||
|
Environment.Exit(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DisplayHelp()
|
||||||
|
{
|
||||||
|
Console.WriteLine("\nFirefly Redis-compatible Server");
|
||||||
|
Console.WriteLine("Usage: firefly [options]\n");
|
||||||
|
Console.WriteLine("Options:");
|
||||||
|
Console.WriteLine(" --port, -p <number> Set server port (default: 6379)");
|
||||||
|
Console.WriteLine(" --bind, -b <address> Set server bind address (default: 127.0.0.1)");
|
||||||
|
Console.WriteLine(" --password, -pass <string> Set server password for AUTH command");
|
||||||
|
Console.WriteLine(" --no-backup, -nb Disable automatic backups");
|
||||||
|
Console.WriteLine(" --backup-interval, -bi <mins> Set backup interval in minutes (default: 5)");
|
||||||
|
Console.WriteLine(" --max-backups, -mb <number> Set maximum number of backup files to keep (default: 10)");
|
||||||
|
Console.WriteLine(" --timeout, -t <seconds> Set client connection timeout in seconds (default: 300)");
|
||||||
|
Console.WriteLine(" --pipeline-test, -pt Run in pipeline test mode");
|
||||||
|
Console.WriteLine(" --help, -h Display this help message\n");
|
||||||
|
|
||||||
|
Console.WriteLine("Pipeline Test Options:");
|
||||||
|
Console.WriteLine(" --host <address> Set test host (default: 127.0.0.1)");
|
||||||
|
Console.WriteLine(" --clients <number> Number of concurrent clients (default: 1)");
|
||||||
|
Console.WriteLine(" --commands <number> Number of commands per client (default: 10000)");
|
||||||
|
Console.WriteLine(" --batch <number> Batch size for batched test (default: 100)");
|
||||||
|
Console.WriteLine(" --buffer <size> Buffer size in KB (default: 1024)\n");
|
||||||
|
|
||||||
|
Console.WriteLine("Examples:");
|
||||||
|
Console.WriteLine(" firefly --port 6380 Run server on port 6380");
|
||||||
|
Console.WriteLine(" firefly --bind 0.0.0.0 Listen on all network interfaces");
|
||||||
|
Console.WriteLine(" firefly --bind 192.168.1.10 Listen on a specific network interface");
|
||||||
|
Console.WriteLine(" firefly --password secret123 Require password authentication");
|
||||||
|
Console.WriteLine(" firefly -nb Run server with backups disabled");
|
||||||
|
Console.WriteLine(" firefly -bi 10 Run server with backups every 10 minutes");
|
||||||
|
Console.WriteLine(" firefly -mb 5 Keep only 5 most recent backup files");
|
||||||
|
Console.WriteLine(" firefly -t 60 Set connection timeout to 60 seconds");
|
||||||
|
Console.WriteLine(" firefly -pt Run pipeline performance test");
|
||||||
|
Console.WriteLine(" firefly -pt --clients 4 Run pipeline test with 4 concurrent clients");
|
||||||
|
Console.WriteLine(" firefly -pt --host 192.168.1.100 --port 6380 --password mypass");
|
||||||
|
Console.WriteLine(" Run pipeline test with custom connection settings");
|
||||||
|
Console.WriteLine(" firefly -pt --commands 5000 --batch 50");
|
||||||
|
Console.WriteLine(" Run pipeline test with custom command count and batch size");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
247
src/Firefly.cs
Normal file
247
src/Firefly.cs
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
#region Lock Class
|
||||||
|
// Simple lock class for synchronization
|
||||||
|
internal class Lock
|
||||||
|
{
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
|
||||||
|
public void Enter(Action action)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Enter<T>(Func<T> action)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
partial class Firefly
|
||||||
|
{
|
||||||
|
#region Constants and Fields
|
||||||
|
// Sharding configuration - each shard will have its own ConcurrentDictionary
|
||||||
|
private const int ShardCount = 64; // Must be a power of 2
|
||||||
|
private const int ShardMask = ShardCount - 1; // Used for fast modulo
|
||||||
|
|
||||||
|
// In-memory storage for key-value pairs using ConcurrentDictionary for better performance
|
||||||
|
private static readonly ConcurrentDictionary<string, string>[] stringStoreShards = CreateShards<ConcurrentDictionary<string, string>>();
|
||||||
|
|
||||||
|
// In-memory storage for lists using ConcurrentDictionary for better performance
|
||||||
|
private static readonly ConcurrentDictionary<string, List<string>>[] listStoreShards = CreateShards<ConcurrentDictionary<string, List<string>>>();
|
||||||
|
// Use ReaderWriterLockSlim for list operations to allow concurrent reads
|
||||||
|
private static readonly ReaderWriterLockSlim[] listStoreLocks = CreateListLocks();
|
||||||
|
|
||||||
|
// In-memory storage for hash tables using ConcurrentDictionary for better performance
|
||||||
|
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>>[] hashStoreShards = CreateShards<ConcurrentDictionary<string, ConcurrentDictionary<string, string>>>();
|
||||||
|
// We no longer need locks for most hash operations
|
||||||
|
|
||||||
|
// Global lock for backup operations
|
||||||
|
private static readonly Lock backupLock = new();
|
||||||
|
|
||||||
|
// Default settings that can be overridden by command-line arguments
|
||||||
|
private static int serverPort = 6379; // Default Redis port
|
||||||
|
private static string bindAddress = "127.0.0.1"; // Default to localhost only
|
||||||
|
private static bool backupsEnabled = true;
|
||||||
|
private static int autoBackupIntervalMinutes = 5;
|
||||||
|
private static int connectionTimeoutSeconds = 300; // 5 minutes default timeout
|
||||||
|
private static string serverPassword = ""; // Default: no password required
|
||||||
|
private static readonly Dictionary<string, bool> authenticatedClients = [];
|
||||||
|
private static readonly ConcurrentDictionary<string, Queue<string>> clientCommandQueues = new();
|
||||||
|
private static readonly int maxBatchSize = 1000; // Maximum number of commands to process in a batch
|
||||||
|
private static readonly int maxPipelineSize = 10000; // Maximum number of commands in a pipeline
|
||||||
|
private static readonly ConcurrentDictionary<string, bool> clientPipelineMode = new(); // Track if client is in pipeline mode
|
||||||
|
|
||||||
|
// Network buffer settings
|
||||||
|
private static readonly int bufferSize = 20480; // Increased from 1024 for better performance
|
||||||
|
|
||||||
|
// Backup settings
|
||||||
|
private static readonly string backupDirectory = "backups";
|
||||||
|
private static readonly string backupFilePrefix = "firefly_data";
|
||||||
|
private static readonly string backupFileExtension = ".fdb";
|
||||||
|
private static int maxBackupFiles = 10;
|
||||||
|
private static System.Timers.Timer? backupTimer;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main Entry Point
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
// Parse command line arguments
|
||||||
|
await ParseArguments(args);
|
||||||
|
|
||||||
|
// Initialize backup system if enabled
|
||||||
|
if (backupsEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Initializing backup system...");
|
||||||
|
InitializeBackupSystem();
|
||||||
|
|
||||||
|
// Load data from the latest backup
|
||||||
|
LoadDataFromBackup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
await StartServerAsync();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Command Processing
|
||||||
|
static byte[] ProcessCommand(string message, string clientId)
|
||||||
|
{
|
||||||
|
// Trim whitespace and newlines
|
||||||
|
message = message.Trim();
|
||||||
|
|
||||||
|
// Split the command into parts, respecting quotes
|
||||||
|
string[] parts = SplitRespectingQuotes(message);
|
||||||
|
if (parts.Length == 0)
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR no command specified\r\n");
|
||||||
|
|
||||||
|
string command = parts[0].ToUpperInvariant();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Always allow AUTH command
|
||||||
|
if (command == "AUTH")
|
||||||
|
{
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'auth' command\r\n");
|
||||||
|
}
|
||||||
|
return HandleAuthCommand(parts[1], clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always allow PING for connectivity checks
|
||||||
|
if (command == "PING")
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("+PONG\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always allow QUIT command for clean disconnection
|
||||||
|
if (command == "QUIT")
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PIPELINE command
|
||||||
|
if (command == "PIPELINE")
|
||||||
|
{
|
||||||
|
clientPipelineMode[clientId] = true;
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle EXEC command
|
||||||
|
if (command == "EXEC")
|
||||||
|
{
|
||||||
|
clientPipelineMode[clientId] = false;
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authentication for all other commands if password is set
|
||||||
|
if (!string.IsNullOrEmpty(serverPassword) && !IsAuthenticated(clientId))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-NOAUTH Authentication required.\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process different commands
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case "ECHO":
|
||||||
|
if (parts.Length > 1)
|
||||||
|
{
|
||||||
|
string echoValue = parts[1];
|
||||||
|
return Encoding.UTF8.GetBytes($"+{echoValue}\r\n");
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'echo' command\r\n");
|
||||||
|
|
||||||
|
case "SET":
|
||||||
|
return HandleSetCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "GET":
|
||||||
|
return HandleGetCommand(parts.Length > 1 ? parts[1] : "");
|
||||||
|
|
||||||
|
case "DEL":
|
||||||
|
return HandleDelCommand(parts.Length > 1 ? parts[1] : "");
|
||||||
|
|
||||||
|
case "TYPE":
|
||||||
|
return HandleTypeCommand(parts.Length > 1 ? parts[1] : "");
|
||||||
|
|
||||||
|
case "LPUSH":
|
||||||
|
return HandleLPushCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "RPUSH":
|
||||||
|
return HandleRPushCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "LPOP":
|
||||||
|
return HandleLPopCommand(parts.Length > 1 ? parts[1] : "");
|
||||||
|
|
||||||
|
case "RPOP":
|
||||||
|
return HandleRPopCommand(parts.Length > 1 ? parts[1] : "");
|
||||||
|
|
||||||
|
case "LINDEX":
|
||||||
|
return HandleLIndexCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "LRANGE":
|
||||||
|
return HandleLRangeCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "LSET":
|
||||||
|
return HandleLSetCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "LPOS":
|
||||||
|
return HandleLPosCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "LTRIM":
|
||||||
|
return HandleLTrimCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "LREM":
|
||||||
|
return HandleLRemCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "HSET":
|
||||||
|
return HandleHSetCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "HGET":
|
||||||
|
return HandleHGetCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "HDEL":
|
||||||
|
return HandleHDelCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "HEXISTS":
|
||||||
|
return HandleHExistsCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "HGETALL":
|
||||||
|
return HandleHGetAllCommand(parts.Length > 1 ? parts[1] : "");
|
||||||
|
|
||||||
|
case "HMSET":
|
||||||
|
return HandleHMSetCommand(parts.Length > 1 ? string.Join(" ", parts.Skip(1)) : "");
|
||||||
|
|
||||||
|
case "KEYS":
|
||||||
|
return HandleKeysCommand(parts.Length > 1 ? parts[1] : "*");
|
||||||
|
|
||||||
|
case "SAVE":
|
||||||
|
return HandleSaveCommand();
|
||||||
|
|
||||||
|
case "BGSAVE":
|
||||||
|
return HandleBgSaveCommand();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR unknown command '{command}'\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Catch any unexpected errors to prevent server crashes
|
||||||
|
Console.WriteLine($"Error processing command: {ex.Message}");
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR internal server error: {ex.Message}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
31
src/FireflyData.cs
Normal file
31
src/FireflyData.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
||||||
|
namespace Firefly
|
||||||
|
#pragma warning restore IDE0130 // Namespace does not match folder structure
|
||||||
|
{
|
||||||
|
// Define FireflyData class to fix the missing type reference
|
||||||
|
/// <summary>
|
||||||
|
/// Container class for all Firefly database data used in serialization and backup operations.
|
||||||
|
/// </summary>
|
||||||
|
public class FireflyData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary containing all string key-value pairs stored in the database.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> StringStore { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary containing all lists stored in the database.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, List<string>> ListStore { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary containing all hash tables stored in the database.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, Dictionary<string, string>> HashStore { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp when the backup was created.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime BackupTime { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
262
src/HashOperations.cs
Normal file
262
src/HashOperations.cs
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region Hash Operations
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the HSET command which sets a field in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key field value"</param>
|
||||||
|
/// <returns>1 if the field was added, 0 if it was updated</returns>
|
||||||
|
static byte[] HandleHSetCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 3)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hset' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string field = parts[1];
|
||||||
|
string value = parts.Length == 3 ? parts[2] : string.Join(" ", parts.Skip(2));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hash = GetOrCreateHash(key);
|
||||||
|
bool isNewField = hash.TryAdd(field, value);
|
||||||
|
if (!isNewField)
|
||||||
|
{
|
||||||
|
hash[field] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($":{(isNewField ? 1 : 0)}\r\n");
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
// Handle the case where the key already exists with a different type
|
||||||
|
string? existingType = GetKeyType(key);
|
||||||
|
if (existingType != null)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR key '{key}' already exists as type '{existingType}'\r\n");
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR {ex.Message}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the HGET command which retrieves the value of a field in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key field"</param>
|
||||||
|
/// <returns>The value of the field, or nil if the field doesn't exist</returns>
|
||||||
|
static byte[] HandleHGetCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hget' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string field = parts[1];
|
||||||
|
|
||||||
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
|
||||||
|
hash != null && hash.TryGetValue(field, out string? fieldValue))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"+{fieldValue}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes("$-1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the HDEL command which removes a field from a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key field"</param>
|
||||||
|
/// <returns>1 if the field was removed, 0 if it didn't exist</returns>
|
||||||
|
static byte[] HandleHDelCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hdel' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string field = parts[1];
|
||||||
|
|
||||||
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) && hash != null)
|
||||||
|
{
|
||||||
|
bool removed = hash.TryRemove(field, out _);
|
||||||
|
|
||||||
|
if (hash.IsEmpty)
|
||||||
|
{
|
||||||
|
HashStoreRemove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($":{(removed ? 1 : 0)}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes(":0\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the HEXISTS command which checks if a field exists in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key field"</param>
|
||||||
|
/// <returns>1 if the field exists, 0 if it doesn't</returns>
|
||||||
|
static byte[] HandleHExistsCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hexists' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string field = parts[1];
|
||||||
|
|
||||||
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
|
||||||
|
hash != null && hash.TryGetValue(field, out _))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes(":1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes(":0\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the HGETALL command which retrieves all fields and values in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key"</param>
|
||||||
|
/// <returns>All fields and values in the hash, or nil if the hash doesn't exist</returns>
|
||||||
|
static byte[] HandleHGetAllCommand(string args)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(args))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hgetall' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = args.Trim();
|
||||||
|
|
||||||
|
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
|
||||||
|
hash != null && !hash.IsEmpty)
|
||||||
|
{
|
||||||
|
var hashSnapshot = hash.ToArray();
|
||||||
|
StringBuilder response = new();
|
||||||
|
response.Append("*\r\n");
|
||||||
|
|
||||||
|
foreach (var kvp in hashSnapshot)
|
||||||
|
{
|
||||||
|
response.Append($"+{kvp.Key}\r\n");
|
||||||
|
response.Append($"+{kvp.Value}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes(response.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes("*\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the HMSET command which sets multiple fields in a hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key field value [field value ...]"</param>
|
||||||
|
/// <returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
static byte[] HandleHMSetCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 3 || (parts.Length - 1) % 2 != 0)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hmset' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
var hash = GetOrCreateHash(key);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 1; i < parts.Length; i += 2)
|
||||||
|
{
|
||||||
|
string field = parts[i];
|
||||||
|
if (i + 1 < parts.Length)
|
||||||
|
{
|
||||||
|
string value = parts[i + 1];
|
||||||
|
if (value.StartsWith('"') && value.EndsWith('"') && value.Length >= 2)
|
||||||
|
{
|
||||||
|
value = value[1..^1];
|
||||||
|
}
|
||||||
|
hash[field] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR internal error: {ex.Message}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Hash Store Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates a hash for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to get or create the hash for</param>
|
||||||
|
/// <returns>The hash</returns>
|
||||||
|
private static ConcurrentDictionary<string, string> GetOrCreateHash(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
|
||||||
|
// If the key doesn't exist in this shard, check if it exists in any other store
|
||||||
|
if (!hashStoreShards[shardIndex].ContainsKey(key))
|
||||||
|
{
|
||||||
|
// Check if key exists in any other store
|
||||||
|
if (!EnsureKeyDoesNotExist(key, "hash"))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Key '{key}' already exists with a different type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashStoreShards[shardIndex].GetOrAdd(key, _ => new ConcurrentDictionary<string, string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a hash exists for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check the hash for</param>
|
||||||
|
/// <returns>True if the hash exists, false otherwise</returns>
|
||||||
|
private static bool HashStoreExists(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return hashStoreShards[shardIndex].ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a hash for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to get the hash for</param>
|
||||||
|
/// <param name="value">The hash</param>
|
||||||
|
/// <returns>True if the hash was found, false otherwise</returns>
|
||||||
|
private static bool HashStoreGet(string key, out ConcurrentDictionary<string, string>? value)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return hashStoreShards[shardIndex].TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a hash for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to remove the hash for</param>
|
||||||
|
/// <returns>True if the hash was removed, false otherwise</returns>
|
||||||
|
private static bool HashStoreRemove(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return hashStoreShards[shardIndex].TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
308
src/Helpers.cs
Normal file
308
src/Helpers.cs
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region Utility Methods
|
||||||
|
/// <summary>
|
||||||
|
/// Splits a string respecting quoted sections.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The input string to split</param>
|
||||||
|
/// <returns>An array of tokens from the input string</returns>
|
||||||
|
static string[] SplitRespectingQuotes(string input)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
bool inQuotes = false;
|
||||||
|
StringBuilder currentToken = new();
|
||||||
|
|
||||||
|
for (int i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
char c = input[i];
|
||||||
|
|
||||||
|
if (c == '"')
|
||||||
|
{
|
||||||
|
// Toggle quote state
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
else if (c == ' ' && !inQuotes)
|
||||||
|
{
|
||||||
|
// Space outside quotes - token boundary
|
||||||
|
if (currentToken.Length > 0)
|
||||||
|
{
|
||||||
|
// Add the completed token
|
||||||
|
string token = currentToken.ToString();
|
||||||
|
result.Add(token);
|
||||||
|
currentToken.Clear();
|
||||||
|
}
|
||||||
|
// Skip the space
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the character to the current token
|
||||||
|
currentToken.Append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add final token if any
|
||||||
|
if (currentToken.Length > 0)
|
||||||
|
{
|
||||||
|
result.Add(currentToken.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [.. result];
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"([^\s""]+)|""([^""]*)""")]
|
||||||
|
private static partial Regex MyRegex();
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Sharding and Storage Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an array of shards of type T.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the shards</typeparam>
|
||||||
|
/// <returns>An array of shards</returns>
|
||||||
|
private static T[] CreateShards<T>() where T : new()
|
||||||
|
{
|
||||||
|
var shards = new T[ShardCount];
|
||||||
|
for (int i = 0; i < ShardCount; i++)
|
||||||
|
{
|
||||||
|
shards[i] = new T();
|
||||||
|
}
|
||||||
|
return shards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an array of ReaderWriterLockSlim instances for list operations.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An array of locks</returns>
|
||||||
|
private static ReaderWriterLockSlim[] CreateListLocks()
|
||||||
|
{
|
||||||
|
var locks = new ReaderWriterLockSlim[ShardCount];
|
||||||
|
for (int i = 0; i < ShardCount; i++)
|
||||||
|
{
|
||||||
|
locks[i] = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
|
||||||
|
}
|
||||||
|
return locks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the shard index for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to get the shard index for</param>
|
||||||
|
/// <returns>The shard index</returns>
|
||||||
|
private static int GetShardIndex(string key)
|
||||||
|
{
|
||||||
|
return key.GetHashCode() & 0x7FFFFFFF & ShardMask; // Ensure positive hash and fast modulo
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lock Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an array of locks.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An array of locks</returns>
|
||||||
|
private static object[] CreateLocks()
|
||||||
|
{
|
||||||
|
var locks = new object[ShardCount];
|
||||||
|
for (int i = 0; i < ShardCount; i++)
|
||||||
|
{
|
||||||
|
locks[i] = new object();
|
||||||
|
}
|
||||||
|
return locks;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Key Management Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a key exists in any store (string, list, or hash).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check</param>
|
||||||
|
/// <returns>True if the key exists in any store, false otherwise</returns>
|
||||||
|
private static bool KeyExistsInAnyStore(string key)
|
||||||
|
{
|
||||||
|
// Check string store
|
||||||
|
if (StringStoreExists(key))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check list store
|
||||||
|
if (ListStoreExists(key))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hash store
|
||||||
|
if (HashStoreExists(key))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of a key if it exists in any store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check</param>
|
||||||
|
/// <returns>The type of the key ("string", "list", "hash", or null if not found)</returns>
|
||||||
|
private static string? GetKeyType(string key)
|
||||||
|
{
|
||||||
|
// Check string store
|
||||||
|
if (StringStoreExists(key))
|
||||||
|
{
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check list store
|
||||||
|
if (ListStoreExists(key))
|
||||||
|
{
|
||||||
|
return "list";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hash store
|
||||||
|
if (HashStoreExists(key))
|
||||||
|
{
|
||||||
|
return "hash";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures a key doesn't exist in any store before creating it in the target store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check</param>
|
||||||
|
/// <param name="targetType">The type of store where the key will be created</param>
|
||||||
|
/// <returns>True if the key can be created, false if it already exists with a different type</returns>
|
||||||
|
private static bool EnsureKeyDoesNotExist(string key, string targetType)
|
||||||
|
{
|
||||||
|
string? existingType = GetKeyType(key);
|
||||||
|
if (existingType != null)
|
||||||
|
{
|
||||||
|
return false; // Key already exists with a different type
|
||||||
|
}
|
||||||
|
return true; // Key doesn't exist in any store
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the DEL command which removes a key from all stores (string, list, hash).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to delete</param>
|
||||||
|
/// <returns>The number of keys that were removed</returns>
|
||||||
|
public static byte[] HandleDelCommand(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'del' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int deletedCount = 0;
|
||||||
|
|
||||||
|
// Try to delete from string store
|
||||||
|
if (StringStoreRemove(key))
|
||||||
|
{
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to delete from list store
|
||||||
|
if (ListStoreRemove(key))
|
||||||
|
{
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to delete from hash store
|
||||||
|
if (HashStoreRemove(key))
|
||||||
|
{
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($":{deletedCount}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the TYPE command which returns the type of a key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check the type of</param>
|
||||||
|
/// <returns>The type of the key as a string response</returns>
|
||||||
|
public static byte[] HandleTypeCommand(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'type' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check string store
|
||||||
|
if (StringStoreExists(key))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("+string\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check list store
|
||||||
|
if (ListStoreExists(key))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("+list\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check hash store
|
||||||
|
if (HashStoreExists(key))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("+hash\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key doesn't exist
|
||||||
|
return Encoding.UTF8.GetBytes("+none\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the KEYS command which returns all keys matching a pattern.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">The pattern to match</param>
|
||||||
|
/// <returns>A list of keys matching the pattern</returns>
|
||||||
|
public static byte[] HandleKeysCommand(string pattern)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var matchingKeys = new List<string>();
|
||||||
|
|
||||||
|
// Convert pattern to regex
|
||||||
|
var regex = new Regex(
|
||||||
|
"^" + Regex.Escape(pattern).Replace("\\*", ".*").Replace("\\?", ".") + "$",
|
||||||
|
RegexOptions.Compiled
|
||||||
|
);
|
||||||
|
|
||||||
|
// Search in string store
|
||||||
|
foreach (var shard in stringStoreShards)
|
||||||
|
{
|
||||||
|
matchingKeys.AddRange(shard.Keys.Where(k => regex.IsMatch(k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in list store
|
||||||
|
foreach (var shard in listStoreShards)
|
||||||
|
{
|
||||||
|
matchingKeys.AddRange(shard.Keys.Where(k => regex.IsMatch(k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in hash store
|
||||||
|
foreach (var shard in hashStoreShards)
|
||||||
|
{
|
||||||
|
matchingKeys.AddRange(shard.Keys.Where(k => regex.IsMatch(k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort keys for consistent results
|
||||||
|
matchingKeys.Sort();
|
||||||
|
|
||||||
|
// Return keys as newline-separated string
|
||||||
|
string result = string.Join("\n", matchingKeys);
|
||||||
|
return Encoding.UTF8.GetBytes($"+{result}\r\n");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR error executing KEYS command: {ex.Message}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
624
src/ListOperations.cs
Normal file
624
src/ListOperations.cs
Normal file
@ -0,0 +1,624 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region List Operations
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LPUSH command which adds an element to the left of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key value"</param>
|
||||||
|
/// <returns>The length of the list after the push operation</returns>
|
||||||
|
static byte[] HandleLPushCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpush' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string value = parts[1];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use helper function to safely modify the list with write lock
|
||||||
|
int newLength = 0;
|
||||||
|
ListStoreWithWriteLock(key, list =>
|
||||||
|
{
|
||||||
|
list.Insert(0, value);
|
||||||
|
newLength = list.Count;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($":{newLength}\r\n");
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
// Handle the case where the key already exists with a different type
|
||||||
|
string? existingType = GetKeyType(key);
|
||||||
|
if (existingType != null)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR key '{key}' already exists as type '{existingType}'\r\n");
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR {ex.Message}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the RPUSH command which adds values to the tail of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key value1 [value2 ...]"</param>
|
||||||
|
/// <returns>Response indicating the new length of the list</returns>
|
||||||
|
static byte[] HandleRPushCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'rpush' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
List<string> result = [];
|
||||||
|
|
||||||
|
// Use helper function to safely modify the list with write lock
|
||||||
|
ListStoreWithWriteLock(key, list =>
|
||||||
|
{
|
||||||
|
// Add all values to the end of the list
|
||||||
|
for (int i = 1; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
list.Add(parts[i]);
|
||||||
|
}
|
||||||
|
result.Add(list.Count.ToString());
|
||||||
|
});
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($":{result[0]}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LPOP command which removes and returns the first element of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments containing the key</param>
|
||||||
|
/// <returns>The popped value or nil if the list is empty</returns>
|
||||||
|
static byte[] HandleLPopCommand(string args)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(args))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpop' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = args.Trim();
|
||||||
|
List<string> result = [];
|
||||||
|
|
||||||
|
// Use helper function to safely modify the list with write lock
|
||||||
|
ListStoreWithWriteLock(key, list =>
|
||||||
|
{
|
||||||
|
if (list.Count > 0)
|
||||||
|
{
|
||||||
|
// Remove and store the first element
|
||||||
|
string value = list[0];
|
||||||
|
list.RemoveAt(0);
|
||||||
|
result.Add(value);
|
||||||
|
|
||||||
|
// Clean up empty lists
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
ListStoreRemove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.Count > 0
|
||||||
|
? Encoding.UTF8.GetBytes($"+{result[0]}\r\n")
|
||||||
|
: Encoding.UTF8.GetBytes("$-1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the RPOP command which removes and returns the last element of a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments containing the key</param>
|
||||||
|
/// <returns>The popped value or nil if the list is empty</returns>
|
||||||
|
static byte[] HandleRPopCommand(string args)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(args))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'rpop' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = args.Trim();
|
||||||
|
List<string> result = [];
|
||||||
|
|
||||||
|
// Use helper function to safely modify the list with write lock
|
||||||
|
ListStoreWithWriteLock(key, list =>
|
||||||
|
{
|
||||||
|
if (list.Count > 0)
|
||||||
|
{
|
||||||
|
// Remove and store the last element
|
||||||
|
int lastIndex = list.Count - 1;
|
||||||
|
string value = list[lastIndex];
|
||||||
|
list.RemoveAt(lastIndex);
|
||||||
|
result.Add(value);
|
||||||
|
|
||||||
|
// Clean up empty lists
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
ListStoreRemove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.Count > 0
|
||||||
|
? Encoding.UTF8.GetBytes($"+{result[0]}\r\n")
|
||||||
|
: Encoding.UTF8.GetBytes("$-1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LRANGE command which returns a range of elements from a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key start stop"</param>
|
||||||
|
/// <returns>Array of elements in the specified range</returns>
|
||||||
|
static byte[] HandleLRangeCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 3)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lrange' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
if (!int.TryParse(parts[1], out int start) || !int.TryParse(parts[2], out int stop))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use helper function to safely read the list with read lock
|
||||||
|
return ListStoreWithReadLock(key, list =>
|
||||||
|
{
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("*\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle negative indices (counting from the end)
|
||||||
|
if (start < 0) start = list.Count + start;
|
||||||
|
if (stop < 0) stop = list.Count + stop;
|
||||||
|
|
||||||
|
// Ensure indices are within bounds
|
||||||
|
start = Math.Max(start, 0);
|
||||||
|
stop = Math.Min(stop, list.Count - 1);
|
||||||
|
|
||||||
|
if (start > stop)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("*\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response with all elements in range
|
||||||
|
StringBuilder response = new();
|
||||||
|
response.Append("*\r\n");
|
||||||
|
|
||||||
|
for (int i = start; i <= stop; i++)
|
||||||
|
{
|
||||||
|
response.Append($"+{list[i]}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes(response.ToString());
|
||||||
|
}) ?? Encoding.UTF8.GetBytes("*\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LINDEX command which returns an element from a list by its index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key index"</param>
|
||||||
|
/// <returns>The element at the specified index or nil if not found</returns>
|
||||||
|
static byte[] HandleLIndexCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lindex' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
if (!int.TryParse(parts[1], out int index))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use helper function to safely read the list with read lock
|
||||||
|
return ListStoreWithReadLock(key, list =>
|
||||||
|
{
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("$-1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle negative index (counting from the end)
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
index = list.Count + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if index is within bounds
|
||||||
|
if (index < 0 || index >= list.Count)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("$-1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($"+{list[index]}\r\n");
|
||||||
|
}) ?? Encoding.UTF8.GetBytes("$-1\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LSET command which sets the value of an element in a list by its index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key index value"</param>
|
||||||
|
/// <returns>OK on success, error if index is out of range</returns>
|
||||||
|
static byte[] HandleLSetCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 3)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lset' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
if (!int.TryParse(parts[1], out int index))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
|
||||||
|
}
|
||||||
|
string value = parts[2];
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
// Use helper function to safely modify the list with write lock
|
||||||
|
ListStoreWithWriteLock(key, list =>
|
||||||
|
{
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle negative index (counting from the end)
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
index = list.Count + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if index is within bounds
|
||||||
|
if (index < 0 || index >= list.Count)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list[index] = value;
|
||||||
|
success = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return success
|
||||||
|
? Encoding.UTF8.GetBytes("+OK\r\n")
|
||||||
|
: Encoding.UTF8.GetBytes("-ERR index out of range\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LPOS command which returns the position of an element in a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key element [RANK rank] [MAXLEN len]"</param>
|
||||||
|
/// <returns>The position of the element or nil if not found</returns>
|
||||||
|
static byte[] HandleLPosCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpos' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string element = parts[1];
|
||||||
|
|
||||||
|
// Optional parameters
|
||||||
|
int rank = 1; // Default to first occurrence
|
||||||
|
int maxlen = 0; // 0 means no limit
|
||||||
|
|
||||||
|
// Parse optional parameters
|
||||||
|
for (int i = 2; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
if (parts[i].Equals("RANK", StringComparison.OrdinalIgnoreCase) && i + 1 < parts.Length)
|
||||||
|
{
|
||||||
|
if (int.TryParse(parts[++i], out int parsedRank))
|
||||||
|
{
|
||||||
|
rank = parsedRank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (parts[i].Equals("MAXLEN", StringComparison.OrdinalIgnoreCase) && i + 1 < parts.Length)
|
||||||
|
{
|
||||||
|
if (int.TryParse(parts[++i], out int parsedMaxlen))
|
||||||
|
{
|
||||||
|
maxlen = parsedMaxlen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!listStoreShards[shardIndex].TryGetValue(key, out List<string>? list) || list == null)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("$-1\r\n"); // Key doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
if (maxlen > 0 && i >= maxlen)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list[i] == element)
|
||||||
|
{
|
||||||
|
found++;
|
||||||
|
if (found == Math.Abs(rank))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($":{i}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes("$-1\r\n"); // Element not found
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LTRIM command which trims a list to the specified range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key start stop"</param>
|
||||||
|
/// <returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
static byte[] HandleLTrimCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 3)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'ltrim' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
if (!int.TryParse(parts[1], out int start) || !int.TryParse(parts[2], out int stop))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!listStoreShards[shardIndex].TryGetValue(key, out List<string>? list) || list == null)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n"); // Non-existent key is treated as empty list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle negative indices
|
||||||
|
if (start < 0) start = list.Count + start;
|
||||||
|
if (stop < 0) stop = list.Count + stop;
|
||||||
|
|
||||||
|
// Normalize boundaries
|
||||||
|
start = Math.Max(start, 0);
|
||||||
|
stop = Math.Min(stop, list.Count - 1);
|
||||||
|
|
||||||
|
if (start > stop || start >= list.Count)
|
||||||
|
{
|
||||||
|
// Clear the list if range is invalid
|
||||||
|
list.Clear();
|
||||||
|
listStoreShards[shardIndex].TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Calculate the new range
|
||||||
|
int newLength = stop - start + 1;
|
||||||
|
if (start > 0 || stop < list.Count - 1)
|
||||||
|
{
|
||||||
|
var trimmed = list.GetRange(start, newLength);
|
||||||
|
list.Clear();
|
||||||
|
list.AddRange(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the LREM command which removes elements equal to the given value from a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key count element"</param>
|
||||||
|
/// <returns>The number of removed elements</returns>
|
||||||
|
static byte[] HandleLRemCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = SplitRespectingQuotes(args);
|
||||||
|
if (parts.Length < 3)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lrem' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
if (!int.TryParse(parts[1], out int count))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
|
||||||
|
}
|
||||||
|
string element = parts[2];
|
||||||
|
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!listStoreShards[shardIndex].TryGetValue(key, out List<string>? list) || list == null)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes(":0\r\n"); // Key doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
int removed = 0;
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
// Remove count occurrences from head to tail
|
||||||
|
for (int i = 0; i < list.Count && removed < count; i++)
|
||||||
|
{
|
||||||
|
if (list[i] == element)
|
||||||
|
{
|
||||||
|
list.RemoveAt(i);
|
||||||
|
removed++;
|
||||||
|
i--; // Adjust index after removal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (count < 0)
|
||||||
|
{
|
||||||
|
// Remove |count| occurrences from tail to head
|
||||||
|
for (int i = list.Count - 1; i >= 0 && removed < -count; i--)
|
||||||
|
{
|
||||||
|
if (list[i] == element)
|
||||||
|
{
|
||||||
|
list.RemoveAt(i);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // count == 0
|
||||||
|
{
|
||||||
|
// Remove all occurrences
|
||||||
|
removed = list.RemoveAll(x => x == element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the key if the list is empty
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
listStoreShards[shardIndex].TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes($":{removed}\r\n");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region List Store Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates a list for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to get or create the list for</param>
|
||||||
|
/// <returns>The list</returns>
|
||||||
|
private static List<string> GetOrCreateList(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
|
||||||
|
// If the key doesn't exist in this shard, check if it exists in any other store
|
||||||
|
if (!listStoreShards[shardIndex].ContainsKey(key))
|
||||||
|
{
|
||||||
|
// Check if key exists in any other store
|
||||||
|
if (!EnsureKeyDoesNotExist(key, "list"))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Key '{key}' already exists with a different type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listStoreShards[shardIndex].GetOrAdd(key, _ => []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a list exists for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check the list for</param>
|
||||||
|
/// <returns>True if the list exists, false otherwise</returns>
|
||||||
|
private static bool ListStoreExists(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return listStoreShards[shardIndex].ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to get the list for</param>
|
||||||
|
/// <param name="value">The list</param>
|
||||||
|
/// <returns>True if the list was found, false otherwise</returns>
|
||||||
|
private static bool ListStoreGet(string key, out List<string>? value)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return listStoreShards[shardIndex].TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a list for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to remove the list for</param>
|
||||||
|
/// <returns>True if the list was removed, false otherwise</returns>
|
||||||
|
private static bool ListStoreRemove(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return listStoreShards[shardIndex].TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action with a write lock on a list for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to execute the action on</param>
|
||||||
|
/// <param name="action">The action to execute</param>
|
||||||
|
private static void ListStoreWithWriteLock(string key, Action<List<string>> action)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = GetOrCreateList(key);
|
||||||
|
action(list);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an action with a read lock on a list for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the result</typeparam>
|
||||||
|
/// <param name="key">The key to execute the action on</param>
|
||||||
|
/// <param name="action">The action to execute</param>
|
||||||
|
private static T ListStoreWithReadLock<T>(string key, Func<List<string>, T> action)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
listStoreLocks[shardIndex].EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ListStoreGet(key, out List<string>? list) && list != null)
|
||||||
|
{
|
||||||
|
return action(list);
|
||||||
|
}
|
||||||
|
return default!;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listStoreLocks[shardIndex].ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
407
src/PipelineTest.cs
Normal file
407
src/PipelineTest.cs
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
||||||
|
namespace Firefly
|
||||||
|
#pragma warning restore IDE0130 // Namespace does not match folder structure
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test client for verifying pipelining and batching functionality
|
||||||
|
/// </summary>
|
||||||
|
public class PipelineTest
|
||||||
|
{
|
||||||
|
private static string Host = "127.0.0.1";
|
||||||
|
private static int Port = 6379;
|
||||||
|
private static string Password = "";
|
||||||
|
private static int CommandCount = 10000;
|
||||||
|
private static int BatchSize = 100;
|
||||||
|
private static int BufferSize = 1024 * 1024; // 1MB buffer
|
||||||
|
private static int ClientCount = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the pipeline test
|
||||||
|
/// </summary>
|
||||||
|
public static async Task RunTest(string[] args)
|
||||||
|
{
|
||||||
|
// Parse command line arguments
|
||||||
|
ParseArguments(args);
|
||||||
|
|
||||||
|
Console.WriteLine("\nStarting Pipeline Test...");
|
||||||
|
Console.WriteLine($"Host: {Host}");
|
||||||
|
Console.WriteLine($"Port: {Port}");
|
||||||
|
Console.WriteLine($"Clients: {ClientCount}");
|
||||||
|
if (!string.IsNullOrEmpty(Password))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Authentication: Enabled");
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Command Count: {CommandCount}");
|
||||||
|
Console.WriteLine($"Batch Size: {BatchSize}");
|
||||||
|
Console.WriteLine($"Buffer Size: {BufferSize / 1024}KB\n");
|
||||||
|
|
||||||
|
// Test 1: Sequential commands (no pipelining)
|
||||||
|
await RunSequentialTest();
|
||||||
|
|
||||||
|
// Test 2: Pipelined commands
|
||||||
|
await RunPipelinedTest();
|
||||||
|
|
||||||
|
// Test 3: Batched commands
|
||||||
|
await RunBatchedTest();
|
||||||
|
|
||||||
|
Console.WriteLine("\nPipeline Test completed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseArguments(string[] args)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
string arg = args[i].ToLower();
|
||||||
|
|
||||||
|
switch (arg)
|
||||||
|
{
|
||||||
|
case "--host":
|
||||||
|
case "-h":
|
||||||
|
if (i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
Host = args[i + 1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--port":
|
||||||
|
case "-p":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int port))
|
||||||
|
{
|
||||||
|
Port = port;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--password":
|
||||||
|
case "-pass":
|
||||||
|
if (i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
Password = args[i + 1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--commands":
|
||||||
|
case "-c":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int commands))
|
||||||
|
{
|
||||||
|
CommandCount = commands;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--batch":
|
||||||
|
case "-b":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int batch))
|
||||||
|
{
|
||||||
|
BatchSize = batch;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--buffer":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int buffer))
|
||||||
|
{
|
||||||
|
BufferSize = buffer * 1024; // Convert KB to bytes
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "--clients":
|
||||||
|
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int clients))
|
||||||
|
{
|
||||||
|
ClientCount = clients;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task Authenticate(NetworkStream stream)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Password))
|
||||||
|
{
|
||||||
|
string authCommand = $"AUTH {Password}\r\n";
|
||||||
|
byte[] authData = Encoding.UTF8.GetBytes(authCommand);
|
||||||
|
await stream.WriteAsync(authData);
|
||||||
|
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
int bytesRead = await stream.ReadAsync(buffer);
|
||||||
|
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||||
|
|
||||||
|
if (!response.StartsWith("+OK"))
|
||||||
|
{
|
||||||
|
throw new Exception($"Authentication failed: {response}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read responses from the stream until we have the expected count
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<int> ReadResponses(NetworkStream stream, byte[] buffer, int expectedCount, string testType)
|
||||||
|
{
|
||||||
|
var responseBuilder = new StringBuilder();
|
||||||
|
int totalResponses = 0;
|
||||||
|
int retryCount = 0;
|
||||||
|
const int maxRetries = 10;
|
||||||
|
|
||||||
|
while (totalResponses < expectedCount && retryCount < maxRetries)
|
||||||
|
{
|
||||||
|
if (stream.DataAvailable)
|
||||||
|
{
|
||||||
|
int bytesRead = await stream.ReadAsync(buffer);
|
||||||
|
if (bytesRead > 0)
|
||||||
|
{
|
||||||
|
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||||
|
responseBuilder.Append(response);
|
||||||
|
|
||||||
|
// Count complete responses
|
||||||
|
int newResponses = response.Split(["\r\n"], StringSplitOptions.RemoveEmptyEntries).Length;
|
||||||
|
totalResponses += newResponses;
|
||||||
|
|
||||||
|
if (totalResponses % 50 == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{testType}: Received {totalResponses}/{expectedCount} responses");
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount = 0; // Reset retry count on successful read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Task.Delay(100); // Wait a bit for more data
|
||||||
|
retryCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalResponses < expectedCount)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Warning: Only received {totalResponses}/{expectedCount} responses");
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalResponses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test with sequential commands (no pipelining)
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunSequentialTest()
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== Sequential Test ===");
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
stopwatch.Start();
|
||||||
|
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
var clientResults = new List<(int clientId, int responses, double opsPerSecond)>();
|
||||||
|
|
||||||
|
for (int clientId = 0; clientId < ClientCount; clientId++)
|
||||||
|
{
|
||||||
|
tasks.Add(Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var client = new TcpClient();
|
||||||
|
await client.ConnectAsync(Host, Port);
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
int totalResponses = 0;
|
||||||
|
|
||||||
|
// Authenticate if password is set
|
||||||
|
await Authenticate(stream);
|
||||||
|
|
||||||
|
for (int i = 0; i < CommandCount; i++)
|
||||||
|
{
|
||||||
|
string key = $"seq:client{clientId}:key:{i}";
|
||||||
|
string value = $"value:{i}";
|
||||||
|
|
||||||
|
// SET command
|
||||||
|
string setCommand = $"SET {key} {value}\r\n";
|
||||||
|
byte[] setData = Encoding.UTF8.GetBytes(setCommand);
|
||||||
|
await stream.WriteAsync(setData);
|
||||||
|
totalResponses += await ReadResponses(stream, buffer, 1, $"Sequential-Client{clientId}");
|
||||||
|
|
||||||
|
// GET command
|
||||||
|
string getCommand = $"GET {key}\r\n";
|
||||||
|
byte[] getData = Encoding.UTF8.GetBytes(getCommand);
|
||||||
|
await stream.WriteAsync(getData);
|
||||||
|
totalResponses += await ReadResponses(stream, buffer, 1, $"Sequential-Client{clientId}");
|
||||||
|
|
||||||
|
if (i % 10 == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Sequential-Client{clientId}: Processed {i}/{CommandCount} commands");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientResults.Add((clientId, totalResponses, (CommandCount * 2.0) / (stopwatch.ElapsedMilliseconds / 1000.0)));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
// Print individual client results
|
||||||
|
foreach (var (clientId, responses, opsPerSecond) in clientResults)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Sequential-Client{clientId}:");
|
||||||
|
Console.WriteLine($" Total responses: {responses}");
|
||||||
|
Console.WriteLine($" Operations per second: {opsPerSecond:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print aggregate results
|
||||||
|
double totalOpsPerSecond = clientResults.Sum(r => r.opsPerSecond);
|
||||||
|
Console.WriteLine($"\nSequential Test Summary:");
|
||||||
|
Console.WriteLine($"Total operations per second: {totalOpsPerSecond:F2}");
|
||||||
|
Console.WriteLine($"Average operations per second per client: {totalOpsPerSecond / ClientCount:F2}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test with pipelined commands
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunPipelinedTest()
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== Pipelined Test ===");
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
stopwatch.Start();
|
||||||
|
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
var clientResults = new List<(int clientId, int responses, double opsPerSecond)>();
|
||||||
|
|
||||||
|
for (int clientId = 0; clientId < ClientCount; clientId++)
|
||||||
|
{
|
||||||
|
tasks.Add(Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var client = new TcpClient();
|
||||||
|
await client.ConnectAsync(Host, Port);
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
|
||||||
|
// Authenticate if password is set
|
||||||
|
await Authenticate(stream);
|
||||||
|
|
||||||
|
// Enter pipeline mode
|
||||||
|
string pipelineCommand = "PIPELINE\r\n";
|
||||||
|
byte[] pipelineData = Encoding.UTF8.GetBytes(pipelineCommand);
|
||||||
|
await stream.WriteAsync(pipelineData);
|
||||||
|
await ReadResponses(stream, buffer, 1, $"Pipeline-Client{clientId}");
|
||||||
|
|
||||||
|
Console.WriteLine($"Pipeline-Client{clientId}: Sending commands...");
|
||||||
|
|
||||||
|
// Send all commands
|
||||||
|
var allCommands = new StringBuilder();
|
||||||
|
for (int i = 0; i < CommandCount; i++)
|
||||||
|
{
|
||||||
|
string key = $"pipe:client{clientId}:key:{i}";
|
||||||
|
string value = $"value:{i}";
|
||||||
|
allCommands.Append($"SET {key} {value}\r\n");
|
||||||
|
allCommands.Append($"GET {key}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] commandsData = Encoding.UTF8.GetBytes(allCommands.ToString());
|
||||||
|
await stream.WriteAsync(commandsData);
|
||||||
|
|
||||||
|
// Execute pipeline
|
||||||
|
string execCommand = "EXEC\r\n";
|
||||||
|
byte[] execData = Encoding.UTF8.GetBytes(execCommand);
|
||||||
|
await stream.WriteAsync(execData);
|
||||||
|
|
||||||
|
// Read all responses
|
||||||
|
int totalResponses = await ReadResponses(stream, buffer, CommandCount * 2 + 1, $"Pipeline-Client{clientId}");
|
||||||
|
clientResults.Add((clientId, totalResponses, (CommandCount * 2.0) / (stopwatch.ElapsedMilliseconds / 1000.0)));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
// Print individual client results
|
||||||
|
foreach (var (clientId, responses, opsPerSecond) in clientResults)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Pipeline-Client{clientId}:");
|
||||||
|
Console.WriteLine($" Total responses: {responses}");
|
||||||
|
Console.WriteLine($" Operations per second: {opsPerSecond:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print aggregate results
|
||||||
|
double totalOpsPerSecond = clientResults.Sum(r => r.opsPerSecond);
|
||||||
|
Console.WriteLine($"\nPipelined Test Summary:");
|
||||||
|
Console.WriteLine($"Total operations per second: {totalOpsPerSecond:F2}");
|
||||||
|
Console.WriteLine($"Average operations per second per client: {totalOpsPerSecond / ClientCount:F2}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test with batched commands
|
||||||
|
/// </summary>
|
||||||
|
private static async Task RunBatchedTest()
|
||||||
|
{
|
||||||
|
Console.WriteLine("=== Batched Test ===");
|
||||||
|
var stopwatch = new Stopwatch();
|
||||||
|
stopwatch.Start();
|
||||||
|
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
var clientResults = new List<(int clientId, int responses, double opsPerSecond)>();
|
||||||
|
|
||||||
|
for (int clientId = 0; clientId < ClientCount; clientId++)
|
||||||
|
{
|
||||||
|
tasks.Add(Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var client = new TcpClient();
|
||||||
|
await client.ConnectAsync(Host, Port);
|
||||||
|
using var stream = client.GetStream();
|
||||||
|
var buffer = new byte[BufferSize];
|
||||||
|
int totalResponses = 0;
|
||||||
|
|
||||||
|
// Authenticate if password is set
|
||||||
|
await Authenticate(stream);
|
||||||
|
|
||||||
|
for (int batch = 0; batch < CommandCount; batch += BatchSize)
|
||||||
|
{
|
||||||
|
int currentBatchSize = Math.Min(BatchSize, CommandCount - batch);
|
||||||
|
var batchCommands = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < currentBatchSize; i++)
|
||||||
|
{
|
||||||
|
int index = batch + i;
|
||||||
|
string key = $"batch:client{clientId}:key:{index}";
|
||||||
|
string value = $"value:{index}";
|
||||||
|
batchCommands.Append($"SET {key} {value}\r\n");
|
||||||
|
batchCommands.Append($"GET {key}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] batchData = Encoding.UTF8.GetBytes(batchCommands.ToString());
|
||||||
|
await stream.WriteAsync(batchData);
|
||||||
|
|
||||||
|
int responses = await ReadResponses(stream, buffer, currentBatchSize * 2, $"Batch-Client{clientId}");
|
||||||
|
totalResponses += responses;
|
||||||
|
|
||||||
|
Console.WriteLine($"Batch-Client{clientId}: Processed batch {batch / BatchSize + 1}/{Math.Ceiling((double)CommandCount / BatchSize)} " +
|
||||||
|
$"({responses} responses)");
|
||||||
|
}
|
||||||
|
|
||||||
|
clientResults.Add((clientId, totalResponses, (CommandCount * 2.0) / (stopwatch.ElapsedMilliseconds / 1000.0)));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
// Print individual client results
|
||||||
|
foreach (var (clientId, responses, opsPerSecond) in clientResults)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Batch-Client{clientId}:");
|
||||||
|
Console.WriteLine($" Total responses: {responses}");
|
||||||
|
Console.WriteLine($" Operations per second: {opsPerSecond:F2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print aggregate results
|
||||||
|
double totalOpsPerSecond = clientResults.Sum(r => r.opsPerSecond);
|
||||||
|
Console.WriteLine($"\nBatched Test Summary:");
|
||||||
|
Console.WriteLine($"Total operations per second: {totalOpsPerSecond:F2}");
|
||||||
|
Console.WriteLine($"Average operations per second per client: {totalOpsPerSecond / ClientCount:F2}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
211
src/Protocol/FireflyProtocol.cs
Normal file
211
src/Protocol/FireflyProtocol.cs
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
#pragma warning disable IDE0130 // Namespace does not match folder structure
|
||||||
|
namespace Firefly.Protocol
|
||||||
|
#pragma warning restore IDE0130 // Namespace does not match folder structure
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the Firefly wire protocol specification for 3rd party clients
|
||||||
|
/// </summary>
|
||||||
|
public static class FireflyProtocol
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The current version of the Firefly protocol
|
||||||
|
/// </summary>
|
||||||
|
public const string PROTOCOL_VERSION = "1.0";
|
||||||
|
|
||||||
|
// Command prefixes
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix for simple string responses (+)
|
||||||
|
/// </summary>
|
||||||
|
public const char STRING_MESSAGE = '+';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix for error responses (-)
|
||||||
|
/// </summary>
|
||||||
|
public const char ERROR_MESSAGE = '-';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix for integer responses (:)
|
||||||
|
/// </summary>
|
||||||
|
public const char INTEGER_MESSAGE = ':';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix for bulk string responses ($)
|
||||||
|
/// </summary>
|
||||||
|
public const char BULK_MESSAGE = '$';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prefix for array responses (*)
|
||||||
|
/// </summary>
|
||||||
|
public const char ARRAY_MESSAGE = '*';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Special message indicating a nil value ($-1)
|
||||||
|
/// </summary>
|
||||||
|
public const string NIL_MESSAGE = "$-1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard message terminator sequence (\r\n)
|
||||||
|
/// </summary>
|
||||||
|
public const string MESSAGE_TERMINATOR = "\r\n";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard OK response (+OK\r\n)
|
||||||
|
/// </summary>
|
||||||
|
public const string OK_RESPONSE = "+OK\r\n";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response for queued pipeline commands (+QUEUED\r\n)
|
||||||
|
/// </summary>
|
||||||
|
public const string QUEUED_RESPONSE = "+QUEUED\r\n";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standard nil response ($-1\r\n)
|
||||||
|
/// </summary>
|
||||||
|
public const string NIL_RESPONSE = "$-1\r\n";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command format specification for external clients
|
||||||
|
/// </summary>
|
||||||
|
public static class Commands
|
||||||
|
{
|
||||||
|
// Authentication
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticate with the server using a password
|
||||||
|
/// </summary>
|
||||||
|
public const string AUTH = "AUTH {password}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test server connection
|
||||||
|
/// </summary>
|
||||||
|
public const string PING = "PING";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close the connection
|
||||||
|
/// </summary>
|
||||||
|
public const string QUIT = "QUIT";
|
||||||
|
|
||||||
|
// String operations
|
||||||
|
/// <summary>
|
||||||
|
/// Set a key to hold a string value
|
||||||
|
/// </summary>
|
||||||
|
public const string SET = "SET {key} {value}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the value of a key
|
||||||
|
/// </summary>
|
||||||
|
public const string GET = "GET {key}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a key
|
||||||
|
/// </summary>
|
||||||
|
public const string DEL = "DEL {key}";
|
||||||
|
|
||||||
|
// List operations
|
||||||
|
/// <summary>
|
||||||
|
/// Insert elements at the head of a list
|
||||||
|
/// </summary>
|
||||||
|
public const string LPUSH = "LPUSH {key} {value} [value...]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Insert elements at the tail of a list
|
||||||
|
/// </summary>
|
||||||
|
public const string RPUSH = "RPUSH {key} {value} [value...]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove and get the first element in a list
|
||||||
|
/// </summary>
|
||||||
|
public const string LPOP = "LPOP {key}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove and get the last element in a list
|
||||||
|
/// </summary>
|
||||||
|
public const string RPOP = "RPOP {key}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a range of elements from a list
|
||||||
|
/// </summary>
|
||||||
|
public const string LRANGE = "LRANGE {key} {start} {stop}";
|
||||||
|
|
||||||
|
// Hash operations
|
||||||
|
/// <summary>
|
||||||
|
/// Set the string value of a hash field
|
||||||
|
/// </summary>
|
||||||
|
public const string HSET = "HSET {key} {field} {value}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the value of a hash field
|
||||||
|
/// </summary>
|
||||||
|
public const string HGET = "HGET {key} {field}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a hash field
|
||||||
|
/// </summary>
|
||||||
|
public const string HDEL = "HDEL {key} {field}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all fields and values in a hash
|
||||||
|
/// </summary>
|
||||||
|
public const string HGETALL = "HGETALL {key}";
|
||||||
|
|
||||||
|
// Pipeline operations
|
||||||
|
/// <summary>
|
||||||
|
/// Start a pipeline for batch processing
|
||||||
|
/// </summary>
|
||||||
|
public const string PIPELINE = "PIPELINE";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute all commands in the pipeline
|
||||||
|
/// </summary>
|
||||||
|
public const string EXEC = "EXEC";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response format specification for external clients
|
||||||
|
/// </summary>
|
||||||
|
public static class Responses
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Format a simple string response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The string value to format</param>
|
||||||
|
/// <returns>Formatted string response</returns>
|
||||||
|
public static string FormatString(string value) => $"+{value}{MESSAGE_TERMINATOR}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format an error response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The error message</param>
|
||||||
|
/// <returns>Formatted error response</returns>
|
||||||
|
public static string FormatError(string message) => $"-ERR {message}{MESSAGE_TERMINATOR}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format an integer response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The integer value to format</param>
|
||||||
|
/// <returns>Formatted integer response</returns>
|
||||||
|
public static string FormatInteger(int value) => $":{value}{MESSAGE_TERMINATOR}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format a bulk string response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The string value to format</param>
|
||||||
|
/// <returns>Formatted bulk string response</returns>
|
||||||
|
public static string FormatBulkString(string value) => $"${value.Length}{MESSAGE_TERMINATOR}{value}{MESSAGE_TERMINATOR}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format an array response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of strings to format</param>
|
||||||
|
/// <returns>Formatted array response</returns>
|
||||||
|
public static string FormatArray(string[] values)
|
||||||
|
{
|
||||||
|
var result = $"*{values.Length}{MESSAGE_TERMINATOR}";
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
result += FormatBulkString(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
310
src/ServerManager.cs
Normal file
310
src/ServerManager.cs
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region Server Management
|
||||||
|
static async Task StartServerAsync()
|
||||||
|
{
|
||||||
|
// Set up server on specified port
|
||||||
|
var listener = new TcpListener(IPAddress.Parse(bindAddress), serverPort);
|
||||||
|
listener.Start();
|
||||||
|
Console.WriteLine($"Server is listening on {bindAddress}, port {serverPort}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
// Accept client connection
|
||||||
|
var client = await listener.AcceptTcpClientAsync();
|
||||||
|
|
||||||
|
// Configure client
|
||||||
|
client.ReceiveBufferSize = bufferSize;
|
||||||
|
client.SendBufferSize = bufferSize;
|
||||||
|
client.ReceiveTimeout = connectionTimeoutSeconds * 1000;
|
||||||
|
client.SendTimeout = connectionTimeoutSeconds * 1000;
|
||||||
|
|
||||||
|
Console.WriteLine("Client connected!");
|
||||||
|
|
||||||
|
// Handle client in a separate task
|
||||||
|
_ = HandleClientAsync(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Server error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listener.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task HandleClientAsync(TcpClient client)
|
||||||
|
{
|
||||||
|
string clientId = client.Client.RemoteEndPoint?.ToString() ?? Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (client)
|
||||||
|
using (var stream = client.GetStream())
|
||||||
|
{
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
var lastActivity = DateTime.Now;
|
||||||
|
var commandQueue = new Queue<string>();
|
||||||
|
clientCommandQueues[clientId] = commandQueue;
|
||||||
|
clientPipelineMode[clientId] = false; // Initialize pipeline mode to false
|
||||||
|
|
||||||
|
while (client.Connected)
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - lastActivity).TotalSeconds > connectionTimeoutSeconds)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Client {clientId} connection timed out due to inactivity");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stream.DataAvailable)
|
||||||
|
{
|
||||||
|
await Task.Delay(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bytesRead = await stream.ReadAsync(buffer);
|
||||||
|
if (bytesRead <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
lastActivity = DateTime.Now;
|
||||||
|
var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
||||||
|
|
||||||
|
// Split message into individual commands
|
||||||
|
var commands = message.Split(["\r\n"], StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
foreach (var cmd in commands)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cmd)) continue;
|
||||||
|
|
||||||
|
// Check if this is a QUIT command
|
||||||
|
if (cmd.Trim().Equals("QUIT", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await ProcessCommandBatch(clientId, commandQueue, stream);
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes("+OK\r\n"));
|
||||||
|
Console.WriteLine($"Client {clientId} requested to quit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a PIPELINE command
|
||||||
|
if (cmd.Trim().Equals("PIPELINE", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
clientPipelineMode[clientId] = true;
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes("+OK\r\n"));
|
||||||
|
Console.WriteLine($"Client {clientId} entered pipeline mode");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is an EXEC command (end pipeline mode)
|
||||||
|
if (cmd.Trim().Equals("EXEC", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
clientPipelineMode[clientId] = false;
|
||||||
|
await ProcessCommandBatch(clientId, commandQueue, stream);
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes("+OK\r\n"));
|
||||||
|
Console.WriteLine($"Client {clientId} executed pipeline");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandQueue.Enqueue(cmd);
|
||||||
|
|
||||||
|
// Process commands if:
|
||||||
|
// 1. We've reached batch size
|
||||||
|
// 2. This is a non-pipeline command
|
||||||
|
// 3. We've reached max pipeline size
|
||||||
|
// 4. Client is not in pipeline mode
|
||||||
|
if (commandQueue.Count >= maxBatchSize ||
|
||||||
|
IsNonPipelineCommand(cmd) ||
|
||||||
|
commandQueue.Count >= maxPipelineSize ||
|
||||||
|
!clientPipelineMode[clientId])
|
||||||
|
{
|
||||||
|
await ProcessCommandBatch(clientId, commandQueue, stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is SocketException socketEx &&
|
||||||
|
(socketEx.SocketErrorCode == SocketError.ConnectionReset ||
|
||||||
|
socketEx.SocketErrorCode == SocketError.ConnectionAborted))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Client {clientId} disconnected unexpectedly");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error handling client {clientId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
clientCommandQueues.TryRemove(clientId, out _);
|
||||||
|
clientPipelineMode.TryRemove(clientId, out _);
|
||||||
|
authenticatedClients.Remove(clientId);
|
||||||
|
Console.WriteLine($"Client {clientId} disconnected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNonPipelineCommand(string command)
|
||||||
|
{
|
||||||
|
var cmd = command.Trim().Split(' ')[0].ToUpperInvariant();
|
||||||
|
return cmd == "AUTH" || cmd == "PING" || cmd == "QUIT" || cmd == "PIPELINE" || cmd == "EXEC";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCommandType(string command)
|
||||||
|
{
|
||||||
|
var cmd = command.Trim().Split(' ')[0].ToUpperInvariant();
|
||||||
|
|
||||||
|
// Categorize commands by data type they work with
|
||||||
|
if (cmd == "SET" || cmd == "GET" || cmd == "DEL")
|
||||||
|
return "string";
|
||||||
|
else if (cmd.StartsWith('L') || cmd == "RPUSH" || cmd == "RPOP")
|
||||||
|
return "list";
|
||||||
|
else if (cmd.StartsWith('H'))
|
||||||
|
return "hash";
|
||||||
|
else
|
||||||
|
return "other"; // Commands that don't fit clearly into a category
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ProcessCommandBatch(string clientId, Queue<string> commandQueue, NetworkStream stream)
|
||||||
|
{
|
||||||
|
if (commandQueue.Count == 0) return;
|
||||||
|
|
||||||
|
var responses = new List<byte[]>();
|
||||||
|
var batchSize = Math.Min(commandQueue.Count, maxBatchSize);
|
||||||
|
var isPipelineMode = clientPipelineMode.TryGetValue(clientId, out bool pipelineMode) && pipelineMode;
|
||||||
|
|
||||||
|
if (isPipelineMode)
|
||||||
|
{
|
||||||
|
// Group commands by type when in pipeline mode
|
||||||
|
var stringCommands = new List<string>();
|
||||||
|
var listCommands = new List<string>();
|
||||||
|
var hashCommands = new List<string>();
|
||||||
|
var otherCommands = new List<string>();
|
||||||
|
|
||||||
|
// Extract all commands from the queue and categorize
|
||||||
|
int commandsToProcess = Math.Min(commandQueue.Count, maxBatchSize);
|
||||||
|
for (int i = 0; i < commandsToProcess; i++)
|
||||||
|
{
|
||||||
|
if (commandQueue.Count == 0) break;
|
||||||
|
string command = commandQueue.Dequeue();
|
||||||
|
|
||||||
|
string commandType = GetCommandType(command);
|
||||||
|
switch (commandType)
|
||||||
|
{
|
||||||
|
case "string":
|
||||||
|
stringCommands.Add(command);
|
||||||
|
break;
|
||||||
|
case "list":
|
||||||
|
listCommands.Add(command);
|
||||||
|
break;
|
||||||
|
case "hash":
|
||||||
|
hashCommands.Add(command);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
otherCommands.Add(command);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each category in separate batches
|
||||||
|
var allResults = new List<byte[]>();
|
||||||
|
|
||||||
|
// Process string commands
|
||||||
|
if (stringCommands.Count > 0)
|
||||||
|
{
|
||||||
|
var stringTasks = stringCommands.Select(cmd => Task.Run(() => ProcessCommand(cmd, clientId))).ToList();
|
||||||
|
var stringResults = await Task.WhenAll(stringTasks);
|
||||||
|
allResults.AddRange(stringResults);
|
||||||
|
Console.WriteLine($"Processed {stringCommands.Count} string commands for client {clientId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process list commands
|
||||||
|
if (listCommands.Count > 0)
|
||||||
|
{
|
||||||
|
var listTasks = listCommands.Select(cmd => Task.Run(() => ProcessCommand(cmd, clientId))).ToList();
|
||||||
|
var listResults = await Task.WhenAll(listTasks);
|
||||||
|
allResults.AddRange(listResults);
|
||||||
|
Console.WriteLine($"Processed {listCommands.Count} list commands for client {clientId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process hash commands
|
||||||
|
if (hashCommands.Count > 0)
|
||||||
|
{
|
||||||
|
var hashTasks = hashCommands.Select(cmd => Task.Run(() => ProcessCommand(cmd, clientId))).ToList();
|
||||||
|
var hashResults = await Task.WhenAll(hashTasks);
|
||||||
|
allResults.AddRange(hashResults);
|
||||||
|
Console.WriteLine($"Processed {hashCommands.Count} hash commands for client {clientId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process other commands
|
||||||
|
if (otherCommands.Count > 0)
|
||||||
|
{
|
||||||
|
var otherTasks = otherCommands.Select(cmd => Task.Run(() => ProcessCommand(cmd, clientId))).ToList();
|
||||||
|
var otherResults = await Task.WhenAll(otherTasks);
|
||||||
|
allResults.AddRange(otherResults);
|
||||||
|
Console.WriteLine($"Processed {otherCommands.Count} other commands for client {clientId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
responses = allResults;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Non-pipeline mode - process in original order as before
|
||||||
|
var tasks = new List<Task<byte[]>>();
|
||||||
|
var commands = new List<string>();
|
||||||
|
|
||||||
|
// Extract commands to process
|
||||||
|
for (int i = 0; i < batchSize; i++)
|
||||||
|
{
|
||||||
|
if (commandQueue.Count == 0) break;
|
||||||
|
commands.Add(commandQueue.Dequeue());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process commands in parallel
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
tasks.Add(Task.Run(() => ProcessCommand(command, clientId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all tasks to complete
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
responses.AddRange(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all responses in a single write
|
||||||
|
if (responses.Count > 0)
|
||||||
|
{
|
||||||
|
var combinedResponse = new byte[responses.Sum(r => r.Length)];
|
||||||
|
var offset = 0;
|
||||||
|
foreach (var response in responses)
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(response, 0, combinedResponse, offset, response.Length);
|
||||||
|
offset += response.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stream.WriteAsync(combinedResponse);
|
||||||
|
|
||||||
|
// Log batch processing information
|
||||||
|
if (isPipelineMode)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Processed grouped pipeline batch of {responses.Count} commands for client {clientId}");
|
||||||
|
}
|
||||||
|
else if (responses.Count > 1)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Processed batch of {responses.Count} commands for client {clientId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
96
src/ServerOperations.cs
Normal file
96
src/ServerOperations.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region Server Operations
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the AUTH command which authenticates a client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="password">The password to authenticate with</param>
|
||||||
|
/// <param name="clientId">The client ID</param>
|
||||||
|
/// <returns>OK on success, error if authentication fails</returns>
|
||||||
|
static byte[] HandleAuthCommand(string password, string clientId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(serverPassword))
|
||||||
|
{
|
||||||
|
Console.WriteLine("AUTH command received but server has no password set");
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR this server does not require authentication\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverPassword == password)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Client {clientId} successfully authenticated");
|
||||||
|
authenticatedClients[clientId] = true;
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Client {clientId} failed authentication attempt with password: {password}");
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR invalid password\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a client is authenticated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clientId">The client ID</param>
|
||||||
|
/// <returns>True if the client is authenticated, false otherwise</returns>
|
||||||
|
static bool IsAuthenticated(string clientId)
|
||||||
|
{
|
||||||
|
// If no password is set, all clients are authenticated by default
|
||||||
|
if (string.IsNullOrEmpty(serverPassword))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticatedClients.TryGetValue(clientId, out bool authenticated) && authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the SAVE command which triggers a manual backup of the data.
|
||||||
|
/// </summary>
|
||||||
|
static byte[] HandleSaveCommand()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BackupData();
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR save failed: {ex.Message}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the BGSAVE command which triggers an asynchronous backup of the data.
|
||||||
|
/// </summary>
|
||||||
|
static byte[] HandleBgSaveCommand()
|
||||||
|
{
|
||||||
|
// Start backup in a separate task
|
||||||
|
Task.Run(() => BackupData());
|
||||||
|
return Encoding.UTF8.GetBytes("+Background saving started\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles server shutdown operations
|
||||||
|
/// </summary>
|
||||||
|
static async Task ShutdownServer()
|
||||||
|
{
|
||||||
|
if (backupsEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Performing final backup before shutdown...");
|
||||||
|
await Task.Run(() => BackupData());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining client connections
|
||||||
|
foreach (var clientId in authenticatedClients.Keys.ToList())
|
||||||
|
{
|
||||||
|
authenticatedClients.Remove(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Server shutdown complete.");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
119
src/StringOperations.cs
Normal file
119
src/StringOperations.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Firefly
|
||||||
|
{
|
||||||
|
public partial class Firefly
|
||||||
|
{
|
||||||
|
#region String Operations
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the SET command which sets a key-value pair in the string store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments in format: "key value"</param>
|
||||||
|
/// <returns>OK on success, error if arguments are invalid</returns>
|
||||||
|
static byte[] HandleSetCommand(string args)
|
||||||
|
{
|
||||||
|
string[] parts = args.Split(' ', 2);
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'set' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = parts[0];
|
||||||
|
string value = parts[1];
|
||||||
|
|
||||||
|
// Store the key-value pair using the sharded helper method
|
||||||
|
if (!StringStoreSet(key, value))
|
||||||
|
{
|
||||||
|
string? existingType = GetKeyType(key);
|
||||||
|
if (existingType != null)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"-ERR key '{key}' already exists as type '{existingType}'\r\n");
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR operation failed\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetBytes("+OK\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the GET command which retrieves a value from the string store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command arguments containing the key</param>
|
||||||
|
/// <returns>The value associated with the key, or nil if the key doesn't exist</returns>
|
||||||
|
static byte[] HandleGetCommand(string args)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(args))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'get' command\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = args.Trim();
|
||||||
|
|
||||||
|
// Retrieve the value using the sharded helper method
|
||||||
|
if (StringStoreGet(key, out string? value))
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes($"+{value}\r\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetBytes("$-1\r\n"); // Redis returns nil for non-existent keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region String Store Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a string exists for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check the string for</param>
|
||||||
|
/// <returns>True if the string exists, false otherwise</returns>
|
||||||
|
private static bool StringStoreExists(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return stringStoreShards[shardIndex].ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a string for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to set the string for</param>
|
||||||
|
/// <param name="value">The value to set</param>
|
||||||
|
/// <returns>True if the string was set, false otherwise</returns>
|
||||||
|
private static bool StringStoreSet(string key, string value)
|
||||||
|
{
|
||||||
|
// Check if key exists in any other store
|
||||||
|
if (!EnsureKeyDoesNotExist(key, "string"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
stringStoreShards[shardIndex][key] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a string for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to get the string for</param>
|
||||||
|
/// <param name="value">The value</param>
|
||||||
|
/// <returns>True if the string was found, false otherwise</returns>
|
||||||
|
private static bool StringStoreGet(string key, out string? value)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return stringStoreShards[shardIndex].TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a string for a given key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to remove the string for</param>
|
||||||
|
/// <returns>True if the string was removed, false otherwise</returns>
|
||||||
|
private static bool StringStoreRemove(string key)
|
||||||
|
{
|
||||||
|
int shardIndex = GetShardIndex(key);
|
||||||
|
return stringStoreShards[shardIndex].TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user