Skip to content

Commit

Permalink
Merge PR #20 from webwarrior-ws/websocket-to-grpc-squashed
Browse files Browse the repository at this point in the history
Replace Websockets with GRPC.
  • Loading branch information
knocte committed Sep 5, 2024
2 parents ce2cef9 + 75ecbfd commit c28a23d
Show file tree
Hide file tree
Showing 34 changed files with 600 additions and 415 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ jobs:
apt install --yes --no-install-recommends ca-certificates
apt install --yes --no-install-recommends dotnet8
# Trust ASP.NET Core HTTPS development certificate so that GRPC server can be contacted through HTTPS.
# HTTPS connection is used in end-to-end GRPC tests.
dotnet dev-certs https
sudo -E dotnet dev-certs https --export-path /usr/local/share/ca-certificates/aspnet/https.crt --format PEM
sudo update-ca-certificates
- name: Restore nuget dependencies
run: dotnet restore
Expand Down
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
6 changes: 5 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
<PackageVersion Include="Giraffe" Version="3.1.0" />
<PackageVersion Include="Giraffe.Razor" Version="1.3.0" />
<PackageVersion Include="Microsoft.AspNetCore.WebSockets" Version="2.1.1" />
<PackageVersion Include="System.Text.Json" Version="8.0.1" />
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="TaskBuilder.fs" Version="2.1.0" />
<PackageVersion Include="FSharp.Core" Version="8.0.101" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.40.0" />
<PackageVersion Include="Google.Protobuf" Version="3.18.0" />
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.40.0" />
<PackageVersion Include="Grpc.Tools" Version="2.40.0" />
</ItemGroup>
</Project>
39 changes: 29 additions & 10 deletions FX.sln
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FX.Tests", "src\FX.Tests\FX.Tests.csproj", "{984CE32C-454B-4FF1-B388-203DFD2CEAD8}"
# Visual Studio Version 17
VisualStudioVersion = 17.8.34511.84
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FX.Tests", "src\FX.Tests\FX.Tests.csproj", "{984CE32C-454B-4FF1-B388-203DFD2CEAD8}"
EndProject
Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "FX.Core", "src\FX.Core\FX.Core.fsproj", "{10A328B6-51E8-40DD-B6D8-361AFAEFABFA}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FX.Core", "src\FX.Core\FX.Core.fsproj", "{10A328B6-51E8-40DD-B6D8-361AFAEFABFA}"
EndProject
Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "FX.Console", "src\FX.Console\FX.Console.fsproj", "{E741A563-9A64-49B9-8506-DEEB03DB340C}"
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FX.Console", "src\FX.Console\FX.Console.fsproj", "{E741A563-9A64-49B9-8506-DEEB03DB340C}"
EndProject
Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "WebSocketApp", "src\WebSocketApp\WebSocketApp.fsproj", "{E41ECB40-3CD7-473B-9B73-6BDE00BD74B4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FX.GrpcService", "src\FX.GrpcService\FX.GrpcService.csproj", "{FB1AC151-A54A-4047-BAD6-424EC4CF3F28}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FX.GrpcClient", "src\FX.GrpcClient\FX.GrpcClient.csproj", "{578D4048-175B-41BC-8EC3-FC83FF137139}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FX.GrpcModels", "src\FX.GrpcModels\FX.GrpcModels.fsproj", "{11B2E30C-FCFE-41EB-A76D-CF9E95A844C4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -27,9 +32,23 @@ Global
{E741A563-9A64-49B9-8506-DEEB03DB340C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E741A563-9A64-49B9-8506-DEEB03DB340C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E741A563-9A64-49B9-8506-DEEB03DB340C}.Release|Any CPU.Build.0 = Release|Any CPU
{E41ECB40-3CD7-473B-9B73-6BDE00BD74B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E41ECB40-3CD7-473B-9B73-6BDE00BD74B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E41ECB40-3CD7-473B-9B73-6BDE00BD74B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E41ECB40-3CD7-473B-9B73-6BDE00BD74B4}.Release|Any CPU.Build.0 = Release|Any CPU
{FB1AC151-A54A-4047-BAD6-424EC4CF3F28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB1AC151-A54A-4047-BAD6-424EC4CF3F28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB1AC151-A54A-4047-BAD6-424EC4CF3F28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB1AC151-A54A-4047-BAD6-424EC4CF3F28}.Release|Any CPU.Build.0 = Release|Any CPU
{578D4048-175B-41BC-8EC3-FC83FF137139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{578D4048-175B-41BC-8EC3-FC83FF137139}.Debug|Any CPU.Build.0 = Debug|Any CPU
{578D4048-175B-41BC-8EC3-FC83FF137139}.Release|Any CPU.ActiveCfg = Release|Any CPU
{578D4048-175B-41BC-8EC3-FC83FF137139}.Release|Any CPU.Build.0 = Release|Any CPU
{11B2E30C-FCFE-41EB-A76D-CF9E95A844C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11B2E30C-FCFE-41EB-A76D-CF9E95A844C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11B2E30C-FCFE-41EB-A76D-CF9E95A844C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11B2E30C-FCFE-41EB-A76D-CF9E95A844C4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8FD72DED-1012-4D35-8BEF-548FC4396131}
EndGlobalSection
EndGlobal
31 changes: 30 additions & 1 deletion src/FX.Core/RedisStorageLayer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,39 @@ module Serialization =
override this.Write(writer, value, _options ) =
writer.WriteStringValue(value.ToString())

// code from https://gist.github.com/mbuhot/c224f15e0266adf5ba8ca4e882f88a75
// Converts Option<T> to/from JSON by projecting to null or T
type OptionValueConverter<'T>() =
inherit JsonConverter<Option<'T>>()

override this.Read (reader: byref<Utf8JsonReader>, _typ: Type, options: JsonSerializerOptions) =
match reader.TokenType with
| JsonTokenType.Null -> None
| _ -> Some <| JsonSerializer.Deserialize<'T>(&reader, options)

override this.Write (writer: Utf8JsonWriter, value: Option<'T>, options: JsonSerializerOptions) =
match value with
| None -> writer.WriteNullValue ()
| Some value -> JsonSerializer.Serialize(writer, value, options)

// Instantiates the correct OptionValueConverter<T>
type OptionConverter() =
inherit JsonConverterFactory()
override this.CanConvert(typ: Type) : bool =
typ.IsGenericType &&
typ.GetGenericTypeDefinition() = typedefof<Option<_>>

override this.CreateConverter(typeToConvert: Type,
_options: JsonSerializerOptions) : JsonConverter =
let typ = typeToConvert.GetGenericArguments() |> Array.head
let converterType = typedefof<OptionValueConverter<_>>.MakeGenericType(typ)
Activator.CreateInstance(converterType) :?> JsonConverter

let serializationOptions =
let options = JsonSerializerOptions()
options.Converters.Add(SideTypeConverter())
options.Converters.Add(CurrencyTypeConverter())
options.Converters.Add(CurrencyTypeConverter())
options.Converters.Add(OptionConverter())
options


Expand Down
29 changes: 29 additions & 0 deletions src/FX.GrpcClient/FX.GrpcClient.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- CS8981 is triggered by auto-generated protobuf types -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8981</WarningsNotAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Net.ClientFactory" />
<PackageReference Include="Grpc.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FX.GrpcModels\FX.GrpcModels.fsproj" />
</ItemGroup>

<ItemGroup>
<Protobuf Include="..\FX.GrpcService\Protos\fx.proto" GrpcServices="Client">
<Link>Protos\fx.proto</Link>
</Protobuf>
</ItemGroup>
</Project>
51 changes: 51 additions & 0 deletions src/FX.GrpcClient/Instance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;

using Grpc.Core;
using Grpc.Net.Client;

using GrpcService;

using GrpcModels;

namespace GrpcClient
{
public class Instance
{
private static string serverFqdn =
"localhost";

public static readonly int Port = 5178;
public static readonly int HttpsPort = 7178;

public FXGrpcService.FXGrpcServiceClient Connect()
{
var channel = GrpcChannel.ForAddress($"https://{serverFqdn}:{HttpsPort}");
var client = new FXGrpcService.FXGrpcServiceClient(channel);
return client;
}

public async Task<string> SendMessage(string message)
{
var client = Connect();
var reply = await client.GenericMethodAsync(
new GenericInputParam { MsgIn = message }
);
Console.WriteLine($"Got response: {reply.MsgOut}");
return reply.MsgOut;
}

public async Task<string> SendMessage<TMessage>(TMessage message)
{
var text = Marshaller.Serialize(message);
return await SendMessage(text);
}

public async Task<TResponse> SendMessage<TMessage, TResponse>(TMessage message)
{
var text = Marshaller.Serialize(message);
var responseText = await SendMessage(text);
return Marshaller.Deserialize<TResponse>(responseText);
}
}
}
22 changes: 22 additions & 0 deletions src/FX.GrpcModels/FX.GrpcModels.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<Compile Include="Models.fs" />
<Compile Include="ModelSerialization.fs" />
<Compile Include="Marshalling.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FX.Core\FX.Core.fsproj" />
</ItemGroup>

</Project>
74 changes: 74 additions & 0 deletions src/FX.GrpcModels/Marshalling.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace GrpcModels

open System
open System.Reflection
open System.Text.Json

open ModelSerialization

module VersionHelper =
let CURRENT_VERSION =
Assembly
.GetExecutingAssembly()
.GetName()
.Version.ToString()

type IMarshallingWrapper =
abstract member Value: obj

type MarshallingWrapper<'T> =
{
Version: string
TypeName: string
Value: 'T
}

static member New(value: 'T) =
{
Value = value
Version = VersionHelper.CURRENT_VERSION
TypeName = typeof<'T>.ToString()
}

interface IMarshallingWrapper with
member this.Value = this.Value :> obj

module Marshaller =

let ExtractMetadata(json: string) : Type * Version =
let wrapper = JsonSerializer.Deserialize<MarshallingWrapper<obj>>(json, serializationOptions)
let typ = Type.GetType wrapper.TypeName
let version = Version wrapper.Version
typ, version

let Serialize<'T>(object: 'T) : string =
let wrapper = MarshallingWrapper.New object
JsonSerializer.Serialize(wrapper, serializationOptions)

let Deserialize<'T>(json: string) : 'T =
if isNull json then
raise <| ArgumentNullException "json"

let wrapper = JsonSerializer.Deserialize<MarshallingWrapper<'T>>(json, serializationOptions)
wrapper.Value

let DeserializeAbstract (json: string) (targetType: Type) : obj =
if isNull json then
raise <| ArgumentNullException "json"

let wrapperGenericType = typedefof<MarshallingWrapper<_>>

let wrapperType =
wrapperGenericType.MakeGenericType(Array.singleton targetType)

let wrapperObj = JsonSerializer.Deserialize(json, wrapperType, serializationOptions)

if isNull wrapperObj then
failwith "Deserialization failed: result is null"
elif wrapperObj.GetType() <> wrapperType then
failwithf
"Deserialization failed, resulting type: %s"
(wrapperObj.GetType().ToString())

let wrapper = wrapperObj :?> IMarshallingWrapper
wrapper.Value
57 changes: 57 additions & 0 deletions src/FX.GrpcModels/ModelSerialization.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace GrpcModels

open System.Text.Json
open System.Text.Json.Serialization

open FsharpExchangeDotNetStandard

module ModelSerialization =
type MatchTypeConverter() =
inherit JsonConverter<Match>()

override this.Read(reader, _typeToConvert, _options) =
if reader.TokenType <> JsonTokenType.StartObject then
raise <| JsonException()

// "Type" key
reader.Read() |> ignore
if reader.TokenType <> JsonTokenType.PropertyName || reader.GetString() <> "Type" then
raise <| JsonException()
// "Type" value
reader.Read() |> ignore
match reader.GetString() with
| "Full" ->
reader.Read() |> ignore
if reader.TokenType <> JsonTokenType.EndObject then
raise <| JsonException()
Match.Full
| "Partial" ->
// "Amount" key
reader.Read() |> ignore
if reader.TokenType <> JsonTokenType.PropertyName || reader.GetString() <> "Amount" then
raise <| JsonException()
// "Amount" value
reader.Read() |> ignore
if reader.TokenType <> JsonTokenType.Number then
raise <| JsonException()
let amount = reader.GetDecimal()
reader.Read() |> ignore
if reader.TokenType <> JsonTokenType.EndObject then
raise <| JsonException()
Match.Partial amount
| typeName -> raise <| JsonException("Unknown Match type: " + typeName)

override this.Write(writer, value, _options ) =
writer.WriteStartObject()
match value with
| Full ->
writer.WriteString("Type", "Full")
| Partial amount ->
writer.WriteString("Type", "Partial")
writer.WriteNumber("Amount", amount)
writer.WriteEndObject()

let serializationOptions =
let options = JsonSerializerOptions(Redis.Serialization.serializationOptions)
options.Converters.Add(MatchTypeConverter())
options
22 changes: 22 additions & 0 deletions src/FX.GrpcModels/Models.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace GrpcModels

open System

type LimitOrder =
{
Price: decimal
Side: string
Quantity: decimal
}

type MarketOrder =
{
Side: string
Quantity: decimal
}

type CancelOrderRequest =
{
OrderId: Guid
// TODO: add Market
}
Loading

0 comments on commit c28a23d

Please sign in to comment.