Initial Repo Setup

This commit is contained in:
Jacob Schmidt 2025-04-10 21:50:41 -05:00
commit de14285314
39 changed files with 5254 additions and 0 deletions

View 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
View File

@ -0,0 +1,9 @@
.editorconfig
nuget.config
*.7z
backups
bin
obj
.venv
.vs
.vscode

62
Firefly.csproj Normal file
View 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
View 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
View 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

View 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>

View 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
View 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
View 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
View 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
View 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

Binary file not shown.

BIN
artifacts/exe/Firefly.dbg Normal file

Binary file not shown.

BIN
artifacts/exe/Firefly.exe Normal file

Binary file not shown.

BIN
artifacts/exe/Firefly.pdb Normal file

Binary file not shown.

677
artifacts/exe/Firefly.xml Normal file
View 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>

Binary file not shown.

Binary file not shown.

BIN
artifacts/native/Firefly.so Normal file

Binary file not shown.

Binary file not shown.

View 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
View 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
View 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
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

BIN
resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

358
src/BackupSystem.cs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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");
}
}
}

View 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
View 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
View 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
View 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
}
}