├── .azure └── build_release.yaml ├── .config └── dotnet-tools.json ├── .gitattributes ├── .github ├── dependabot.yaml └── workflows │ └── pr_validation.yaml ├── .gitignore ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE.md ├── README.md ├── RELEASE_NOTES.md ├── TurboMqtt.sln ├── TurboMqtt.sln.DotSettings ├── benchmarks └── TurboMqtt.Benchmarks │ ├── MicroBenchmarkConfig.cs │ ├── Mqtt311 │ ├── Mqtt311ConnectCodecBenchmarks.cs │ ├── Mqtt311End2EndTcpBenchmarks.cs │ ├── Mqtt311PacketSizeEstimatorBenchmark.cs │ └── Mqtt311PublishCodecBenchmarks.cs │ ├── Program.cs │ ├── README.md │ ├── TurboMqtt.Benchmarks.csproj │ ├── start-emqx.ps1 │ └── stop-eqmx.ps1 ├── build.ps1 ├── docs ├── Performance.md ├── Telemetry.md ├── img │ └── emqx-mqtt3111.png └── logo.png ├── global.json ├── nuget.config ├── samples ├── TurboMqtt.Samples.BackpressureProducer │ ├── MqttConfig.cs │ ├── MqttProducerService.cs │ ├── Program.cs │ ├── TurboMqtt.Samples.BackpressureProducer.csproj │ ├── appsettings.json │ └── run5.ps1 └── TurboMqtt.Samples.DevNullConsumer │ ├── MqttConfig.cs │ ├── MqttConsumerService.cs │ ├── Program.cs │ ├── TurboMqtt.Samples.DevNullConsumer.csproj │ └── appsettings.json ├── scripts ├── bumpVersion.ps1 ├── getReleaseNotes.ps1 ├── signPackages.ps1 └── signsettings.json ├── src └── TurboMqtt │ ├── Client │ ├── ClientManagerActor.cs │ ├── ClientStreamInstance.cs │ ├── ClientStreamOwner.cs │ ├── IMqttClient.cs │ ├── IMqttClientFactory.cs │ ├── LoggingHelpers.cs │ ├── MqttClientConnectOptions.cs │ └── MqttClientTcpOptions.cs │ ├── ControlPacketHeaders.cs │ ├── IO │ ├── DisconnectToBinary.cs │ ├── FakeServerHandle.cs │ ├── IFakeServerHandleFactory.cs │ ├── InMem │ │ ├── FakeServerAckingFlow.cs │ │ └── InMemoryMqttTransport.cs │ ├── MqttTransport.cs │ ├── Tcp │ │ ├── FakeMqttTcpServer.cs │ │ ├── ITransportManager.cs │ │ ├── TcpConnectionManager.cs │ │ ├── TcpTransport.cs │ │ └── TcpTransportActor.cs │ └── UnsharedMemoryOwner.cs │ ├── MqttClientIdValidator.cs │ ├── MqttMessage.cs │ ├── MqttTopicValidator.cs │ ├── NonZeroUInt16.cs │ ├── PacketTypes │ ├── AuthPacket.cs │ ├── ConnAckPacket.cs │ ├── ConnectPacket.cs │ ├── DisconnectPacket.cs │ ├── MqttPacket.cs │ ├── PingReqPacket.cs │ ├── PingRespPacket.cs │ ├── PubCompPacket.cs │ ├── PubRecPacket.cs │ ├── PubRelPacket.cs │ ├── PublishAckPacket.cs │ ├── PublishPacket.cs │ ├── SubscribeAckPacket.cs │ ├── SubscribePacket.cs │ ├── UnsubAckPacket.cs │ └── UnsubscribePacket.cs │ ├── Properties │ └── Friends.cs │ ├── Protocol │ ├── AckProtocol.cs │ ├── ClientAcksActor.cs │ ├── HeartBeatActor.cs │ ├── Mqtt311Decoder.cs │ ├── Mqtt311Encoder.cs │ ├── MqttDecoderException.cs │ ├── MqttPacketSizeEstimator.cs │ ├── MqttProtocolVersion.cs │ ├── PacketSize.cs │ └── Pub │ │ ├── AtLeastOncePublishRetryActor.cs │ │ ├── ExactlyOncePublishRetryActor.cs │ │ ├── PublishProtocolDefaults.cs │ │ └── PublishingProtocol.cs │ ├── QualityOfService.cs │ ├── Streams │ ├── ClientAckingFlow.cs │ ├── MqttClientStreams.cs │ ├── MqttDecodingFlows.cs │ ├── MqttEncodingFlows.cs │ ├── MqttReceiverFlows.cs │ ├── MqttRequiredActors.cs │ ├── OpenTelemetryFlows.cs │ └── PacketSizeFilter.cs │ ├── Telemetry │ ├── OpenTelemetryConfig.cs │ └── OpenTelemetrySupport.cs │ ├── TurboMqtt.csproj │ ├── TurbotMqttHostingExtensions.cs │ └── Utility │ ├── Deadline.cs │ ├── SimpleLruCache.cs │ ├── TopicCacheManager.cs │ └── UShortCounter.cs └── tests ├── TestContainers.TurboMqtt ├── EMQX │ ├── EmqxBuilder.cs │ ├── EmqxConfiguration.cs │ └── EmqxContainer.cs ├── NanoMq │ ├── NanoMqBuilder.cs │ ├── NanoMqConfiguration.cs │ └── NanoMqContainer.cs ├── TestContainers.TurboMqtt.csproj └── Usings.cs ├── TurboMqtt.Container.Tests ├── EmqxFixture.cs ├── End2End │ └── EmqxMqtt311End2EndSpecs.cs └── TurboMqtt.Container.Tests.csproj └── TurboMqtt.Tests ├── End2End ├── InMemoryMqtt311End2EndSpecs.cs ├── TcpMqtt311End2EndSpecs.cs ├── TcpMqtt311HeartbeatFailureEnd2EndSpecs.cs └── TransportSpecBase.cs ├── GlobalUsings.cs ├── IO └── FinalDisconnectPacketSpecs.cs ├── MqttClientIdValidatorTests.cs ├── MqttTopicValidatorSpecs.cs ├── NonWindowsTheoryAttribute.cs ├── NonZeroUintTests.cs ├── PacketGenerators.cs ├── Packets ├── ConnAck │ ├── ConnAckPacketMqtt311EndToEndCodecSpecs.cs │ └── ConnAckPacketSpecs.cs ├── Connect │ ├── ConnectFlagsSpecs.cs │ ├── ConnectPacketMqtt311EndToEndCodecSpecs.cs │ ├── ConnectPacketMqtt311Specs.cs │ └── ConnectPacketMqtt5Specs.cs ├── Disconnect │ └── DisconnectPacketMqtt311End2EndCodecSpecs.cs ├── PacketEncodingTestHelper.cs ├── PingPackets │ ├── PingReqPacketMqtt311End2EndCodecSpecs.cs │ └── PingRespMqtt311End2EndCodecSpecs.cs ├── PubPackets │ ├── PubAckPacketMqtt311EndToEndCodecSpecs.cs │ ├── PubCompPacketMqtt311EndToEndCodecSpecs.cs │ ├── PubRecPacketMqtt311EndToEndCodecSpecs.cs │ ├── PubRelPacketMqtt311EndToEndCodecSpecs.cs │ └── PublishPacketMqtt311EndToEndCodecSpecs.cs ├── SubscribePackets │ ├── SubAckPacketMqtt311EndToEndCodecSpecs.cs │ └── SubscribePacketMqtt311EndToEndCodecSpecs.cs └── UnsubscribePackets │ ├── UnsubAckPacketMqt311End2EndCodecSpecs.cs │ └── UnsubscribePacketMqtt3111End2EndCodecSpecs.cs ├── Protocol ├── AtLeastOncePublishRetryActorSpecs.cs ├── ClientAcksActorSpecs.cs ├── ExactlyOncePublishRetryActorSpecs.cs ├── Mqtt311DecoderSpecs.cs └── Mqtt311EncoderSpecs.cs ├── Streams ├── MqttDecodingFlowSpecs.cs └── MqttEncodingFlowSpecs.cs ├── TurboMqtt.Tests.csproj └── Utility ├── SimpleLruCacheSpecs.cs └── UShortCounterSpecs.cs /.azure/build_release.yaml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - refs/tags/* 5 | pr: none 6 | 7 | pool: 8 | vmImage: 'windows-latest' 9 | 10 | variables: 11 | - group: signingSecrets #create this group with SECRET variables `signingUsername` and `signingPassword` 12 | - group: nugetKeys #create this group with SECRET variables `nugetKey` 13 | - group: sdkbinNuget 14 | - name: githubConnectionName 15 | value: TurboMqttReleases 16 | - name: projectName 17 | value: TurboMqtt 18 | - name: githubRepositoryName 19 | value: https://github.com/petabridge/TurboMqtt 20 | - name: buildConfiguration 21 | value: 'Release' 22 | - name: productUrl 23 | value: 'https://turbomqtt.org' 24 | 25 | stages: 26 | - stage: BuildAndSign 27 | displayName: 'Build and Sign' 28 | jobs: 29 | - job: Sign 30 | displayName: 'Sign and Push Packages' 31 | steps: 32 | - checkout: self 33 | - task: UseDotNet@2 34 | displayName: 'Install .NET SDK' 35 | inputs: 36 | packageType: 'sdk' 37 | useGlobalJson: true 38 | 39 | - powershell: ./build.ps1 40 | displayName: 'Update Release Notes' 41 | 42 | - script: 'dotnet pack --configuration $(buildConfiguration) -o ./bin/nuget' 43 | displayName: 'Build Package' 44 | 45 | - script: 'dotnet tool restore' 46 | displayName: 'Restore .NET Tools' 47 | 48 | - powershell: | 49 | echo "Starting the signing process..." 50 | dotnet tool run SignClient sign ./scripts/signsettings.json ` 51 | -UserName "$(signingUsername)" ` 52 | -Password "$(signingPassword)" ` 53 | -ProductName "TurboMqtt" ` 54 | -ProductDescription "TurboMqtt tools and drivers by Petabridge." ` 55 | -ProductUrl "$(productUrl)" ` 56 | -DirectoryPath "./bin/nuget" 57 | displayName: 'Sign Artifacts' 58 | 59 | # PowerShell script to push all NuGet packages to SdkBin 60 | - powershell: | 61 | $ErrorActionPreference = "Stop" # Makes the script stop on errors 62 | Get-ChildItem "bin\nuget\*.nupkg" -Recurse | ForEach-Object { 63 | dotnet nuget push $_.FullName --api-key $(sdkbinKey) --source $(sdkbinUri) 64 | } 65 | displayName: 'Push to SdkBin' 66 | 67 | # PowerShell script to push all NuGet packages to NuGet.org 68 | - powershell: | 69 | $ErrorActionPreference = "Stop" # Makes the script stop on errors 70 | Get-ChildItem "bin\nuget\*.nupkg" -Recurse | ForEach-Object { 71 | dotnet nuget push $_.FullName --api-key $(nugetKey) --source https://api.nuget.org/v3/index.json 72 | } 73 | displayName: 'Push to NuGet.org' 74 | 75 | - task: GitHubRelease@0 76 | displayName: 'GitHub release (create)' 77 | inputs: 78 | gitHubConnection: $(githubConnectionName) 79 | repositoryName: $(githubRepositoryName) 80 | title: '$(projectName) v$(Build.SourceBranchName)' 81 | releaseNotesFile: 'RELEASE_NOTES.md' 82 | assets: | 83 | bin\nuget\*.nupkg 84 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "signclient": { 6 | "version": "1.2.109", 7 | "commands": [ 8 | "SignClient" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | 5 | # Custom for Visual Studio 6 | *.cs diff=csharp 7 | *.sln merge=union 8 | *.csproj merge=union 9 | *.vbproj merge=union 10 | *.fsproj merge=union 11 | *.dbproj merge=union 12 | 13 | # Standard to msysgit 14 | *.doc diff=astextplain 15 | *.DOC diff=astextplain 16 | *.docx diff=astextplain 17 | *.DOCX diff=astextplain 18 | *.dot diff=astextplain 19 | *.DOT diff=astextplain 20 | *.pdf diff=astextplain 21 | *.PDF diff=astextplain 22 | *.rtf diff=astextplain 23 | *.RTF diff=astextplain 24 | 25 | # Needed for Mono build shell script 26 | *.sh -text eol=lf 27 | 28 | # Needed for API Approvals 29 | *.txt text eol=crlf 30 | 31 | build.sh eol=lf -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "11:00" 9 | 10 | - package-ecosystem: nuget 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | time: "11:00" 15 | -------------------------------------------------------------------------------- /.github/workflows/pr_validation.yaml: -------------------------------------------------------------------------------- 1 | name: pr_validation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - main 9 | pull_request: 10 | branches: 11 | - master 12 | - dev 13 | - main 14 | 15 | permissions: 16 | checks: write 17 | pull-requests: write 18 | issues: write 19 | contents: read 20 | 21 | jobs: 22 | test: 23 | timeout-minutes: 20 # Increase this timeout value as needed 24 | # Permissions this GitHub Action needs for other things in GitHub 25 | name: Test-${{matrix.os}} 26 | runs-on: ${{matrix.os}} 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | os: [ubuntu-latest, windows-latest] 32 | 33 | steps: 34 | - name: "Checkout" 35 | uses: actions/checkout@v4.2.2 36 | with: 37 | lfs: true 38 | fetch-depth: 0 39 | 40 | - name: "Install .NET SDK" 41 | uses: actions/setup-dotnet@v4.3.0 42 | with: 43 | global-json-file: "./global.json" 44 | 45 | - name: "Update release notes" 46 | shell: pwsh 47 | run: | 48 | ./build.ps1 49 | 50 | - name: "dotnet build" 51 | run: dotnet build -c Release 52 | 53 | - name: "dotnet pack" 54 | run: dotnet pack -c Release 55 | 56 | - name: "dotnet test (Linux)" 57 | if: runner.os == 'Linux' 58 | run: dotnet test --configuration Release --verbosity normal --logger trx --collect:"XPlat Code Coverage" 59 | 60 | - name: "dotnet test (Windows)" 61 | if: runner.os == 'Windows' 62 | run: dotnet test --configuration Release --verbosity normal --logger trx --collect:"XPlat Code Coverage" ./tests/TurboMqtt.Tests/TurboMqtt.Tests.csproj 63 | 64 | - name: Combine Coverage Reports 65 | if: runner.os == 'Linux' 66 | uses: danielpalme/ReportGenerator-GitHub-Action@5.4.5 67 | with: 68 | reports: "**/*.cobertura.xml" 69 | targetdir: "${{ github.workspace }}" 70 | reporttypes: "Cobertura" 71 | verbosity: "Info" 72 | title: "Code Coverage" 73 | tag: "${{ github.run_number }}_${{ github.run_id }}" 74 | customSettings: "" 75 | toolpath: "reportgeneratortool" 76 | 77 | - name: Publish Code Coverage Report 78 | if: runner.os == 'Linux' 79 | uses: irongut/CodeCoverageSummary@v1.3.0 80 | with: 81 | filename: "Cobertura.xml" 82 | badge: true 83 | fail_below_min: false 84 | format: markdown 85 | hide_branch_rate: false 86 | hide_complexity: false 87 | indicators: true 88 | output: both 89 | thresholds: "10 30" 90 | 91 | - name: Add Coverage PR Comment 92 | if: github.event_name == 'pull_request_target' && runner.os == 'Linux' 93 | uses: marocchino/sticky-pull-request-comment@v2 94 | with: 95 | recreate: true 96 | path: code-coverage-results.md 97 | 98 | - name: Upload Test Result Files 99 | if: runner.os == 'Linux' 100 | uses: actions/upload-artifact@v4 101 | with: 102 | name: test-results 103 | path: ${{ github.workspace }}/**/TestResults/**/* 104 | retention-days: 5 105 | 106 | - name: Publish Test Results 107 | if: always() && runner.os == 'Linux' 108 | uses: EnricoMi/publish-unit-test-result-action@v2.19.0 109 | with: 110 | trx_files: "${{ github.workspace }}/**/*.trx" 111 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright © 2024 Petabridge, LLC 4 | $(NoWarn);CS1591 5 | true 6 | 0.1.1 7 | Petabridge 8 | TurboMqtt v0.1.1 includes critical bug fixes and massive performance improvements over v0.1.0. 9 | 10 | **Bug Fixes and Improvements** 11 | 12 | * [Fixed QoS=1 packet handling - was previously treating it like QoS=2](https://github.com/petabridge/TurboMqtt/pull/103). 13 | * [Improved flow control inside `ClientAckHandler`](https://github.com/petabridge/TurboMqtt/pull/105) - result is a massive performance improvement when operating at QoS 1 and 2. 14 | * [Fix OpenTelemetry `TagList` for clientId and MQTT version](https://github.com/petabridge/TurboMqtt/pull/104) - now we can accurately track metrics per clientId via OpenTelemetry. 15 | 16 | **Performance** 17 | 18 | ``` 19 | 20 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3447/23H2/2023Update/SunValley3) 21 | 12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores 22 | .NET SDK 8.0.101 23 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 24 | Job-FBXRHG : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 25 | 26 | InvocationCount=1 LaunchCount=10 RunStrategy=Monitoring 27 | UnrollFactor=1 WarmupCount=10 28 | 29 | ``` 30 | | Method | QoSLevel | PayloadSizeBytes | ProtocolVersion | Mean | Error | StdDev | Median | Req/sec | 31 | |-------------------------- |------------ |----------------- |---------------- |----------:|----------:|---------:|----------:|-----------:| 32 | | **PublishAndReceiveMessages** | **AtMostOnce** | **10** | **V3_1_1** | **5.175 μs** | **0.6794 μs** | **2.003 μs** | **4.345 μs** | **193,230.35** | 33 | | **PublishAndReceiveMessages** | **AtLeastOnce** | **10** | **V3_1_1** | **26.309 μs** | **1.4071 μs** | **4.149 μs** | **25.906 μs** | **38,010.35** | 34 | | **PublishAndReceiveMessages** | **ExactlyOnce** | **10** | **V3_1_1** | **44.501 μs** | **2.2778 μs** | **6.716 μs** | **42.175 μs** | **22,471.53** | 35 | 36 | 37 | [Learn more about TurboMqtt's performance figures here](https://github.com/petabridge/TurboMqtt/blob/dev/docs/Performance.md). 38 | 39 | 40 | latest 41 | enable 42 | enable 43 | 44 | 45 | https://github.com/petabridge/TurboMqtt 46 | Apache-2.0 47 | README.md 48 | logo.png 49 | 50 | https://raw.githubusercontent.com/petabridge/TurboMqtt/dev/docs/logo.png 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | https://github.com/petabridge/TurboMqtt 64 | 65 | true 66 | 67 | true 68 | snupkg 69 | 70 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 1.5.37 7 | 1.5.31.1 8 | 1.10.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 0.2.0 June 14th 2024 #### 2 | 3 | * License has been migrated to Apache 2.0 4 | * Upgraded to [Akka.NET v1.5.25](https://github.com/akkadotnet/akka.net/releases/tag/1.5.25). 5 | 6 | #### 0.1.1 May 2nd 2024 #### 7 | 8 | TurboMqtt v0.1.1 includes critical bug fixes and massive performance improvements over v0.1.0. 9 | 10 | **Bug Fixes and Improvements** 11 | 12 | * [Fixed QoS=1 packet handling - was previously treating it like QoS=2](https://github.com/petabridge/TurboMqtt/pull/103). 13 | * [Improved flow control inside `ClientAckHandler`](https://github.com/petabridge/TurboMqtt/pull/105) - result is a massive performance improvement when operating at QoS 1 and 2. 14 | * [Fix OpenTelemetry `TagList` for clientId and MQTT version](https://github.com/petabridge/TurboMqtt/pull/104) - now we can accurately track metrics per clientId via OpenTelemetry. 15 | 16 | **Performance** 17 | 18 | ``` 19 | 20 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3447/23H2/2023Update/SunValley3) 21 | 12th Gen Intel Core i7-1260P, 1 CPU, 16 logical and 12 physical cores 22 | .NET SDK 8.0.101 23 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 24 | Job-FBXRHG : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 25 | 26 | InvocationCount=1 LaunchCount=10 RunStrategy=Monitoring 27 | UnrollFactor=1 WarmupCount=10 28 | 29 | ``` 30 | | Method | QoSLevel | PayloadSizeBytes | ProtocolVersion | Mean | Error | StdDev | Median | Req/sec | 31 | |-------------------------- |------------ |----------------- |---------------- |----------:|----------:|---------:|----------:|-----------:| 32 | | **PublishAndReceiveMessages** | **AtMostOnce** | **10** | **V3_1_1** | **5.175 μs** | **0.6794 μs** | **2.003 μs** | **4.345 μs** | **193,230.35** | 33 | | **PublishAndReceiveMessages** | **AtLeastOnce** | **10** | **V3_1_1** | **26.309 μs** | **1.4071 μs** | **4.149 μs** | **25.906 μs** | **38,010.35** | 34 | | **PublishAndReceiveMessages** | **ExactlyOnce** | **10** | **V3_1_1** | **44.501 μs** | **2.2778 μs** | **6.716 μs** | **42.175 μs** | **22,471.53** | 35 | 36 | 37 | [Learn more about TurboMqtt's performance figures here](https://github.com/petabridge/TurboMqtt/blob/dev/docs/Performance.md). 38 | -------------------------------------------------------------------------------- /TurboMqtt.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | ----------------------------------------------------------------------- 3 | <copyright file="${File.FileName}" company="Petabridge, LLC"> 4 | Copyright (C) ${File.CreatedYear} - ${CurrentDate.Year} Petabridge, LLC <https://petabridge.com> 5 | </copyright> 6 | ----------------------------------------------------------------------- 7 | True -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/MicroBenchmarkConfig.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Reflection; 8 | using BenchmarkDotNet.Attributes; 9 | using BenchmarkDotNet.Columns; 10 | using BenchmarkDotNet.Configs; 11 | using BenchmarkDotNet.Diagnosers; 12 | using BenchmarkDotNet.Exporters; 13 | using BenchmarkDotNet.Reports; 14 | using BenchmarkDotNet.Running; 15 | 16 | namespace TurboMqtt.Benchmarks; 17 | 18 | public class RequestsPerSecondColumn : IColumn 19 | { 20 | public string Id => nameof(RequestsPerSecondColumn); 21 | public string ColumnName => "Req/sec"; 22 | 23 | public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; 24 | public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => GetValue(summary, benchmarkCase, SummaryStyle.Default); 25 | public bool IsAvailable(Summary summary) => true; 26 | public bool AlwaysShow => true; 27 | public ColumnCategory Category => ColumnCategory.Custom; 28 | public int PriorityInCategory => -1; 29 | public bool IsNumeric => true; 30 | public UnitType UnitType => UnitType.Dimensionless; 31 | public string Legend => "Requests per Second"; 32 | 33 | public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) 34 | { 35 | var benchmarkAttribute = benchmarkCase.Descriptor.WorkloadMethod.GetCustomAttribute(); 36 | var totalOperations = benchmarkAttribute?.OperationsPerInvoke ?? 1; 37 | 38 | if (!summary.HasReport(benchmarkCase)) 39 | return ""; 40 | 41 | var report = summary[benchmarkCase]; 42 | var statistics = report?.ResultStatistics; 43 | if(statistics is null) 44 | return ""; 45 | 46 | var nsPerOperation = statistics.Mean; 47 | var operationsPerSecond = 1 / (nsPerOperation / 1e9); 48 | 49 | return operationsPerSecond.ToString("N2"); // or format as you like 50 | 51 | } 52 | } 53 | 54 | /// 55 | /// Basic BenchmarkDotNet configuration used for microbenchmarks. 56 | /// 57 | public class MicroBenchmarkConfig : ManualConfig 58 | { 59 | public MicroBenchmarkConfig() 60 | { 61 | AddDiagnoser(MemoryDiagnoser.Default); 62 | AddExporter(MarkdownExporter.GitHub); 63 | AddColumn(new RequestsPerSecondColumn()); 64 | } 65 | } 66 | 67 | /// 68 | /// BenchmarkDotNet configuration used for monitored jobs (not for microbenchmarks). 69 | /// 70 | public class MonitoringConfig : ManualConfig 71 | { 72 | public MonitoringConfig() 73 | { 74 | AddExporter(MarkdownExporter.GitHub); 75 | AddColumn(new RequestsPerSecondColumn()); 76 | } 77 | } -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/Mqtt311/Mqtt311ConnectCodecBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Collections.Immutable; 8 | using BenchmarkDotNet.Attributes; 9 | using TurboMqtt.PacketTypes; 10 | using TurboMqtt.Protocol; 11 | 12 | namespace TurboMqtt.Benchmarks.Mqtt311; 13 | 14 | [Config(typeof(MicroBenchmarkConfig))] 15 | public class Mqtt311ConnectCodecBenchmarks 16 | { 17 | private readonly Mqtt311Decoder _decoder = new(); 18 | 19 | private readonly ConnectPacket _connectPacket = new ConnectPacket(MqttProtocolVersion.V3_1_1) 20 | { 21 | ClientId = "benchmark-client", 22 | UserName = "benchmark-user", 23 | Password = "benchmark-password", 24 | ProtocolName = "MQTT", 25 | KeepAliveSeconds = 2, 26 | ConnectFlags = new ConnectFlags 27 | { 28 | CleanSession = true, 29 | WillFlag = false, 30 | WillQoS = QualityOfService.AtMostOnce, 31 | WillRetain = false 32 | }, 33 | Will = new MqttLastWill("benchmark-topic", new ReadOnlyMemory([0x1, 0x2, 0x3, 0x4])) 34 | { 35 | ResponseTopic = null, 36 | WillCorrelationData = null, 37 | ContentType = null, 38 | PayloadFormatIndicator = PayloadFormatIndicator.Unspecified, 39 | DelayInterval = default, 40 | MessageExpiryInterval = 0, 41 | WillProperties = null 42 | } 43 | }; 44 | 45 | private byte[] _writeableBytes = Array.Empty(); 46 | private ReadOnlyMemory _encodedConnectPacket; 47 | private PacketSize _estimatedConnectPacketSize; 48 | 49 | [GlobalSetup] 50 | public void Setup() 51 | { 52 | var estimate = MqttPacketSizeEstimator.EstimateMqtt3PacketSize(_connectPacket); 53 | _writeableBytes = new byte[estimate.TotalSize]; 54 | var memory = new Memory(new byte[estimate.TotalSize]); 55 | _encodedConnectPacket = memory; 56 | Mqtt311Encoder.EncodePacket(_connectPacket, ref memory, estimate); 57 | _estimatedConnectPacketSize = estimate; 58 | } 59 | 60 | private Memory _writeableBuffer; 61 | 62 | [IterationSetup] 63 | public void IterationSetup() 64 | { 65 | _writeableBuffer = new Memory(_writeableBytes); 66 | } 67 | 68 | [Benchmark] 69 | public ImmutableList DecodeConnectPacket() 70 | { 71 | _decoder.TryDecode(_encodedConnectPacket, out var packets); 72 | return packets; 73 | } 74 | 75 | [Benchmark] 76 | public int EncodeConnectPacket() 77 | { 78 | return Mqtt311Encoder.EncodePacket(_connectPacket, ref _writeableBuffer, _estimatedConnectPacketSize); 79 | } 80 | } -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/Mqtt311/Mqtt311PacketSizeEstimatorBenchmark.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using BenchmarkDotNet.Attributes; 8 | using TurboMqtt.PacketTypes; 9 | using TurboMqtt.Protocol; 10 | 11 | namespace TurboMqtt.Benchmarks.Mqtt311; 12 | 13 | [Config(typeof(MicroBenchmarkConfig))] 14 | public class Mqtt311PacketSizeEstimatorBenchmark 15 | { 16 | public static IEnumerable Packets() // for multiple arguments it's an IEnumerable of array of objects (object[]) 17 | { 18 | yield return PingReqPacket.Instance; 19 | yield return PingRespPacket.Instance; 20 | yield return DisconnectPacket.Instance; 21 | yield return new ConnectPacket(MqttProtocolVersion.V3_1_1) 22 | { 23 | ClientId = "client1", 24 | UserName = "user1", 25 | Password = "password1", 26 | KeepAliveSeconds = 5, 27 | ConnectFlags = new ConnectFlags() 28 | { 29 | CleanSession = true, 30 | PasswordFlag = true, 31 | UsernameFlag = true 32 | } 33 | }; 34 | yield return new ConnAckPacket() { ReasonCode = ConnAckReasonCode.Success }; 35 | yield return new SubscribePacket() 36 | { 37 | PacketId = 1, 38 | Topics = new[] 39 | { 40 | new TopicSubscription("test1") 41 | { Options = new SubscriptionOptions() { QoS = QualityOfService.AtLeastOnce } } 42 | } 43 | }; 44 | yield return new SubAckPacket() 45 | { 46 | PacketId = 1, 47 | ReasonCodes = new[] { MqttSubscribeReasonCode.GrantedQoS0 } 48 | }; 49 | yield return new UnsubscribePacket() 50 | { 51 | PacketId = 1, 52 | Topics = new[] { "test1" } 53 | }; 54 | yield return new UnsubAckPacket() { PacketId = 1, Duplicate = false }; 55 | yield return new PublishPacket(QualityOfService.AtLeastOnce, false, false, "topic1") 56 | { 57 | PacketId = 1, 58 | Payload = new ReadOnlyMemory(new byte[1024]) 59 | }; 60 | yield return new PubAckPacket() { PacketId = 1 }; 61 | yield return new PubRecPacket() { PacketId = 1 }; 62 | yield return new PubRelPacket() { PacketId = 1 }; 63 | yield return new PubCompPacket() { PacketId = 1 }; 64 | } 65 | 66 | 67 | [Benchmark] 68 | [ArgumentsSource(nameof(Packets))] 69 | public PacketSize EstimateMqttPacketSize(MqttPacket packet) 70 | { 71 | return MqttPacketSizeEstimator.EstimateMqtt3PacketSize(packet); 72 | } 73 | } -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/Mqtt311/Mqtt311PublishCodecBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Collections.Immutable; 8 | using BenchmarkDotNet.Attributes; 9 | using TurboMqtt.PacketTypes; 10 | using TurboMqtt.Protocol; 11 | 12 | namespace TurboMqtt.Benchmarks.Mqtt311; 13 | 14 | [Config(typeof(MicroBenchmarkConfig))] 15 | public class Mqtt311PublishCodecBenchmarks 16 | { 17 | private readonly Mqtt311Decoder _decoder = new(); 18 | 19 | [Params(1024, 2048, 4096, 8192)] 20 | public int PayloadSize { get; set; } 21 | 22 | private PublishPacket _publishPacket = null!; 23 | 24 | private ReadOnlyMemory _encodedPublishPacket; 25 | private PacketSize _estimatedPublishPacketSize; 26 | 27 | [GlobalSetup] 28 | public void Setup() 29 | { 30 | _publishPacket = new PublishPacket(QualityOfService.AtLeastOnce, false, false, "topic1") 31 | { 32 | PacketId = 1, 33 | Payload = new ReadOnlyMemory(new byte[PayloadSize]) 34 | }; 35 | var estimate = MqttPacketSizeEstimator.EstimateMqtt3PacketSize(_publishPacket); 36 | var memory = new Memory(new byte[estimate.TotalSize]); 37 | _encodedPublishPacket = memory; 38 | Mqtt311Encoder.EncodePacket(_publishPacket, ref memory, estimate); 39 | _estimatedPublishPacketSize = estimate; 40 | } 41 | 42 | private Memory _writeableBuffer; 43 | 44 | [IterationSetup] 45 | public void IterationSetup() 46 | { 47 | _writeableBuffer = new Memory(new byte[_estimatedPublishPacketSize.TotalSize]); 48 | } 49 | 50 | [Benchmark] 51 | public ImmutableList DecodePublishPacket() 52 | { 53 | _decoder.TryDecode(_encodedPublishPacket, out var packets); 54 | return packets; 55 | } 56 | 57 | [Benchmark] 58 | public void EncodePublishPacket() 59 | { 60 | Mqtt311Encoder.EncodePacket(_publishPacket, ref _writeableBuffer, _estimatedPublishPacketSize); 61 | } 62 | } -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using BenchmarkDotNet.Running; 3 | 4 | BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args); -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # TurboMqtt.Benchmarks 2 | 3 | End to End TCP Benchmarks require [EMQX](https://www.emqx.io/), which we will run using Docker behind the scenes. 4 | 5 | ## Start EMQX 6 | 7 | ```shell 8 | ./start-emqx.ps1 9 | ``` 10 | 11 | ## Stop EMQX 12 | 13 | ```shell 14 | ./stop-eqmx.ps1 15 | ``` -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/TurboMqtt.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | false 9 | TurboMqtt.Benchmarks 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/start-emqx.ps1: -------------------------------------------------------------------------------- 1 | docker run -d --name emqx-bench -p 1883:1883 -p 18083:18083 ` 2 | -e EMQX_MQTT__MAX_MQUEUE_LEN=100000 ` 3 | -e EMQX_FORCE_SHUTDOWN__ENABLE=false ` 4 | -e EMQX_MQTT__MAX_INFLIGHT=1000 ` 5 | emqx/emqx -------------------------------------------------------------------------------- /benchmarks/TurboMqtt.Benchmarks/stop-eqmx.ps1: -------------------------------------------------------------------------------- 1 | docker rm -f emqx-bench -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | . "$PSScriptRoot\scripts\getReleaseNotes.ps1" 2 | . "$PSScriptRoot\scripts\bumpVersion.ps1" 3 | 4 | ###################################################################### 5 | # Step 1: Grab release notes and update solution metadata 6 | ###################################################################### 7 | $releaseNotes = Get-ReleaseNotes -MarkdownFile (Join-Path -Path $PSScriptRoot -ChildPath "RELEASE_NOTES.md") 8 | 9 | # inject release notes into Directory.Buil 10 | UpdateVersionAndReleaseNotes -ReleaseNotesResult $releaseNotes -XmlFilePath (Join-Path -Path $PSScriptRoot -ChildPath "Directory.Build.props") 11 | 12 | Write-Output "Added release notes $releaseNotes" -------------------------------------------------------------------------------- /docs/Telemetry.md: -------------------------------------------------------------------------------- 1 | # TurboMqtt Telemetry 2 | 3 | TurboMqtt supports [OpenTelemetry](https://opentelemetry.io/) - this page explains how to enable it. 4 | 5 | ## Subscribing to TurboMqtt `Meter` and `ActivitySource` 6 | 7 | We provide some helpful extension methods to be used alongside the `OpenTelemetryBuilder` type: 8 | 9 | * `AddTurboMqttMetrics()` - subscribes to all TurboMqtt metric sources. 10 | * `AddTurboMqttTracing()` - subscribes to all TurboMqtt trace sources, which we currently do not support. 11 | 12 | An end to end example of how to use these settings: 13 | 14 | ```csharp 15 | var builder = new HostBuilder(); 16 | 17 | builder 18 | .ConfigureAppConfiguration(configBuilder => 19 | { 20 | configBuilder 21 | .AddJsonFile("appsettings.json", optional: false); 22 | }) 23 | .ConfigureLogging(logging => 24 | { 25 | logging.ClearProviders(); 26 | logging.AddConsole(); 27 | }) 28 | .ConfigureServices(s => 29 | { 30 | // parse MqttConfig from appsettings.json 31 | var optionsBuilder = s.AddOptions(); 32 | optionsBuilder.BindConfiguration("MqttConfig"); 33 | s.AddTurboMqttClientFactory(); 34 | 35 | var resourceBuilder = ResourceBuilder.CreateDefault().AddService("DevNullConsumer", 36 | "TurboMqtt.Examples", 37 | serviceInstanceId: Dns.GetHostName()); 38 | 39 | s.AddOpenTelemetry() 40 | .WithMetrics(m => 41 | { 42 | m 43 | .SetResourceBuilder(resourceBuilder) 44 | .AddTurboMqttMetrics() 45 | .AddOtlpExporter(options => 46 | { 47 | options.Endpoint = new Uri("http://localhost:4317"); // Replace with the appropriate endpoint 48 | options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; // or HttpProtobuf 49 | }); 50 | }) 51 | .WithTracing(t => 52 | { 53 | t 54 | .SetResourceBuilder(resourceBuilder) 55 | .AddTurboMqttTracing() 56 | .AddOtlpExporter(options => 57 | { 58 | options.Endpoint = new Uri("http://localhost:4317"); // Replace with the appropriate endpoint 59 | options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; // or HttpProtobuf 60 | }); 61 | }); 62 | s.AddHostedService(); 63 | }); 64 | 65 | var host = builder.Build(); 66 | 67 | await host.RunAsync(); 68 | ``` 69 | 70 | ## Collected Metrics 71 | 72 | What metrics does TurboMqtt expose? 73 | 74 | * `recv_messages` - by `clientId`, `PacketType`, `MqttProtocolVersion` 75 | * `recv_bytes` - by `clientId`, `MqttProtocolVersion` 76 | * `sent_messages` - by `clientId`, `PacketType`, `MqttProtocolVersion` 77 | * `sent_bytes` - by `clientId`, `MqttProtocolVersion` 78 | 79 | ## Disabling TurboMqtt OpenTelemetry for Performance Reasons 80 | 81 | If you want to disable the low-level emission of OpenTelemetry metrics and traces, we support that on the `MqttClientConnectOptions` class you have to use when creating an `IMqttClient`: 82 | 83 | ```csharp 84 | var tcpClientOptions = new MqttClientTcpOptions(config.Host, config.Port); 85 | var clientConnectOptions = new MqttClientConnectOptions(config.ClientId, MqttProtocolVersion.V3_1_1) 86 | { 87 | UserName = config.User, 88 | Password = config.Password, 89 | KeepAliveSeconds = 5, 90 | EnableOpenTelemetry = false // disable telemetry 91 | }; 92 | 93 | var client = await _clientFactory.CreateTcpClient(clientConnectOptions, tcpClientOptions); 94 | ``` -------------------------------------------------------------------------------- /docs/img/emqx-mqtt3111.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/TurboMqtt/b5577871a63572b032b13ee12845644257720519/docs/img/emqx-mqtt3111.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petabridge/TurboMqtt/b5577871a63572b032b13ee12845644257720519/docs/logo.png -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "rollForward": "latestMinor", 4 | "version": "8.0.400" 5 | } 6 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.BackpressureProducer/MqttConfig.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt; 8 | 9 | namespace TurboMqtt.Samples.BackpressureProducer; 10 | 11 | public class MqttConfig 12 | { 13 | public string Host { get; set; } = "localhost"; 14 | public int Port { get; set; } = 1883; 15 | public string ClientId { get; set; } = "dev-null-producer"; 16 | public string Topic { get; set; } = "dev/null"; 17 | public string User { get; set; } = "dev-null-consumer"; 18 | public string Password { get; set; } = "dev-null-consumer"; 19 | public QualityOfService QoS { get; set; } = 0; 20 | 21 | public int MessageCount { get; set; } = 1_000_000; 22 | } -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.BackpressureProducer/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using OpenTelemetry.Metrics; 7 | using OpenTelemetry.Resources; 8 | using OpenTelemetry.Trace; 9 | using TurboMqtt; 10 | using TurboMqtt.Telemetry; 11 | using TurboMqtt.Samples.BackpressureProducer; 12 | 13 | var builder = new HostBuilder(); 14 | 15 | builder 16 | .ConfigureAppConfiguration(configBuilder => 17 | { 18 | configBuilder 19 | .AddJsonFile("appsettings.json", optional: false); 20 | }) 21 | .ConfigureLogging(logging => 22 | { 23 | logging.ClearProviders(); 24 | logging.AddConsole(); 25 | }) 26 | .ConfigureServices(s => 27 | { 28 | // parse MqttConfig from appsettings.json 29 | var optionsBuilder = s.AddOptions(); 30 | optionsBuilder.BindConfiguration("MqttConfig"); 31 | s.AddTurboMqttClientFactory(); 32 | s.AddHostedService(); 33 | 34 | var resourceBuilder = ResourceBuilder.CreateDefault().AddService("BackPressureProducer", 35 | "TurboMqtt.Examples", 36 | serviceInstanceId: Dns.GetHostName()); 37 | 38 | s.AddOpenTelemetry() 39 | .WithMetrics(m => 40 | { 41 | m 42 | .SetResourceBuilder(resourceBuilder) 43 | .AddTurboMqttMetrics() 44 | .AddOtlpExporter(options => 45 | { 46 | options.Endpoint = new Uri("http://localhost:4317"); // Replace with the appropriate endpoint 47 | options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; // or HttpProtobuf 48 | }); 49 | }) 50 | .WithTracing(t => 51 | { 52 | t 53 | .SetResourceBuilder(resourceBuilder) 54 | .AddTurboMqttTracing() 55 | .AddOtlpExporter(options => 56 | { 57 | options.Endpoint = new Uri("http://localhost:4317"); // Replace with the appropriate endpoint 58 | options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; // or HttpProtobuf 59 | }); 60 | }); 61 | 62 | }); 63 | 64 | var host = builder.Build(); 65 | 66 | await host.RunAsync(); -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.BackpressureProducer/TurboMqtt.Samples.BackpressureProducer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.BackpressureProducer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "MqttConfig":{ 10 | "Server": "localhost", 11 | "Port": 1883, 12 | "User": "admin", 13 | "Password": "public", 14 | "ClientId": "turbo.pub.1", 15 | "Topic": "dev/topic_1", 16 | "QoS": 1, 17 | "MessageCount": 10000000 18 | } 19 | } -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.BackpressureProducer/run5.ps1: -------------------------------------------------------------------------------- 1 | # PowerShell script to run 5 instances of a process with 'dotnet run -c Release' 2 | # and terminate all instances if the script is stopped with Control+C 3 | 4 | # Array to keep track of process objects 5 | $processes = @() 6 | 7 | # Register an event handler to respond to process exit/interruption 8 | $exitEvent = Register-ObjectEvent -InputObject $([System.Console]) -EventName "CancelKeyPress" -Action { 9 | Write-Host "Stopping all started processes..." 10 | global:processes | ForEach-Object { 11 | $_ | Stop-Process -Force 12 | } 13 | Unregister-Event -SourceIdentifier $exitEvent.SourceIdentifier 14 | exit 15 | } 16 | 17 | try { 18 | # Loop to start 5 instances 19 | for ($i = 0; $i -lt 5; $i++) { 20 | $process = Start-Process -PassThru -NoNewWindow -FilePath "dotnet" -ArgumentList "run -c Release --no-build" 21 | $processes += $process 22 | Write-Host "Started instance $i with PID $($process.Id)" 23 | } 24 | 25 | Write-Host "All instances started. Press Control+C to stop all and exit." 26 | 27 | # Keep script running in the foreground 28 | while ($true) { 29 | Start-Sleep -Seconds 10 30 | } 31 | } 32 | catch { 33 | # If the script exits for any reason, attempt to clean up 34 | $processes | ForEach-Object { 35 | if ($_ -ne $null) { 36 | $_ | Stop-Process -Force -ErrorAction SilentlyContinue 37 | } 38 | } 39 | } 40 | finally { 41 | # Cleanup code to unregister the event and ensure processes are stopped 42 | Unregister-Event -SourceIdentifier $exitEvent.SourceIdentifier 43 | $processes | ForEach-Object { 44 | $_ | Stop-Process -Force -ErrorAction SilentlyContinue 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.DevNullConsumer/MqttConfig.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt; 8 | 9 | namespace TurboMqtt.Samples.DevNullConsumer; 10 | 11 | public class MqttConfig 12 | { 13 | public string Host { get; set; } = "localhost"; 14 | public int Port { get; set; } = 1883; 15 | public string ClientId { get; set; } = "dev-null-consumer"; 16 | public string Topic { get; set; } = "dev/null"; 17 | public string User { get; set; } = "dev-null-consumer"; 18 | public string Password { get; set; } = "dev-null-consumer"; 19 | public QualityOfService QoS { get; set; } = 0; 20 | } -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.DevNullConsumer/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | 3 | using System.Net; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using OpenTelemetry.Metrics; 9 | using OpenTelemetry.Resources; 10 | using OpenTelemetry.Trace; 11 | using TurboMqtt; 12 | using TurboMqtt.Telemetry; 13 | using TurboMqtt.Samples.DevNullConsumer; 14 | 15 | var builder = new HostBuilder(); 16 | 17 | builder 18 | .ConfigureAppConfiguration(configBuilder => 19 | { 20 | configBuilder 21 | .AddJsonFile("appsettings.json", optional: false); 22 | }) 23 | .ConfigureLogging(logging => 24 | { 25 | logging.ClearProviders(); 26 | logging.AddConsole(); 27 | }) 28 | .ConfigureServices(s => 29 | { 30 | // parse MqttConfig from appsettings.json 31 | var optionsBuilder = s.AddOptions(); 32 | optionsBuilder.BindConfiguration("MqttConfig"); 33 | s.AddTurboMqttClientFactory(); 34 | 35 | var resourceBuilder = ResourceBuilder.CreateDefault().AddService("DevNullConsumer", 36 | "TurboMqtt.Examples", 37 | serviceInstanceId: Dns.GetHostName()); 38 | 39 | s.AddOpenTelemetry() 40 | .WithMetrics(m => 41 | { 42 | m 43 | .SetResourceBuilder(resourceBuilder) 44 | .AddTurboMqttMetrics() 45 | .AddOtlpExporter(options => 46 | { 47 | options.Endpoint = new Uri("http://localhost:4317"); // Replace with the appropriate endpoint 48 | options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; // or HttpProtobuf 49 | }); 50 | }) 51 | .WithTracing(t => 52 | { 53 | t 54 | .SetResourceBuilder(resourceBuilder) 55 | .AddTurboMqttTracing() 56 | .AddOtlpExporter(options => 57 | { 58 | options.Endpoint = new Uri("http://localhost:4317"); // Replace with the appropriate endpoint 59 | options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc; // or HttpProtobuf 60 | }); 61 | }); 62 | s.AddHostedService(); 63 | }); 64 | 65 | var host = builder.Build(); 66 | 67 | await host.RunAsync(); -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.DevNullConsumer/TurboMqtt.Samples.DevNullConsumer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/TurboMqtt.Samples.DevNullConsumer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "MqttConfig":{ 10 | "Server": "localhost", 11 | "Port": 1883, 12 | "User": "admin", 13 | "Password": "public", 14 | "ClientId": "turbo.1", 15 | "Topic": "$share/topic1/dev/topic_1", 16 | "QoS": 1 17 | } 18 | } -------------------------------------------------------------------------------- /scripts/bumpVersion.ps1: -------------------------------------------------------------------------------- 1 | function UpdateVersionAndReleaseNotes { 2 | param ( 3 | [Parameter(Mandatory=$true)] 4 | [PSCustomObject]$ReleaseNotesResult, 5 | 6 | [Parameter(Mandatory=$true)] 7 | [string]$XmlFilePath 8 | ) 9 | 10 | # Load XML 11 | $xmlContent = New-Object XML 12 | $xmlContent.Load($XmlFilePath) 13 | 14 | # Update VersionPrefix and PackageReleaseNotes 15 | $versionPrefixElement = $xmlContent.SelectSingleNode("//VersionPrefix") 16 | $versionPrefixElement.InnerText = $ReleaseNotesResult.Version 17 | 18 | $packageReleaseNotesElement = $xmlContent.SelectSingleNode("//PackageReleaseNotes") 19 | $packageReleaseNotesElement.InnerText = $ReleaseNotesResult.ReleaseNotes 20 | 21 | # Save the updated XML 22 | $xmlContent.Save($XmlFilePath) 23 | } 24 | 25 | # Usage example: 26 | # $notes = Get-ReleaseNotes -MarkdownFile "$PSScriptRoot\RELEASE_NOTES.md" 27 | # $propsPath = Join-Path -Path (Get-Item $PSScriptRoot).Parent.FullName -ChildPath "Directory.Build.props" 28 | # UpdateVersionAndReleaseNotes -ReleaseNotesResult $notes -XmlFilePath $propsPath 29 | -------------------------------------------------------------------------------- /scripts/getReleaseNotes.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReleaseNotes { 2 | param ( 3 | [Parameter(Mandatory=$true)] 4 | [string]$MarkdownFile 5 | ) 6 | 7 | # Read markdown file content 8 | $content = Get-Content -Path $MarkdownFile -Raw 9 | 10 | # Split content based on headers 11 | $sections = $content -split "####" 12 | 13 | # Output object to store result 14 | $outputObject = [PSCustomObject]@{ 15 | Version = $null 16 | Date = $null 17 | ReleaseNotes = $null 18 | } 19 | 20 | # Check if we have at least 3 sections (1. Before the header, 2. Header, 3. Release notes) 21 | if ($sections.Count -ge 3) { 22 | $header = $sections[1].Trim() 23 | $releaseNotes = $sections[2].Trim() 24 | 25 | # Extract version and date from the header 26 | $headerParts = $header -split " ", 2 27 | if ($headerParts.Count -eq 2) { 28 | $outputObject.Version = $headerParts[0] 29 | $outputObject.Date = $headerParts[1] 30 | } 31 | 32 | $outputObject.ReleaseNotes = $releaseNotes 33 | } 34 | 35 | # Return the output object 36 | return $outputObject 37 | } 38 | 39 | # Call function example: 40 | #$result = Get-ReleaseNotes -MarkdownFile "$PSScriptRoot\RELEASE_NOTES.md" 41 | #Write-Output "Version: $($result.Version)" 42 | #Write-Output "Date: $($result.Date)" 43 | #Write-Output "Release Notes:" 44 | #Write-Output $result.ReleaseNotes 45 | -------------------------------------------------------------------------------- /scripts/signPackages.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$ConfigPath, 3 | [string]$UserName, 4 | [string]$Password, 5 | [string]$ProductName, 6 | [string]$ProductDescription, 7 | [string]$ProductUrl, 8 | [string]$DirectoryPath 9 | ) 10 | 11 | # Logging the received parameters (optional, for debugging) 12 | Write-Output "Using configuration: $ConfigPath" 13 | Write-Output "Product Name: $ProductName" 14 | Write-Output "Product Description: $ProductDescription" 15 | Write-Output "Product URL: $ProductUrl" 16 | Write-Output "Directory for signing: $DirectoryPath" 17 | 18 | # Validate that the directory exists 19 | if (-Not (Test-Path $DirectoryPath)) { 20 | Write-Error "Directory does not exist: $DirectoryPath" 21 | exit 1 22 | } 23 | 24 | # Loop over each .nupkg and .snupkg file in the directory 25 | Get-ChildItem -Path $DirectoryPath -Include *.nupkg,*.snupkg -Recurse | ForEach-Object { 26 | $filePath = $_.FullName 27 | 28 | Write-Output "Signing file: $filePath" 29 | 30 | # Define the command and parameters 31 | $command = "SignClient" 32 | $arguments = "--config", $ConfigPath, 33 | "-r", $UserName, 34 | "-s", $Password, 35 | "-n", $ProductName, 36 | "-d", $ProductDescription, 37 | "-u", $ProductUrl, 38 | "-i", $filePath 39 | 40 | # Execute SignClient and capture the output directly 41 | try { 42 | SignClient sign $arguments 43 | if ($LASTEXITCODE -ne 0) { 44 | Write-Error "Failed to sign $filePath." 45 | } 46 | } catch { 47 | Write-Error "An error occurred: $_" 48 | } 49 | } -------------------------------------------------------------------------------- /scripts/signsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "SignClient": { 3 | "AzureAd": { 4 | "AADInstance": "https://login.microsoftonline.com/", 5 | "ClientId": "1e983f21-9ea5-4f21-ab99-28080225efc9", 6 | "TenantId": "2fa36080-af12-4894-a64b-a17d8f29ec52" 7 | }, 8 | "Service": { 9 | "Url": "https://pb-sign.azurewebsites.net/", 10 | "ResourceId": "https://SignService/eef8e2e7-24b1-4a3b-a73b-a84d66f9abee" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Client/ClientManagerActor.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using TurboMqtt.IO.Tcp; 9 | 10 | namespace TurboMqtt.Client; 11 | 12 | /// 13 | /// Aggregate root actor for managing all TurboMqtt clients. 14 | /// 15 | internal sealed class ClientManagerActor : UntypedActor 16 | { 17 | public sealed class StartClientActor(string clientId) 18 | { 19 | public string ClientId { get; } = clientId; 20 | } 21 | 22 | public sealed class ClientDied(string clientId) 23 | { 24 | public string ClientId { get; } = clientId; 25 | } 26 | 27 | /// 28 | /// Used to help generate unique actor names for each client. 29 | /// 30 | private int _clientCounter = 0; 31 | private readonly HashSet _activeClientIds = new(); 32 | private IActorRef _tcpConnectionManager = ActorRefs.Nobody; 33 | 34 | protected override void OnReceive(object message) 35 | { 36 | switch (message) 37 | { 38 | case StartClientActor start: 39 | { 40 | if (!_activeClientIds.Add(start.ClientId)) 41 | { 42 | Sender.Tell(new Status.Failure(new InvalidOperationException($"Client with ID {start.ClientId} already exists."))); 43 | } 44 | else 45 | { 46 | var actorName = Uri.EscapeDataString($"mqttclient-{start.ClientId}-{_clientCounter++}"); 47 | var client = Context.ActorOf(Props.Create(() => new ClientStreamOwner()), actorName); 48 | Context.WatchWith(client, new ClientDied(start.ClientId)); 49 | Sender.Tell(client); 50 | } 51 | break; 52 | } 53 | case ClientDied died: 54 | { 55 | _activeClientIds.Remove(died.ClientId); 56 | break; 57 | } 58 | case TcpConnectionManager.CreateTcpTransport start: 59 | { 60 | // forward this message to our TcpConnectionManager 61 | _tcpConnectionManager.Forward(start); 62 | break; 63 | } 64 | } 65 | } 66 | 67 | protected override void PreStart() 68 | { 69 | _tcpConnectionManager = Context.ActorOf(Props.Create(), "tcp"); 70 | } 71 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Client/IMqttClientFactory.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using TurboMqtt.IO; 9 | using TurboMqtt.Streams; 10 | using TurboMqtt.IO.InMem; 11 | using TurboMqtt.IO.Tcp; 12 | using TurboMqtt.PacketTypes; 13 | using TurboMqtt.Protocol; 14 | 15 | namespace TurboMqtt.Client; 16 | 17 | public interface IMqttClientFactory 18 | { 19 | /// 20 | /// Creates a TCP-based MQTT client. 21 | /// 22 | /// Options for our to the broker. 23 | /// Options for controlling our TCP socket. 24 | /// 25 | Task CreateTcpClient(MqttClientConnectOptions options, MqttClientTcpOptions tcpOptions); 26 | } 27 | 28 | /// 29 | /// Used for testing purposes 30 | /// 31 | internal interface IInternalMqttClientFactory 32 | { 33 | Task CreateInMemoryClient(MqttClientConnectOptions options); 34 | } 35 | 36 | /// 37 | /// Used to create instances of for use in end-user applications. 38 | /// 39 | /// 40 | /// Requires an to function properly. 41 | /// 42 | public sealed class MqttClientFactory : IMqttClientFactory, IInternalMqttClientFactory 43 | { 44 | private readonly ActorSystem _system; 45 | private readonly IActorRef _mqttClientManager; 46 | 47 | public MqttClientFactory(ActorSystem system) 48 | { 49 | _system = system; 50 | _mqttClientManager = _system.ActorOf(Props.Create(), "turbomqtt-clients"); 51 | } 52 | 53 | public async Task CreateTcpClient(MqttClientConnectOptions options, MqttClientTcpOptions tcpOptions) 54 | { 55 | AssertMqtt311(options); 56 | var transportManager = new TcpMqttTransportManager(tcpOptions, _mqttClientManager, options.ProtocolVersion); 57 | 58 | // create the client 59 | var clientActor = 60 | await _mqttClientManager.Ask(new ClientManagerActor.StartClientActor(options.ClientId)) 61 | .ConfigureAwait(false); 62 | 63 | var client = await clientActor.Ask(new ClientStreamOwner.CreateClient(transportManager, options)) 64 | .ConfigureAwait(false); 65 | 66 | return client; 67 | } 68 | 69 | public async Task CreateInMemoryClient(MqttClientConnectOptions options) 70 | { 71 | AssertMqtt311(options); 72 | var transportManager = new InMemoryMqttTransportManager((int)options.MaximumPacketSize * 2, 73 | _system.CreateLogger(options.ClientId), options.ProtocolVersion); 74 | 75 | var clientActor = 76 | await _mqttClientManager.Ask(new ClientManagerActor.StartClientActor(options.ClientId)); 77 | return await clientActor.Ask(new ClientStreamOwner.CreateClient( 78 | transportManager, 79 | options)); 80 | } 81 | 82 | private static void AssertMqtt311(MqttClientConnectOptions options) 83 | { 84 | if (options.ProtocolVersion != MqttProtocolVersion.V3_1_1) 85 | { 86 | throw new NotSupportedException("Only MQTT 3.1.1 is supported."); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Client/LoggingHelpers.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using Akka.Event; 9 | 10 | namespace TurboMqtt.Client; 11 | 12 | /// 13 | /// INTERNAL API 14 | /// 15 | /// 16 | /// Aimed at making it easier to create friendly names for our loggers. 17 | /// 18 | internal static class LoggingHelpers 19 | { 20 | public static ILoggingAdapter CreateLogger(this ActorSystem sys, string name) 21 | { 22 | var fullName = $"<{typeof(T).Name}> {name}"; 23 | 24 | return new BusLogging(sys.EventStream, fullName, typeof(T), sys.Settings.LogFormatter); 25 | } 26 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Client/MqttClientTcpOptions.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Net; 8 | using System.Net.Sockets; 9 | 10 | namespace TurboMqtt.Client; 11 | 12 | /// 13 | /// Used to configure the TCP connection for the MQTT client. 14 | /// 15 | public sealed record MqttClientTcpOptions 16 | { 17 | public MqttClientTcpOptions(string host, int port) 18 | { 19 | Host = host; 20 | Port = port; 21 | } 22 | 23 | /// 24 | /// Would love to just do IPV6, but that still meets resistance everywhere 25 | /// 26 | public AddressFamily AddressFamily { get; set; } = AddressFamily.Unspecified; 27 | 28 | /// 29 | /// Frames are limited to this size in bytes. A frame can contain multiple packets. 30 | /// 31 | public int MaxFrameSize { get; set; } = 128 * 1024; // 128kb 32 | 33 | public string Host { get; } 34 | 35 | public int Port { get; } 36 | 37 | /// 38 | /// How long should we wait before attempting to reconnect the client? 39 | /// 40 | public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); 41 | 42 | /// 43 | /// Maximum number of times we should attempt to reconnect the client before giving up. 44 | /// 45 | /// Resets back to 0 after a successful connection. 46 | /// 47 | public int MaxReconnectAttempts { get; set; } = 10; 48 | } -------------------------------------------------------------------------------- /src/TurboMqtt/ControlPacketHeaders.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | namespace TurboMqtt; 7 | 8 | /// 9 | /// The type of MQTT packet. 10 | /// 11 | /// 12 | /// Aligns to the MQTT 3.1.1 specification: https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html 13 | /// 14 | /// Also supports MQTT 5.0. 15 | /// 16 | public enum MqttPacketType 17 | { 18 | Connect = 1, 19 | ConnAck =2, 20 | Publish = 3, 21 | PubAck = 4, 22 | PubRec = 5, 23 | PubRel = 6, 24 | PubComp = 7, 25 | Subscribe = 8, 26 | SubAck = 9, 27 | Unsubscribe = 10, 28 | UnsubAck = 11, 29 | PingReq = 12, 30 | PingResp = 13, 31 | Disconnect = 14, 32 | Auth = 15 33 | } -------------------------------------------------------------------------------- /src/TurboMqtt/IO/DisconnectToBinary.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using System.Diagnostics; 9 | using TurboMqtt.PacketTypes; 10 | using TurboMqtt.Protocol; 11 | 12 | namespace TurboMqtt.IO; 13 | 14 | /// 15 | /// Utility class designed to ensure that we always flush a disconnect packet to transports even when there are none. 16 | /// 17 | internal static class DisconnectToBinary 18 | { 19 | /// 20 | /// Used when the broker disconnects from us normally. 21 | /// 22 | public static readonly DisconnectPacket NormalDisconnectPacket = new() 23 | { 24 | ReasonCode = DisconnectReasonCode.NormalDisconnection, 25 | Duplicate = true 26 | }; 27 | 28 | public static (IMemoryOwner buffer, int estimatedSize) ToBinary(this DisconnectPacket packet, 29 | MqttProtocolVersion version) 30 | { 31 | if (version == MqttProtocolVersion.V5_0) 32 | throw new NotSupportedException(); 33 | 34 | var estimate = MqttPacketSizeEstimator.EstimatePacketSize(packet, version); 35 | var fullSize = estimate.TotalSize; 36 | Memory bytes = new byte[fullSize]; 37 | 38 | switch (version) 39 | { 40 | case MqttProtocolVersion.V3_1_1: 41 | { 42 | var actualSize = Mqtt311Encoder.EncodePacket(packet, ref bytes, estimate); 43 | Debug.Assert(actualSize == fullSize, 44 | $"Actual size {actualSize} did not match estimated size {fullSize}"); 45 | break; 46 | } 47 | } 48 | 49 | return (new UnsharedMemoryOwner(bytes), fullSize); 50 | } 51 | } -------------------------------------------------------------------------------- /src/TurboMqtt/IO/IFakeServerHandleFactory.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using Akka.Event; 9 | using TurboMqtt.Protocol; 10 | 11 | namespace TurboMqtt.IO; 12 | 13 | /// 14 | /// Simple factory for creating instances. 15 | /// 16 | internal interface IFakeServerHandleFactory 17 | { 18 | IFakeServerHandle CreateServerHandle(Func<(IMemoryOwner buffer, int estimatedSize), bool> pushMessage, 19 | Func closingAction, ILoggingAdapter log, MqttProtocolVersion protocolVersion = MqttProtocolVersion.V3_1_1, TimeSpan? heartbeatDelay = null); 20 | } 21 | 22 | /// 23 | /// INTERNAL API 24 | /// 25 | internal sealed class DefaultFakeServerHandleFactory : IFakeServerHandleFactory 26 | { 27 | public IFakeServerHandle CreateServerHandle(Func<(IMemoryOwner buffer, int estimatedSize), bool> pushMessage, Func closingAction, ILoggingAdapter log, 28 | MqttProtocolVersion protocolVersion = MqttProtocolVersion.V3_1_1, TimeSpan? heartbeatDelay = null) 29 | { 30 | return protocolVersion switch 31 | { 32 | MqttProtocolVersion.V3_1_1 => new FakeMqtt311ServerHandle(pushMessage, closingAction, log, heartbeatDelay), 33 | _ => throw new NotSupportedException($"Protocol version {protocolVersion} not supported.") 34 | }; 35 | } 36 | } -------------------------------------------------------------------------------- /src/TurboMqtt/IO/Tcp/ITransportManager.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.IO.Tcp; 8 | 9 | /// 10 | /// Encapsulates all of the connection-specific details for a given transport and can be 11 | /// used by the to initially create and recreate connections. 12 | /// 13 | internal interface IMqttTransportManager 14 | { 15 | Task CreateTransportAsync(CancellationToken ct = default); 16 | } -------------------------------------------------------------------------------- /src/TurboMqtt/IO/Tcp/TcpConnectionManager.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using Akka.Event; 9 | using TurboMqtt.Client; 10 | using TurboMqtt.Protocol; 11 | 12 | namespace TurboMqtt.IO.Tcp; 13 | 14 | /// 15 | /// Actor responsible for managing all TCP connections for the MQTT client. 16 | /// 17 | internal sealed class TcpConnectionManager : UntypedActor 18 | { 19 | public sealed record CreateTcpTransport( 20 | MqttClientTcpOptions Options, 21 | MqttProtocolVersion ProtocolVersion); 22 | 23 | private readonly ILoggingAdapter _log = Context.GetLogger(); 24 | 25 | protected override void OnReceive(object message) 26 | { 27 | switch (message) 28 | { 29 | case CreateTcpTransport create: 30 | { 31 | _log.Debug("Creating new TCP transport for [{0}]", create); 32 | var tcpTransport = Context.ActorOf(Props.Create(() => new TcpTransportActor(create.Options))); 33 | Sender.Tell(tcpTransport); 34 | break; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/TurboMqtt/IO/Tcp/TcpTransport.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using System.Threading.Channels; 9 | using Akka.Actor; 10 | using Akka.Event; 11 | using TurboMqtt.Client; 12 | using TurboMqtt.PacketTypes; 13 | using TurboMqtt.Protocol; 14 | 15 | namespace TurboMqtt.IO.Tcp; 16 | 17 | /// 18 | /// INTERNAL API 19 | /// 20 | internal sealed class TcpMqttTransportManager : IMqttTransportManager 21 | { 22 | private readonly MqttClientTcpOptions _tcpOptions; 23 | private readonly MqttProtocolVersion _protocolVersion; 24 | private readonly IActorRef _mqttClientManager; 25 | 26 | public TcpMqttTransportManager(MqttClientTcpOptions tcpOptions, IActorRef mqttClientManager, MqttProtocolVersion protocolVersion) 27 | { 28 | _tcpOptions = tcpOptions; 29 | _mqttClientManager = mqttClientManager; 30 | _protocolVersion = protocolVersion; 31 | } 32 | 33 | public async Task CreateTransportAsync(CancellationToken ct = default) 34 | { 35 | var tcpTransportActor = 36 | await _mqttClientManager.Ask(new TcpConnectionManager.CreateTcpTransport(_tcpOptions, _protocolVersion), cancellationToken: ct) 37 | .ConfigureAwait(false); 38 | 39 | // get the TCP transport 40 | var tcpTransport = await tcpTransportActor.Ask(TcpTransportActor.CreateTcpTransport.Instance, cancellationToken: ct) 41 | .ConfigureAwait(false); 42 | 43 | return tcpTransport; 44 | } 45 | } 46 | 47 | /// 48 | /// TCP implementation of . 49 | /// 50 | internal sealed class TcpTransport : IMqttTransport 51 | { 52 | internal TcpTransport(ILoggingAdapter log, TcpTransportActor.ConnectionState state, IActorRef connectionActor) 53 | { 54 | Log = log; 55 | State = state; 56 | _connectionActor = connectionActor; 57 | Reader = state.Reader; 58 | Writer = state.Writer; 59 | MaxFrameSize = state.MaxFrameSize; 60 | } 61 | 62 | public ILoggingAdapter Log { get; } 63 | public ConnectionStatus Status => State.Status; 64 | 65 | private TcpTransportActor.ConnectionState State { get; } 66 | private readonly IActorRef _connectionActor; 67 | 68 | public Task WhenTerminated => State.WhenTerminated; 69 | 70 | public Task WaitForPendingWrites => State.WaitForPendingWrites; 71 | 72 | public Task CloseAsync(CancellationToken ct = default) 73 | { 74 | var watch = _connectionActor.WatchAsync(ct); 75 | // mark the writer as complete 76 | _connectionActor.Tell(new TcpTransportActor.DoClose(ct)); 77 | return watch; 78 | } 79 | 80 | public Task AbortAsync(CancellationToken ct = default) 81 | { 82 | // just force a shutdown 83 | var watch = _connectionActor.WatchAsync(ct); 84 | _connectionActor.Tell(PoisonPill.Instance); 85 | return watch; 86 | } 87 | 88 | public async Task ConnectAsync(CancellationToken ct = default) 89 | { 90 | var result = await _connectionActor.Ask(new TcpTransportActor.DoConnect(ct), ct) 91 | .ConfigureAwait(false); 92 | 93 | return result.Status == ConnectionStatus.Connected; 94 | } 95 | 96 | public int MaxFrameSize { get; } 97 | public ChannelWriter<(IMemoryOwner buffer, int readableBytes)> Writer { get; } 98 | public ChannelReader<(IMemoryOwner buffer, int readableBytes)> Reader { get; } 99 | } -------------------------------------------------------------------------------- /src/TurboMqtt/IO/UnsharedMemoryOwner.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | 9 | namespace TurboMqtt.IO; 10 | 11 | /// 12 | /// INTERNAL API 13 | /// 14 | /// 15 | /// Used on the read-side of the Akka.Streams graph to ensure that we don't accidentally share memory buffers 16 | /// during async operations. 17 | /// 18 | /// The type of content being shared - usually bytes.s 19 | internal sealed class UnsharedMemoryOwner : IMemoryOwner 20 | { 21 | public UnsharedMemoryOwner(Memory memory) 22 | { 23 | Memory = memory; 24 | } 25 | 26 | public void Dispose() 27 | { 28 | // no-op 29 | } 30 | 31 | public Memory Memory { get; } 32 | } -------------------------------------------------------------------------------- /src/TurboMqtt/MqttClientIdValidator.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Text.RegularExpressions; 8 | 9 | namespace TurboMqtt; 10 | 11 | internal static class MqttClientIdValidator 12 | { 13 | /// 14 | /// Validates an MQTT client ID according to MQTT 3.1.1 and 5.0 specifications. 15 | /// 16 | /// The client ID to validate. 17 | /// A tuple indicating whether the client ID is valid and an error message if it is not. 18 | public static (bool IsValid, string ErrorMessage) ValidateClientId(string clientId) 19 | { 20 | // Check if the client ID is empty - this is allowed under MQTT 3.1.1 and 5.0 as the server assigns a ClientID. 21 | if (string.IsNullOrEmpty(clientId)) 22 | { 23 | return (true, "Client ID is valid or will be assigned by the server."); 24 | } 25 | 26 | // Check for maximum length of 65535 characters 27 | if (clientId.Length > 65535) 28 | { 29 | return (false, "Client ID exceeds the maximum length allowed (65535 characters)."); 30 | } 31 | 32 | // Check if the client ID contains only valid UTF-8 characters (printable ASCII and some extended sets) 33 | // This regex checks for printable ASCII characters and disallows control characters 34 | if (!Regex.IsMatch(clientId, @"^[ -~]+$")) // This regex covers the range of printable ASCII characters from space (32) to tilde (126) 35 | { 36 | return (false, "Client ID contains invalid characters."); 37 | } 38 | 39 | // If all checks pass 40 | return (true, "Client ID is valid."); 41 | } 42 | } -------------------------------------------------------------------------------- /src/TurboMqtt/MqttMessage.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Text; 8 | using TurboMqtt.PacketTypes; 9 | 10 | namespace TurboMqtt; 11 | 12 | /// 13 | /// Represents a message received from the MQTT broker. 14 | /// 15 | public sealed record MqttMessage 16 | { 17 | public MqttMessage(string topic, string payload) : this(topic, Encoding.UTF8.GetBytes(payload)) 18 | { 19 | } 20 | 21 | public MqttMessage(string topic, ReadOnlyMemory payload) 22 | { 23 | Topic = topic; 24 | 25 | // validate the topic 26 | var (isValid, errorMessage) = MqttTopicValidator.ValidateSubscribeTopic(topic); 27 | if (!isValid) 28 | { 29 | throw new ArgumentException(errorMessage, nameof(topic)); 30 | } 31 | 32 | Payload = payload; 33 | } 34 | 35 | public string Topic { get; } 36 | public ReadOnlyMemory Payload { get; } 37 | public QualityOfService QoS { get; init; } 38 | public bool Retain { get; init; } 39 | 40 | public PayloadFormatIndicator PayloadFormatIndicator { get; init; } // MQTT 5.0 only 41 | 42 | /// 43 | /// The Content Type property, available in MQTT 5.0. 44 | /// This property is optional and indicates the MIME type of the application message. 45 | /// 46 | public string? ContentType { get; init; } // MQTT 5.0 only 47 | 48 | /// 49 | /// Response Topic property, available in MQTT 5.0. 50 | /// It specifies the topic name for a response message. 51 | /// 52 | public string? ResponseTopic { get; init; } // MQTT 5.0 only 53 | 54 | /// 55 | /// Correlation Data property, available in MQTT 5.0. 56 | /// This property is used by the sender of the request message to identify which request the response message is for when it receives a response. 57 | /// 58 | public ReadOnlyMemory? CorrelationData { get; init; } // MQTT 5.0 only 59 | 60 | /// 61 | /// User Property, available in MQTT 5.0. 62 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 63 | /// 64 | public IReadOnlyDictionary? UserProperties { get; init; } // MQTT 5.0 only 65 | } 66 | 67 | /// 68 | /// INTERNAL API 69 | /// 70 | internal static class MqttMessageExtensions 71 | { 72 | internal static MqttMessage FromPacket(this PublishPacket packet) 73 | { 74 | return new MqttMessage(packet.TopicName, packet.Payload) 75 | { 76 | QoS = packet.QualityOfService, 77 | Retain = packet.RetainRequested, 78 | PayloadFormatIndicator = packet.PayloadFormatIndicator, 79 | ContentType = packet.ContentType, 80 | ResponseTopic = packet.ResponseTopic, 81 | CorrelationData = packet.CorrelationData, 82 | UserProperties = packet.UserProperties 83 | }; 84 | } 85 | 86 | // create a ToPacket method here 87 | internal static PublishPacket ToPacket(this MqttMessage message) 88 | { 89 | var packet = new PublishPacket(message.QoS, false, message.Retain, message.Topic) 90 | { 91 | Payload = message.Payload, 92 | PayloadFormatIndicator = message.PayloadFormatIndicator, 93 | ContentType = message.ContentType, 94 | ResponseTopic = message.ResponseTopic, 95 | CorrelationData = message.CorrelationData, 96 | UserProperties = message.UserProperties 97 | }; 98 | 99 | return packet; 100 | } 101 | } -------------------------------------------------------------------------------- /src/TurboMqtt/MqttTopicValidator.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt; 8 | 9 | internal static class MqttTopicValidator 10 | { 11 | /// 12 | /// Validates an MQTT topic ID for publishing. 13 | /// 14 | /// The topic to validate. 15 | /// A tuple indicating whether the topic is valid and an error message if it is not. 16 | public static (bool IsValid, string ErrorMessage) ValidatePublishTopic(string topic) 17 | { 18 | if (string.IsNullOrEmpty(topic)) 19 | { 20 | return (false, "Topic must not be empty."); 21 | } 22 | 23 | if (topic.Contains('\0')) 24 | { 25 | return (false, "Topic must not contain null characters."); 26 | } 27 | 28 | if (topic.Contains('+') || topic.Contains('#')) 29 | { 30 | return (false, "Wildcards ('+' and '#') are not allowed in topics for publishing."); 31 | } 32 | 33 | if (topic.StartsWith('$')) 34 | { 35 | return (false, "Topics starting with '$' are reserved and should not be used by clients for publishing."); 36 | } 37 | 38 | if (topic.Length > 65535) // Example maximum length 39 | { 40 | return (false, "Topic exceeds the maximum length allowed."); 41 | } 42 | 43 | return (true, "Topic is valid."); 44 | } 45 | 46 | /// 47 | /// Validates an MQTT topic ID for subscribing. 48 | /// 49 | /// The topic to validate. 50 | /// A tuple indicating whether the topic is valid and an error message if it is not. 51 | public static (bool IsValid, string ErrorMessage) ValidateSubscribeTopic(string topic) 52 | { 53 | if (string.IsNullOrEmpty(topic)) 54 | { 55 | return (false, "Topic must not be empty."); 56 | } 57 | 58 | if (topic.Contains('\0')) 59 | { 60 | return (false, "Topic must not contain null characters."); 61 | } 62 | 63 | int indexOfPlus = topic.IndexOf('+'); 64 | while (indexOfPlus != -1) 65 | { 66 | if ((indexOfPlus > 0 && topic[indexOfPlus - 1] != '/') || 67 | (indexOfPlus < topic.Length - 1 && topic[indexOfPlus + 1] != '/')) 68 | { 69 | return (false, "Single-level wildcard '+' must be located between slashes or at the beginning/end of the topic."); 70 | } 71 | indexOfPlus = topic.IndexOf('+', indexOfPlus + 1); 72 | } 73 | 74 | if (topic.Contains('#') && !topic.EndsWith("/#") && !topic.Equals("#")) 75 | { 76 | return (false, "Multi-level wildcard '#' must be at the end of the topic or after a '/'."); 77 | } 78 | 79 | if (topic.StartsWith('$')) 80 | { 81 | return (false, "Topics starting with '$' are reserved and should not be used by clients for publishing."); 82 | } 83 | 84 | if (topic.Length > 65535) // Example maximum length 85 | { 86 | return (false, "Topic exceeds the maximum length allowed."); 87 | } 88 | 89 | return (true, "Topic is valid."); 90 | } 91 | } -------------------------------------------------------------------------------- /src/TurboMqtt/NonZeroUInt16.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt; 8 | 9 | /// 10 | /// Subscription and packet identifiers must be greater than 0. 11 | /// 12 | public readonly struct NonZeroUInt16 13 | { 14 | public static readonly NonZeroUInt16 MinValue = new(1); 15 | 16 | /// 17 | /// The value of the identifier. 18 | /// 19 | public ushort Value { get; } 20 | 21 | public NonZeroUInt16() : this(1) 22 | { 23 | } 24 | 25 | public NonZeroUInt16(ushort value) 26 | { 27 | if (value == 0) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(value), "Value must be greater than 0."); 30 | } 31 | 32 | Value = value; 33 | } 34 | 35 | public static implicit operator ushort(NonZeroUInt16 value) => value.Value; 36 | public static implicit operator NonZeroUInt16(ushort value) => new(value); 37 | 38 | public override string ToString() 39 | { 40 | return Value.ToString(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/AuthPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Used for authentication exchange or error reporting concerning authentication. 11 | /// 12 | /// 13 | /// This packet is only applicable in MQTT 5.0 and is used both in the initial connection phase and for dynamic re-authentication. 14 | /// 15 | public sealed class AuthPacket(string authenticationMethod, AuthReasonCode reasonCode) : MqttPacket 16 | { 17 | // turn the reason code into a ReasonString 18 | 19 | public override MqttPacketType PacketType => MqttPacketType.Auth; 20 | 21 | /// 22 | /// The Reason Code for the AUTH packet, which indicates the status of the authentication or any authentication errors. 23 | /// 24 | public AuthReasonCode ReasonCode { get; } = reasonCode; 25 | 26 | // MQTT 5.0 - Optional Properties 27 | /// 28 | /// Authentication Method, used to specify the method of authentication. 29 | /// 30 | public string AuthenticationMethod { get; } = authenticationMethod; // Required if Auth Packet is used 31 | 32 | /// 33 | /// Authentication Data, typically containing credentials or challenge/response data, depending on the auth method. 34 | /// 35 | public ReadOnlyMemory AuthenticationData { get; set; } 36 | 37 | /// 38 | /// User Properties, available in MQTT 5.0. 39 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 40 | /// 41 | public IReadOnlyDictionary? UserProperties { get; set; } 42 | 43 | /// 44 | /// Reason String providing additional information about the authentication status. 45 | /// 46 | public string? ReasonString { get; set; } = reasonCode.ToReasonString(); 47 | 48 | public override string ToString() 49 | { 50 | return $"Auth: [ReasonCode={ReasonCode}]"; 51 | } 52 | } 53 | 54 | /// 55 | /// Enumerates the reason codes applicable to the AUTH packet in MQTT 5.0. 56 | /// 57 | public enum AuthReasonCode : byte 58 | { 59 | Success = 0x00, 60 | ContinueAuthentication = 0x18, 61 | ReAuthenticate = 0x19 62 | } 63 | 64 | internal static class AuthReasonCodeHelpers 65 | { 66 | public static string ToReasonString(this AuthReasonCode reasonCode) 67 | { 68 | return reasonCode switch 69 | { 70 | AuthReasonCode.Success => "Success", 71 | AuthReasonCode.ContinueAuthentication => "Continue Authentication", 72 | AuthReasonCode.ReAuthenticate => "Re-Authenticate", 73 | _ => "Unknown Reason Code" 74 | }; 75 | } 76 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/ConnAckPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Used by the broker to acknowledge a connection request from a client. 11 | /// 12 | public sealed class ConnAckPacket : MqttPacket 13 | { 14 | public override MqttPacketType PacketType => MqttPacketType.ConnAck; 15 | 16 | public bool SessionPresent { get; set; } 17 | public ConnAckReasonCode ReasonCode { get; set; } // Enum defined below 18 | 19 | public uint MaximumPacketSize { get; set; } 20 | 21 | public ushort ReceiveMaximum { get; set; } 22 | 23 | // MQTT 5.0 - Optional Properties 24 | public IReadOnlyDictionary? UserProperties { get; set; } 25 | 26 | public string? ReasonString { get; set; } 27 | 28 | public override string ToString() 29 | { 30 | return $"ConnAck: [SessionPresent={SessionPresent}] [ReasonCode={ReasonCode}]"; 31 | } 32 | } 33 | 34 | public enum ConnAckReasonCode : byte 35 | { 36 | Success = 0x00, 37 | UnspecifiedError = 0x80, 38 | MalformedPacket = 0x81, 39 | ProtocolError = 0x82, 40 | ImplementationSpecificError = 0x83, 41 | UnsupportedProtocolVersion = 0x84, 42 | ClientIdentifierNotValid = 0x85, 43 | BadUsernameOrPassword = 0x86, 44 | NotAuthorized = 0x87, 45 | ServerUnavailable = 0x88, 46 | ServerBusy = 0x89, 47 | Banned = 0x8A, 48 | BadAuthenticationMethod = 0x8C, 49 | TopicNameInvalid = 0x90, 50 | PacketTooLarge = 0x95, 51 | QuotaExceeded = 0x97, 52 | PayloadFormatInvalid = 0x99, 53 | RetainNotSupported = 0x9A, 54 | QoSNotSupported = 0x9B, 55 | UseAnotherServer = 0x9C, 56 | ServerMoved = 0x9D, 57 | ConnectionRateExceeded = 0x9F 58 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/DisconnectPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using System.Diagnostics; 9 | using TurboMqtt.IO; 10 | using TurboMqtt.Protocol; 11 | 12 | namespace TurboMqtt.PacketTypes; 13 | 14 | /// 15 | /// Used by a client to indicate that it will disconnect cleanly, or by the server to notify of a disconnect. 16 | /// 17 | public sealed class DisconnectPacket : MqttPacket 18 | { 19 | /// 20 | /// Used for MQTT 3.1.1, since no additional properties are supported. 21 | /// 22 | internal static readonly DisconnectPacket Instance = new(); 23 | 24 | public override MqttPacketType PacketType => MqttPacketType.Disconnect; 25 | 26 | // MQTT 5.0 - Optional Reason Code and Properties 27 | public DisconnectReasonCode? ReasonCode { get; set; } // MQTT 5.0 only 28 | 29 | /// 30 | /// User Properties, available in MQTT 5.0. 31 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 32 | /// 33 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 34 | 35 | /// 36 | /// The Server Reference property, available in MQTT 5.0. 37 | /// This optional property can suggest another server for the client to use. 38 | /// 39 | public string? ServerReference { get; set; } // MQTT 5.0 only 40 | 41 | /// 42 | /// Session Expiry Interval, available in MQTT 5.0. 43 | /// This optional property can indicate the session expiry interval in seconds when the disconnect is initiated. 44 | /// 45 | public uint? SessionExpiryInterval { get; set; } // MQTT 5.0 only 46 | 47 | public override string ToString() 48 | { 49 | return $"Disconnect: [ReasonCode={ReasonCode}]"; 50 | } 51 | } 52 | 53 | public enum DisconnectReasonCode : byte 54 | { 55 | NormalDisconnection = 0x00, 56 | DisconnectWithWillMessage = 0x04, 57 | UnspecifiedError = 0x80, 58 | MalformedPacket = 0x81, 59 | ProtocolError = 0x82, 60 | ImplementationSpecificError = 0x83, 61 | NotAuthorized = 0x87, 62 | ServerBusy = 0x89, 63 | ServerShuttingDown = 0x8B, 64 | KeepAliveTimeout = 0x8D, 65 | SessionTakenOver = 0x8E, 66 | TopicFilterInvalid = 0x8F, 67 | TopicNameInvalid = 0x90, 68 | ReceiveMaximumExceeded = 0x93, 69 | TopicAliasInvalid = 0x94, 70 | PacketTooLarge = 0x95, 71 | MessageRateTooHigh = 0x96, 72 | QuotaExceeded = 0x97, 73 | AdministrativeAction = 0x98, 74 | PayloadFormatInvalid = 0x99, 75 | RetainNotSupported = 0x9A, 76 | QoSNotSupported = 0x9B, 77 | UseAnotherServer = 0x9C, 78 | ServerMoved = 0x9D, 79 | SharedSubscriptionsNotSupported = 0x9E, 80 | ConnectionRateExceeded = 0x9F, 81 | MaximumConnectTime = 0xA0, 82 | SubscriptionIdentifiersNotSupported = 0xA1, 83 | WildcardSubscriptionsNotSupported = 0xA2 84 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/MqttPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Base for all MQTT packets. 11 | /// 12 | public abstract class MqttPacket 13 | { 14 | public abstract MqttPacketType PacketType { get; } 15 | 16 | public bool Duplicate { get; set; } = false; 17 | 18 | public virtual QualityOfService QualityOfService => QualityOfService.AtMostOnce; 19 | 20 | public virtual bool RetainRequested => false; 21 | 22 | public override string ToString() 23 | { 24 | return 25 | $"{GetType().Name}[Type={PacketType}, QualityOfService={QualityOfService}, Duplicate={Duplicate}, Retain={RetainRequested}]"; 26 | } 27 | } 28 | 29 | /// 30 | /// Base for MQTT packets that require a packet identifier. 31 | /// 32 | public abstract class MqttPacketWithId : MqttPacket 33 | { 34 | /// 35 | /// The unique identifier assigned to the packet. 36 | /// 37 | /// 38 | /// Not all packets require an identifier. 39 | /// 40 | public NonZeroUInt16 PacketId { get; set; } 41 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/PingReqPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Packet sent to the client by the server in response to a . 11 | /// 12 | /// 13 | /// Used to keep the connection alive. 14 | /// 15 | public sealed class PingReqPacket : MqttPacket 16 | { 17 | public static readonly PingReqPacket Instance = new PingReqPacket(); 18 | 19 | private PingReqPacket() 20 | { 21 | } 22 | 23 | public override MqttPacketType PacketType => MqttPacketType.PingReq; 24 | 25 | public override string ToString() 26 | { 27 | return "PingReq"; 28 | } 29 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/PingRespPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Packet sent to the client by the server in response to a . 11 | /// 12 | public sealed class PingRespPacket : MqttPacket 13 | { 14 | public static readonly PingRespPacket Instance = new PingRespPacket(); 15 | 16 | private PingRespPacket() 17 | { 18 | } 19 | 20 | public override MqttPacketType PacketType => MqttPacketType.PingResp; 21 | 22 | public override string ToString() 23 | { 24 | return "PingResp"; 25 | } 26 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/PubCompPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Used to acknowledge the receipt of a from the client. 11 | /// This is the final packet in the QoS 2 message flow. 12 | /// 13 | public sealed class PubCompPacket : MqttPacketWithId 14 | { 15 | public override MqttPacketType PacketType => MqttPacketType.PubComp; 16 | 17 | // MQTT 5.0 - Optional Reason Code and Properties 18 | /// 19 | /// The Reason Code for the PUBCOMP, available in MQTT 5.0. 20 | /// 21 | public PubCompReasonCode? ReasonCode { get; set; } // MQTT 5.0 only 22 | 23 | /// 24 | /// The Reason String for the PUBREC, available in MQTT 5.0. 25 | /// 26 | public string ReasonString { get; set; } = string.Empty; 27 | 28 | /// 29 | /// User Properties, available in MQTT 5.0. 30 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 31 | /// 32 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 33 | 34 | public override string ToString() 35 | { 36 | return $"PubComp: [PacketIdentifier={PacketId}], [ReasonCode={ReasonCode}], [ReasonString={ReasonString}]"; 37 | } 38 | } 39 | 40 | /// 41 | /// Enum for PUBCOMP reason codes, using the same as PUBREC for simplicity and because MQTT 5.0 reuses these 42 | /// 43 | public enum PubCompReasonCode : byte 44 | { 45 | Success = 0x00, 46 | PacketIdentifierNotFound = 0x92 47 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/PubRecPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Used to acknowledge the receipt of a Pub packet with . 11 | /// This packet type is part of the QoS 2 message flow. 12 | /// 13 | public sealed class PubRecPacket : MqttPacketWithId 14 | { 15 | public override MqttPacketType PacketType => MqttPacketType.PubRec; 16 | 17 | // MQTT 5.0 - Optional Reason Code and Properties 18 | /// 19 | /// The Reason Code for the PUBREC, available in MQTT 5.0. 20 | /// 21 | public PubRecReasonCode? ReasonCode { get; set; } // MQTT 5.0 only 22 | 23 | /// 24 | /// The Reason String for the PUBREC, available in MQTT 5.0. 25 | /// 26 | public string ReasonString { get; set; } = string.Empty; 27 | 28 | /// 29 | /// User Properties, available in MQTT 5.0. 30 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 31 | /// 32 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 33 | 34 | public override string ToString() 35 | { 36 | return $"PubRec: [PacketIdentifier={PacketId}], [ReasonCode={ReasonCode}], [ReasonString={ReasonString}]"; 37 | } 38 | } 39 | 40 | /// 41 | /// Enum for PUBREC and PUBCOMP reason codes (as they share the same codes) 42 | /// 43 | public enum PubRecReasonCode : byte 44 | { 45 | Success = 0x00, 46 | NoMatchingSubscribers = 0x10, 47 | UnspecifiedError = 0x80, 48 | ImplementationSpecificError = 0x83, 49 | NotAuthorized = 0x87, 50 | TopicNameInvalid = 0x90, 51 | PacketIdentifierInUse = 0x91, 52 | QuotaExceeded = 0x97, 53 | PayloadFormatInvalid = 0x99 54 | } 55 | 56 | internal static class PubRecHelpers 57 | { 58 | public static PubRelPacket ToPubRel(this PubRecPacket packet) 59 | { 60 | return new PubRelPacket 61 | { 62 | PacketId = packet.PacketId, 63 | ReasonCode = PubRelReasonCode.Success 64 | }; 65 | } 66 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/PubRelPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Used to acknowledge the receipt of a from the broker. 11 | /// This packet type is part of the QoS 2 message flow. 12 | /// 13 | public sealed class PubRelPacket : MqttPacketWithId 14 | { 15 | public override MqttPacketType PacketType => MqttPacketType.PubRel; 16 | 17 | /// 18 | /// Required QoS level for PUBREL. 19 | /// 20 | public override QualityOfService QualityOfService => QualityOfService.AtLeastOnce; 21 | 22 | // MQTT 5.0 - Optional Reason Code and Properties 23 | /// 24 | /// The Reason Code for the PUBREL, available in MQTT 5.0. 25 | /// 26 | public PubRelReasonCode? ReasonCode { get; set; } // MQTT 5.0 only 27 | 28 | /// 29 | /// The Reason String for the PUBREC, available in MQTT 5.0. 30 | /// 31 | public string ReasonString { get; set; } = string.Empty; 32 | 33 | /// 34 | /// User Properties, available in MQTT 5.0. 35 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 36 | /// 37 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 38 | 39 | public override string ToString() 40 | { 41 | return $"PubRel: [PacketIdentifier={PacketId}], [ReasonCode={ReasonCode}], [ReasonString={ReasonString}]"; 42 | } 43 | } 44 | 45 | /// 46 | /// Enum for PUBREL reason codes (typically these would be simpler as successful flow is usually assumed) 47 | /// 48 | public enum PubRelReasonCode : byte 49 | { 50 | Success = 0x00, 51 | PacketIdentifierNotFound = 0x92 52 | } 53 | 54 | /// 55 | /// INTERNAL API 56 | /// 57 | internal static class PubRelHelpers 58 | { 59 | public static PubCompPacket ToPubComp(this PubRelPacket packet) 60 | { 61 | return new PubCompPacket 62 | { 63 | PacketId = packet.PacketId, 64 | ReasonCode = PubCompReasonCode.Success 65 | }; 66 | } 67 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/PublishAckPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// All possible reason codes for the PubAck packet. 11 | /// 12 | public enum MqttPubAckReasonCode : byte 13 | { 14 | Success = 0x00, 15 | NoMatchingSubscribers = 0x10, 16 | UnspecifiedError = 0x80, 17 | ImplementationSpecificError = 0x83, 18 | NotAuthorized = 0x87, 19 | TopicNameInvalid = 0x90, 20 | PacketIdentifierInUse = 0x91, 21 | QuotaExceeded = 0x97, 22 | PayloadFormatInvalid = 0x99 23 | } 24 | 25 | // add a static helper method that can turn a MqttPubAckReason code into a hard-coded string representation 26 | internal static class MqttPubAckHelpers 27 | { 28 | public static string ReasonCodeToString(MqttPubAckReasonCode reasonCode) 29 | { 30 | return reasonCode switch 31 | { 32 | MqttPubAckReasonCode.Success => "Success", 33 | MqttPubAckReasonCode.NoMatchingSubscribers => "NoMatchingSubscribers", 34 | MqttPubAckReasonCode.UnspecifiedError => "UnspecifiedError", 35 | MqttPubAckReasonCode.ImplementationSpecificError => "ImplementationSpecificError", 36 | MqttPubAckReasonCode.NotAuthorized => "NotAuthorized", 37 | MqttPubAckReasonCode.TopicNameInvalid => "TopicNameInvalid", 38 | MqttPubAckReasonCode.PacketIdentifierInUse => "PacketIdentifierInUse", 39 | MqttPubAckReasonCode.QuotaExceeded => "QuotaExceeded", 40 | MqttPubAckReasonCode.PayloadFormatInvalid => "PayloadFormatInvalid", 41 | _ => throw new ArgumentOutOfRangeException(nameof(reasonCode), reasonCode, null) 42 | }; 43 | } 44 | } 45 | 46 | /// 47 | /// Used to acknowledge the receipt of a Pub packet. 48 | /// 49 | public sealed class PubAckPacket : MqttPacketWithId 50 | { 51 | public override MqttPacketType PacketType => MqttPacketType.PubAck; 52 | 53 | // MQTT 5.0 Optional Properties 54 | 55 | /// 56 | /// Reason Code for the acknowledgment, available in MQTT 5.0. 57 | /// This field is optional and provides more detailed acknowledgment information. 58 | /// 59 | public MqttPubAckReasonCode ReasonCode { get; set; } 60 | 61 | /// 62 | /// User Properties, available in MQTT 5.0. 63 | /// These are key-value pairs that can be sent to provide additional information in the acknowledgment. 64 | /// 65 | public string ReasonString => MqttPubAckHelpers.ReasonCodeToString(ReasonCode); 66 | 67 | public override string ToString() 68 | { 69 | return $"PubAck: [PacketIdentifier={PacketId}] [ReasonCode={ReasonCode}]"; 70 | } 71 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/SubscribeAckPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | public enum MqttSubscribeReasonCode : byte 10 | { 11 | // Common reason codes in MQTT 3.1.1 and earlier versions (implicitly used, typically not explicitly specified in these versions) 12 | GrantedQoS0 = 0x00, // Maximum QoS 0, MQTT 3.0, 3.1.1 13 | GrantedQoS1 = 0x01, // Maximum QoS 1, MQTT 3.0, 3.1.1 14 | GrantedQoS2 = 0x02, // Maximum QoS 2, MQTT 3.0, 3.1.1 15 | UnspecifiedError = 0x80, // MQTT 3.0, 3.1.1, MQTT 5.0 16 | 17 | // MQTT 5.0 specific reason codes 18 | ImplementationSpecificError = 0x83, // MQTT 5.0 19 | NotAuthorized = 0x87, // MQTT 5.0 20 | TopicFilterInvalid = 0x8F, // MQTT 5.0 21 | PacketIdentifierInUse = 0x91, // MQTT 5.0 22 | QuotaExceeded = 0x97, // MQTT 5.0 23 | SharedSubscriptionsNotSupported = 0x9E, // MQTT 5.0 24 | SubscriptionIdentifiersNotSupported = 0xA1, // MQTT 5.0 25 | WildcardSubscriptionsNotSupported = 0xA2, // MQTT 5.0 26 | } 27 | 28 | /// 29 | /// Represents the acknowledgement packet for a subscription request. 30 | /// 31 | public sealed class SubAckPacket : MqttPacketWithId 32 | { 33 | public override MqttPacketType PacketType => MqttPacketType.SubAck; 34 | 35 | /// 36 | /// The reason codes for each topic subscription. 37 | /// 38 | public IReadOnlyList ReasonCodes { get; set; } = Array.Empty(); 39 | 40 | /// 41 | /// The reason string for the subscription. 42 | /// 43 | /// 44 | /// This property is only used in MQTT v5.0.0 and later. 45 | /// 46 | public string? ReasonString { get; set; } 47 | 48 | /// 49 | /// User Properties, available in MQTT 5.0. 50 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 51 | /// 52 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 53 | 54 | public override string ToString() 55 | { 56 | var reasonCodesText = string.Join(",", ReasonCodes.Select(f => f.ToString())); 57 | 58 | return $"SubAck: [PacketIdentifier={PacketId}] [ReasonCode={reasonCodesText}]"; 59 | } 60 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/UnsubAckPacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Unsubscribe Acknowledgment Reason Codes. 11 | /// 12 | /// 13 | /// This is an MQTT 5.0 feature. 14 | /// 15 | public enum MqttUnsubscribeReasonCode : byte 16 | { 17 | // MQTT 5.0 specific reason codes 18 | Success = 0x00, // The subscription is deleted successfully, MQTT 5.0 19 | NoSubscriptionExisted = 0x11, // No subscription existed for the specified topic filter, MQTT 5.0 20 | UnspecifiedError = 0x80, // The unsubscribe could not be completed and the reason is not specified, MQTT 5.0 21 | 22 | ImplementationSpecificError = 23 | 0x83, // The unsubscribe could not be completed due to an implementation-specific error, MQTT 5.0 24 | NotAuthorized = 0x87, // The client was not authorized to unsubscribe, MQTT 5.0 25 | TopicFilterInvalid = 0x8F, // The specified topic filter is invalid, MQTT 5.0 26 | PacketIdentifierInUse = 0x91, // The Packet Identifier is already in use, MQTT 5.0 27 | } 28 | 29 | /// 30 | /// Used to acknowledge an unsubscribe request. 31 | /// 32 | public sealed class UnsubAckPacket : MqttPacketWithId 33 | { 34 | public override MqttPacketType PacketType => MqttPacketType.UnsubAck; 35 | 36 | /// 37 | /// Set of unsubscribe reason codes. 38 | /// 39 | /// 40 | /// Available in MQTT 5.0. 41 | /// 42 | public IReadOnlyList ReasonCodes { get; set; } = 43 | Array.Empty(); 44 | 45 | /// 46 | /// Reason given by the server for the unsubscribe. 47 | /// 48 | /// 49 | /// Available in MQTT 5.0. 50 | /// 51 | public string ReasonString { get; set; } = string.Empty; 52 | 53 | /// 54 | /// User Property, available in MQTT 5.0. 55 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 56 | /// 57 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 58 | 59 | public override string ToString() 60 | { 61 | return $"Unsubscribe Ack: [PacketIdentifier={PacketId}] [ReasonCodes={string.Join(", ", ReasonCodes)}]"; 62 | } 63 | } -------------------------------------------------------------------------------- /src/TurboMqtt/PacketTypes/UnsubscribePacket.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.PacketTypes; 8 | 9 | /// 10 | /// Used to unsubscribe from topics. 11 | /// 12 | public sealed class UnsubscribePacket : MqttPacketWithId 13 | { 14 | public override MqttPacketType PacketType => MqttPacketType.Unsubscribe; 15 | 16 | public override QualityOfService QualityOfService => QualityOfService.AtLeastOnce; 17 | 18 | /// 19 | /// The set of topics we're unsubscribing from. 20 | /// 21 | public IReadOnlyList Topics { get; set; } = Array.Empty(); 22 | 23 | /// 24 | /// User Property, available in MQTT 5.0. 25 | /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. 26 | /// 27 | public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only 28 | 29 | public override string ToString() 30 | { 31 | return $"Unsubscribe: [PacketIdentifier={PacketId}] [Topics={string.Join(", ", Topics)}]"; 32 | } 33 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Properties/Friends.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("TurboMqtt.Tests")] 4 | [assembly: InternalsVisibleTo("TurboMqtt.Benchmarks")] -------------------------------------------------------------------------------- /src/TurboMqtt/Protocol/MqttDecoderException.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Protocol; 8 | 9 | /// 10 | /// Thrown when the decoder encounters an error while parsing the MQTT packet. 11 | /// 12 | public sealed class MqttDecoderException : Exception 13 | { 14 | public MqttPacketType? PacketType { get; } 15 | public MqttProtocolVersion ProtocolVersion { get; } 16 | 17 | public MqttDecoderException(string message, MqttProtocolVersion protocolVersion, MqttPacketType? packetType = null) 18 | : base(message) 19 | { 20 | ProtocolVersion = protocolVersion; 21 | PacketType = packetType; 22 | } 23 | 24 | public MqttDecoderException(string message, Exception innerEx, MqttProtocolVersion protocolVersion, 25 | MqttPacketType? packetType = null) : base(message, 26 | innerException: innerEx) 27 | { 28 | ProtocolVersion = protocolVersion; 29 | PacketType = packetType; 30 | } 31 | 32 | public override string ToString() 33 | { 34 | // check if there's an InnerException and log that too 35 | return $"MqttDecoderException[ProtocolVersion={ProtocolVersion}, PacketType={PacketType}]" + 36 | Environment.NewLine + base.ToString(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Protocol/MqttProtocolVersion.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Protocol; 8 | 9 | /// 10 | /// The version of the MQTT protocol being used. 11 | /// 12 | public enum MqttProtocolVersion : byte 13 | { 14 | V3_1_1 = 4, // MQTT 3.1.1 is usually represented by the protocol level 4 15 | V5_0 = 5 // MQTT 5.0 is represented by the protocol level 5 16 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Protocol/PacketSize.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Protocol; 8 | 9 | /// 10 | /// Data structure representing the size of a packet, including the content size and the size of the variable length header. 11 | /// 12 | public readonly struct PacketSize 13 | { 14 | public static readonly PacketSize NoContent = new(0); 15 | 16 | public PacketSize(int contentSize) 17 | { 18 | if (contentSize < 0) 19 | throw new ArgumentOutOfRangeException(nameof(contentSize), 20 | "Content size must be greater than or equal to 0."); 21 | ContentSize = contentSize; 22 | } 23 | 24 | /// 25 | /// The value computed by our estimator 26 | /// 27 | public int ContentSize { get; } 28 | 29 | public int VariableLengthHeaderSize => MqttPacketSizeEstimator.GetPacketLengthHeaderSize(ContentSize); 30 | 31 | /// 32 | /// We add 1 byte for the fixed header, which is not included in the length 33 | /// 34 | public int TotalSize => ContentSize + VariableLengthHeaderSize + 1; 35 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Protocol/Pub/PublishProtocolDefaults.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Protocol.Pub; 8 | 9 | /// 10 | /// INTERNAL API 11 | /// 12 | internal static class PublishProtocolDefaults 13 | { 14 | public sealed class CheckTimeout 15 | { 16 | public static CheckTimeout Instance { get; } = new(); 17 | private CheckTimeout() { } 18 | } 19 | 20 | /// 21 | /// if we don't receive an acknowledgement from the server within this time frame, we'll retry the publish 22 | /// 23 | public static readonly TimeSpan DefaultPublishTimeout = TimeSpan.FromSeconds(5); 24 | public const int DefaultMaxRetries = 3; 25 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Protocol/Pub/PublishingProtocol.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | 9 | namespace TurboMqtt.Protocol.Pub; 10 | 11 | public interface IPublishResult : INoSerializationVerificationNeeded 12 | { 13 | public PublishingStatus Status { get; } 14 | 15 | public bool IsSuccess => Status == PublishingStatus.Completed; 16 | 17 | public string Reason { get; } 18 | } 19 | 20 | public enum PublishingStatus 21 | { 22 | /// 23 | /// Pub is being sent to the broker. 24 | /// 25 | Publishing, 26 | 27 | /// 28 | /// Only used in QoS 2.0 29 | /// 30 | PubRecReceived, 31 | 32 | /// 33 | /// The message was successfully published. 34 | /// 35 | /// 36 | /// PubAck for QoS 1.0, PubComp for QoS 2.0 37 | /// 38 | Completed, 39 | 40 | /// 41 | /// Message failed to be fully published for some reason 42 | /// 43 | Failed 44 | 45 | } 46 | 47 | /// 48 | /// INTERNAL API - messaging protocol used to communicate with outbound reliable delivery actors. 49 | /// 50 | public static class PublishingProtocol{ 51 | 52 | /// 53 | /// Message was successfully published and fully received. 54 | /// 55 | public sealed class PublishSuccess : IPublishResult 56 | { 57 | public static readonly PublishSuccess Instance = new(); 58 | private PublishSuccess(){} 59 | public PublishingStatus Status => PublishingStatus.Completed; 60 | public string Reason => string.Empty; 61 | } 62 | 63 | public sealed class PublishFailure(string reason) : IPublishResult 64 | { 65 | public string Reason { get; } = reason; 66 | public PublishingStatus Status => PublishingStatus.Failed; 67 | 68 | public override string ToString() 69 | { 70 | return $"PublishFailure({Reason})"; 71 | } 72 | } 73 | 74 | /// 75 | /// Sent implicitly by the end-user when a expires 76 | /// on a publish operation. 77 | /// 78 | public sealed class PublishCancelled : IPublishResult 79 | { 80 | public PublishCancelled(NonZeroUInt16 packetId) 81 | { 82 | PacketId = packetId; 83 | } 84 | 85 | public NonZeroUInt16 PacketId { get; } 86 | public PublishingStatus Status => PublishingStatus.Failed; 87 | public string Reason => "Publish operation was cancelled."; 88 | } 89 | } -------------------------------------------------------------------------------- /src/TurboMqtt/QualityOfService.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt; 8 | 9 | /// 10 | /// QoS value - corresponds to the MQTT specification. 11 | /// 12 | public enum QualityOfService 13 | { 14 | AtMostOnce = 0, 15 | AtLeastOnce = 1, 16 | ExactlyOnce = 2 17 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Streams/MqttClientStreams.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using System.Threading.Channels; 9 | using Akka; 10 | using Akka.Streams.Dsl; 11 | using TurboMqtt.IO; 12 | using TurboMqtt.PacketTypes; 13 | using TurboMqtt.Protocol; 14 | using TurboMqtt.Telemetry; 15 | 16 | namespace TurboMqtt.Streams; 17 | 18 | /// 19 | /// INTERNAL API 20 | /// 21 | internal static class MqttClientStreams 22 | { 23 | public static Sink Mqtt311OutboundPacketSink(string clientId, IMqttTransport transport, 24 | MemoryPool memoryPool, int maxFrameSize, int maxPacketSize, bool withTelemetry = true) 25 | { 26 | var finalSink = Sink.FromWriter(transport.Writer, true); 27 | if (withTelemetry) 28 | return Flow.Create() 29 | .Via(OpenTelemetryFlows.MqttPacketRateTelemetryFlow(MqttProtocolVersion.V3_1_1, clientId, 30 | OpenTelemetrySupport.Direction.Outbound)) 31 | .Via(MqttEncodingFlows.Mqtt311Encoding(memoryPool, maxFrameSize, maxPacketSize)) 32 | .Via(OpenTelemetryFlows.MqttBitRateTelemetryFlow(MqttProtocolVersion.V3_1_1, clientId, 33 | OpenTelemetrySupport.Direction.Outbound)) 34 | .To(finalSink); 35 | 36 | return Flow.Create() 37 | .Via(MqttEncodingFlows.Mqtt311Encoding(memoryPool, maxFrameSize, maxPacketSize)) 38 | .To(finalSink); 39 | } 40 | 41 | public static Source Mqtt311InboundMessageSource(string clientId, IMqttTransport transport, 42 | ChannelWriter outboundPackets, 43 | MqttRequiredActors actors, int maxRememberedPacketIds, TimeSpan packetIdExpiry, TaskCompletionSource disconnectPromise, bool withTelemetry = true) 44 | 45 | { 46 | if (withTelemetry) 47 | return (ChannelSource.FromReader(transport.Reader) 48 | .Via(OpenTelemetryFlows.MqttBitRateTelemetryFlow(MqttProtocolVersion.V3_1_1, clientId, 49 | OpenTelemetrySupport.Direction.Inbound)) 50 | .Via(MqttDecodingFlows.Mqtt311Decoding()) 51 | .Async() 52 | .Via(OpenTelemetryFlows.MqttMultiPacketRateTelemetryFlow(MqttProtocolVersion.V3_1_1, clientId, 53 | OpenTelemetrySupport.Direction.Inbound)) 54 | .Via(MqttReceiverFlows.ClientAckingFlow(outboundPackets, 55 | actors, disconnectPromise)) 56 | .Async() 57 | .Where(c => c.PacketType == MqttPacketType.Publish) 58 | .Select(c => ((PublishPacket)c).FromPacket())); 59 | 60 | return (ChannelSource.FromReader(transport.Reader) 61 | .Via(MqttDecodingFlows.Mqtt311Decoding()) 62 | .Async() 63 | .Via(MqttReceiverFlows.ClientAckingFlow(outboundPackets, actors, disconnectPromise)) 64 | .Async() 65 | .Where(c => c.PacketType == MqttPacketType.Publish) 66 | .Select(c => ((PublishPacket)c).FromPacket())); 67 | } 68 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Streams/MqttReceiverFlows.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Collections.Immutable; 8 | using System.Threading.Channels; 9 | using Akka; 10 | using Akka.Streams; 11 | using TurboMqtt.PacketTypes; 12 | 13 | namespace TurboMqtt.Streams; 14 | 15 | /// 16 | /// Used to power the business logic for receiving MQTT packets from the broker. 17 | /// 18 | internal static class MqttReceiverFlows 19 | { 20 | public static IGraph, MqttPacket>, NotUsed> ClientAckingFlow(ChannelWriter outboundPackets, MqttRequiredActors actors, TaskCompletionSource disconnectPromise) 21 | { 22 | var g = new ClientAckingFlow(outboundPackets, actors, disconnectPromise); 23 | return g; 24 | } 25 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Streams/MqttRequiredActors.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using TurboMqtt.Protocol; 9 | using TurboMqtt.Protocol.Pub; 10 | 11 | namespace TurboMqtt.Streams; 12 | 13 | /// 14 | /// All of the actors needed to power the MQTT client. 15 | /// 16 | /// The 17 | /// The 18 | /// The 19 | /// The 20 | internal sealed record MqttRequiredActors(IActorRef Qos2Actor, IActorRef Qos1Actor, IActorRef ClientAck, IActorRef HeartBeatActor); -------------------------------------------------------------------------------- /src/TurboMqtt/Streams/PacketSizeFilter.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Event; 8 | using Akka.Streams; 9 | using Akka.Streams.Stage; 10 | using TurboMqtt.PacketTypes; 11 | using TurboMqtt.Protocol; 12 | 13 | namespace TurboMqtt.Streams; 14 | 15 | /// 16 | /// Drops all packets greater than the maximum allowable size 17 | /// 18 | internal sealed class PacketSizeFilter : GraphStage> 19 | { 20 | private readonly int _maxPacketSize; 21 | public Inlet<(MqttPacket, PacketSize)> In { get; } = new("PacketSizeFilter.In"); 22 | public Outlet<(MqttPacket, PacketSize)> Out { get; } = new("PacketSizeFilter.Out"); 23 | 24 | public PacketSizeFilter(int maxPacketSize) 25 | { 26 | _maxPacketSize = maxPacketSize; 27 | Shape = new FlowShape<(MqttPacket, PacketSize), (MqttPacket, PacketSize)>(In, Out); 28 | } 29 | 30 | public override FlowShape<(MqttPacket, PacketSize), (MqttPacket, PacketSize)> Shape { get; } 31 | 32 | protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); 33 | 34 | private class Logic : InAndOutGraphStageLogic 35 | { 36 | private readonly PacketSizeFilter _stage; 37 | 38 | public Logic(PacketSizeFilter stage) : base(stage.Shape) 39 | { 40 | _stage = stage; 41 | 42 | SetHandler(stage.In, this); 43 | SetHandler(stage.Out, this); 44 | } 45 | 46 | public override void OnPush() 47 | { 48 | var (packet, size) = Grab(_stage.In); 49 | 50 | // have to adjust the packet size to account for the length header 51 | if (size.TotalSize > _stage._maxPacketSize) 52 | { 53 | Log.Warning("Dropping MQTT packet [{0}] for exceeding max size: {1} bytes.", packet, _stage._maxPacketSize); 54 | Pull(_stage.In); // Request next element 55 | } 56 | else 57 | { 58 | Push(_stage.Out, (packet, size)); 59 | } 60 | } 61 | 62 | public override void OnPull() => Pull(_stage.In); 63 | } 64 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Telemetry/OpenTelemetryConfig.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using OpenTelemetry.Metrics; 8 | using OpenTelemetry.Trace; 9 | using TurboMqtt.Client; 10 | 11 | namespace TurboMqtt.Telemetry; 12 | 13 | /// 14 | /// Used to configure the OpenTelemetry SDK for TurboMqtt. 15 | /// 16 | public static class OpenTelemetryConfig 17 | { 18 | /// 19 | /// Adds TurboMqtt metrics to the OpenTelemetry SDK. 20 | /// 21 | /// 22 | /// You must configure in order to enable this feature 23 | /// on instances. 24 | /// 25 | public static MeterProviderBuilder AddTurboMqttMetrics(this MeterProviderBuilder builder) 26 | { 27 | return builder.AddMeter(OpenTelemetrySupport.Meter.Name); 28 | } 29 | 30 | /// 31 | /// Adds TurboMqtt tracing to the OpenTelemetry SDK. 32 | /// 33 | /// 34 | /// **Only works with MQTT 5.0 protocol** 35 | /// 36 | /// You must configure in order to enable this feature 37 | /// on instances. 38 | /// 39 | public static TracerProviderBuilder AddTurboMqttTracing(this TracerProviderBuilder builder) 40 | { 41 | return builder.AddSource(OpenTelemetrySupport.ActivitySource.Name); 42 | } 43 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Telemetry/OpenTelemetrySupport.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Diagnostics; 8 | using System.Diagnostics.Metrics; 9 | using TurboMqtt.Client; 10 | using TurboMqtt.Protocol; 11 | 12 | namespace TurboMqtt.Telemetry; 13 | 14 | /// 15 | /// INTERNAL API - defines all TurboMqtt OTEL metric and activity sources. 16 | /// 17 | internal static class OpenTelemetrySupport 18 | { 19 | private static readonly string 20 | Version = typeof(IMqttClient).Assembly.GetName().Version?.ToString() ?? string.Empty; 21 | 22 | public static readonly ActivitySource ActivitySource = new("TurboMqtt", Version); 23 | 24 | public static readonly Meter Meter = new Meter("TurboMqtt", Version); 25 | 26 | public static TagList Mqtt311Tags { get; } = new TagList 27 | { 28 | { MqttVersionTag, "3.1.1" } 29 | }; 30 | 31 | public static TagList Mqtt5Tags { get; } = new TagList 32 | { 33 | { MqttVersionTag, "5.0" } 34 | }; 35 | 36 | public const string QoSLevelTag = "qos"; 37 | public const string ClientIdTag = "client.id"; 38 | public const string MqttVersionTag = "mqtt.version"; 39 | public const string PacketTypeTag = "packet.type"; 40 | 41 | public enum Direction 42 | { 43 | Inbound, 44 | Outbound 45 | } 46 | 47 | public static TagList CreateTags(string clientId, MqttProtocolVersion version) 48 | { 49 | var tags = version switch 50 | { 51 | MqttProtocolVersion.V3_1_1 => Mqtt311Tags, 52 | MqttProtocolVersion.V5_0 => Mqtt5Tags, 53 | _ => new TagList() 54 | }; 55 | 56 | tags.Add(ClientIdTag, clientId); 57 | return tags; 58 | } 59 | 60 | public static Counter CreateMessagesCounter(Direction direction) 61 | { 62 | 63 | var operationName = direction == Direction.Inbound ? "recv_messages" : "sent_messages"; 64 | var description = direction == Direction.Inbound 65 | ? "The number of MQTT messages received from a broker." 66 | : "The number of MQTT messages sent to a broker."; 67 | 68 | return Meter.CreateCounter(operationName, "packets", 69 | description); 70 | } 71 | 72 | public static Counter CreateBitRateCounter(Direction direction) 73 | { 74 | var operationName = direction == Direction.Inbound ? "recv_bytes" : "sent_bytes"; 75 | var description = direction == Direction.Inbound 76 | ? "The number of MQTT bytes received from a broker." 77 | : "The number of MQTT bytes sent to a broker."; 78 | 79 | return Meter.CreateCounter(operationName, "bytes", 80 | description); 81 | } 82 | } -------------------------------------------------------------------------------- /src/TurboMqtt/TurboMqtt.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | mqtt, iot, mqtt 3.1.1, mqtt 5.0, akka.net 8 | The fastest Message Queue Telemetry Transport (MQTT) client for .NET. 9 | 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/TurboMqtt/TurbotMqttHostingExtensions.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Actor; 8 | using Akka.Event; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using TurboMqtt.Client; 11 | 12 | namespace TurboMqtt; 13 | 14 | /// 15 | /// Used to tie into the Akka.NET and start up the TurboMqtt server. 16 | /// 17 | public static class TurbotMqttHostingExtensions 18 | { 19 | /// 20 | /// Registers the with the . 21 | /// 22 | /// 23 | /// Needs a to run - will create one if none is found in the . 24 | /// 25 | /// For best results, use Akka.Hosting to create and manage the for you. 26 | /// https://www.nuget.org/packages/Akka.Hosting 27 | /// 28 | public static IServiceCollection AddTurboMqttClientFactory(this IServiceCollection services) 29 | { 30 | services.AddSingleton(provider => 31 | { 32 | var system = provider.GetService(); 33 | if (system is null) 34 | { 35 | // start our own local ActorSystem 36 | system = ActorSystem.Create("turbomqtt"); 37 | system.Log.Info("Created new Akka.NET ActorSystem {0} - none found in IServiceCollection", system.Name); 38 | } 39 | 40 | return new MqttClientFactory(system); 41 | 42 | }); 43 | 44 | return services; 45 | } 46 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Utility/Deadline.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Utility; 8 | 9 | /// 10 | /// A sortable deadline structure used to indicate when requests are due. 11 | /// 12 | internal readonly struct Deadline : IComparable 13 | { 14 | public Deadline(DateTimeOffset time) 15 | { 16 | Time = time; 17 | } 18 | 19 | public DateTimeOffset Time { get; } 20 | 21 | public bool IsOverdue => DateTimeOffset.UtcNow >= Time; 22 | 23 | public int CompareTo(Deadline other) 24 | { 25 | return Time.CompareTo(other.Time); 26 | } 27 | 28 | public static Deadline Now => new(DateTimeOffset.UtcNow); 29 | 30 | public static Deadline FromNow(TimeSpan duration) => new(DateTimeOffset.UtcNow + duration); 31 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Utility/SimpleLruCache.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Utility; 8 | 9 | /// 10 | /// For de-duplicating packets 11 | /// 12 | internal sealed class SimpleLruCache where TKey : notnull 13 | { 14 | public SimpleLruCache(int capacity) : this(capacity, TimeSpan.FromSeconds(5)) 15 | { 16 | } 17 | 18 | public SimpleLruCache(int capacity, TimeSpan timeToLive) 19 | { 20 | Capacity = capacity; 21 | TimeToLive = timeToLive; 22 | _cache = new Dictionary(capacity); 23 | } 24 | 25 | public TimeSpan TimeToLive { get; } 26 | 27 | public int Capacity { get; } 28 | 29 | public int Count => _cache.Count; 30 | 31 | private readonly Dictionary _cache; 32 | 33 | public bool Contains(TKey key) 34 | { 35 | return _cache.ContainsKey(key); 36 | } 37 | 38 | public void Add(TKey key, Deadline deadline) 39 | { 40 | if (_cache.Count >= Capacity) 41 | { 42 | // remove the oldest item 43 | var oldest = _cache.MinBy(x => x.Value); 44 | _cache.Remove(oldest.Key); 45 | } 46 | 47 | _cache[key] = deadline; 48 | } 49 | 50 | public void Add(TKey key) 51 | { 52 | Add(key, Deadline.FromNow(TimeToLive)); 53 | } 54 | 55 | public int EvictExpired() 56 | { 57 | var expired = _cache.Where(x => x.Value.IsOverdue).ToList(); 58 | foreach (var kvp in expired) 59 | { 60 | _cache.Remove(kvp.Key); 61 | } 62 | 63 | return expired.Count; 64 | } 65 | 66 | public void Clear() 67 | { 68 | _cache.Clear(); 69 | } 70 | 71 | public void Remove(TKey key) 72 | { 73 | _cache.Remove(key); 74 | } 75 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Utility/TopicCacheManager.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | 9 | namespace TurboMqtt.Utility; 10 | 11 | /// 12 | /// Used for managing a cache of items associated with a specific topic. 13 | /// 14 | /// 15 | /// Used to help track packet ids for messages per-topic. 16 | /// 17 | /// The type of identifier we're caching - usually a 18 | internal sealed class TopicCacheManager where T : notnull 19 | { 20 | private readonly Dictionary> _topicCaches; 21 | private readonly int _defaultCapacity; 22 | private readonly TimeSpan _defaultExpiry; 23 | 24 | public TopicCacheManager(int defaultCapacity, TimeSpan defaultExpiry) 25 | { 26 | _topicCaches = new Dictionary>(); 27 | _defaultCapacity = defaultCapacity; 28 | _defaultExpiry = defaultExpiry; 29 | } 30 | 31 | public SimpleLruCache GetCacheForTopic(string topic) 32 | { 33 | if (!_topicCaches.TryGetValue(topic, out var cache)) 34 | { 35 | cache = new SimpleLruCache(_defaultCapacity); 36 | _topicCaches[topic] = cache; 37 | } 38 | return cache; 39 | } 40 | 41 | public void AddItem(string topic, T item) 42 | { 43 | SimpleLruCache cache = GetCacheForTopic(topic); 44 | cache.Add(item); 45 | } 46 | 47 | public bool ContainsItem(string topic, T item) 48 | { 49 | SimpleLruCache cache = GetCacheForTopic(topic); 50 | return cache.Contains(item); 51 | } 52 | 53 | public void RemoveItem(string topic, T item) 54 | { 55 | SimpleLruCache cache = GetCacheForTopic(topic); 56 | cache.Remove(item); 57 | } 58 | 59 | public void ClearCache(string topic) 60 | { 61 | if (_topicCaches.TryGetValue(topic, out var cache)) 62 | { 63 | cache.Clear(); 64 | } 65 | } 66 | 67 | public void RemoveCache(string topic) 68 | { 69 | _topicCaches.Remove(topic); 70 | } 71 | 72 | public int EvictExpiredItems(string topic) 73 | { 74 | if (_topicCaches.TryGetValue(topic, out var cache)) 75 | { 76 | return cache.EvictExpired(); 77 | } 78 | return 0; 79 | } 80 | 81 | public int EvictAllExpiredItems() 82 | { 83 | int totalEvicted = 0; 84 | foreach (var cache in _topicCaches.Values) 85 | { 86 | totalEvicted += cache.EvictExpired(); 87 | } 88 | return totalEvicted; 89 | } 90 | } -------------------------------------------------------------------------------- /src/TurboMqtt/Utility/UShortCounter.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Utility; 8 | 9 | public sealed class UShortCounter(ushort start = 0) 10 | { 11 | private int _current = start; 12 | 13 | public ushort GetNextValue() 14 | { 15 | while (true) 16 | { 17 | var original = _current; 18 | var incremented = original + 1; 19 | 20 | if (incremented > ushort.MaxValue) // Check if we exceed the ushort maximum value 21 | incremented = 1; // Reset to 1 if we exceed ushort.MaxValue 22 | 23 | // Atomically update the 'current' if it is still the 'original' value 24 | var old = Interlocked.CompareExchange(ref _current, incremented, original); 25 | if (old == original) 26 | return (ushort)incremented; // Successfully updated 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/EMQX/EmqxBuilder.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TestContainers.Emqx; 8 | 9 | /// 10 | [PublicAPI] 11 | public class EmqxBuilder: ContainerBuilder 12 | { 13 | public const string EmqxImage = "emqx/emqx:5.5.1"; 14 | 15 | public const ushort EmqxTcpPort = 1883; 16 | 17 | public const ushort EmqxSslPort = 8883; 18 | 19 | public const ushort EmqxWebSocketPort = 8083; 20 | 21 | public const ushort EmqxSecureWebSocketPort = 8084; 22 | 23 | public const ushort EmqxDashboardPort = 18083; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | public EmqxBuilder() 29 | : this(new EmqxConfiguration()) 30 | { 31 | DockerResourceConfiguration = Init().DockerResourceConfiguration; 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// The Docker resource configuration. 38 | private EmqxBuilder(EmqxConfiguration resourceConfiguration) 39 | : base(resourceConfiguration) 40 | { 41 | DockerResourceConfiguration = resourceConfiguration; 42 | } 43 | 44 | /// 45 | protected override EmqxConfiguration DockerResourceConfiguration { get; } 46 | 47 | /// 48 | public override EmqxContainer Build() 49 | { 50 | Validate(); 51 | return new EmqxContainer(DockerResourceConfiguration); 52 | } 53 | 54 | /// 55 | protected override EmqxBuilder Init() 56 | { 57 | return base.Init() 58 | .WithImage(EmqxImage) 59 | .WithPortBinding(EmqxTcpPort, true) 60 | .WithPortBinding(EmqxSslPort, true) 61 | .WithPortBinding(EmqxWebSocketPort, true) 62 | .WithPortBinding(EmqxSecureWebSocketPort, true) 63 | .WithPortBinding(EmqxDashboardPort, true) 64 | .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("EMQX .* is running now!")); 65 | } 66 | 67 | /// 68 | protected override EmqxBuilder Clone(IResourceConfiguration resourceConfiguration) 69 | { 70 | return Merge(DockerResourceConfiguration, new EmqxConfiguration(resourceConfiguration)); 71 | } 72 | 73 | /// 74 | protected override EmqxBuilder Clone(IContainerConfiguration resourceConfiguration) 75 | { 76 | return Merge(DockerResourceConfiguration, new EmqxConfiguration(resourceConfiguration)); 77 | } 78 | 79 | /// 80 | protected override EmqxBuilder Merge(EmqxConfiguration oldValue, EmqxConfiguration newValue) 81 | { 82 | return new EmqxBuilder(new EmqxConfiguration(oldValue, newValue)); 83 | } 84 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/EMQX/EmqxConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace TestContainers.Emqx; 2 | 3 | /// 4 | [PublicAPI] 5 | public class EmqxConfiguration : ContainerConfiguration 6 | { 7 | /// 8 | /// Initializes a new instance of the class. 9 | /// 10 | public EmqxConfiguration() 11 | { 12 | } 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The Docker resource configuration. 18 | public EmqxConfiguration(IResourceConfiguration resourceConfiguration) 19 | : base(resourceConfiguration) 20 | { 21 | // Passes the configuration upwards to the base implementations to create an updated immutable copy. 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The Docker resource configuration. 28 | public EmqxConfiguration(IContainerConfiguration resourceConfiguration) 29 | : base(resourceConfiguration) 30 | { 31 | // Passes the configuration upwards to the base implementations to create an updated immutable copy. 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// The Docker resource configuration. 38 | public EmqxConfiguration(EmqxConfiguration resourceConfiguration) 39 | : this(new EmqxConfiguration(), resourceConfiguration) 40 | { 41 | // Passes the configuration upwards to the base implementations to create an updated immutable copy. 42 | } 43 | 44 | /// 45 | /// Initializes a new instance of the class. 46 | /// 47 | /// The old Docker resource configuration. 48 | /// The new Docker resource configuration. 49 | public EmqxConfiguration(EmqxConfiguration oldValue, EmqxConfiguration newValue) 50 | : base(oldValue, newValue) 51 | { 52 | } 53 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/EMQX/EmqxContainer.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TestContainers.Emqx; 8 | 9 | /// 10 | [PublicAPI] 11 | public class EmqxContainer: DockerContainer 12 | { 13 | private readonly EmqxConfiguration _configuration; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The container configuration. 19 | public EmqxContainer(EmqxConfiguration configuration) 20 | : base(configuration) 21 | { 22 | _configuration = configuration; 23 | } 24 | 25 | public int BrokerTcpPort => GetMappedPublicPort(EmqxBuilder.EmqxTcpPort); 26 | 27 | public int BrokerSslPort => GetMappedPublicPort(EmqxBuilder.EmqxSslPort); 28 | 29 | public int BrokerWebSocketPort => GetMappedPublicPort(EmqxBuilder.EmqxWebSocketPort); 30 | 31 | public int BrokerSecureWebSocketPort => GetMappedPublicPort(EmqxBuilder.EmqxSecureWebSocketPort); 32 | 33 | public int BrokerDashboardPort => GetMappedPublicPort(EmqxBuilder.EmqxDashboardPort); 34 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/NanoMq/NanoMqBuilder.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TestContainers.NanoMq; 8 | 9 | /// 10 | [PublicAPI] 11 | public class NanoMqBuilder: ContainerBuilder 12 | { 13 | public const string NanoMqImage = "emqx/nanomq:0.21-slim"; 14 | 15 | public const ushort NanoMqTcpPort = 1883; 16 | 17 | public const ushort NanoMqWebSocketPort = 8883; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | public NanoMqBuilder() 23 | : this(new NanoMqConfiguration()) 24 | { 25 | DockerResourceConfiguration = Init().DockerResourceConfiguration; 26 | } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The Docker resource configuration. 32 | private NanoMqBuilder(NanoMqConfiguration resourceConfiguration) 33 | : base(resourceConfiguration) 34 | { 35 | DockerResourceConfiguration = resourceConfiguration; 36 | } 37 | 38 | /// 39 | protected override NanoMqConfiguration DockerResourceConfiguration { get; } 40 | 41 | /// 42 | public override NanoMqContainer Build() 43 | { 44 | Validate(); 45 | return new NanoMqContainer(DockerResourceConfiguration); 46 | } 47 | 48 | /// 49 | protected override NanoMqBuilder Init() 50 | { 51 | return base.Init() 52 | .WithImage(NanoMqImage) 53 | .WithPortBinding(NanoMqTcpPort, true) 54 | .WithPortBinding(NanoMqWebSocketPort, true) 55 | .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("NanoMQ Broker is started successfully!")); 56 | } 57 | 58 | /// 59 | protected override NanoMqBuilder Clone(IResourceConfiguration resourceConfiguration) 60 | { 61 | return Merge(DockerResourceConfiguration, new NanoMqConfiguration(resourceConfiguration)); 62 | } 63 | 64 | /// 65 | protected override NanoMqBuilder Clone(IContainerConfiguration resourceConfiguration) 66 | { 67 | return Merge(DockerResourceConfiguration, new NanoMqConfiguration(resourceConfiguration)); 68 | } 69 | 70 | /// 71 | protected override NanoMqBuilder Merge(NanoMqConfiguration oldValue, NanoMqConfiguration newValue) 72 | { 73 | return new NanoMqBuilder(new NanoMqConfiguration(oldValue, newValue)); 74 | } 75 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/NanoMq/NanoMqConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace TestContainers.NanoMq; 2 | 3 | /// 4 | [PublicAPI] 5 | public class NanoMqConfiguration : ContainerConfiguration 6 | { 7 | /// 8 | /// Initializes a new instance of the class. 9 | /// 10 | public NanoMqConfiguration() 11 | { 12 | } 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The Docker resource configuration. 18 | public NanoMqConfiguration(IResourceConfiguration resourceConfiguration) 19 | : base(resourceConfiguration) 20 | { 21 | // Passes the configuration upwards to the base implementations to create an updated immutable copy. 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The Docker resource configuration. 28 | public NanoMqConfiguration(IContainerConfiguration resourceConfiguration) 29 | : base(resourceConfiguration) 30 | { 31 | // Passes the configuration upwards to the base implementations to create an updated immutable copy. 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// The Docker resource configuration. 38 | public NanoMqConfiguration(NanoMqConfiguration resourceConfiguration) 39 | : this(new NanoMqConfiguration(), resourceConfiguration) 40 | { 41 | // Passes the configuration upwards to the base implementations to create an updated immutable copy. 42 | } 43 | 44 | /// 45 | /// Initializes a new instance of the class. 46 | /// 47 | /// The old Docker resource configuration. 48 | /// The new Docker resource configuration. 49 | public NanoMqConfiguration(NanoMqConfiguration oldValue, NanoMqConfiguration newValue) 50 | : base(oldValue, newValue) 51 | { 52 | } 53 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/NanoMq/NanoMqContainer.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TestContainers.NanoMq; 8 | 9 | /// 10 | [PublicAPI] 11 | public class NanoMqContainer: DockerContainer 12 | { 13 | private readonly NanoMqConfiguration _configuration; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The container configuration. 19 | public NanoMqContainer(NanoMqConfiguration configuration) 20 | : base(configuration) 21 | { 22 | _configuration = configuration; 23 | } 24 | 25 | public string DefaultUserName => "admin"; 26 | public string DefaultPassword => "public"; 27 | 28 | public int BrokerTcpPort => GetMappedPublicPort(NanoMqBuilder.NanoMqTcpPort); 29 | public int BrokerWebSocketPort => GetMappedPublicPort(NanoMqBuilder.NanoMqWebSocketPort); 30 | } -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/TestContainers.TurboMqtt.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;netstandard2.0;netstandard2.1 5 | latest 6 | enable 7 | enable 8 | TestContainers 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/TestContainers.TurboMqtt/Usings.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | global using System; 8 | global using Docker.DotNet.Models; 9 | global using DotNet.Testcontainers; 10 | global using DotNet.Testcontainers.Builders; 11 | global using DotNet.Testcontainers.Configurations; 12 | global using DotNet.Testcontainers.Containers; 13 | global using JetBrains.Annotations; -------------------------------------------------------------------------------- /tests/TurboMqtt.Container.Tests/EmqxFixture.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TestContainers.Emqx; 8 | 9 | namespace TurboMqtt.Container.Tests; 10 | 11 | [CollectionDefinition(nameof(EmqxCollection))] 12 | public class EmqxCollection : ICollectionFixture 13 | { 14 | // This class has no code, and is never created. Its purpose is simply 15 | // to be the place to apply [CollectionDefinition] and all the 16 | // ICollectionFixture<> interfaces. 17 | } 18 | 19 | public class EmqxFixture: IAsyncLifetime 20 | { 21 | public readonly EmqxContainer Container; 22 | 23 | public EmqxFixture() 24 | { 25 | Container = new EmqxBuilder() 26 | .WithEnvironment("EMQX_SESSION__UPGRADE_QOS", "true") 27 | .Build(); 28 | } 29 | 30 | public int MqttPort => Container.BrokerTcpPort; 31 | 32 | public async Task InitializeAsync() 33 | { 34 | await Container.StartAsync(); 35 | } 36 | 37 | public async Task DisposeAsync() 38 | { 39 | await Container.StopAsync(); 40 | await Container.DisposeAsync(); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Container.Tests/End2End/EmqxMqtt311End2EndSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Text; 8 | using Akka.Configuration; 9 | using Akka.TestKit.Xunit2; 10 | using FluentAssertions; 11 | using TurboMqtt.Client; 12 | using TurboMqtt.Protocol; 13 | using TurboMqtt.Tests.End2End; 14 | using Xunit.Abstractions; 15 | 16 | namespace TurboMqtt.Container.Tests.End2End; 17 | 18 | [Collection(nameof(EmqxCollection))] 19 | public class EmqxMqtt311End2EndSpecs: TransportSpecBase 20 | { 21 | public static readonly Config DebugLogging = 22 | """ 23 | akka.loglevel = DEBUG 24 | """; 25 | 26 | private readonly EmqxFixture _fixture; 27 | 28 | public EmqxMqtt311End2EndSpecs(ITestOutputHelper output, EmqxFixture fixture, Config? config = null) : base(output:output, config:config) 29 | { 30 | _fixture = fixture; 31 | } 32 | 33 | public override async Task CreateClient() 34 | => await ClientFactory.CreateTcpClient(DefaultConnectOptions, DefaultTcpOptions); 35 | 36 | private MqttClientTcpOptions DefaultTcpOptions => new("localhost", _fixture.MqttPort); 37 | 38 | public override MqttClientConnectOptions DefaultConnectOptions => 39 | new MqttClientConnectOptions("test-client", MqttProtocolVersion.V3_1_1) 40 | { 41 | UserName = "test", 42 | Password = "test", 43 | //PublishRetryInterval = TimeSpan.FromSeconds(1), 44 | KeepAliveSeconds = 60 // so it's not a relevant factor during testing 45 | }; 46 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Container.Tests/TurboMqtt.Container.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/End2End/InMemoryMqtt311End2EndSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.Client; 8 | using Xunit.Abstractions; 9 | 10 | namespace TurboMqtt.Tests.End2End; 11 | 12 | public class InMemoryMqtt311End2EndSpecs : TransportSpecBase 13 | { 14 | // enable debug logging 15 | public static readonly string Config = """ 16 | akka.loglevel = DEBUG 17 | """; 18 | 19 | 20 | public InMemoryMqtt311End2EndSpecs(ITestOutputHelper output) : base(output: output, config: Config) 21 | { 22 | 23 | } 24 | 25 | public override Task CreateClient() 26 | { 27 | var client = ClientFactory.CreateInMemoryClient(DefaultConnectOptions); 28 | return client; 29 | } 30 | 31 | // create a spec where we keep recreating the same client with the same clientId each time we disconnect - it should reconnect successfully 32 | [Fact] 33 | public async Task ShouldAutomaticallyReconnectandSubscribeAfterServerDisconnect() 34 | { 35 | await RunClientLifeCycle(); 36 | await RunClientLifeCycle(); 37 | await RunClientLifeCycle(); 38 | 39 | async Task RunClientLifeCycle() 40 | { 41 | var client = await ClientFactory.CreateInMemoryClient(DefaultConnectOptions); 42 | 43 | using var cts = new CancellationTokenSource(RemainingOrDefault); 44 | var connectResult = await client.ConnectAsync(cts.Token); 45 | connectResult.IsSuccess.Should().BeTrue(); 46 | 47 | // subscribe 48 | var subscribeResult = await client.SubscribeAsync(DefaultTopic, QualityOfService.AtMostOnce, cts.Token); 49 | subscribeResult.IsSuccess.Should().BeTrue(); 50 | 51 | // disconnect 52 | await client.DisconnectAsync(cts.Token); 53 | await client.WhenTerminated; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/End2End/TcpMqtt311HeartbeatFailureEnd2EndSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using Akka.Configuration; 8 | using Akka.Event; 9 | using Akka.TestKit.Xunit2; 10 | using TurboMqtt.Client; 11 | using TurboMqtt.IO; 12 | using TurboMqtt.IO.Tcp; 13 | using TurboMqtt.Protocol; 14 | using Xunit.Abstractions; 15 | 16 | namespace TurboMqtt.Tests.End2End; 17 | 18 | public class TcpMqtt311HeartbeatFailureEnd2EndSpecs : TestKit 19 | { 20 | public TcpMqtt311HeartbeatFailureEnd2EndSpecs(ITestOutputHelper output, Config? config = null) : base(output:output, config:config) 21 | { 22 | ClientFactory = new MqttClientFactory(Sys); 23 | var logger = new BusLogging(Sys.EventStream, "FakeMqttTcpServer", typeof(FakeMqttTcpServer), 24 | Sys.Settings.LogFormatter); 25 | 26 | _server = new FakeMqttTcpServer(new MqttTcpServerOptions("localhost", Port), MqttProtocolVersion.V3_1_1, logger, 27 | TimeSpan.FromMinutes(1), new DefaultFakeServerHandleFactory()); 28 | _server.Bind(); 29 | } 30 | 31 | private const string DefaultTopic = "topic"; 32 | private const int Port = 21887; 33 | private readonly FakeMqttTcpServer _server; 34 | 35 | public MqttClientFactory ClientFactory { get; } 36 | 37 | private MqttClientConnectOptions DefaultConnectOptions => 38 | new MqttClientConnectOptions("test-client", MqttProtocolVersion.V3_1_1) 39 | { 40 | UserName = "testuser", 41 | Password = "testpassword", 42 | KeepAliveSeconds = 1, // can't make it any lower than 1 second without disabling it, curse you type system 43 | MaxReconnectAttempts = 1, // allow 1 reconnection attempt 44 | PublishRetryInterval = TimeSpan.FromMilliseconds(250) 45 | }; 46 | 47 | public async Task CreateClient() 48 | { 49 | var client = await ClientFactory.CreateTcpClient(DefaultConnectOptions, DefaultTcpOptions); 50 | return client; 51 | } 52 | 53 | public MqttClientTcpOptions DefaultTcpOptions => new("localhost", Port); 54 | 55 | protected override void AfterAll() 56 | { 57 | // shut down our local TCP server 58 | _server.Shutdown(); 59 | base.AfterAll(); 60 | } 61 | 62 | [Fact] 63 | public async Task ShouldAutomaticallyReconnectandSubscribeAfterHeartbeatFailure() 64 | { 65 | var client = await ClientFactory.CreateTcpClient(DefaultConnectOptions, DefaultTcpOptions); 66 | 67 | // need a longer timeout for this test 68 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); 69 | var connectResult = await client.ConnectAsync(cts.Token); 70 | connectResult.IsSuccess.Should().BeTrue(); 71 | 72 | // subscribe 73 | var subResult = await client.SubscribeAsync(DefaultTopic, QualityOfService.AtLeastOnce, cts.Token); 74 | subResult.IsSuccess.Should().BeTrue(); 75 | 76 | await EventFilter.Error(contains:"No heartbeat received from broker in") 77 | .ExpectAsync(1, async () => 78 | { 79 | // wait for the server to disconnect us 80 | await Task.Delay(1500, cts.Token); 81 | }, cts.Token); 82 | 83 | // we should automatically reconnect and resubscribe - publish a message to verify 84 | var pubResult = await client.PublishAsync(new MqttMessage(DefaultTopic, "foo"){ QoS = QualityOfService.AtLeastOnce }, cts.Token); 85 | pubResult.IsSuccess.Should().BeTrue(); 86 | 87 | // now we should receive the message 88 | (await client.ReceivedMessages.WaitToReadAsync()).Should().BeTrue(); 89 | client.ReceivedMessages.TryRead(out var receivedMessage).Should().BeTrue(); 90 | receivedMessage!.Topic.Should().Be(DefaultTopic); 91 | } 92 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using FluentAssertions; -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/IO/FinalDisconnectPacketSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.IO; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.IO; 11 | 12 | public class FinalDisconnectPacketSpecs 13 | { 14 | [Fact] 15 | public void ShouldEncodeDisconnectPacket() 16 | { 17 | var (buf, size) = DisconnectToBinary.NormalDisconnectPacket.ToBinary(MqttProtocolVersion.V3_1_1); 18 | buf.Memory.Length.Should().Be(size); 19 | size.Should().BeGreaterThan(0); 20 | } 21 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/MqttClientIdValidatorTests.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Tests; 8 | 9 | public class MqttClientIdValidatorTests 10 | { 11 | [Theory] 12 | [InlineData("", true, "Client ID is valid or will be assigned by the server.")] 13 | [InlineData("validClientID123", true, "Client ID is valid.")] 14 | [InlineData("client-ID_with.multiple:validCharacters", true, "Client ID is valid.")] 15 | public void ValidateClientId_ValidCases(string clientId, bool expectedIsValid, string expectedMessage) 16 | { 17 | var result = MqttClientIdValidator.ValidateClientId(clientId); 18 | Assert.Equal(expectedIsValid, result.IsValid); 19 | Assert.Equal(expectedMessage, result.ErrorMessage); 20 | } 21 | 22 | [Theory] 23 | [InlineData("\u0001\u0002\u0003", false, "Client ID contains invalid characters.")] 24 | [InlineData("client\u007FID", false, "Client ID contains invalid characters.")] 25 | public void ValidateClientId_InvalidCases(string clientId, bool expectedIsValid, string expectedMessage) 26 | { 27 | var result = MqttClientIdValidator.ValidateClientId(clientId); 28 | Assert.Equal(expectedIsValid, result.IsValid); 29 | Assert.Equal(expectedMessage, result.ErrorMessage); 30 | } 31 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/MqttTopicValidatorSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | namespace TurboMqtt.Tests; 8 | 9 | public class MqttTopicValidatorSpecs 10 | { 11 | public class WhenSubscribing 12 | { 13 | public static readonly TheoryData FailureCases = new TheoryData( 14 | 15 | "foo/bar+", // invalid wildcard position 16 | "home/kit+chen/light", // invalid wildcard position 17 | "home/+/kitchen+", // invalid wildcard position 18 | "home+kitchen/light", // invalid wildcard position 19 | 20 | // generate some invalid uses of the '#' wildcard 21 | "foo/#/bar", // invalid wildcard position 22 | 23 | // generate some invalid uses of the '$' characterq 24 | "$foo/bar", // invalid use of '$' character 25 | 26 | // generate some invalid uses of the null character 27 | "foo\0bar" // invalid use of null character 28 | ); 29 | 30 | [Theory] 31 | [MemberData(nameof(FailureCases))] 32 | public void ShouldFailValidationForTopicSubscription(string topic) 33 | { 34 | var result = MqttTopicValidator.ValidateSubscribeTopic(topic); 35 | result.IsValid.Should().BeFalse(); 36 | } 37 | 38 | public static readonly TheoryData SuccessCases = new TheoryData( 39 | "home/kitchen/light", 40 | "home/kitchen/temperature", 41 | "home/kitchen/humidity", 42 | "home/+/pressure", 43 | "home/kitchen/pressure/#" 44 | ); 45 | 46 | [Theory] 47 | [MemberData(nameof(SuccessCases))] 48 | public void ShouldPassValidationForTopicSubscription(string topic) 49 | { 50 | var result = MqttTopicValidator.ValidateSubscribeTopic(topic); 51 | result.IsValid.Should().BeTrue(); 52 | } 53 | } 54 | 55 | public class WhenPublishing 56 | { 57 | public static readonly TheoryData FailureCases = new TheoryData( 58 | "foo/bar+", // invalid wildcard position 59 | "home/kit+chen/light", // invalid wildcard position 60 | "home/+/kitchen+", // invalid wildcard position 61 | "home+kitchen/light", // invalid wildcard position 62 | 63 | // generate some invalid uses of the '#' wildcard 64 | "foo/#/bar", // invalid wildcard position 65 | 66 | // generate some invalid uses of the '$' characterq 67 | "$foo/bar", // invalid use of '$' character 68 | 69 | // generate some invalid uses of the null character 70 | "foo\0bar" // invalid use of null character 71 | ); 72 | 73 | [Theory] 74 | [MemberData(nameof(FailureCases))] 75 | public void ShouldFailValidationForTopicPublishing(string topic) 76 | { 77 | var result = MqttTopicValidator.ValidatePublishTopic(topic); 78 | result.IsValid.Should().BeFalse(); 79 | } 80 | 81 | public static readonly TheoryData SuccessCases = new TheoryData( 82 | "home/kitchen/light", 83 | "home/kitchen/temperature", 84 | "home/kitchen/humidity", 85 | "home/pressure", 86 | "home/kitchen/pressure" 87 | ); 88 | 89 | [Theory] 90 | [MemberData(nameof(SuccessCases))] 91 | public void ShouldPassValidationForTopicPublishing(string topic) 92 | { 93 | var result = MqttTopicValidator.ValidatePublishTopic(topic); 94 | result.IsValid.Should().BeTrue(); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/NonWindowsTheoryAttribute.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Runtime.InteropServices; 8 | 9 | namespace TurboMqtt.Tests; 10 | 11 | /// 12 | /// 13 | /// This custom XUnit Fact attribute will skip unit tests if the run-time environment is windows 14 | /// 15 | /// 16 | /// Note that the original property takes precedence over this attribute, 17 | /// any unit tests with with its property 18 | /// set will always be skipped, regardless of the environment variable content. 19 | /// 20 | /// 21 | public class NonWindowsFactAttribute : FactAttribute 22 | { 23 | private string? _skip; 24 | 25 | /// 26 | /// Marks the test so that it will not be run, and gets or sets the skip reason 27 | /// 28 | public override string Skip 29 | { 30 | get 31 | { 32 | if (_skip != null) 33 | return _skip; 34 | 35 | var platform = Environment.OSVersion.Platform; 36 | var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 37 | return isWindows ? SkipUnix ?? "Skipped on Windows platforms" : string.Empty; 38 | } 39 | set => _skip = value; 40 | } 41 | 42 | /// 43 | /// The reason why this unit test is being skipped by the . 44 | /// Note that the original property takes precedence over this message. 45 | /// 46 | public string? SkipUnix { get; set; } 47 | } 48 | 49 | /// 50 | /// 51 | /// This custom XUnit Fact attribute will skip unit tests if the run-time environment is windows 52 | /// 53 | /// 54 | /// Note that the original property takes precedence over this attribute, 55 | /// any unit tests with with its property 56 | /// set will always be skipped, regardless of the environment variable content. 57 | /// 58 | /// 59 | public class NonWindowsTheoryAttribute : TheoryAttribute 60 | { 61 | private string? _skip; 62 | 63 | /// 64 | /// Marks the test so that it will not be run, and gets or sets the skip reason 65 | /// 66 | public override string Skip 67 | { 68 | get 69 | { 70 | if (_skip != null) 71 | return _skip; 72 | 73 | var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 74 | return isWindows ? SkipUnix ?? "Skipped on Windows platforms" : string.Empty; 75 | } 76 | set => _skip = value; 77 | } 78 | 79 | /// 80 | /// The reason why this unit test is being skipped by the . 81 | /// Note that the original property takes precedence over this message. 82 | /// 83 | public string? SkipUnix { get; set; } 84 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/NonZeroUintTests.cs: -------------------------------------------------------------------------------- 1 | namespace TurboMqtt.Tests; 2 | 3 | public class NonZeroUintTests 4 | { 5 | [Fact] 6 | public void ShouldFailOnZero() 7 | { 8 | Assert.Throws(() => new NonZeroUInt16(0)); 9 | } 10 | 11 | [Fact] 12 | public void ShouldSucceedOnNonZero() 13 | { 14 | var nonZero = new NonZeroUInt16(1); 15 | Assert.Equal(1u, nonZero.Value); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/ConnAck/ConnAckPacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.ConnAck; 11 | 12 | public class ConnAckPacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class MustWorkWithSingleMessage() 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData ConnAckPackets = new() 19 | { 20 | // happy path case 21 | new ConnAckPacket(){ SessionPresent = true, ReasonCode = ConnAckReasonCode.Success }, 22 | 23 | // sad path cases 24 | new ConnAckPacket(){ SessionPresent = false, ReasonCode = ConnAckReasonCode.QuotaExceeded }, 25 | new ConnAckPacket(){ SessionPresent = false, ReasonCode = ConnAckReasonCode.NotAuthorized }, 26 | new ConnAckPacket(){ SessionPresent = false, ReasonCode = ConnAckReasonCode.ServerUnavailable } 27 | }; 28 | 29 | [Theory] 30 | [MemberData(nameof(ConnAckPackets))] 31 | public void ShouldEncodeAndDecodeConnAckPacket(ConnAckPacket packet) 32 | { 33 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 34 | decodedPacket.Should().BeEquivalentTo(packet); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/ConnAck/ConnAckPacketSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.ConnAck; 11 | 12 | public class ConnAckPacketSpecs 13 | { 14 | public class WhenCreatingConnAckPacket 15 | { 16 | [Fact] 17 | public void ShouldHaveCorrectPacketType() 18 | { 19 | var packet = new ConnAckPacket(); 20 | packet.PacketType.Should().Be(MqttPacketType.ConnAck); 21 | } 22 | 23 | [Fact] 24 | public void ShouldHaveCorrectSessionPresent() 25 | { 26 | var packet = new ConnAckPacket(); 27 | packet.SessionPresent.Should().BeFalse(); 28 | } 29 | 30 | [Fact] 31 | public void ShouldHaveCorrectReasonCode() 32 | { 33 | var packet = new ConnAckPacket(); 34 | packet.ReasonCode.Should().Be(ConnAckReasonCode.Success); 35 | } 36 | 37 | [Fact] 38 | public void ShouldHaveCorrectProperties() 39 | { 40 | var packet = new ConnAckPacket(); 41 | packet.UserProperties.Should().BeNull(); 42 | } 43 | } 44 | 45 | // create size estimator specs for MQTT 3.1.1 ConnAck 46 | public class WhenEstimatingSizeInMqtt311 47 | { 48 | [Theory] 49 | [InlineData(true, ConnAckReasonCode.Success)] 50 | [InlineData(false, ConnAckReasonCode.QuotaExceeded)] // no need to test all cases 51 | public void ShouldEstimateSizeCorrectly(bool sessionCreated, ConnAckReasonCode reasonCode) 52 | { 53 | var packet = new ConnAckPacket() 54 | { 55 | SessionPresent = sessionCreated, 56 | ReasonCode = reasonCode 57 | }; 58 | MqttPacketSizeEstimator.EstimatePacketSize(packet, MqttProtocolVersion.V3_1_1).Should().Be(new PacketSize(2)); 59 | } 60 | } 61 | 62 | // create size estimator specs for MQTT 5.0 ConnAck 63 | public class WhenEstimatingSizeInMqtt5 64 | { 65 | [Theory] 66 | [InlineData(true, ConnAckReasonCode.Success)] 67 | [InlineData(false, ConnAckReasonCode.QuotaExceeded)] // no need to test all cases 68 | public void ShouldEstimateSizeCorrectly(bool sessionCreated, ConnAckReasonCode reasonCode) 69 | { 70 | var packet = new ConnAckPacket() 71 | { 72 | SessionPresent = sessionCreated, 73 | ReasonCode = reasonCode 74 | }; 75 | MqttPacketSizeEstimator.EstimatePacketSize(packet, MqttProtocolVersion.V5_0).Should().Be(new PacketSize(2)); 76 | } 77 | 78 | [Fact] 79 | public void ShouldEstimateSizeCorrectlyWithProperties() 80 | { 81 | var packet = new ConnAckPacket() 82 | { 83 | SessionPresent = true, 84 | ReasonCode = ConnAckReasonCode.Success, 85 | UserProperties = new Dictionary 86 | { 87 | { "key1", "value1" }, 88 | { "key2", "value2" } 89 | } 90 | }; 91 | MqttPacketSizeEstimator.EstimatePacketSize(packet, MqttProtocolVersion.V5_0).Should().Be(new PacketSize(32)); 92 | } 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/Disconnect/DisconnectPacketMqtt311End2EndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.Disconnect; 11 | 12 | public class DisconnectPacketMqtt311End2EndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | [Fact] 19 | public void ShouldEncodeAndDecodeCorrectly() 20 | { 21 | var packet = DisconnectPacket.Instance; 22 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 23 | decodedPacket.PacketType.Should().Be(packet.PacketType); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PacketEncodingTestHelper.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets; 11 | 12 | public static class PacketEncodingTestHelper 13 | { 14 | public static TPacket EncodeAndDecodeMqtt311Packet(TPacket packet,Mqtt311Decoder decoder) 15 | where TPacket : MqttPacket 16 | { 17 | var buffer = EncodePacketOnly(packet); 18 | 19 | var decoded = decoder.TryDecode(buffer, out var packets); 20 | decoded.Should().BeTrue(); 21 | packets.Count.Should().Be(1); 22 | 23 | return (TPacket)packets[0]; 24 | } 25 | 26 | public static Memory EncodePacketOnly(TPacket packet) where TPacket : MqttPacket 27 | { 28 | var estimatedSize = MqttPacketSizeEstimator.EstimateMqtt3PacketSize(packet); 29 | var buffer = new Memory(new byte[estimatedSize.TotalSize]); 30 | 31 | var actualBytesWritten = Mqtt311Encoder.EncodePacket(packet, ref buffer, estimatedSize); 32 | actualBytesWritten.Should().Be(estimatedSize.TotalSize); 33 | return buffer; 34 | } 35 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PingPackets/PingReqPacketMqtt311End2EndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.PingPackets; 11 | 12 | public class PingReqPacketMqtt311End2EndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | 19 | [Fact] 20 | public void ShouldEncodeAndDecodeCorrectly() 21 | { 22 | var packet = PingReqPacket.Instance; 23 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 24 | decodedPacket.PacketType.Should().Be(packet.PacketType); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PingPackets/PingRespMqtt311End2EndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.PingPackets; 11 | 12 | public class PingRespMqtt311End2EndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | 19 | [Fact] 20 | public void ShouldEncodeAndDecodeCorrectly() 21 | { 22 | var packet = PingRespPacket.Instance; 23 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 24 | decodedPacket.PacketType.Should().Be(packet.PacketType); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PubPackets/PubAckPacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.PubPackets; 11 | 12 | public sealed class PubAckPacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData PubAckPackets = new() 19 | { 20 | new PubAckPacket 21 | { 22 | PacketId = 1 23 | }, 24 | new PubAckPacket 25 | { 26 | PacketId = 2 27 | }, 28 | }; 29 | 30 | [Theory] 31 | [MemberData(nameof(PubAckPackets))] 32 | public void ShouldEncodeAndDecodeCorrectly(PubAckPacket packet) 33 | { 34 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 35 | decodedPacket.PacketId.Should().Be(packet.PacketId); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PubPackets/PubCompPacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.PubPackets; 11 | 12 | public class PubCompPacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData PubCompPackets = new() 19 | { 20 | new PubCompPacket 21 | { 22 | PacketId = 1 23 | }, 24 | new PubCompPacket 25 | { 26 | PacketId = 2 27 | }, 28 | }; 29 | 30 | [Theory] 31 | [MemberData(nameof(PubCompPackets))] 32 | public void ShouldEncodeAndDecodeCorrectly(PubCompPacket packet) 33 | { 34 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 35 | decodedPacket.PacketId.Should().Be(packet.PacketId); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PubPackets/PubRecPacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.PubPackets; 11 | 12 | public class PubRecPacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData PubRecPackets = new() 19 | { 20 | new PubRecPacket 21 | { 22 | PacketId = 1 23 | }, 24 | new PubRecPacket 25 | { 26 | PacketId = 2 27 | }, 28 | }; 29 | 30 | [Theory] 31 | [MemberData(nameof(PubRecPackets))] 32 | public void ShouldEncodeAndDecodeCorrectly(PubRecPacket packet) 33 | { 34 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 35 | decodedPacket.PacketId.Should().Be(packet.PacketId); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/PubPackets/PubRelPacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.PubPackets; 11 | 12 | public class PubRelPacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData PubRelPackets = new() 19 | { 20 | new PubRelPacket 21 | { 22 | PacketId = 1 23 | }, 24 | new PubRelPacket 25 | { 26 | PacketId = 2 27 | }, 28 | }; 29 | 30 | [Theory] 31 | [MemberData(nameof(PubRelPackets))] 32 | public void ShouldEncodeAndDecodeCorrectly(PubRelPacket packet) 33 | { 34 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 35 | decodedPacket.PacketId.Should().Be(packet.PacketId); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/SubscribePackets/SubAckPacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.SubscribePackets; 11 | 12 | public class SubAckPacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData SubAckPackets = new() 19 | { 20 | new SubAckPacket 21 | { 22 | PacketId = 1, 23 | ReasonCodes = new List 24 | { 25 | MqttSubscribeReasonCode.GrantedQoS0 26 | } 27 | }, 28 | new SubAckPacket 29 | { 30 | PacketId = 2, 31 | ReasonCodes = new List 32 | { 33 | MqttSubscribeReasonCode.GrantedQoS0, 34 | MqttSubscribeReasonCode.GrantedQoS1, 35 | } 36 | }, 37 | new SubAckPacket 38 | { 39 | PacketId = 3, 40 | ReasonCodes = new List 41 | { 42 | MqttSubscribeReasonCode.GrantedQoS0, 43 | MqttSubscribeReasonCode.GrantedQoS1, 44 | MqttSubscribeReasonCode.GrantedQoS2, 45 | } 46 | }, 47 | new SubAckPacket 48 | { 49 | PacketId = 4, 50 | ReasonCodes = new List 51 | { 52 | MqttSubscribeReasonCode.GrantedQoS1, 53 | MqttSubscribeReasonCode.GrantedQoS2, 54 | } 55 | }, 56 | new SubAckPacket 57 | { 58 | PacketId = 5, 59 | ReasonCodes = new List 60 | { 61 | MqttSubscribeReasonCode.UnspecifiedError 62 | } 63 | } 64 | }; 65 | 66 | [Theory] 67 | [MemberData(nameof(SubAckPackets))] 68 | public void ShouldEncodeAndDecodeCorrectly(SubAckPacket packet) 69 | { 70 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 71 | decodedPacket.PacketId.Should().Be(packet.PacketId); 72 | decodedPacket.ReasonCodes.Should().BeEquivalentTo(packet.ReasonCodes); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/SubscribePackets/SubscribePacketMqtt311EndToEndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.SubscribePackets; 11 | 12 | public class SubscribePacketMqtt311EndToEndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData SubscribePackets = new() 19 | { 20 | // simple case 21 | new SubscribePacket 22 | { 23 | PacketId = 1, 24 | Topics = new List 25 | { 26 | new TopicSubscription("topic1") 27 | { 28 | 29 | } 30 | } 31 | }, 32 | 33 | // subscription with QoS 34 | new SubscribePacket 35 | { 36 | PacketId = 2, 37 | Topics = new List 38 | { 39 | new TopicSubscription("topic1") 40 | { 41 | Options = new SubscriptionOptions() 42 | { 43 | // all other options aren't supported in MQTT 3.1.1 44 | QoS = QualityOfService.AtLeastOnce 45 | } 46 | } 47 | } 48 | }, 49 | 50 | // multiple subscriptions with different QoS 51 | new SubscribePacket 52 | { 53 | PacketId = 3, 54 | Topics = new List 55 | { 56 | new TopicSubscription("topic1") 57 | { 58 | Options = new SubscriptionOptions() 59 | { 60 | // all other options aren't supported in MQTT 3.1.1 61 | QoS = QualityOfService.AtLeastOnce 62 | } 63 | }, 64 | new TopicSubscription("topic2") 65 | { 66 | Options = new SubscriptionOptions() 67 | { 68 | // all other options aren't supported in MQTT 3.1.1 69 | QoS = QualityOfService.ExactlyOnce 70 | } 71 | } 72 | } 73 | }, 74 | }; 75 | 76 | [Theory] 77 | [MemberData(nameof(SubscribePackets))] 78 | public void ShouldEncodeAndDecodeCorrectly(SubscribePacket packet) 79 | { 80 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 81 | decodedPacket.PacketId.Should().Be(packet.PacketId); 82 | decodedPacket.Topics.Should().BeEquivalentTo(packet.Topics); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/UnsubscribePackets/UnsubAckPacketMqt311End2EndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.UnsubscribePackets; 11 | 12 | public class UnsubAckPacketMqt311End2EndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | [Fact] 19 | public void ShouldEncodeAndDecodeCorrectly() 20 | { 21 | var packet = new UnsubAckPacket() 22 | { 23 | PacketId = 1 24 | }; 25 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 26 | decodedPacket.PacketType.Should().Be(packet.PacketType); 27 | decodedPacket.PacketId.Should().Be(packet.PacketId); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Packets/UnsubscribePackets/UnsubscribePacketMqtt3111End2EndCodecSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.PacketTypes; 8 | using TurboMqtt.Protocol; 9 | 10 | namespace TurboMqtt.Tests.Packets.UnsubscribePackets; 11 | 12 | public class UnsubscribePacketMqtt3111End2EndCodecSpecs 13 | { 14 | public class SingleMessage 15 | { 16 | private readonly Mqtt311Decoder _decoder = new(); 17 | 18 | public static readonly TheoryData UnsubscribePackets = new() 19 | { 20 | new UnsubscribePacket 21 | { 22 | PacketId = 1, 23 | Topics = new List {"topic1"} 24 | }, 25 | new UnsubscribePacket 26 | { 27 | PacketId = 2, 28 | Topics = new List {"topic1", "topic2"} 29 | }, 30 | new UnsubscribePacket 31 | { 32 | PacketId = 3, 33 | Topics = new List {"topic1", "topic2", "topic3"} 34 | }, 35 | new UnsubscribePacket 36 | { 37 | PacketId = 4, 38 | Topics = new List {"topic1", "topic2", "topic3", "topic4"} 39 | }, 40 | new UnsubscribePacket 41 | { 42 | PacketId = 5, 43 | Topics = new List {"topic1", "topic2", "topic3", "topic4", "topic5"} 44 | }, 45 | }; 46 | 47 | [Theory] 48 | [MemberData(nameof(UnsubscribePackets))] 49 | public void ShouldEncodeAndDecodeCorrectly(UnsubscribePacket packet) 50 | { 51 | var decodedPacket = PacketEncodingTestHelper.EncodeAndDecodeMqtt311Packet(packet, _decoder); 52 | decodedPacket.PacketType.Should().Be(packet.PacketType); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Protocol/Mqtt311EncoderSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.Protocol; 8 | 9 | namespace TurboMqtt.Tests.Protocol; 10 | 11 | public class Mqtt311EncoderSpecs 12 | { 13 | [Theory] 14 | [InlineData(0, new byte[] { 0x00 })] 15 | [InlineData(1, new byte[] { 0x01 })] 16 | [InlineData(127, new byte[] { 0x7F })] 17 | [InlineData(128, new byte[] { 0x80, 0x01 })] 18 | [InlineData(1000, new byte[] { 232, 0x07 })] 19 | [InlineData(16384, new byte[] { 0x80, 0x80, 0x01 })] 20 | [InlineData(50000, new byte[] { 0xD0, 0x86, 0x03 })] 21 | [InlineData(2097152, new byte[] { 0x80, 0x80, 0x80, 0x01 })] 22 | [InlineData(10000000, new byte[] { 128, 173, 226, 4 })] 23 | public void ShouldEncodeFrameHeader(int length, byte[] expected) 24 | { 25 | var buffer = new Span(new byte[4]); 26 | var written = Mqtt311Encoder.EncodeFrameHeader(ref buffer, 0, length); 27 | buffer.Slice(0, written).ToArray().Should().BeEquivalentTo(expected); 28 | Assert.Equal(expected.Length, written); 29 | } 30 | 31 | public class SanityChecks() 32 | { 33 | private readonly Mqtt311Decoder _decoder = new(); 34 | 35 | [Theory] 36 | [InlineData(0u)] 37 | [InlineData(1u)] 38 | [InlineData(127u)] 39 | [InlineData(128u)] 40 | [InlineData(1000u)] 41 | [InlineData(16384u)] 42 | public void ShouldEncodeAndDecodeUnsignedShortsCorrectly(ushort value) 43 | { 44 | var bytes = new byte[2]; 45 | var buffer = new Memory(bytes); 46 | var span = buffer.Span; 47 | var written = Mqtt311Encoder.WriteUnsignedShort(ref span, value); 48 | Assert.Equal(2, written); 49 | 50 | var readonlyMem = new ReadOnlyMemory(bytes); 51 | var remainingLength = 2; 52 | var decoded = Mqtt311Decoder.DecodeUnsignedShort(ref readonlyMem, ref remainingLength); 53 | Assert.Equal(value, decoded); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Streams/MqttDecodingFlowSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using System.Collections.Immutable; 9 | using Akka.Streams.Dsl; 10 | using Akka.TestKit.Xunit2; 11 | using TurboMqtt.PacketTypes; 12 | using TurboMqtt.Protocol; 13 | using TurboMqtt.Streams; 14 | 15 | namespace TurboMqtt.Tests.Streams; 16 | 17 | public class MqttDecodingFlowSpecs : TestKit 18 | { 19 | [Fact] 20 | public async Task MqttDecodingFlow_should_decode_single_MqttPacket() 21 | { 22 | var connectPacket = new ConnectPacket(MqttProtocolVersion.V3_1_1) 23 | { 24 | ClientId = "test", ConnectFlags = new ConnectFlags { CleanSession = true }, ProtocolName = "MQTT" 25 | }; 26 | 27 | var encodingFlow = MqttEncodingFlows.Mqtt311Encoding(MemoryPool.Shared, 1024, 1024); 28 | var decodingFlow = MqttDecodingFlows.Mqtt311Decoding(); 29 | 30 | var processedPackets = await Source 31 | .Single(connectPacket) 32 | .Via(encodingFlow) 33 | .Via(decodingFlow) 34 | .Select(c => 1) 35 | .RunAggregate(0, (a, b) => a + 1, Sys); 36 | 37 | processedPackets.Should().Be(1); 38 | } 39 | 40 | [Fact] 41 | public async Task MqttDecodingFlow_should_decode_multiple_MqttPackets() 42 | { 43 | var connectPacket = new ConnectPacket(MqttProtocolVersion.V3_1_1) 44 | { 45 | ClientId = "test", ConnectFlags = new ConnectFlags { CleanSession = true }, ProtocolName = "MQTT" 46 | }; 47 | var connAckPacket = new ConnAckPacket() 48 | { 49 | SessionPresent = true, ReasonCode = ConnAckReasonCode.Success 50 | }; 51 | 52 | var publishPacket1 = new PublishPacket(QualityOfService.AtLeastOnce, false, false, "topic1") 53 | { 54 | PacketId = 1, 55 | Payload = new byte[] { 0x01, 0x02, 0x03 } 56 | }; 57 | 58 | var encodingFlow = MqttEncodingFlows.Mqtt311Encoding(MemoryPool.Shared, 1024, 1024); 59 | var decodingFlow = MqttDecodingFlows.Mqtt311Decoding(); 60 | 61 | var decodedPackets = await Source 62 | .From(new MqttPacket[] { connectPacket, connAckPacket, publishPacket1 }) 63 | .Via(encodingFlow) 64 | .Via(decodingFlow) 65 | .RunAggregate(ImmutableList.Empty, (a, b) => a.AddRange(b), Sys); 66 | 67 | decodedPackets.Count.Should().Be(3); 68 | } 69 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Streams/MqttEncodingFlowSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using System.Buffers; 8 | using Akka.Streams.Dsl; 9 | using Akka.TestKit.Xunit2; 10 | using TurboMqtt.PacketTypes; 11 | using TurboMqtt.Protocol; 12 | using TurboMqtt.Streams; 13 | 14 | namespace TurboMqtt.Tests.Streams; 15 | 16 | public class MqttEncodingFlowSpecs : TestKit 17 | { 18 | [Fact] 19 | public async Task MqttEncodingFlow_should_encode_single_MqttPacket() 20 | { 21 | var connectPacket = new ConnectPacket(MqttProtocolVersion.V3_1_1) 22 | { 23 | ClientId = "test", ConnectFlags = new ConnectFlags { CleanSession = true }, ProtocolName = "MQTT" 24 | }; 25 | var flow = MqttEncodingFlows.Mqtt311Encoding(MemoryPool.Shared, 1024, 1024); 26 | 27 | var bytes = await Source 28 | .Single(connectPacket) 29 | .Via(flow) 30 | .Select(c => 31 | { 32 | var byteCount = c.readableBytes; 33 | // return the memory back to the pool 34 | c.buffer.Dispose(); 35 | return byteCount; 36 | }) 37 | .RunAggregate(0, (a, b) => a + b, Sys); 38 | 39 | bytes.Should().BeGreaterThan(0); 40 | } 41 | 42 | [Fact] 43 | public async Task MqttEncodingFlow_should_encode_multiple_MqttPackets() 44 | { 45 | var connectPacket = new ConnectPacket(MqttProtocolVersion.V3_1_1) 46 | { 47 | ClientId = "test", ConnectFlags = new ConnectFlags { CleanSession = true }, ProtocolName = "MQTT" 48 | }; 49 | var connAckPacket = new ConnAckPacket() 50 | { 51 | SessionPresent = true, ReasonCode = ConnAckReasonCode.Success 52 | }; 53 | 54 | var publishPacket1 = new PublishPacket(QualityOfService.AtLeastOnce, false, false, "topic1") 55 | { 56 | PacketId = 1, 57 | Payload = new byte[] { 0x01, 0x02, 0x03 } 58 | }; 59 | 60 | var publishPacket2 = new PublishPacket(QualityOfService.AtLeastOnce, false, false, "topic2") 61 | { 62 | PacketId = 2, 63 | Payload = new byte[] { 0x04, 0x05, 0x06 } 64 | }; 65 | 66 | var packets = new List 67 | { 68 | connectPacket, 69 | connAckPacket, 70 | publishPacket1, 71 | publishPacket2 72 | }; 73 | 74 | // compute the total packet size 75 | var totalPayloadSize = packets.Select(MqttPacketSizeEstimator.EstimateMqtt3PacketSize) 76 | .Select(c => c.TotalSize).Sum(); 77 | 78 | // set a ridiculous frame size to force the packets to be split up 79 | var flow = MqttEncodingFlows.Mqtt311Encoding(MemoryPool.Shared, 1024, 1024); 80 | 81 | var bytes = await Source 82 | .From(packets) 83 | .Via(flow) 84 | .Select(c => 85 | { 86 | var byteCount = c.readableBytes; 87 | // return the memory back to the pool 88 | c.buffer.Dispose(); 89 | return byteCount; 90 | }) 91 | .RunAggregate(0, (a, b) => a + b, Sys); 92 | 93 | bytes.Should().Be(totalPayloadSize); 94 | } 95 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/TurboMqtt.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | true 9 | TurboMqtt.Tests 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Utility/SimpleLruCacheSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.Utility; 8 | 9 | namespace TurboMqtt.Tests.Utility; 10 | 11 | public class SimpleLruCacheSpecs 12 | { 13 | [Fact] 14 | public void SimpleLruCache_should_evict_oldest_item_when_capacity_reached() 15 | { 16 | var cache = new SimpleLruCache(capacity: 3); 17 | cache.Add(1, Deadline.Now); // should be removed 18 | cache.Add(2); 19 | cache.Add(3); 20 | cache.Add(4); 21 | 22 | cache.Count.Should().Be(3); 23 | cache.Contains(1).Should().BeFalse(); 24 | cache.Contains(2).Should().BeTrue(); 25 | cache.Contains(3).Should().BeTrue(); 26 | cache.Contains(4).Should().BeTrue(); 27 | } 28 | 29 | // add a spec to demonstrate that we can evict expired items 30 | [Fact] 31 | public void SimpleLruCache_should_evict_expired_items() 32 | { 33 | var cache = new SimpleLruCache(capacity: 10); 34 | cache.Add(1, Deadline.Now); 35 | cache.Add(2, Deadline.Now); 36 | cache.Add(3, Deadline.Now); 37 | 38 | var evicted = cache.EvictExpired(); 39 | 40 | evicted.Should().Be(3); 41 | cache.Count.Should().Be(0); 42 | } 43 | 44 | 45 | [Fact] 46 | public void SimpleLruCache_should_remove_item_when_key_removed() 47 | { 48 | var cache = new SimpleLruCache(capacity: 3); 49 | cache.Add(1); 50 | cache.Add(2); 51 | cache.Add(3); 52 | 53 | cache.Remove(2); 54 | 55 | cache.Count.Should().Be(2); 56 | cache.Contains(2).Should().BeFalse(); 57 | } 58 | 59 | [Fact] 60 | public void SimpleLruCache_should_clear_all_items() 61 | { 62 | var cache = new SimpleLruCache(capacity: 3); 63 | cache.Add(1); 64 | cache.Add(2); 65 | cache.Add(3); 66 | 67 | cache.Clear(); 68 | 69 | cache.Count.Should().Be(0); 70 | } 71 | } -------------------------------------------------------------------------------- /tests/TurboMqtt.Tests/Utility/UShortCounterSpecs.cs: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------- 2 | // 3 | // Copyright (C) 2024 - 2024 Petabridge, LLC 4 | // 5 | // ----------------------------------------------------------------------- 6 | 7 | using TurboMqtt.Utility; 8 | 9 | namespace TurboMqtt.Tests.Utility; 10 | 11 | public class UShortCounterSpecs 12 | { 13 | [Fact] 14 | public void UShortCounter_should_increment_by_one() 15 | { 16 | var counter = new UShortCounter(); 17 | var first = counter.GetNextValue(); 18 | var second = counter.GetNextValue(); 19 | var third = counter.GetNextValue(); 20 | 21 | first.Should().Be(1); 22 | second.Should().Be(2); 23 | third.Should().Be(3); 24 | } 25 | 26 | [Fact] 27 | public void UShortCounter_should_reset_to_one_when_exceeding_max_value() 28 | { 29 | var counter = new UShortCounter(ushort.MaxValue); 30 | var first = counter.GetNextValue(); 31 | var second = counter.GetNextValue(); 32 | var third = counter.GetNextValue(); 33 | 34 | first.Should().Be(1); 35 | second.Should().Be(2); 36 | third.Should().Be(3); 37 | } 38 | } --------------------------------------------------------------------------------