Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Websockets with GRPC #20

Merged
merged 11 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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>
webwarrior-ws marked this conversation as resolved.
Show resolved Hide resolved
</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;
webwarrior-ws marked this conversation as resolved.
Show resolved Hide resolved
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
}
knocte marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading