From c3da18b675a60642a6e5ac2a45a23dfaaccfc7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 24 Dec 2024 17:27:46 +0800 Subject: [PATCH 01/29] feat(OpDict): add rollback operation and update comments --- src/DtmCommon/Constant.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/DtmCommon/Constant.cs b/src/DtmCommon/Constant.cs index 71961cb..e3523b5 100644 --- a/src/DtmCommon/Constant.cs +++ b/src/DtmCommon/Constant.cs @@ -67,8 +67,9 @@ public class Barrier public static readonly Dictionary OpDict = new Dictionary() { - { "cancel", "try" }, - { "compensate", "action" }, + { "cancel", "try" }, // tcc + { "compensate", "action" }, // saga + { "rollback", "action" }, // workflow }; public static readonly string REDIS_LUA_CheckAdjustAmount = @" -- RedisCheckAdjustAmount local v = redis.call('GET', KEYS[1]) From c7ba7198c294bd3b3798d6534c046ccb4521c799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 12 Mar 2025 17:50:13 +0800 Subject: [PATCH 02/29] feat(workflow): add support for HTTP branches interceptor in workflows --- src/Dtmcli/Constant.cs | 3 +- src/Dtmcli/DtmClient.cs | 4 +- src/Dtmcli/DtmImp/Utils.cs | 8 + src/Dtmcli/TransGlobal.cs | 6 +- .../ServiceCollectionExtensions.cs | 18 ++- src/Dtmworkflow/Workflow.Imp.cs | 12 +- src/Dtmworkflow/Workflow.cs | 10 +- src/Dtmworkflow/WorkflowHttpInterceptor.cs | 39 +++++ tests/BusiGrpcService/Program.cs | 11 ++ .../WorkflowGrpcTest.cs | 149 ++++++++++++++++-- 10 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 src/Dtmworkflow/WorkflowHttpInterceptor.cs diff --git a/src/Dtmcli/Constant.cs b/src/Dtmcli/Constant.cs index 3bc3959..a132433 100644 --- a/src/Dtmcli/Constant.cs +++ b/src/Dtmcli/Constant.cs @@ -4,7 +4,8 @@ internal static class Constant { internal const string DtmClientHttpName = "dtmClient"; internal const string BranchClientHttpName = "branchClient"; - + internal const string WorkflowBranchClientHttpName = "WF"; + internal static class Request { internal const string CONTENT_TYPE = "application/json"; diff --git a/src/Dtmcli/DtmClient.cs b/src/Dtmcli/DtmClient.cs index 7a811e3..246b69b 100644 --- a/src/Dtmcli/DtmClient.cs +++ b/src/Dtmcli/DtmClient.cs @@ -148,7 +148,7 @@ public async Task Query(string gid, CancellationToken cancellationT var client = _httpClientFactory.CreateClient(Constant.DtmClientHttpName); var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false); var dtmContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - DtmImp.Utils.CheckStatus(response.StatusCode, dtmContent); + DtmImp.Utils.CheckStatusCode(response.StatusCode); return JsonSerializer.Deserialize(dtmContent, _jsonOptions); } @@ -167,7 +167,7 @@ public async Task QueryStatus(string gid, CancellationToken cancellation var client = _httpClientFactory.CreateClient(Constant.DtmClientHttpName); var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false); var dtmContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - DtmImp.Utils.CheckStatus(response.StatusCode, dtmContent); + DtmImp.Utils.CheckStatusCode(response.StatusCode); var graph = JsonSerializer.Deserialize(dtmContent, _jsonOptions); return graph.Transaction == null ? string.Empty diff --git a/src/Dtmcli/DtmImp/Utils.cs b/src/Dtmcli/DtmImp/Utils.cs index 0b374e9..54f7f16 100644 --- a/src/Dtmcli/DtmImp/Utils.cs +++ b/src/Dtmcli/DtmImp/Utils.cs @@ -40,6 +40,14 @@ public static void CheckStatus(HttpStatusCode status, string dtmResult) } } + public static void CheckStatusCode(HttpStatusCode status) + { + if (status != HttpStatusCode.OK) + { + throw new DtmException(string.Format(CheckStatusMsgFormat, status.ToString(), string.Empty)); + } + } + /// /// OrString return the first not null or not empty string /// diff --git a/src/Dtmcli/TransGlobal.cs b/src/Dtmcli/TransGlobal.cs index 03f3002..4953604 100644 --- a/src/Dtmcli/TransGlobal.cs +++ b/src/Dtmcli/TransGlobal.cs @@ -28,7 +28,7 @@ public class DtmTransaction { [JsonPropertyName("id")] public int Id { get; set; } - [JsonPropertyName("create_time")] public DateTimeOffset CreateTime { get; set; } + [JsonPropertyName("create_time")] public DateTimeOffset? CreateTime { get; set; } [JsonPropertyName("update_time")] public DateTimeOffset UpdateTime { get; set; } @@ -64,9 +64,9 @@ public class DtmBranch { [JsonPropertyName("id")] public int Id { get; set; } - [JsonPropertyName("create_time")] public DateTimeOffset CreateTime { get; set; } + [JsonPropertyName("create_time")] public DateTimeOffset? CreateTime { get; set; } - [JsonPropertyName("update_time")] public DateTimeOffset UpdateTime { get; set; } + [JsonPropertyName("update_time")] public DateTimeOffset? UpdateTime { get; set; } [JsonPropertyName("gid")] public string Gid { get; set; } diff --git a/src/Dtmworkflow/ServiceCollectionExtensions.cs b/src/Dtmworkflow/ServiceCollectionExtensions.cs index 55d759b..7fc36a7 100644 --- a/src/Dtmworkflow/ServiceCollectionExtensions.cs +++ b/src/Dtmworkflow/ServiceCollectionExtensions.cs @@ -23,6 +23,8 @@ public static IServiceCollection AddDtmWorkflow(this IServiceCollection services services.TryAddSingleton(); services.TryAddSingleton(); + AddHttpClient(services); + return services; } @@ -33,8 +35,22 @@ public static IServiceCollection AddDtmWorkflow(this IServiceCollection services services.TryAddSingleton(); services.TryAddSingleton(); + + AddHttpClient(services); return services; } + + private static void AddHttpClient(IServiceCollection services /*, DtmOptions options*/) + { + services.AddHttpClient(Dtmcli.Constant.WorkflowBranchClientHttpName, client => + { + // TODO DtmOptions + // client.Timeout = TimeSpan.FromMilliseconds(options.BranchTimeout); + }).AddHttpMessageHandler(); + + // TODO how to inject workflow instance? + services.AddTransient(); + } } -} +} \ No newline at end of file diff --git a/src/Dtmworkflow/Workflow.Imp.cs b/src/Dtmworkflow/Workflow.Imp.cs index b1264eb..efdba49 100644 --- a/src/Dtmworkflow/Workflow.Imp.cs +++ b/src/Dtmworkflow/Workflow.Imp.cs @@ -64,7 +64,7 @@ internal async Task Process(WfFunc2 handler, byte[] data) } err = Utils.GrpcError2DtmError(err); - + if (err != null && err is not DtmCommon.DtmFailureException) throw err; try @@ -196,7 +196,7 @@ private StepResult StepResultFromGrpc(IMessage reply, Exception err) return sr; } - private HttpResponseMessage StepResultToHttp(StepResult r) + public HttpResponseMessage StepResultToHttp(StepResult r) { if (r.Error != null) { @@ -206,7 +206,7 @@ private HttpResponseMessage StepResultToHttp(StepResult r) return Utils.NewJSONResponse(HttpStatusCode.OK, r.Data); } - private StepResult StepResultFromHTTP(HttpResponseMessage resp, Exception err) + public StepResult StepResultFromHTTP(HttpResponseMessage resp, Exception err) { var sr = new StepResult { @@ -215,7 +215,7 @@ private StepResult StepResultFromHTTP(HttpResponseMessage resp, Exception err) if (err == null) { - // HTTPResp2DtmError + (sr.Data, sr.Error) = Utils.HTTPResp2DtmError(resp); // TODO go 使用了 this.Options.HTTPResp2DtmError(resp), 方便定制 sr.Status = WfErrorToStatus(sr.Error); } @@ -237,9 +237,9 @@ private string WfErrorToStatus(Exception err) } - private async Task RecordedDo(Func> fn) + public async Task RecordedDo(Func> fn) { - var sr = await this.RecordedDoInner(fn); + StepResult sr = await this.RecordedDoInner(fn); // do not compensate the failed branch if !CompensateErrorBranch if (this.Options.CompensateErrorBranch && sr.Status == DtmCommon.Constant.StatusFailed) diff --git a/src/Dtmworkflow/Workflow.cs b/src/Dtmworkflow/Workflow.cs index 930a02c..8753405 100644 --- a/src/Dtmworkflow/Workflow.cs +++ b/src/Dtmworkflow/Workflow.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Dtmworkflow { @@ -32,7 +34,13 @@ public Workflow(IDtmClient httpClient, IDtmgRPCClient grpcClient, Dtmcli.IBranch public System.Net.Http.HttpClient NewRequest() { - return _httpClient.GetHttpClient("WF"); + if(true) + return new HttpClient(new WorkflowHttpInterceptor(this)); + else + { + var client = _httpClient.GetHttpClient("WF"); + return client; + } } /// diff --git a/src/Dtmworkflow/WorkflowHttpInterceptor.cs b/src/Dtmworkflow/WorkflowHttpInterceptor.cs new file mode 100644 index 0000000..81110d3 --- /dev/null +++ b/src/Dtmworkflow/WorkflowHttpInterceptor.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Dtmworkflow; + +internal class WorkflowHttpInterceptor : DelegatingHandler +{ + private readonly Workflow _wf; + + public WorkflowHttpInterceptor(Workflow wf) + { + this._wf = wf; + InnerHandler = new HttpClientHandler(); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Func> origin = async (barrier) => + { + var response = await base.SendAsync(request, cancellationToken); + return _wf.StepResultFromHTTP(response, null); + }; + + StepResult sr; + // in phase 2, do not save, because it is saved outer + if (_wf.WorkflowImp.CurrentOp != DtmCommon.Constant.OpAction) + { + sr = await origin(null); + } + else + { + sr = await _wf.RecordedDo(origin); + } + + return _wf.StepResultToHttp(sr); + } +} \ No newline at end of file diff --git a/tests/BusiGrpcService/Program.cs b/tests/BusiGrpcService/Program.cs index be2cc0d..11aa92a 100644 --- a/tests/BusiGrpcService/Program.cs +++ b/tests/BusiGrpcService/Program.cs @@ -8,6 +8,8 @@ { // Setup a HTTP/2 endpoint without TLS. options.ListenLocalhost(5005, o => o.Protocols = HttpProtocols.Http2); + // test for workflow http branch + options.ListenLocalhost(5006, o => o.Protocols = HttpProtocols.Http1); }); builder.Services.AddGrpc(); @@ -20,6 +22,15 @@ // Configure the HTTP request pipeline. app.MapGrpcService(); + +// test for workflow http branch +app.MapGet("/test-http-ok1", () => "SUCCESS"); +app.MapGet("/test-http-ok2", () => "SUCCESS"); +app.MapGet("/409", context => +{ + context.Response.StatusCode = 409; + return context.Response.WriteAsync("i am body, the http branch is 409"); // FAILURE +}); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.Run(); diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 9d3735e..860c4f7 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -1,19 +1,33 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using busi; +using Dtmcli; +using DtmCommon; using Dtmworkflow; using Grpc.Net.Client; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MySqlConnector; using Newtonsoft.Json; using Xunit; +using Xunit.Abstractions; namespace Dtmgrpc.IntegrationTests { public class WorkflowGrpcTest { + private readonly ITestOutputHelper _testOutputHelper; + + public WorkflowGrpcTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public async Task Execute_Http_Should_Succeed() { @@ -42,7 +56,7 @@ public async Task Execute_gPRC_Should_Succeed() WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); string wfName1 = $"wf-simple-{Guid.NewGuid().ToString("D")[..8]}"; - workflowGlobalTransaction.Register(wfName1, async (workflow, data) => await Task.FromResult("my result"u8.ToArray())); + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => await Task.FromResult("fmy result"u8.ToArray())); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); @@ -53,7 +67,7 @@ public async Task Execute_gPRC_Should_Succeed() } [Fact] - public async Task Execute_Success() + public async Task Execute_DoAndHttpSuccess() { var provider = ITTestHelper.AddDtmGrpc(); var workflowFactory = provider.GetRequiredService(); @@ -66,33 +80,150 @@ public async Task Execute_Success() workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. local workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("1. local 回滚"); + + }).Do(async (barrier) => + { + return ("my result"u8.ToArray(), null); + }); + + // 2. http1 + HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("4. http1 回滚"); + }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); + + // 3. http2 + HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("4. http2 回滚"); + }).NewRequest().GetAsync("http://localhost:5006/test-http-ok2"); + + // 4. grpc1 + var wf = workflow.NewBranch().OnRollback(async (barrier) => { await busiClient.TransInRevertAsync(request); + _testOutputHelper.WriteLine("2. grpc1 回滚"); }); await busiClient.TransInAsync(request); - workflow.NewBranch().OnRollback(async (barrier) => + // 5. grpc2 + wf = workflow.NewBranch().OnRollback(async (barrier) => { await busiClient.TransOutRevertAsync(request); + _testOutputHelper.WriteLine("3. grpc2 回滚"); }); await busiClient.TransOutAsync(request); - + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); + + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); - string status = await ITTestHelper.GetTranStatus(gid); - Assert.Equal("succeed", status); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); // 1.Do 2.Http 3.Http + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("succeed", trans.Branches[2].Status); - // again + // same gid again result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); - status = await ITTestHelper.GetTranStatus(gid); - Assert.Equal("succeed", status); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); // 1.Do 2.Http 3.Http + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("succeed", trans.Branches[2].Status); + } + + + [Fact] + public async Task Execute_DoAndHttp_Failed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + Busi.BusiClient busiClient = new Busi.BusiClient(GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol)); + + string wfName1 = $"wf-simple-{Guid.NewGuid().ToString("D")[..8]}"; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. local + workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("1. local rollback"); + }).Do(async (barrier) => + { + return ("my result"u8.ToArray(), null); + }); + + // 2. http1 + HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("4. http1 rollback"); + }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); + + // 3. http2 + HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("4. http2 rollback"); + }).NewRequest().GetAsync("http://localhost:5006/409"); // 409 + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Null(result); + // same gid again + await Assert.ThrowsAsync( async () => + { + await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + + return; + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action succeed + // 03 action failed + // 02 rollback succeed + // 01 rollback succeed + Assert.Equal(5, trans.Branches.Count); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[2].Op); + Assert.Equal("failed", trans.Branches[2].Status); + Assert.Equal("rollback", trans.Branches[3].Op); + Assert.Equal("succeed", trans.Branches[3].Status); + Assert.Equal("rollback", trans.Branches[4].Op); + Assert.Equal("succeed", trans.Branches[4].Status); } } } \ No newline at end of file From f872c464cf698af8f739051500ae3ac6ff3cdbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 12 Mar 2025 18:09:03 +0800 Subject: [PATCH 03/29] ci: update DTM server UpdateBranchSync: 1 - use config.yml(UpdateBranchSync: 0 with bug, maybe~~) --- .github/workflows/build_and_it.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_and_it.yml b/.github/workflows/build_and_it.yml index d5fafaf..c7b3edd 100644 --- a/.github/workflows/build_and_it.yml +++ b/.github/workflows/build_and_it.yml @@ -51,7 +51,8 @@ jobs: tar -xvf dtm_1.18.0_linux_amd64.tar.gz pwd mkdir /home/runner/work/client-csharp/client-csharp/logs - nohup ./dtm > /home/runner/work/client-csharp/client-csharp/logs/dtm.log 2>&1 & + echo "UpdateBranchSync: 1" > ./config.yml + nohup ./dtm -c ./config.yml > /home/runner/work/client-csharp/client-csharp/logs/dtm.log 2>&1 & sleep 5 curl "127.0.0.1:36789/api/dtmsvr/newGid" - name: Setup Busi Service From b0f192dc7538b028034505f91b79e703145aa01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Thu, 13 Mar 2025 09:27:06 +0800 Subject: [PATCH 04/29] test: simplify workflow test --- .../WorkflowGrpcTest.cs | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 860c4f7..acc7183 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -84,8 +84,7 @@ public async Task Execute_DoAndHttpSuccess() // 1. local workflow.NewBranch().OnRollback(async (barrier) => { - _testOutputHelper.WriteLine("1. local 回滚"); - + _testOutputHelper.WriteLine("1. local rollback"); }).Do(async (barrier) => { return ("my result"u8.ToArray(), null); @@ -94,29 +93,15 @@ public async Task Execute_DoAndHttpSuccess() // 2. http1 HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => { - _testOutputHelper.WriteLine("4. http1 回滚"); + _testOutputHelper.WriteLine("4. http1 rollback"); }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); // 3. http2 HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => { - _testOutputHelper.WriteLine("4. http2 回滚"); + _testOutputHelper.WriteLine("4. http2 rollback"); }).NewRequest().GetAsync("http://localhost:5006/test-http-ok2"); - // 4. grpc1 - var wf = workflow.NewBranch().OnRollback(async (barrier) => - { - await busiClient.TransInRevertAsync(request); - _testOutputHelper.WriteLine("2. grpc1 回滚"); - }); - await busiClient.TransInAsync(request); - - // 5. grpc2 - wf = workflow.NewBranch().OnRollback(async (barrier) => - { - await busiClient.TransOutRevertAsync(request); - _testOutputHelper.WriteLine("3. grpc2 回滚"); - }); await busiClient.TransOutAsync(request); return await Task.FromResult("my result"u8.ToArray()); @@ -125,7 +110,6 @@ public async Task Execute_DoAndHttpSuccess() string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); - DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; @@ -150,7 +134,6 @@ public async Task Execute_DoAndHttpSuccess() Assert.Equal("succeed", trans.Branches[2].Status); } - [Fact] public async Task Execute_DoAndHttp_Failed() { @@ -203,8 +186,6 @@ public async Task Execute_DoAndHttp_Failed() { await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); - - return; trans = await dtmClient.Query(gid, CancellationToken.None); Assert.Equal("failed", trans.Transaction.Status); // BranchID Op Status CreateTime UpdateTime Url From dc5300f841680a1082bd2bee450638361b040f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Thu, 13 Mar 2025 11:05:24 +0800 Subject: [PATCH 05/29] test(Dtmgrpc.IntegrationTests): enhance workflow test to cover SAGA and TCC patterns --- .../WorkflowGrpcTest.cs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index acc7183..89b6736 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -67,7 +67,7 @@ public async Task Execute_gPRC_Should_Succeed() } [Fact] - public async Task Execute_DoAndHttpSuccess() + public async Task Execute_DoAndHttp_ShouldSuccess() { var provider = ITTestHelper.AddDtmGrpc(); var workflowFactory = provider.GetRequiredService(); @@ -90,17 +90,25 @@ public async Task Execute_DoAndHttpSuccess() return ("my result"u8.ToArray(), null); }); - // 2. http1 + // 2. http1, SAGA HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http1 rollback"); + await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); - // 3. http2 + // 3. http2, TCC HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => { - _testOutputHelper.WriteLine("4. http2 rollback"); - }).NewRequest().GetAsync("http://localhost:5006/test-http-ok2"); + _testOutputHelper.WriteLine("4. http2 cancel"); + + await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); + }).OnCommit(async (barrier) => + { + _testOutputHelper.WriteLine("4. http2 commit"); + // NOT must use workflow.NewRequest() + await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); + }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); await busiClient.TransOutAsync(request); @@ -113,25 +121,40 @@ public async Task Execute_DoAndHttpSuccess() DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; + // BranchID Op Status + // 01 action succeed + // 02 action succeed + // 03 action succeed + // 03 commit succeed // first byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); trans = await dtmClient.Query(gid, CancellationToken.None); Assert.Equal("succeed", trans.Transaction.Status); - Assert.Equal(3, trans.Branches.Count); // 1.Do 2.Http 3.Http + Assert.Equal(4, trans.Branches.Count); // 1.Do x1, 2.http, saga x1, 3.Http tcc x2 + Assert.Equal("action", trans.Branches[0].Op); Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[1].Op); Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[2].Op); Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("commit", trans.Branches[3].Op); + Assert.Equal("succeed", trans.Branches[3].Status); // same gid again result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); trans = await dtmClient.Query(gid, CancellationToken.None); Assert.Equal("succeed", trans.Transaction.Status); - Assert.Equal(3, trans.Branches.Count); // 1.Do 2.Http 3.Http + Assert.Equal(4, trans.Branches.Count); // 1.Do x1, 2.http, saga x1, 3.Http tcc x2 + Assert.Equal("action", trans.Branches[0].Op); Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[1].Op); Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[2].Op); Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("commit", trans.Branches[3].Op); + Assert.Equal("succeed", trans.Branches[3].Status); } [Fact] From 9e49ec2bf748b461368c64d30b0522cdf9558989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Thu, 13 Mar 2025 16:24:26 +0800 Subject: [PATCH 06/29] refactor(workflow): fix github action CI warnings --- src/Dtmworkflow/Workflow.Imp.cs | 6 +++--- src/Dtmworkflow/Workflow.cs | 9 ++------- tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs | 9 ++++++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Dtmworkflow/Workflow.Imp.cs b/src/Dtmworkflow/Workflow.Imp.cs index efdba49..650484f 100644 --- a/src/Dtmworkflow/Workflow.Imp.cs +++ b/src/Dtmworkflow/Workflow.Imp.cs @@ -196,7 +196,7 @@ private StepResult StepResultFromGrpc(IMessage reply, Exception err) return sr; } - public HttpResponseMessage StepResultToHttp(StepResult r) + internal HttpResponseMessage StepResultToHttp(StepResult r) { if (r.Error != null) { @@ -206,7 +206,7 @@ public HttpResponseMessage StepResultToHttp(StepResult r) return Utils.NewJSONResponse(HttpStatusCode.OK, r.Data); } - public StepResult StepResultFromHTTP(HttpResponseMessage resp, Exception err) + internal StepResult StepResultFromHTTP(HttpResponseMessage resp, Exception err) { var sr = new StepResult { @@ -237,7 +237,7 @@ private string WfErrorToStatus(Exception err) } - public async Task RecordedDo(Func> fn) + internal async Task RecordedDo(Func> fn) { StepResult sr = await this.RecordedDoInner(fn); diff --git a/src/Dtmworkflow/Workflow.cs b/src/Dtmworkflow/Workflow.cs index 8753405..21d86c7 100644 --- a/src/Dtmworkflow/Workflow.cs +++ b/src/Dtmworkflow/Workflow.cs @@ -34,13 +34,8 @@ public Workflow(IDtmClient httpClient, IDtmgRPCClient grpcClient, Dtmcli.IBranch public System.Net.Http.HttpClient NewRequest() { - if(true) - return new HttpClient(new WorkflowHttpInterceptor(this)); - else - { - var client = _httpClient.GetHttpClient("WF"); - return client; - } + // return _httpClient.GetHttpClient("WF"); + return new HttpClient(new WorkflowHttpInterceptor(this)); } /// diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 89b6736..5f3e1b7 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -85,9 +85,10 @@ public async Task Execute_DoAndHttp_ShouldSuccess() workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("1. local rollback"); + await Task.CompletedTask; }).Do(async (barrier) => { - return ("my result"u8.ToArray(), null); + return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); }); // 2. http1, SAGA @@ -101,7 +102,6 @@ public async Task Execute_DoAndHttp_ShouldSuccess() HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http2 cancel"); - await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); }).OnCommit(async (barrier) => { @@ -176,21 +176,24 @@ public async Task Execute_DoAndHttp_Failed() workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("1. local rollback"); + await Task.CompletedTask; }).Do(async (barrier) => { - return ("my result"u8.ToArray(), null); + return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); }); // 2. http1 HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http1 rollback"); + await Task.CompletedTask; }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); // 3. http2 HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http2 rollback"); + await Task.CompletedTask; }).NewRequest().GetAsync("http://localhost:5006/409"); // 409 return await Task.FromResult("my result"u8.ToArray()); From 07dc028118c7073fa7c00e2f43730cad4a5a4858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Thu, 13 Mar 2025 09:47:47 +0800 Subject: [PATCH 07/29] test(Dtmgrpc.IntegrationTests): add test for workflow grpc success --- .../WorkflowGrpcTest.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 5f3e1b7..f8f774f 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -232,5 +232,75 @@ public async Task Execute_DoAndHttp_Failed() Assert.Equal("rollback", trans.Branches[4].Op); Assert.Equal("succeed", trans.Branches[4].Status); } + + [Fact] + public async Task Execute_DoAndGrpc_Should_Success() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + Busi.BusiClient busiClient = new Busi.BusiClient(GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol)); + + string wfName1 = $"{nameof(this.Execute_DoAndGrpc_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. local + workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("1. local rollback"); + }).Do(async (barrier) => + { + return ("my result"u8.ToArray(), null); + }); + + // 2. grpc1 + var wf = workflow.NewBranch().OnRollback(async (barrier) => + { + await busiClient.TransInRevertAsync(request); + _testOutputHelper.WriteLine("2. grpc1 rollback"); + }); + await busiClient.TransInAsync(request); + + // 3. grpc2 + wf = workflow.NewBranch().OnRollback(async (barrier) => + { + await busiClient.TransOutRevertAsync(request); + _testOutputHelper.WriteLine("3. grpc2 rollback"); + }); + await busiClient.TransOutAsync(request); + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); // 1.Do 2.grpc 3.grpc + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("succeed", trans.Branches[2].Status); + + // same gid again + result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); // 1.Do 2.Http 3.Http + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("succeed", trans.Branches[2].Status); + } } } \ No newline at end of file From 5fd5cd280cee78486164f9362cf70b458085d894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Thu, 13 Mar 2025 16:06:41 +0800 Subject: [PATCH 08/29] =?UTF-8?q?Execute=5FDoAndGrpc=5FShould=5FSuccess?= =?UTF-8?q?=E5=8B=89=E5=BC=BA=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Dtmworkflow/Workflow.Imp.cs | 12 +- src/Dtmworkflow/WorkflowGrpcInterceptor.cs | 139 ++++++++++++++++++ .../WorkflowGrpcTest.cs | 18 ++- 3 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 src/Dtmworkflow/WorkflowGrpcInterceptor.cs diff --git a/src/Dtmworkflow/Workflow.Imp.cs b/src/Dtmworkflow/Workflow.Imp.cs index 650484f..41f9f5f 100644 --- a/src/Dtmworkflow/Workflow.Imp.cs +++ b/src/Dtmworkflow/Workflow.Imp.cs @@ -164,23 +164,23 @@ private StepResult StepResultFromLocal(byte[] data, Exception err) }; } - private Exception StepResultToGrpc(StepResult r, IMessage reply) + internal Exception StepResultToGrpc(StepResult r, IMessage reply) { if (r.Error == null && r.Status == DtmCommon.Constant.StatusSucceed) { - // Check - + // TODO Check + // dtmgimp.MustProtoUnmarshal(s.Data, reply.(protoreflect.ProtoMessage)); } return r.Error; } - private StepResult StepResultFromGrpc(IMessage reply, Exception err) + internal StepResult StepResultFromGrpc(IMessage reply, Exception err) { var sr = new StepResult { - // GRPCError2DtmError - Error = null, + // TODO GRPCError2DtmError + Error = Utils.GrpcError2DtmError(err), }; sr.Status = WfErrorToStatus(sr.Error); diff --git a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs new file mode 100644 index 0000000..db0401d --- /dev/null +++ b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs @@ -0,0 +1,139 @@ +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Microsoft.Extensions.Logging; + +namespace Dtmworkflow; + +public class WorkflowGrpcInterceptor : Interceptor +{ + private Workflow _wf; + private readonly ILogger _logger; + + /// + /// + /// + /// 没找到可行的方法把wf调用时传进来(如UserState等) + /// + public WorkflowGrpcInterceptor(Workflow wf, ILogger logger) + { + _wf = wf; + _logger = logger; + } + + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + _logger.LogDebug($"grpc client calling: {context.Host}{context.Method.FullName}"); //{dtmimp.MustMarshalString(request)} + + Workflow wf = _wf; + if (context.Options.Headers != null) + { + // 这里需要根据实际情况从 context 中获取 Workflow 对象 + // 假设 Workflow 对象是通过某种方式存储在 Metadata 中 + // 这里只是占位,实际使用时要替换为真实的逻辑 + + wf = (Workflow)(new object()); + } + + if (wf == null) + { + return base.AsyncUnaryCall(request, context, continuation); + } + + // async Task<(TResponse response, Status status, Metadata trailers, Func dispose)> Origin() + async Task Origin() + { + var newContext = Dtmgimp.TransInfo2Ctx(context, wf.TransBase.Gid, wf.TransBase.TransType, wf.WorkflowImp.CurrentBranch, wf.WorkflowImp.CurrentOp, wf.TransBase.Dtm); + TResponse response = await continuation(request, newContext); + // var res = + // $"grpc client called: {context.Host}{context.Method.FullName} {dtmimp.MustMarshalString(request)} result: {dtmimp.MustMarshalString(call.response)} err: {call.status.StatusCode}"; + // if (response.status.StatusCode != StatusCode.OK) + // { + // _logger.LogError(res); + // } + // else + // { + // _logger.LogDebug(res); + // } + + return response; + } + + if (wf.WorkflowImp.CurrentOp != DtmCommon.Constant.OpAction) + { + var response = Origin(); + return new AsyncUnaryCall(response, null, null, null, null); + } + + StepResult sr = wf.RecordedDo(bb => + { + Task task = Origin(); + task.Wait(); + // var err = task.Result.status.StatusCode != StatusCode.OK ? new RpcException(task.Result.status) : null; + // return wf.StepResultFromGrpc(task.Result.response, err); + + return Task.FromResult(new StepResult() + { + Error = null, + Data = "my result"u8.ToArray(), + Status = DtmCommon.Constant.StatusSucceed, + }); + }).GetAwaiter().GetResult(); + + var ex = wf.StepResultToGrpc(sr, null); + if (ex != null) + { + throw ex; + } + + return base.AsyncUnaryCall(request, context, continuation); + } +} + + +public class Dtmgimp +{ +// // TransInfo2Ctx add trans info to grpc context +// func TransInfo2Ctx(ctx context.Context, gid, transType, branchID, op, dtm string) context.Context { +// nctx := ctx +// if ctx == nil { +// nctx = context.Background() +// } +// return metadata.AppendToOutgoingContext( +// nctx, +// dtmpre+"gid", gid, +// dtmpre+"trans_type", transType, +// dtmpre+"branch_id", branchID, +// dtmpre+"op", op, +// dtmpre+"dtm", dtm, +// ) +// } + public static ClientInterceptorContext TransInfo2Ctx( + ClientInterceptorContext ctx, + string gid, + string transType, + string branchID, + string op, + string dtm) where TRequest : class where TResponse : class + { + // 创建一个新的元数据对象 + var headers = new Metadata(); + // 添加自定义元数据 + const string dtmpre = "dtm-"; + headers.Add(dtmpre + "gid", gid); + headers.Add(dtmpre + "trans_type", transType); + headers.Add(dtmpre + "branch_id", branchID); + headers.Add(dtmpre + "op", op); + headers.Add(dtmpre + "dtm", dtm); + // 修改上下文的元数据 + var nctx = new ClientInterceptorContext( + ctx.Method, + ctx.Host, + new CallOptions(headers: headers, deadline: ctx.Options.Deadline, cancellationToken: ctx.Options.CancellationToken)); + + return nctx; + } +} \ No newline at end of file diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index f8f774f..7c08dfd 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -9,6 +9,7 @@ using Dtmcli; using DtmCommon; using Dtmworkflow; +using Grpc.Core.Interceptors; using Grpc.Net.Client; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -241,8 +242,6 @@ public async Task Execute_DoAndGrpc_Should_Success() var loggerFactory = provider.GetRequiredService(); WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - Busi.BusiClient busiClient = new Busi.BusiClient(GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol)); - string wfName1 = $"{nameof(this.Execute_DoAndGrpc_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { @@ -256,13 +255,15 @@ public async Task Execute_DoAndGrpc_Should_Success() { return ("my result"u8.ToArray(), null); }); - + // 2. grpc1 + Busi.BusiClient busiClient = null; var wf = workflow.NewBranch().OnRollback(async (barrier) => { await busiClient.TransInRevertAsync(request); _testOutputHelper.WriteLine("2. grpc1 rollback"); }); + busiClient = GetBusiClientWithWf(wf, provider); await busiClient.TransInAsync(request); // 3. grpc2 @@ -302,5 +303,16 @@ public async Task Execute_DoAndGrpc_Should_Success() Assert.Equal("succeed", trans.Branches[1].Status); Assert.Equal("succeed", trans.Branches[2].Status); } + + private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) + { + var loggerFactory = provider.GetRequiredService(); + var channel = GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol); + var logger = loggerFactory.CreateLogger(); + var interceptor = new WorkflowGrpcInterceptor(wf, logger); // inject client interceptor, and workflow instance + var callInvoker = channel.Intercept(interceptor); + Busi.BusiClient busiClient = new Busi.BusiClient(callInvoker); + return busiClient; + } } } \ No newline at end of file From 16dd6ec82e56036dc4ebe4ad4fbe64c032c36cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Fri, 14 Mar 2025 14:46:34 +0800 Subject: [PATCH 09/29] feat: workflow interceptor --- src/Dtmworkflow/WorkflowGrpcInterceptor.cs | 162 +++++++----------- .../Services/BusiApiService.cs | 6 +- .../WorkflowGrpcTest.cs | 94 ++++++++-- 3 files changed, 150 insertions(+), 112 deletions(-) diff --git a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs index db0401d..c9e64b4 100644 --- a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs +++ b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs @@ -1,24 +1,16 @@ +using System; using System.Threading.Tasks; +using Google.Protobuf; using Grpc.Core; using Grpc.Core.Interceptors; using Microsoft.Extensions.Logging; namespace Dtmworkflow; -public class WorkflowGrpcInterceptor : Interceptor +public class WorkflowGrpcInterceptor(Workflow wf, ILogger logger) : Interceptor { - private Workflow _wf; - private readonly ILogger _logger; - - /// - /// - /// - /// 没找到可行的方法把wf调用时传进来(如UserState等) - /// - public WorkflowGrpcInterceptor(Workflow wf, ILogger logger) + public WorkflowGrpcInterceptor(Workflow wf) : this(wf, null) { - _wf = wf; - _logger = logger; } public override AsyncUnaryCall AsyncUnaryCall( @@ -26,114 +18,86 @@ public override AsyncUnaryCall AsyncUnaryCall( ClientInterceptorContext context, AsyncUnaryCallContinuation continuation) { - _logger.LogDebug($"grpc client calling: {context.Host}{context.Method.FullName}"); //{dtmimp.MustMarshalString(request)} - - Workflow wf = _wf; - if (context.Options.Headers != null) - { - // 这里需要根据实际情况从 context 中获取 Workflow 对象 - // 假设 Workflow 对象是通过某种方式存储在 Metadata 中 - // 这里只是占位,实际使用时要替换为真实的逻辑 - - wf = (Workflow)(new object()); - } + logger?.LogDebug($"grpc client calling: {context.Host}{context.Method.FullName}"); if (wf == null) { return base.AsyncUnaryCall(request, context, continuation); } - // async Task<(TResponse response, Status status, Metadata trailers, Func dispose)> Origin() - async Task Origin() + async Task<(AsyncUnaryCall, TResponse, Status)> Origin() { var newContext = Dtmgimp.TransInfo2Ctx(context, wf.TransBase.Gid, wf.TransBase.TransType, wf.WorkflowImp.CurrentBranch, wf.WorkflowImp.CurrentOp, wf.TransBase.Dtm); - TResponse response = await continuation(request, newContext); - // var res = - // $"grpc client called: {context.Host}{context.Method.FullName} {dtmimp.MustMarshalString(request)} result: {dtmimp.MustMarshalString(call.response)} err: {call.status.StatusCode}"; - // if (response.status.StatusCode != StatusCode.OK) - // { - // _logger.LogError(res); - // } - // else - // { - // _logger.LogDebug(res); - // } - return response; + var call = continuation(request, newContext); + TResponse response; + try + { + response = await call.ResponseAsync; + } + catch (Exception e) + { + logger?.LogDebug($"grpc client: {context.Host}{context.Method.FullName} ex: {e}"); + response = null; + } + + Status status = call.GetStatus(); + return ( + new AsyncUnaryCall( + call.ResponseAsync, + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose), + response, + status + ); } if (wf.WorkflowImp.CurrentOp != DtmCommon.Constant.OpAction) { - var response = Origin(); - return new AsyncUnaryCall(response, null, null, null, null); + var (newCall, _, _) = Origin().GetAwaiter().GetResult(); + return newCall; } + AsyncUnaryCall call = null; StepResult sr = wf.RecordedDo(bb => { - Task task = Origin(); - task.Wait(); - // var err = task.Result.status.StatusCode != StatusCode.OK ? new RpcException(task.Result.status) : null; - // return wf.StepResultFromGrpc(task.Result.response, err); - - return Task.FromResult(new StepResult() - { - Error = null, - Data = "my result"u8.ToArray(), - Status = DtmCommon.Constant.StatusSucceed, - }); + (call, TResponse data, Status status) = Origin().GetAwaiter().GetResult(); + RpcException err = status.StatusCode != StatusCode.OK ? new RpcException(status) : null; + return Task.FromResult(wf.StepResultFromGrpc(data as IMessage, err)); }).GetAwaiter().GetResult(); + wf.StepResultToGrpc(sr, null); - var ex = wf.StepResultToGrpc(sr, null); - if (ex != null) - { - throw ex; - } - - return base.AsyncUnaryCall(request, context, continuation); + return call; } -} - -public class Dtmgimp -{ -// // TransInfo2Ctx add trans info to grpc context -// func TransInfo2Ctx(ctx context.Context, gid, transType, branchID, op, dtm string) context.Context { -// nctx := ctx -// if ctx == nil { -// nctx = context.Background() -// } -// return metadata.AppendToOutgoingContext( -// nctx, -// dtmpre+"gid", gid, -// dtmpre+"trans_type", transType, -// dtmpre+"branch_id", branchID, -// dtmpre+"op", op, -// dtmpre+"dtm", dtm, -// ) -// } - public static ClientInterceptorContext TransInfo2Ctx( - ClientInterceptorContext ctx, - string gid, - string transType, - string branchID, - string op, - string dtm) where TRequest : class where TResponse : class + private class Dtmgimp { - // 创建一个新的元数据对象 - var headers = new Metadata(); - // 添加自定义元数据 - const string dtmpre = "dtm-"; - headers.Add(dtmpre + "gid", gid); - headers.Add(dtmpre + "trans_type", transType); - headers.Add(dtmpre + "branch_id", branchID); - headers.Add(dtmpre + "op", op); - headers.Add(dtmpre + "dtm", dtm); - // 修改上下文的元数据 - var nctx = new ClientInterceptorContext( - ctx.Method, - ctx.Host, - new CallOptions(headers: headers, deadline: ctx.Options.Deadline, cancellationToken: ctx.Options.CancellationToken)); - - return nctx; + public static ClientInterceptorContext TransInfo2Ctx( + ClientInterceptorContext ctx, + string gid, + string transType, + string branchID, + string op, + string dtm) where TRequest : class where TResponse : class + { + // 创建一个新的元数据对象 + var headers = new Metadata(); + // 添加自定义元数据 + const string dtmpre = "dtm-"; + headers.Add(dtmpre + "gid", gid); + headers.Add(dtmpre + "trans_type", transType); + headers.Add(dtmpre + "branch_id", branchID); + headers.Add(dtmpre + "op", op); + headers.Add(dtmpre + "dtm", dtm); + // 修改上下文的元数据 + var nctx = new ClientInterceptorContext( + ctx.Method, + ctx.Host, + new CallOptions(headers: headers, deadline: ctx.Options.Deadline, cancellationToken: ctx.Options.CancellationToken)); + + return nctx; + } } } \ No newline at end of file diff --git a/tests/BusiGrpcService/Services/BusiApiService.cs b/tests/BusiGrpcService/Services/BusiApiService.cs index a8f1370..66a8bd1 100644 --- a/tests/BusiGrpcService/Services/BusiApiService.cs +++ b/tests/BusiGrpcService/Services/BusiApiService.cs @@ -26,7 +26,8 @@ public BusiApiService(ILogger logger, Dtmgrpc.IDtmgRPCClient cli public override async Task TransIn(BusiReq request, ServerCallContext context) { - _logger.LogInformation("TransIn req={req}", JsonSerializer.Serialize(request)); + string gid = context.RequestHeaders.Get("dtm-gid")?.Value; + _logger.LogInformation("TransIn gid={gid} req={req}", gid, JsonSerializer.Serialize(request)); if (string.IsNullOrWhiteSpace(request.TransInResult) || request.TransInResult.Equals("SUCCESS")) { @@ -86,7 +87,8 @@ public override async Task TransInRevert(BusiReq request, ServerCallConte public override async Task TransOut(BusiReq request, ServerCallContext context) { - _logger.LogInformation("TransOut req={req}", JsonSerializer.Serialize(request)); + string gid = context.RequestHeaders.Get("dtm-gid")?.Value; + _logger.LogInformation("TransOut gid={gid} req={req}", gid, JsonSerializer.Serialize(request)); await Task.CompletedTask; return new Empty(); } diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 7c08dfd..5d82225 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -9,6 +9,7 @@ using Dtmcli; using DtmCommon; using Dtmworkflow; +using Google.Protobuf.WellKnownTypes; using Grpc.Core.Interceptors; using Grpc.Net.Client; using Microsoft.Extensions.Logging; @@ -75,8 +76,6 @@ public async Task Execute_DoAndHttp_ShouldSuccess() var loggerFactory = provider.GetRequiredService(); WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - Busi.BusiClient busiClient = new Busi.BusiClient(GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol)); - string wfName1 = $"wf-simple-{Guid.NewGuid().ToString("D")[..8]}"; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { @@ -111,8 +110,6 @@ public async Task Execute_DoAndHttp_ShouldSuccess() await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); - await busiClient.TransOutAsync(request); - return await Task.FromResult("my result"u8.ToArray()); }); @@ -165,14 +162,10 @@ public async Task Execute_DoAndHttp_Failed() var workflowFactory = provider.GetRequiredService(); var loggerFactory = provider.GetRequiredService(); WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - - Busi.BusiClient busiClient = new Busi.BusiClient(GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol)); - + string wfName1 = $"wf-simple-{Guid.NewGuid().ToString("D")[..8]}"; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { - BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - // 1. local workflow.NewBranch().OnRollback(async (barrier) => { @@ -264,7 +257,7 @@ public async Task Execute_DoAndGrpc_Should_Success() _testOutputHelper.WriteLine("2. grpc1 rollback"); }); busiClient = GetBusiClientWithWf(wf, provider); - await busiClient.TransInAsync(request); + await busiClient.TransOutAsync(request); // 3. grpc2 wf = workflow.NewBranch().OnRollback(async (barrier) => @@ -272,7 +265,7 @@ public async Task Execute_DoAndGrpc_Should_Success() await busiClient.TransOutRevertAsync(request); _testOutputHelper.WriteLine("3. grpc2 rollback"); }); - await busiClient.TransOutAsync(request); + await busiClient.TransInAsync(request); return await Task.FromResult("my result"u8.ToArray()); }); @@ -303,6 +296,85 @@ public async Task Execute_DoAndGrpc_Should_Success() Assert.Equal("succeed", trans.Branches[1].Status); Assert.Equal("succeed", trans.Branches[2].Status); } + + [Fact] + public async Task Execute_DoAndGrpc_Should_Failed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_DoAndGrpc_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. local + workflow.NewBranch().OnRollback(async (barrier) => + { + _testOutputHelper.WriteLine("1. local rollback"); + }).Do(async (barrier) => + { + return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); + }); + + // 2. grpc1 + Busi.BusiClient busiClient = null; + var wf = workflow.NewBranch().OnRollback(async (barrier) => + { + await busiClient.TransInRevertAsync(request); + _testOutputHelper.WriteLine("2. grpc1 rollback"); + }); + busiClient = GetBusiClientWithWf(wf, provider); + Empty response1 = await busiClient.TransOutAsync(request); + + // 3. grpc2 + wf = workflow.NewBranch().OnRollback(async (barrier) => + { + await busiClient.TransOutRevertAsync(request); + _testOutputHelper.WriteLine("3. grpc2 rollback"); + }); + Empty response2 = await busiClient.TransInAsync(request); + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(outFailed: false, inFailed: true); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // Assert.Null(result); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action succeed + // 03 action failed + // 02 rollback succeed + // 01 rollback succeed + Assert.Equal(5, trans.Branches.Count); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[2].Op); + Assert.Equal("failed", trans.Branches[2].Status); + Assert.Equal("rollback", trans.Branches[3].Op); + Assert.Equal("succeed", trans.Branches[3].Status); + Assert.Equal("rollback", trans.Branches[4].Op); + Assert.Equal("succeed", trans.Branches[4].Status); + + // same gid again + await Assert.ThrowsAsync( async () => + { + result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + } private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) { From 50e9f430e56b23ddeb9b0841bcce5c8f506a5694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Thu, 17 Apr 2025 14:48:52 +0800 Subject: [PATCH 10/29] test(dtm): Add tests for Do and TCC workflow success and failure --- .../Services/BusiApiService.cs | 20 +- .../WorkflowGrpcTest.cs | 253 +++++++++++++++++- 2 files changed, 266 insertions(+), 7 deletions(-) diff --git a/tests/BusiGrpcService/Services/BusiApiService.cs b/tests/BusiGrpcService/Services/BusiApiService.cs index 66a8bd1..7c41bc0 100644 --- a/tests/BusiGrpcService/Services/BusiApiService.cs +++ b/tests/BusiGrpcService/Services/BusiApiService.cs @@ -95,9 +95,23 @@ public override async Task TransOut(BusiReq request, ServerCallContext co public override async Task TransOutTcc(BusiReq request, ServerCallContext context) { - _logger.LogInformation("TransOut req={req}", JsonSerializer.Serialize(request)); - await Task.CompletedTask; - return new Empty(); + _logger.LogInformation("TransOutTry req={req}", JsonSerializer.Serialize(request)); + + if (string.IsNullOrWhiteSpace(request.TransOutResult) || request.TransOutResult.Equals("SUCCESS")) + { + await Task.CompletedTask; + return new Empty(); + } + else if (request.TransOutResult.Equals("FAILURE")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); + } + else if (request.TransOutResult.Equals("ONGOING")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); + } + + throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.TransOutResult}")); } public override async Task TransOutConfirm(BusiReq request, ServerCallContext context) diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 5d82225..fa87e5e 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -228,14 +228,14 @@ public async Task Execute_DoAndHttp_Failed() } [Fact] - public async Task Execute_DoAndGrpc_Should_Success() + public async Task Execute_DoAndGrpcSAGA_Should_Success() { var provider = ITTestHelper.AddDtmGrpc(); var workflowFactory = provider.GetRequiredService(); var loggerFactory = provider.GetRequiredService(); WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - string wfName1 = $"{nameof(this.Execute_DoAndGrpc_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + string wfName1 = $"{nameof(this.Execute_DoAndGrpcSAGA_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); @@ -298,14 +298,14 @@ public async Task Execute_DoAndGrpc_Should_Success() } [Fact] - public async Task Execute_DoAndGrpc_Should_Failed() + public async Task Execute_DoAndGrpcSAGA_Should_Failed() { var provider = ITTestHelper.AddDtmGrpc(); var workflowFactory = provider.GetRequiredService(); var loggerFactory = provider.GetRequiredService(); WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - string wfName1 = $"{nameof(this.Execute_DoAndGrpc_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + string wfName1 = $"{nameof(this.Execute_DoAndGrpcSAGA_Should_Failed)}-{Guid.NewGuid().ToString("D")[..8]}"; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); @@ -375,7 +375,252 @@ public async Task Execute_DoAndGrpc_Should_Failed() result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); } + + + [Fact] + public async Task Execute_GrpcTccAndDo_Should_Success() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_GrpcTccAndDo_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + Busi.BusiClient busiClient = null; + Workflow wf = workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await busiClient.TransOutConfirmAsync(request); + }) + .OnRollback(async (barrier) => // cancel + { + await busiClient.TransOutRevertAsync(request); + _testOutputHelper.WriteLine("1. grpc1 cancel"); + }); + busiClient = GetBusiClientWithWf(wf, provider); // busiClient的构建依赖Workflow实例,只能这么写 + // try + await busiClient.TransOutTccAsync(request); + + // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + workflow.NewBranch() + // .OnRollback(async (barrier) => // 反向 rollback + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + return ("my result"u8.ToArray(), null); + }); // 正向 + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + trans = await dtmClient.Query(gid, CancellationToken.None); + // BranchID Op Status + // 01 action succeed + // 02 action succeed + // 01 commit succeed + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("commit", trans.Branches[2].Op); + + // same gid again + result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("commit", trans.Branches[2].Op); + } + + [Fact] + public async Task Execute_GrpcTccAndDo_Should_TryFailed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_GrpcTccAndDo_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + Busi.BusiClient busiClient = null; + Workflow wf = workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await busiClient.TransOutConfirmAsync(request); + }) + .OnRollback(async (barrier) => // cancel + { + await busiClient.TransOutRevertAsync(request); + _testOutputHelper.WriteLine("1. grpc1 cancel"); + }); + busiClient = GetBusiClientWithWf(wf, provider); // busiClient reference Workflow instance + // try + await busiClient.TransOutTccAsync(request); + + // 2. local, it's the tail, rollback is NOT necessary + workflow.NewBranch() + // .OnRollback(async (barrier) => // rollback + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + return ("my result"u8.ToArray(), null); + }); + + return await Task.FromResult("my result"u8.ToArray()); + }); + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(outFailed: true, inFailed: false); // 1. trans out try failed + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Null(result); + trans = await dtmClient.Query(gid, CancellationToken.None); + // BranchID Op Status + // 01 action failed + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("failed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + + // same gid again + Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // DtmCommon.DtmFailureException: Status(StatusCode="Aborted", Detail="FAILURE") + // + // DtmCommon.DtmFailureException + // Status(StatusCode="Aborted", Detail="FAILURE") + // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + }); + + trans = await dtmClient.Query(gid, CancellationToken.None); + // BranchID Op Status + // 01 action failed + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("failed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + } + + + [Fact] + public async Task Execute_GrpcTccAndDo_Should_DoFailed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_GrpcTccAndDo_Should_Success)}-{Guid.NewGuid().ToString("D")[..8]}"; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + Busi.BusiClient busiClient = null; + Workflow wf = workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await busiClient.TransOutConfirmAsync(request); + }) + .OnRollback(async (barrier) => // cancel + { + await busiClient.TransOutRevertAsync(request); + _testOutputHelper.WriteLine("1. grpc1 cancel"); + }); + busiClient = GetBusiClientWithWf(wf, provider); // busiClient reference Workflow instance + // try + await busiClient.TransOutTccAsync(request); + + // 2. local, it's the tail, rollback is NOT necessary + (byte[] doResult, Exception ex) = await workflow.NewBranch() + .OnRollback(async (barrier) => // rollback + { + _testOutputHelper.WriteLine("1. local rollback"); + }) + .Do(async (barrier) => + { + // throw new DtmFailureException("db do failed"); // can't throw + var ex = new DtmFailureException("db do failed"); + return ("my result"u8.ToArray(), ex); + }); + if (ex != null) + throw ex; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(outFailed: false, inFailed: false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Null(result); + trans = await dtmClient.Query(gid, CancellationToken.None); + // BranchID Op Status + // 01 action succeed + // 02 action failed + // 01 rollback succeed + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("failed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("rollback", trans.Branches[2].Op); + + // same gid again + Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // DtmCommon.DtmFailureException + // db do failed + // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs + }); + } + private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) { var loggerFactory = provider.GetRequiredService(); From db8efabcd93da56b5aa3f60a52abcce6a50263bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 21 Apr 2025 13:47:48 +0800 Subject: [PATCH 11/29] refactor: rollback AddHttpClient method in ServiceCollectionExtensions - The unfinished version has an impact on DtmSample. --- .../ServiceCollectionExtensions.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Dtmworkflow/ServiceCollectionExtensions.cs b/src/Dtmworkflow/ServiceCollectionExtensions.cs index 7fc36a7..c440ff0 100644 --- a/src/Dtmworkflow/ServiceCollectionExtensions.cs +++ b/src/Dtmworkflow/ServiceCollectionExtensions.cs @@ -23,7 +23,7 @@ public static IServiceCollection AddDtmWorkflow(this IServiceCollection services services.TryAddSingleton(); services.TryAddSingleton(); - AddHttpClient(services); + // AddHttpClient(services); return services; } @@ -36,21 +36,21 @@ public static IServiceCollection AddDtmWorkflow(this IServiceCollection services services.TryAddSingleton(); services.TryAddSingleton(); - AddHttpClient(services); + // AddHttpClient(services); return services; } - private static void AddHttpClient(IServiceCollection services /*, DtmOptions options*/) - { - services.AddHttpClient(Dtmcli.Constant.WorkflowBranchClientHttpName, client => - { - // TODO DtmOptions - // client.Timeout = TimeSpan.FromMilliseconds(options.BranchTimeout); - }).AddHttpMessageHandler(); - - // TODO how to inject workflow instance? - services.AddTransient(); - } + // private static void AddHttpClient(IServiceCollection services /*, DtmOptions options*/) + // { + // services.AddHttpClient(Dtmcli.Constant.WorkflowBranchClientHttpName, client => + // { + // // TODO DtmOptions + // // client.Timeout = TimeSpan.FromMilliseconds(options.BranchTimeout); + // }).AddHttpMessageHandler(); + // + // // TODO how to inject workflow instance? + // services.AddTransient(); + // } } } \ No newline at end of file From b2cb647cf3b8cbcc0d2b3aab71f4a9d939b43f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 22 Apr 2025 15:59:26 +0800 Subject: [PATCH 12/29] feat(BusiGrpcService): implement stream TCC gRPC service and add basic integration tests --- tests/BusiGrpcService/BusiGrpcService.csproj | 9 + tests/BusiGrpcService/Program.cs | 5 + .../Services/BusiApiService.cs | 23 +- .../Services/BusiApiService_Stream.cs | 130 +++++++++++ .../BusiApiServiceTest.cs | 207 ++++++++++++++++++ tests/protos/busi.proto | 23 ++ 6 files changed, 375 insertions(+), 22 deletions(-) create mode 100644 tests/BusiGrpcService/Services/BusiApiService_Stream.cs create mode 100644 tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs diff --git a/tests/BusiGrpcService/BusiGrpcService.csproj b/tests/BusiGrpcService/BusiGrpcService.csproj index b896af4..efa0382 100644 --- a/tests/BusiGrpcService/BusiGrpcService.csproj +++ b/tests/BusiGrpcService/BusiGrpcService.csproj @@ -8,6 +8,7 @@ + @@ -20,4 +21,12 @@ + + + + + + + + diff --git a/tests/BusiGrpcService/Program.cs b/tests/BusiGrpcService/Program.cs index 11aa92a..0440ba0 100644 --- a/tests/BusiGrpcService/Program.cs +++ b/tests/BusiGrpcService/Program.cs @@ -13,6 +13,7 @@ }); builder.Services.AddGrpc(); +builder.Services.AddGrpcReflection(); builder.Services.AddDtmGrpc(x => { x.DtmGrpcUrl = "http://localhost:36790"; @@ -23,6 +24,10 @@ // Configure the HTTP request pipeline. app.MapGrpcService(); +IWebHostEnvironment env = app.Environment; +if (env.IsDevelopment()) + app.MapGrpcReflectionService(); + // test for workflow http branch app.MapGet("/test-http-ok1", () => "SUCCESS"); app.MapGet("/test-http-ok2", () => "SUCCESS"); diff --git a/tests/BusiGrpcService/Services/BusiApiService.cs b/tests/BusiGrpcService/Services/BusiApiService.cs index 7c41bc0..36fe0ce 100644 --- a/tests/BusiGrpcService/Services/BusiApiService.cs +++ b/tests/BusiGrpcService/Services/BusiApiService.cs @@ -10,7 +10,7 @@ namespace BusiGrpcService.Services { - public class BusiApiService : Busi.BusiBase + public partial class BusiApiService : Busi.BusiBase { private readonly ILogger _logger; @@ -46,27 +46,6 @@ public override async Task TransIn(BusiReq request, ServerCallContext con throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.TransInResult}")); } - public override async Task TransInTcc(BusiReq request, ServerCallContext context) - { - _logger.LogInformation("TransIn req={req}", JsonSerializer.Serialize(request)); - - if (string.IsNullOrWhiteSpace(request.TransInResult) || request.TransInResult.Equals("SUCCESS")) - { - await Task.CompletedTask; - return new Empty(); - } - else if (request.TransInResult.Equals("FAILURE")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); - } - else if (request.TransInResult.Equals("ONGOING")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); - } - - throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.TransInResult}")); - } - public override async Task TransInConfirm(BusiReq request, ServerCallContext context) { var tb = _client.TransBaseFromGrpc(context); diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs new file mode 100644 index 0000000..2c18a4a --- /dev/null +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -0,0 +1,130 @@ +using System.Text.Json; +using busi; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; + +namespace BusiGrpcService.Services; + +public partial class BusiApiService +{ + public override async Task StreamTransOutTcc(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + // stream try -> confirm/cancel + + await foreach (var request in requestStream.ReadAllAsync()) + { + string gid = context.RequestHeaders.Get("dtm-gid")?.Value; + // _logger.LogInformation($"{nameof(StreamTransOutTcc)} gid={gid} req={request}", gid, JsonSerializer.Serialize(request)); + + switch (request.OperateType) + { + case OperateType.Try: + { + if (string.IsNullOrWhiteSpace(request.BusiRequest.TransOutResult) || request.BusiRequest.TransOutResult.Equals("SUCCESS")) + { + await Task.CompletedTask; + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Tried, waiting your confirm..." }); + } + else if (request.BusiRequest.TransOutResult.Equals("FAILURE")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); + } + else if (request.BusiRequest.TransOutResult.Equals("ONGOING")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); + } + else + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransOutResult}")); + } + + break; + } + case OperateType.Confirm: + { + var tb = _client.TransBaseFromGrpc(context); + // _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Confirmed" }); + break; + } + case OperateType.Cancel: + { + var tb = _client.TransBaseFromGrpc(context); + // _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Canceled" }); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + _logger.LogInformation($"{nameof(StreamTransOutTcc)} completed"); + } + + public override async Task StreamTransInTcc(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + // stream try -> confirm/cancel + + await foreach (var request in requestStream.ReadAllAsync()) + { + // string gid = context.RequestHeaders.Get("dtm-gid")?.Value; + // _logger.LogInformation($"{nameof(StreamTransInTcc)} gid={gid} req={request}", gid, JsonSerializer.Serialize(request)); + + switch (request.OperateType) + { + case OperateType.Try: + { + if (string.IsNullOrWhiteSpace(request.BusiRequest.TransInResult) || request.BusiRequest.TransInResult.Equals("SUCCESS")) + { + await responseStream.WriteAsync(new StreamReply + { + OperateType = request.OperateType, + Message = "Tried, waiting your confirm..." + }); + } + else if (request.BusiRequest.TransInResult.Equals("FAILURE")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); + } + else if (request.BusiRequest.TransInResult.Equals("ONGOING")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); + } + else + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransInResult}")); + } + + break; + } + case OperateType.Confirm: + { + var tb = _client.TransBaseFromGrpc(context); + // _logger.LogInformation($"{nameof(StreamTransInTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); + await responseStream.WriteAsync(new StreamReply + { + OperateType = request.OperateType, + Message = "Confirmed" + }); + break; + } + case OperateType.Cancel: + { + var tb = _client.TransBaseFromGrpc(context); + // _logger.LogInformation($"{nameof(StreamTransInTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); + await responseStream.WriteAsync(new StreamReply + { + OperateType = request.OperateType, + Message = "Canceled" + }); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + _logger.LogInformation($"{nameof(StreamTransInTcc)} completed"); + } +} \ No newline at end of file diff --git a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs new file mode 100644 index 0000000..1483628 --- /dev/null +++ b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using busi; +using Dtmworkflow; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Grpc.Net.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Xunit; +using Xunit.Abstractions; + +namespace Dtmgrpc.IntegrationTests; + +// [Call gRPC services with the .NET client | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/grpc/client?view=aspnetcore-9.0#bi-directional-streaming-call) + +public class BusiApiServiceTest(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task StreamTransOutTcc_Try_Confirm() + { + var provider = ITTestHelper.AddDtmGrpc(); + Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); + + ConcurrentDictionary progess = new ConcurrentDictionary(); + + using var call = busiClient.StreamTransOutTcc(); + testOutputHelper.WriteLine("Starting background task to receive messages"); + Task readTask = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + progess[response.OperateType] = new Status(StatusCode.OK, ""); + } + } + catch (RpcException ex) + { + testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + progess[OperateType.Try] = ex.Status; // how assess response.OperateType + } + catch (Exception ex) + { + throw; + } + }); + + testOutputHelper.WriteLine("Starting to send messages"); + BusiReq busiRequest = ITTestHelper.GenBusiReq(false, false); + + // try + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + while (!progess.ContainsKey(OperateType.Try)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); + + // confirm + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progess.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); + + await call.RequestStream.CompleteAsync(); + await readTask; + } + + [Fact] + public async Task StreamTransOutTcc_Try_Failed() + { + var provider = ITTestHelper.AddDtmGrpc(); + Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); + + ConcurrentDictionary progess = new ConcurrentDictionary(); + + using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); + testOutputHelper.WriteLine("Starting background task to receive messages"); + Task readTask = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + progess[response.OperateType] = new Status(StatusCode.OK, ""); + } + } + catch (RpcException ex) + { + testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + progess[OperateType.Try] = ex.Status; // how assess response.OperateType + } + }); + + testOutputHelper.WriteLine("Starting to send messages"); + BusiReq busiRequest = ITTestHelper.GenBusiReq(true, false); + + // try + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + while (!progess.ContainsKey(OperateType.Try)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.Aborted, progess[OperateType.Try].StatusCode); + Assert.Equal("FAILURE", progess[OperateType.Try].Detail); + + await call.RequestStream.CompleteAsync(); + await readTask; + } + + + [Description("try-cancel")] + [Fact] + public async Task StreamTransOutTcc_Try_Cancel() + { + var provider = ITTestHelper.AddDtmGrpc(); + Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); + + ConcurrentDictionary progess = new ConcurrentDictionary(); + + using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); + testOutputHelper.WriteLine("Starting background task to receive messages"); + Task readTask = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + progess[response.OperateType] = new Status(StatusCode.OK, ""); + } + } + catch (RpcException ex) + { + testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + progess[OperateType.Try] = ex.Status; // how assess response.OperateType + } + }); + + testOutputHelper.WriteLine("Starting to send messages"); + BusiReq busiRequest = ITTestHelper.GenBusiReq(false, false); + + // try + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + while (!progess.ContainsKey(OperateType.Try)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); + + // cancel + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Cancel, + BusiRequest = busiRequest, + }); + // wait cancel + while (!progess.ContainsKey(OperateType.Cancel)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progess[OperateType.Cancel].StatusCode); + + await call.RequestStream.CompleteAsync(); + await readTask; + } + + private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) + { + var loggerFactory = provider.GetRequiredService(); + var channel = GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol); + var logger = loggerFactory.CreateLogger(); + + Busi.BusiClient busiClient; + if (wf != null) + { + var interceptor = new WorkflowGrpcInterceptor(wf, logger); // inject client interceptor, and workflow instance + var callInvoker = channel.Intercept(interceptor); + busiClient = new Busi.BusiClient(callInvoker); + } + else + { + busiClient = new Busi.BusiClient(channel); + } + + return busiClient; + } +} \ No newline at end of file diff --git a/tests/protos/busi.proto b/tests/protos/busi.proto index 932ef1e..8b34364 100644 --- a/tests/protos/busi.proto +++ b/tests/protos/busi.proto @@ -16,6 +16,25 @@ message BusiReq { message BusiReply { string message = 1; } + +enum OperateType { + Try = 0; + Confirm = 1; + Cancel = 2; +} +enum OperateResult { + Success = 0; + Fail = 1; +} +message StreamRequest { + OperateType OperateType = 1; + BusiReq busiRequest = 3; +} +message StreamReply { + OperateType OperateType = 1; + string Message = 3; +} + // The dtm service definition. service Busi { rpc TransIn(BusiReq) returns (google.protobuf.Empty) {} @@ -47,4 +66,8 @@ service Busi { rpc QueryPrepared(BusiReq) returns (BusiReply) {} rpc QueryPreparedMySqlReal(BusiReq) returns (google.protobuf.Empty) {} rpc QueryPreparedRedis(BusiReq) returns (google.protobuf.Empty) {} + + // stream TCC + rpc StreamTransInTcc(stream StreamRequest) returns (stream StreamReply) {} + rpc StreamTransOutTcc(stream StreamRequest) returns (stream StreamReply) {} } \ No newline at end of file From 8786c315e970f3094c8c7a830eeeac47a98dea02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 23 Apr 2025 14:05:53 +0800 Subject: [PATCH 13/29] test(dtm): add stream grpc tcc and do integration tests for workflow --- .../BusiApiServiceTest.cs | 29 +- .../WorkflowGrpcStreamTest.cs | 427 ++++++++++++++++++ .../WorkflowGrpcTest.cs | 4 +- 3 files changed, 448 insertions(+), 12 deletions(-) create mode 100644 tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs diff --git a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs index 1483628..ea4e4a1 100644 --- a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs @@ -26,10 +26,11 @@ public async Task StreamTransOutTcc_Try_Confirm() var provider = ITTestHelper.AddDtmGrpc(); Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); - ConcurrentDictionary progess = new ConcurrentDictionary(); + ConcurrentDictionary progress = new ConcurrentDictionary(); using var call = busiClient.StreamTransOutTcc(); testOutputHelper.WriteLine("Starting background task to receive messages"); + bool callDisposed = false; Task readTask = Task.Run(async () => { try @@ -37,16 +38,18 @@ public async Task StreamTransOutTcc_Try_Confirm() await foreach (var response in call.ResponseStream.ReadAllAsync()) { testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progess[response.OperateType] = new Status(StatusCode.OK, ""); + progress[response.OperateType] = new Status(StatusCode.OK, ""); } } catch (RpcException ex) { testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progess[OperateType.Try] = ex.Status; // how assess response.OperateType + progress[OperateType.Try] = ex.Status; // how assess response.OperateType + callDisposed = true; } catch (Exception ex) { + callDisposed = true; throw; } }); @@ -61,9 +64,9 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait try - while (!progess.ContainsKey(OperateType.Try)) + while (callDisposed || !progress.ContainsKey(OperateType.Try)) Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); // confirm await call.RequestStream.WriteAsync(new StreamRequest() @@ -72,9 +75,9 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait Confirm - while (!progess.ContainsKey(OperateType.Confirm)) + while (callDisposed || !progress.ContainsKey(OperateType.Confirm)) Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); await call.RequestStream.CompleteAsync(); await readTask; @@ -90,6 +93,7 @@ public async Task StreamTransOutTcc_Try_Failed() using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); testOutputHelper.WriteLine("Starting background task to receive messages"); + bool callDisposed = false; Task readTask = Task.Run(async () => { try @@ -98,12 +102,14 @@ public async Task StreamTransOutTcc_Try_Failed() { testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); progess[response.OperateType] = new Status(StatusCode.OK, ""); + callDisposed = true; } } catch (RpcException ex) { testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); progess[OperateType.Try] = ex.Status; // how assess response.OperateType + callDisposed = true; } }); @@ -117,7 +123,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait try - while (!progess.ContainsKey(OperateType.Try)) + while (callDisposed || !progess.ContainsKey(OperateType.Try)) Thread.Sleep(1000); Assert.Equal(StatusCode.Aborted, progess[OperateType.Try].StatusCode); Assert.Equal("FAILURE", progess[OperateType.Try].Detail); @@ -138,6 +144,7 @@ public async Task StreamTransOutTcc_Try_Cancel() using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); testOutputHelper.WriteLine("Starting background task to receive messages"); + bool callDisposed = false; Task readTask = Task.Run(async () => { try @@ -146,12 +153,14 @@ public async Task StreamTransOutTcc_Try_Cancel() { testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); progess[response.OperateType] = new Status(StatusCode.OK, ""); + callDisposed = true; } } catch (RpcException ex) { testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); progess[OperateType.Try] = ex.Status; // how assess response.OperateType + callDisposed = true; } }); @@ -165,7 +174,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait try - while (!progess.ContainsKey(OperateType.Try)) + while (callDisposed || !progess.ContainsKey(OperateType.Try)) Thread.Sleep(1000); Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); @@ -176,7 +185,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait cancel - while (!progess.ContainsKey(OperateType.Cancel)) + while (callDisposed || !progess.ContainsKey(OperateType.Cancel)) Thread.Sleep(1000); Assert.Equal(StatusCode.OK, progess[OperateType.Cancel].StatusCode); diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs new file mode 100644 index 0000000..537e785 --- /dev/null +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -0,0 +1,427 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using busi; +using Dtmcli; +using DtmCommon; +using Dtmworkflow; +using Grpc.Core; +using Grpc.Core.Interceptors; +using Grpc.Net.Client; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Xunit; +using Xunit.Abstractions; + +namespace Dtmgrpc.IntegrationTests +{ + public class WorkflowGrpcStreamTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public WorkflowGrpcStreamTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TryConfirm() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; + Task readTask = null; + AsyncDuplexStreamingCall call = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + Busi.BusiClient busiClient = null; + + ConcurrentDictionary progress = new ConcurrentDictionary(); + + // 1. grpc1 TCC + Workflow wf = workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progress.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progress.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + }); + busiClient = GetBusiClientWithWf(wf, provider); + call = busiClient.StreamTransOutTcc(); + using var call2 = call; + readTask = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + progress[response.OperateType] = new Status(StatusCode.OK, ""); + } + } + catch (RpcException ex) + { + _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + progress[OperateType.Try] = ex.Status; // how assess response.OperateType + } + catch (Exception ex) + { + _testOutputHelper.WriteLine($"Exception caught: {ex}"); + throw; + } + }); + + // try + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + while (!progress.ContainsKey(OperateType.Try)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + + // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + (byte[] doResult, Exception ex) = await workflow.NewBranch() + // .OnRollback(async (barrier) => // 反向 rollback + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => { return ("my result"u8.ToArray(), null); }); // 正向 + if (ex != null) + throw ex; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + await readTask; + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + trans = await dtmClient.Query(gid, CancellationToken.None); + // BranchID Op Status + // 01 action succeed + // 02 action succeed + // 01 commit succeed + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("commit", trans.Branches[2].Op); + + // same gid again + result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("commit", trans.Branches[2].Op); + } + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TryCancel() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryCancel)}-{Guid.NewGuid().ToString("D")[..8]}"; + Task readTask = null; + AsyncDuplexStreamingCall call = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + Busi.BusiClient busiClient = null; + + ConcurrentDictionary progress = new ConcurrentDictionary(); + + // 1. grpc1 TCC + Workflow wf = workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progress.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progress.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + }); + busiClient = GetBusiClientWithWf(wf, provider); + call = busiClient.StreamTransOutTcc(); + using var call2 = call; + readTask = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + progress[response.OperateType] = new Status(StatusCode.OK, ""); + } + } + catch (RpcException ex) + { + _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + progress[OperateType.Try] = ex.Status; // how assess response.OperateType + } + catch (Exception ex) + { + _testOutputHelper.WriteLine($"Exception caught: {ex}"); + throw; + } + }); + + // try + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + while (!progress.ContainsKey(OperateType.Try)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + + // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + (byte[] doResult, Exception ex) = await workflow.NewBranch() + // .OnRollback(async (barrier) => // 反向 rollback + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + // throw new DtmFailureException("db do failed"); // can't throw + var ex = new DtmFailureException("db do failed"); + return ("my result"u8.ToArray(), ex); + }); // 正向 + if (ex != null) + throw ex; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + await readTask; + Assert.Null(result); + trans = await dtmClient.Query(gid, CancellationToken.None); + // BranchID Op Status + // 01 action succeed + // 02 action succeed + // 01 rollback succeed + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("rollback", trans.Branches[2].Op); + + // same gid again + Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // DtmCommon.DtmFailureException + // db do failed + // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs + }); + } + + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TryFailed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryCancel)}-{Guid.NewGuid().ToString("D")[..8]}"; + Task readTask = null; + AsyncDuplexStreamingCall call = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + Busi.BusiClient busiClient = null; + + ConcurrentDictionary progress = new ConcurrentDictionary(); + + // 1. grpc1 TCC + Workflow wf = workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progress.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + while (!progress.ContainsKey(OperateType.Confirm)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + }); + busiClient = GetBusiClientWithWf(wf, provider); + call = busiClient.StreamTransOutTcc(); + using var call2 = call; + readTask = Task.Run(async () => + { + try + { + await foreach (var response in call.ResponseStream.ReadAllAsync()) + { + _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + progress[response.OperateType] = new Status(StatusCode.OK, ""); + } + } + catch (RpcException ex) + { + _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + progress[OperateType.Try] = ex.Status; // how assess response.OperateType + } + catch (Exception ex) + { + _testOutputHelper.WriteLine($"Exception caught: {ex}"); + throw; + } + }); + + // try failed + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + while (!progress.ContainsKey(OperateType.Try)) + Thread.Sleep(1000); + Assert.Equal(StatusCode.Aborted, progress[OperateType.Try].StatusCode); + Assert.Equal("FAILURE", progress[OperateType.Try].Detail); + throw new DtmFailureException($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); + // throw new Exception($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(outFailed: true, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + TransGlobal trans; + + // first + // await Assert.ThrowsAsync(async () => + { + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + } + // ); + await readTask; + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(0, trans.Branches.Count); + + + // same gid again + await Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // DtmCommon.DtmFailureException + // sub trans1 try failed(grpc): FAILURE + // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs + } + ); + } + + private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) + { + var loggerFactory = provider.GetRequiredService(); + var channel = GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol); + var logger = loggerFactory.CreateLogger(); + var interceptor = new WorkflowGrpcInterceptor(wf, logger); // inject client interceptor, and workflow instance + var callInvoker = channel.Intercept(interceptor); + Busi.BusiClient busiClient = new Busi.BusiClient(callInvoker); + return busiClient; + } + } +} \ No newline at end of file diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index fa87e5e..04c4da1 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -518,7 +518,7 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() Assert.Equal("action", trans.Branches[0].Op); // same gid again - Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); // DtmCommon.DtmFailureException: Status(StatusCode="Aborted", Detail="FAILURE") @@ -610,7 +610,7 @@ public async Task Execute_GrpcTccAndDo_Should_DoFailed() Assert.Equal("rollback", trans.Branches[2].Op); // same gid again - Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); // DtmCommon.DtmFailureException From b10f0664d99a8a534832cb987b3bf5f73c5e7830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Fri, 25 Apr 2025 13:55:18 +0800 Subject: [PATCH 14/29] refactor(Dtmgrpc.IntegrationTests): improve gRPC streaming test with a dedicated processor class --- .../BusiApiServiceTest.cs | 146 +++++++++--------- 1 file changed, 70 insertions(+), 76 deletions(-) diff --git a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs index ea4e4a1..0cfdf2d 100644 --- a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -18,41 +19,78 @@ namespace Dtmgrpc.IntegrationTests; // [Call gRPC services with the .NET client | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/grpc/client?view=aspnetcore-9.0#bi-directional-streaming-call) -public class BusiApiServiceTest(ITestOutputHelper testOutputHelper) +public class MyGrpcProcesser(AsyncDuplexStreamingCall call, ITestOutputHelper testOutputHelper) { - [Fact] - public async Task StreamTransOutTcc_Try_Confirm() - { - var provider = ITTestHelper.AddDtmGrpc(); - Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); + private TaskCompletionSource _callDisposed = new TaskCompletionSource(); + private readonly ConcurrentDictionary> progress = new(); - ConcurrentDictionary progress = new ConcurrentDictionary(); - - using var call = busiClient.StreamTransOutTcc(); - testOutputHelper.WriteLine("Starting background task to receive messages"); - bool callDisposed = false; + public Task HandleResponse() + { + IAsyncEnumerable asyncEnumerable = call.ResponseStream.ReadAllAsync(); Task readTask = Task.Run(async () => { try { - await foreach (var response in call.ResponseStream.ReadAllAsync()) + await foreach (var response in asyncEnumerable) { testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progress[response.OperateType] = new Status(StatusCode.OK, ""); + if (progress.TryGetValue(response.OperateType, out var tcs)) + { + tcs.TrySetResult(new Status(StatusCode.OK, "")); + } + else + { + progress[response.OperateType] = new TaskCompletionSource(new Status(StatusCode.OK, "")); + } } } catch (RpcException ex) { testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progress[OperateType.Try] = ex.Status; // how assess response.OperateType - callDisposed = true; + if (progress.TryGetValue(OperateType.Try, out var tcs)) + { + bool _ = tcs.TrySetResult(ex.Status); + } + else + progress[OperateType.Try] = new TaskCompletionSource(ex.Status); // TODO 应答对应的哪个请求 + + _callDisposed.SetResult(); + throw; } catch (Exception ex) { - callDisposed = true; + _callDisposed.SetResult(); throw; } }); + return readTask; + } + + public async Task GetResult(OperateType operateType) + { + if (!progress.TryGetValue(operateType, out var tcs)) + { + tcs = new TaskCompletionSource(); + progress[operateType] = tcs; + } + + Task.WaitAny(_callDisposed.Task, tcs.Task); + return await tcs.Task; + } +} + +public class BusiApiServiceTest(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task StreamTransOutTcc_Try_Confirm() + { + var provider = ITTestHelper.AddDtmGrpc(); + Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); + + using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); + testOutputHelper.WriteLine("Starting background task to receive messages"); + var myGrpcProcesser = new MyGrpcProcesser(call, testOutputHelper); + var readTask = myGrpcProcesser.HandleResponse(); testOutputHelper.WriteLine("Starting to send messages"); BusiReq busiRequest = ITTestHelper.GenBusiReq(false, false); @@ -63,10 +101,8 @@ await call.RequestStream.WriteAsync(new StreamRequest() OperateType = OperateType.Try, BusiRequest = busiRequest, }); - // wait try - while (callDisposed || !progress.ContainsKey(OperateType.Try)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + Grpc.Core.Status tryStatus = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.OK, tryStatus.StatusCode); // confirm await call.RequestStream.WriteAsync(new StreamRequest() @@ -75,9 +111,8 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait Confirm - while (callDisposed || !progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + Grpc.Core.Status confirmStatus = await myGrpcProcesser.GetResult(OperateType.Confirm); + Assert.Equal(StatusCode.OK, confirmStatus.StatusCode); await call.RequestStream.CompleteAsync(); await readTask; @@ -89,29 +124,10 @@ public async Task StreamTransOutTcc_Try_Failed() var provider = ITTestHelper.AddDtmGrpc(); Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); - ConcurrentDictionary progess = new ConcurrentDictionary(); - using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); testOutputHelper.WriteLine("Starting background task to receive messages"); - bool callDisposed = false; - Task readTask = Task.Run(async () => - { - try - { - await foreach (var response in call.ResponseStream.ReadAllAsync()) - { - testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progess[response.OperateType] = new Status(StatusCode.OK, ""); - callDisposed = true; - } - } - catch (RpcException ex) - { - testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progess[OperateType.Try] = ex.Status; // how assess response.OperateType - callDisposed = true; - } - }); + var myGrpcProcesser = new MyGrpcProcesser(call, testOutputHelper); + var readTask = myGrpcProcesser.HandleResponse(); testOutputHelper.WriteLine("Starting to send messages"); BusiReq busiRequest = ITTestHelper.GenBusiReq(true, false); @@ -123,13 +139,12 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait try - while (callDisposed || !progess.ContainsKey(OperateType.Try)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.Aborted, progess[OperateType.Try].StatusCode); - Assert.Equal("FAILURE", progess[OperateType.Try].Detail); + var tryStatus = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.Aborted, tryStatus.StatusCode); + Assert.Equal("FAILURE", tryStatus.Detail); await call.RequestStream.CompleteAsync(); - await readTask; + await Assert.ThrowsAsync(async () => { await readTask; }); // because try action aborted. } @@ -140,29 +155,10 @@ public async Task StreamTransOutTcc_Try_Cancel() var provider = ITTestHelper.AddDtmGrpc(); Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); - ConcurrentDictionary progess = new ConcurrentDictionary(); - using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); testOutputHelper.WriteLine("Starting background task to receive messages"); - bool callDisposed = false; - Task readTask = Task.Run(async () => - { - try - { - await foreach (var response in call.ResponseStream.ReadAllAsync()) - { - testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progess[response.OperateType] = new Status(StatusCode.OK, ""); - callDisposed = true; - } - } - catch (RpcException ex) - { - testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progess[OperateType.Try] = ex.Status; // how assess response.OperateType - callDisposed = true; - } - }); + var myGrpcProcesser = new MyGrpcProcesser(call, testOutputHelper); + var readTask = myGrpcProcesser.HandleResponse(); testOutputHelper.WriteLine("Starting to send messages"); BusiReq busiRequest = ITTestHelper.GenBusiReq(false, false); @@ -174,9 +170,8 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait try - while (callDisposed || !progess.ContainsKey(OperateType.Try)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progess[OperateType.Try].StatusCode); + var tryStatus = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.OK, tryStatus.StatusCode); // cancel await call.RequestStream.WriteAsync(new StreamRequest() @@ -185,9 +180,8 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait cancel - while (callDisposed || !progess.ContainsKey(OperateType.Cancel)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progess[OperateType.Cancel].StatusCode); + var cancelStatus = await myGrpcProcesser.GetResult(OperateType.Cancel); + Assert.Equal(StatusCode.OK, cancelStatus.StatusCode); await call.RequestStream.CompleteAsync(); await readTask; From 0f49350028840123b9cbdeb5341140dae800cf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Fri, 25 Apr 2025 15:56:00 +0800 Subject: [PATCH 15/29] refactor(grpc): stream unt test tcc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化StreamTransOutTcc的单测,与普通业务调用单测保持一致。仅保留它为写拦截器做好用例 --- src/Dtmworkflow/WorkflowGrpcInterceptor.cs | 101 ++++ .../Services/BusiApiService_Stream.cs | 18 +- .../BusiApiServiceTest.cs | 63 -- .../MyGrpcProcesser.cs | 69 +++ .../WorkflowGrpcStreamTest.cs | 536 ++++++++---------- 5 files changed, 423 insertions(+), 364 deletions(-) create mode 100644 tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs diff --git a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs index c9e64b4..b6f9470 100644 --- a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs +++ b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Google.Protobuf; using Grpc.Core; @@ -7,6 +8,7 @@ namespace Dtmworkflow; +// [gRPC interceptors on .NET | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/grpc/interceptors?view=aspnetcore-9.0) public class WorkflowGrpcInterceptor(Workflow wf, ILogger logger) : Interceptor { public WorkflowGrpcInterceptor(Workflow wf) : this(wf, null) @@ -72,6 +74,95 @@ public override AsyncUnaryCall AsyncUnaryCall( return call; } + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) + { + logger?.LogDebug($"grpc client calling: {context.Host}{context.Method.FullName}"); + + if (wf == null) + { + return base.AsyncDuplexStreamingCall(context, continuation); + } + + var newContext = Dtmgimp.TransInfo2Ctx(context, wf.TransBase.Gid, wf.TransBase.TransType, wf.WorkflowImp.CurrentBranch, wf.WorkflowImp.CurrentOp, wf.TransBase.Dtm); + + var call = continuation(newContext); + + return new AsyncDuplexStreamingCall( + new InterceptingClientStreamWriter(call.RequestStream, async (request) => + { + logger?.LogDebug($"grpc client sending: {context.Host}{context.Method.FullName}"); + await call.RequestStream.WriteAsync(request); + }), + new InterceptingClientStreamReader(call.ResponseStream, async () => + { + try + { + var response = await call.ResponseStream.MoveNext(); + logger?.LogDebug($"grpc client received: {context.Host}{context.Method.FullName}"); + return response; + } + catch (Exception e) + { + logger?.LogError($"grpc client: {context.Host}{context.Method.FullName} ex: {e}"); + throw; + } + }), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose + ); + } + + private class InterceptingClientStreamWriter : IClientStreamWriter + { + private readonly IClientStreamWriter _innerWriter; + private readonly Func _onWrite; + + public InterceptingClientStreamWriter(IClientStreamWriter innerWriter, Func onWrite) + { + _innerWriter = innerWriter; + _onWrite = onWrite; + } + + public WriteOptions WriteOptions + { + get => _innerWriter.WriteOptions; + set => _innerWriter.WriteOptions = value; + } + + public Task WriteAsync(T message) + { + return _onWrite(message).ContinueWith(_ => _innerWriter.WriteAsync(message)).Unwrap(); + } + + public Task CompleteAsync() + { + return _innerWriter.CompleteAsync(); + } + } + + private class InterceptingClientStreamReader : IAsyncStreamReader + { + private readonly IAsyncStreamReader _innerReader; + private readonly Func> _onMoveNext; + + public InterceptingClientStreamReader(IAsyncStreamReader innerReader, Func> onMoveNext) + { + _innerReader = innerReader; + _onMoveNext = onMoveNext; + } + + public T Current => _innerReader.Current; + + public Task MoveNext(CancellationToken cancellationToken) + { + return _onMoveNext(); + } + } + private class Dtmgimp { public static ClientInterceptorContext TransInfo2Ctx( @@ -84,6 +175,15 @@ public static ClientInterceptorContext TransInfo2Ctx TransInfo2Ctx( ctx.Method, diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs index 2c18a4a..b698cd0 100644 --- a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -10,11 +10,10 @@ public partial class BusiApiService public override async Task StreamTransOutTcc(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { // stream try -> confirm/cancel - await foreach (var request in requestStream.ReadAllAsync()) { - string gid = context.RequestHeaders.Get("dtm-gid")?.Value; - // _logger.LogInformation($"{nameof(StreamTransOutTcc)} gid={gid} req={request}", gid, JsonSerializer.Serialize(request)); + var tb = _client.TransBaseFromGrpc(context); + _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={JsonSerializer.Serialize(tb)}, req={JsonSerializer.Serialize(request)}"); switch (request.OperateType) { @@ -42,15 +41,11 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r } case OperateType.Confirm: { - var tb = _client.TransBaseFromGrpc(context); - // _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Confirmed" }); break; } case OperateType.Cancel: { - var tb = _client.TransBaseFromGrpc(context); - // _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Canceled" }); break; } @@ -65,11 +60,10 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r public override async Task StreamTransInTcc(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { // stream try -> confirm/cancel - await foreach (var request in requestStream.ReadAllAsync()) { - // string gid = context.RequestHeaders.Get("dtm-gid")?.Value; - // _logger.LogInformation($"{nameof(StreamTransInTcc)} gid={gid} req={request}", gid, JsonSerializer.Serialize(request)); + var tb = _client.TransBaseFromGrpc(context); + _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={JsonSerializer.Serialize(tb)}, req={JsonSerializer.Serialize(request)}"); switch (request.OperateType) { @@ -100,8 +94,6 @@ await responseStream.WriteAsync(new StreamReply } case OperateType.Confirm: { - var tb = _client.TransBaseFromGrpc(context); - // _logger.LogInformation($"{nameof(StreamTransInTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, @@ -111,8 +103,6 @@ await responseStream.WriteAsync(new StreamReply } case OperateType.Cancel: { - var tb = _client.TransBaseFromGrpc(context); - // _logger.LogInformation($"{nameof(StreamTransInTcc)} tb={tb}, req={request}", JsonSerializer.Serialize(tb), JsonSerializer.Serialize(request)); await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, diff --git a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs index 0cfdf2d..f509ef5 100644 --- a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; @@ -19,66 +16,6 @@ namespace Dtmgrpc.IntegrationTests; // [Call gRPC services with the .NET client | Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/grpc/client?view=aspnetcore-9.0#bi-directional-streaming-call) -public class MyGrpcProcesser(AsyncDuplexStreamingCall call, ITestOutputHelper testOutputHelper) -{ - private TaskCompletionSource _callDisposed = new TaskCompletionSource(); - private readonly ConcurrentDictionary> progress = new(); - - public Task HandleResponse() - { - IAsyncEnumerable asyncEnumerable = call.ResponseStream.ReadAllAsync(); - Task readTask = Task.Run(async () => - { - try - { - await foreach (var response in asyncEnumerable) - { - testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - if (progress.TryGetValue(response.OperateType, out var tcs)) - { - tcs.TrySetResult(new Status(StatusCode.OK, "")); - } - else - { - progress[response.OperateType] = new TaskCompletionSource(new Status(StatusCode.OK, "")); - } - } - } - catch (RpcException ex) - { - testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - if (progress.TryGetValue(OperateType.Try, out var tcs)) - { - bool _ = tcs.TrySetResult(ex.Status); - } - else - progress[OperateType.Try] = new TaskCompletionSource(ex.Status); // TODO 应答对应的哪个请求 - - _callDisposed.SetResult(); - throw; - } - catch (Exception ex) - { - _callDisposed.SetResult(); - throw; - } - }); - return readTask; - } - - public async Task GetResult(OperateType operateType) - { - if (!progress.TryGetValue(operateType, out var tcs)) - { - tcs = new TaskCompletionSource(); - progress[operateType] = tcs; - } - - Task.WaitAny(_callDisposed.Task, tcs.Task); - return await tcs.Task; - } -} - public class BusiApiServiceTest(ITestOutputHelper testOutputHelper) { [Fact] diff --git a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs new file mode 100644 index 0000000..b89278b --- /dev/null +++ b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using busi; +using Grpc.Core; +using Xunit.Abstractions; + +namespace Dtmgrpc.IntegrationTests; + +public class MyGrpcProcesser(AsyncDuplexStreamingCall call, ITestOutputHelper testOutputHelper) +{ + private TaskCompletionSource _callDisposed = new TaskCompletionSource(); + private readonly ConcurrentDictionary> progress = new(); + + public Task HandleResponse() + { + IAsyncEnumerable asyncEnumerable = call.ResponseStream.ReadAllAsync(); + Task readTask = Task.Run(async () => + { + try + { + await foreach (var response in asyncEnumerable) + { + testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + if (progress.TryGetValue(response.OperateType, out var tcs)) + { + tcs.TrySetResult(new Status(StatusCode.OK, "")); + } + else + { + progress[response.OperateType] = new TaskCompletionSource(new Status(StatusCode.OK, "")); + } + } + } + catch (RpcException ex) + { + testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + if (progress.TryGetValue(OperateType.Try, out var tcs)) + { + bool _ = tcs.TrySetResult(ex.Status); + } + else + progress[OperateType.Try] = new TaskCompletionSource(ex.Status); // TODO 应答对应的哪个请求 + + _callDisposed.SetResult(); + throw; + } + catch (Exception ex) + { + _callDisposed.SetResult(); + throw; + } + }); + return readTask; + } + + public async Task GetResult(OperateType operateType) + { + if (!progress.TryGetValue(operateType, out var tcs)) + { + tcs = new TaskCompletionSource(); + progress[operateType] = tcs; + } + + Task.WaitAny(_callDisposed.Task, tcs.Task); + return await tcs.Task; + } +} \ No newline at end of file diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs index 537e785..7be18fc 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -38,16 +38,15 @@ public async Task Execute_StreamGrpcTccAndDo_TryConfirm() WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; - Task readTask = null; AsyncDuplexStreamingCall call = null; + MyGrpcProcesser myGrpcProcesser = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); Busi.BusiClient busiClient = null; - ConcurrentDictionary progress = new ConcurrentDictionary(); - // 1. grpc1 TCC Workflow wf = workflow.NewBranch() .OnCommit(async (barrier) => // confirm @@ -58,9 +57,8 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait Confirm - while (!progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + var result = await myGrpcProcesser.GetResult(OperateType.Confirm); + Assert.Equal(StatusCode.OK, result.StatusCode); }) .OnRollback(async (barrier) => // cancel { @@ -70,34 +68,16 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait Confirm - while (!progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + var result = await myGrpcProcesser.GetResult(OperateType.Cancel); + Assert.Equal(StatusCode.OK, result.StatusCode); }); + busiClient = GetBusiClientWithWf(wf, provider); call = busiClient.StreamTransOutTcc(); + myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); + Task readTask = myGrpcProcesser.HandleResponse(); + using var call2 = call; - readTask = Task.Run(async () => - { - try - { - await foreach (var response in call.ResponseStream.ReadAllAsync()) - { - _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progress[response.OperateType] = new Status(StatusCode.OK, ""); - } - } - catch (RpcException ex) - { - _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progress[OperateType.Try] = ex.Status; // how assess response.OperateType - } - catch (Exception ex) - { - _testOutputHelper.WriteLine($"Exception caught: {ex}"); - throw; - } - }); // try await call.RequestStream.WriteAsync(new StreamRequest() @@ -106,9 +86,8 @@ await call.RequestStream.WriteAsync(new StreamRequest() BusiRequest = busiRequest, }); // wait try - while (!progress.ContainsKey(OperateType.Try)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + var result = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.OK, result.StatusCode); // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 (byte[] doResult, Exception ex) = await workflow.NewBranch() @@ -120,6 +99,9 @@ await call.RequestStream.WriteAsync(new StreamRequest() if (ex != null) throw ex; + await call.RequestStream.CompleteAsync(); + await readTask; + return await Task.FromResult("my result"u8.ToArray()); }); @@ -127,13 +109,14 @@ await call.RequestStream.WriteAsync(new StreamRequest() var req = ITTestHelper.GenBusiReq(false, false); DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); - TransGlobal trans; // first byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - await readTask; + Assert.Equal("my result", Encoding.UTF8.GetString(result)); - trans = await dtmClient.Query(gid, CancellationToken.None); + TransGlobal trans = await dtmClient.Query(gid, CancellationToken.None); + + // BranchID Op Status // 01 action succeed // 02 action succeed @@ -151,6 +134,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); Assert.Equal(3, trans.Branches.Count); Assert.Equal("succeed", trans.Branches[0].Status); @@ -161,257 +145,235 @@ await call.RequestStream.WriteAsync(new StreamRequest() Assert.Equal("commit", trans.Branches[2].Op); } - [Fact] - public async Task Execute_StreamGrpcTccAndDo_TryCancel() - { - var provider = ITTestHelper.AddDtmGrpc(); - var workflowFactory = provider.GetRequiredService(); - var loggerFactory = provider.GetRequiredService(); - WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - - string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryCancel)}-{Guid.NewGuid().ToString("D")[..8]}"; - Task readTask = null; - AsyncDuplexStreamingCall call = null; - workflowGlobalTransaction.Register(wfName1, async (workflow, data) => - { - BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - - Busi.BusiClient busiClient = null; - - ConcurrentDictionary progress = new ConcurrentDictionary(); - - // 1. grpc1 TCC - Workflow wf = workflow.NewBranch() - .OnCommit(async (barrier) => // confirm - { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - BusiRequest = busiRequest, - }); - // wait Confirm - while (!progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - }) - .OnRollback(async (barrier) => // cancel - { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - BusiRequest = busiRequest, - }); - // wait Confirm - while (!progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - }); - busiClient = GetBusiClientWithWf(wf, provider); - call = busiClient.StreamTransOutTcc(); - using var call2 = call; - readTask = Task.Run(async () => - { - try - { - await foreach (var response in call.ResponseStream.ReadAllAsync()) - { - _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progress[response.OperateType] = new Status(StatusCode.OK, ""); - } - } - catch (RpcException ex) - { - _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progress[OperateType.Try] = ex.Status; // how assess response.OperateType - } - catch (Exception ex) - { - _testOutputHelper.WriteLine($"Exception caught: {ex}"); - throw; - } - }); - - // try - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Try, - BusiRequest = busiRequest, - }); - // wait try - while (!progress.ContainsKey(OperateType.Try)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - - // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 - (byte[] doResult, Exception ex) = await workflow.NewBranch() - // .OnRollback(async (barrier) => // 反向 rollback - // { - // _testOutputHelper.WriteLine("1. local rollback"); - // }) - .Do(async (barrier) => - { - // throw new DtmFailureException("db do failed"); // can't throw - var ex = new DtmFailureException("db do failed"); - return ("my result"u8.ToArray(), ex); - }); // 正向 - if (ex != null) - throw ex; - - return await Task.FromResult("my result"u8.ToArray()); - }); - - string gid = wfName1 + Guid.NewGuid().ToString()[..8]; - var req = ITTestHelper.GenBusiReq(false, false); - - DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); - TransGlobal trans; - - // first - byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - await readTask; - Assert.Null(result); - trans = await dtmClient.Query(gid, CancellationToken.None); - // BranchID Op Status - // 01 action succeed - // 02 action succeed - // 01 rollback succeed - Assert.Equal("failed", trans.Transaction.Status); - Assert.Equal(3, trans.Branches.Count); - Assert.Equal("succeed", trans.Branches[0].Status); - Assert.Equal("action", trans.Branches[0].Op); - Assert.Equal("succeed", trans.Branches[1].Status); - Assert.Equal("action", trans.Branches[1].Op); - Assert.Equal("succeed", trans.Branches[2].Status); - Assert.Equal("rollback", trans.Branches[2].Op); - - // same gid again - Assert.ThrowsAsync(async () => - { - var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // DtmCommon.DtmFailureException - // db do failed - // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs - // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs - // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs - }); - } - - - [Fact] - public async Task Execute_StreamGrpcTccAndDo_TryFailed() - { - var provider = ITTestHelper.AddDtmGrpc(); - var workflowFactory = provider.GetRequiredService(); - var loggerFactory = provider.GetRequiredService(); - WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - - string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryCancel)}-{Guid.NewGuid().ToString("D")[..8]}"; - Task readTask = null; - AsyncDuplexStreamingCall call = null; - workflowGlobalTransaction.Register(wfName1, async (workflow, data) => - { - BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - - Busi.BusiClient busiClient = null; - - ConcurrentDictionary progress = new ConcurrentDictionary(); - - // 1. grpc1 TCC - Workflow wf = workflow.NewBranch() - .OnCommit(async (barrier) => // confirm - { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - BusiRequest = busiRequest, - }); - // wait Confirm - while (!progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - }) - .OnRollback(async (barrier) => // cancel - { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - BusiRequest = busiRequest, - }); - // wait Confirm - while (!progress.ContainsKey(OperateType.Confirm)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - }); - busiClient = GetBusiClientWithWf(wf, provider); - call = busiClient.StreamTransOutTcc(); - using var call2 = call; - readTask = Task.Run(async () => - { - try - { - await foreach (var response in call.ResponseStream.ReadAllAsync()) - { - _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - progress[response.OperateType] = new Status(StatusCode.OK, ""); - } - } - catch (RpcException ex) - { - _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - progress[OperateType.Try] = ex.Status; // how assess response.OperateType - } - catch (Exception ex) - { - _testOutputHelper.WriteLine($"Exception caught: {ex}"); - throw; - } - }); - - // try failed - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Try, - BusiRequest = busiRequest, - }); - // wait try - while (!progress.ContainsKey(OperateType.Try)) - Thread.Sleep(1000); - Assert.Equal(StatusCode.Aborted, progress[OperateType.Try].StatusCode); - Assert.Equal("FAILURE", progress[OperateType.Try].Detail); - throw new DtmFailureException($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); - // throw new Exception($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); - }); - - string gid = wfName1 + Guid.NewGuid().ToString()[..8]; - var req = ITTestHelper.GenBusiReq(outFailed: true, false); - - DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); - TransGlobal trans; - - // first - // await Assert.ThrowsAsync(async () => - { - byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - } - // ); - await readTask; - trans = await dtmClient.Query(gid, CancellationToken.None); - Assert.Equal("failed", trans.Transaction.Status); - Assert.Equal(0, trans.Branches.Count); - - - // same gid again - await Assert.ThrowsAsync(async () => - { - var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // DtmCommon.DtmFailureException - // sub trans1 try failed(grpc): FAILURE - // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs - // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs - // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs - } - ); - } + // [Fact] + // public async Task Execute_StreamGrpcTccAndDo_TryCancel() + // { + // var provider = ITTestHelper.AddDtmGrpc(); + // var workflowFactory = provider.GetRequiredService(); + // var loggerFactory = provider.GetRequiredService(); + // WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + // + // string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryCancel)}-{Guid.NewGuid().ToString("D")[..8]}"; + // AsyncDuplexStreamingCall call = null; + // workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + // { + // BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + // + // Busi.BusiClient busiClient = null; + // + // ConcurrentDictionary progress = new ConcurrentDictionary(); + // + // // 1. grpc1 TCC + // Workflow wf = workflow.NewBranch() + // .OnCommit(async (barrier) => // confirm + // { + // await call.RequestStream.WriteAsync(new StreamRequest() + // { + // OperateType = OperateType.Confirm, + // BusiRequest = busiRequest, + // }); + // // wait Confirm + // while (!progress.ContainsKey(OperateType.Confirm)) + // Thread.Sleep(1000); + // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + // }) + // .OnRollback(async (barrier) => // cancel + // { + // await call.RequestStream.WriteAsync(new StreamRequest() + // { + // OperateType = OperateType.Confirm, + // BusiRequest = busiRequest, + // }); + // // wait Confirm + // while (!progress.ContainsKey(OperateType.Confirm)) + // Thread.Sleep(1000); + // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + // }); + // busiClient = GetBusiClientWithWf(wf, provider); + // call = busiClient.StreamTransOutTcc(); + // using var call2 = call; + // + // // try + // await call.RequestStream.WriteAsync(new StreamRequest() + // { + // OperateType = OperateType.Try, + // BusiRequest = busiRequest, + // }); + // // wait try + // while (!progress.ContainsKey(OperateType.Try)) + // Thread.Sleep(1000); + // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + // + // // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + // (byte[] doResult, Exception ex) = await workflow.NewBranch() + // // .OnRollback(async (barrier) => // 反向 rollback + // // { + // // _testOutputHelper.WriteLine("1. local rollback"); + // // }) + // .Do(async (barrier) => + // { + // // throw new DtmFailureException("db do failed"); // can't throw + // var ex = new DtmFailureException("db do failed"); + // return ("my result"u8.ToArray(), ex); + // }); // 正向 + // if (ex != null) + // throw ex; + // + // return await Task.FromResult("my result"u8.ToArray()); + // }); + // + // string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + // var req = ITTestHelper.GenBusiReq(false, false); + // + // DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + // TransGlobal trans; + // + // // first + // byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // await readTask; + // Assert.Null(result); + // trans = await dtmClient.Query(gid, CancellationToken.None); + // // BranchID Op Status + // // 01 action succeed + // // 02 action succeed + // // 01 rollback succeed + // Assert.Equal("failed", trans.Transaction.Status); + // Assert.Equal(3, trans.Branches.Count); + // Assert.Equal("succeed", trans.Branches[0].Status); + // Assert.Equal("action", trans.Branches[0].Op); + // Assert.Equal("succeed", trans.Branches[1].Status); + // Assert.Equal("action", trans.Branches[1].Op); + // Assert.Equal("succeed", trans.Branches[2].Status); + // Assert.Equal("rollback", trans.Branches[2].Op); + // + // // same gid again + // Assert.ThrowsAsync(async () => + // { + // var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // // DtmCommon.DtmFailureException + // // db do failed + // // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + // // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs + // }); + // } + // + // + // [Fact] + // public async Task Execute_StreamGrpcTccAndDo_TryFailed() + // { + // var provider = ITTestHelper.AddDtmGrpc(); + // var workflowFactory = provider.GetRequiredService(); + // var loggerFactory = provider.GetRequiredService(); + // WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + // + // string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryFailed)}-{Guid.NewGuid().ToString("D")[..8]}"; + // Task readTask = null; + // AsyncDuplexStreamingCall call = null; + // workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + // { + // BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + // + // Busi.BusiClient busiClient = null; + // + // ConcurrentDictionary progress = new ConcurrentDictionary(); + // + // // 1. grpc1 TCC + // Workflow wf = workflow.NewBranch() + // .OnCommit(async (barrier) => // confirm + // { + // await call.RequestStream.WriteAsync(new StreamRequest() + // { + // OperateType = OperateType.Confirm, + // BusiRequest = busiRequest, + // }); + // // wait Confirm + // while (!progress.ContainsKey(OperateType.Confirm)) + // Thread.Sleep(1000); + // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + // }) + // .OnRollback(async (barrier) => // cancel + // { + // await call.RequestStream.WriteAsync(new StreamRequest() + // { + // OperateType = OperateType.Confirm, + // BusiRequest = busiRequest, + // }); + // // wait Confirm + // while (!progress.ContainsKey(OperateType.Confirm)) + // Thread.Sleep(1000); + // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); + // }); + // busiClient = GetBusiClientWithWf(wf, provider); + // call = busiClient.StreamTransOutTcc(); + // using var call2 = call; + // readTask = Task.Run(async () => + // { + // try + // { + // await foreach (var response in call.ResponseStream.ReadAllAsync()) + // { + // _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); + // progress[response.OperateType] = new Status(StatusCode.OK, ""); + // } + // } + // catch (RpcException ex) + // { + // _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); + // progress[OperateType.Try] = ex.Status; // how assess response.OperateType + // } + // catch (Exception ex) + // { + // _testOutputHelper.WriteLine($"Exception caught: {ex}"); + // throw; + // } + // }); + // + // // try failed + // await call.RequestStream.WriteAsync(new StreamRequest() + // { + // OperateType = OperateType.Try, + // BusiRequest = busiRequest, + // }); + // // wait try + // while (!progress.ContainsKey(OperateType.Try)) + // Thread.Sleep(1000); + // Assert.Equal(StatusCode.Aborted, progress[OperateType.Try].StatusCode); + // Assert.Equal("FAILURE", progress[OperateType.Try].Detail); + // throw new DtmFailureException($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); + // // throw new Exception($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); + // }); + // + // string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + // var req = ITTestHelper.GenBusiReq(outFailed: true, false); + // + // DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + // TransGlobal trans; + // + // // first + // // await Assert.ThrowsAsync(async () => + // { + // byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // } + // // ); + // await readTask; + // trans = await dtmClient.Query(gid, CancellationToken.None); + // Assert.Equal("failed", trans.Transaction.Status); + // Assert.Equal(0, trans.Branches.Count); + // + // + // // same gid again + // await Assert.ThrowsAsync(async () => + // { + // var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // // DtmCommon.DtmFailureException + // // sub trans1 try failed(grpc): FAILURE + // // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + // // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs + // } + // ); + // } private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) { From 4ac3642897b27f0d055f5253b84bebc1ab0b7fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 28 Apr 2025 17:41:29 +0800 Subject: [PATCH 16/29] =?UTF-8?q?=E6=9B=BF=E6=8D=A2grpc=20stream=E7=9A=84?= =?UTF-8?q?=E5=8F=8C=E5=90=91=E6=B5=81=E6=8B=A6=E6=88=AA=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=8C=E6=94=B9=E4=B8=BA=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E7=94=A8workflow.Do=E5=AE=9E=E7=8E=B0=E5=88=86=E6=94=AF?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E5=AE=9E=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Dtmworkflow/WorkflowGrpcInterceptor.cs | 95 +--- .../Services/BusiApiService_Stream.cs | 7 +- .../BusiApiServiceTest.cs | 5 +- .../MyGrpcProcesser.cs | 1 + .../WorkflowGrpcStreamTest.cs | 425 ++++++++++++------ 5 files changed, 299 insertions(+), 234 deletions(-) diff --git a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs index b6f9470..245e53f 100644 --- a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs +++ b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs @@ -56,6 +56,7 @@ public override AsyncUnaryCall AsyncUnaryCall( ); } + // intercept phase1 only. CallPhase2 comes with RecordedDo if (wf.WorkflowImp.CurrentOp != DtmCommon.Constant.OpAction) { var (newCall, _, _) = Origin().GetAwaiter().GetResult(); @@ -69,100 +70,11 @@ public override AsyncUnaryCall AsyncUnaryCall( RpcException err = status.StatusCode != StatusCode.OK ? new RpcException(status) : null; return Task.FromResult(wf.StepResultFromGrpc(data as IMessage, err)); }).GetAwaiter().GetResult(); - wf.StepResultToGrpc(sr, null); + Exception exception = wf.StepResultToGrpc(sr, null); return call; } - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation) - { - logger?.LogDebug($"grpc client calling: {context.Host}{context.Method.FullName}"); - - if (wf == null) - { - return base.AsyncDuplexStreamingCall(context, continuation); - } - - var newContext = Dtmgimp.TransInfo2Ctx(context, wf.TransBase.Gid, wf.TransBase.TransType, wf.WorkflowImp.CurrentBranch, wf.WorkflowImp.CurrentOp, wf.TransBase.Dtm); - - var call = continuation(newContext); - - return new AsyncDuplexStreamingCall( - new InterceptingClientStreamWriter(call.RequestStream, async (request) => - { - logger?.LogDebug($"grpc client sending: {context.Host}{context.Method.FullName}"); - await call.RequestStream.WriteAsync(request); - }), - new InterceptingClientStreamReader(call.ResponseStream, async () => - { - try - { - var response = await call.ResponseStream.MoveNext(); - logger?.LogDebug($"grpc client received: {context.Host}{context.Method.FullName}"); - return response; - } - catch (Exception e) - { - logger?.LogError($"grpc client: {context.Host}{context.Method.FullName} ex: {e}"); - throw; - } - }), - call.ResponseHeadersAsync, - call.GetStatus, - call.GetTrailers, - call.Dispose - ); - } - - private class InterceptingClientStreamWriter : IClientStreamWriter - { - private readonly IClientStreamWriter _innerWriter; - private readonly Func _onWrite; - - public InterceptingClientStreamWriter(IClientStreamWriter innerWriter, Func onWrite) - { - _innerWriter = innerWriter; - _onWrite = onWrite; - } - - public WriteOptions WriteOptions - { - get => _innerWriter.WriteOptions; - set => _innerWriter.WriteOptions = value; - } - - public Task WriteAsync(T message) - { - return _onWrite(message).ContinueWith(_ => _innerWriter.WriteAsync(message)).Unwrap(); - } - - public Task CompleteAsync() - { - return _innerWriter.CompleteAsync(); - } - } - - private class InterceptingClientStreamReader : IAsyncStreamReader - { - private readonly IAsyncStreamReader _innerReader; - private readonly Func> _onMoveNext; - - public InterceptingClientStreamReader(IAsyncStreamReader innerReader, Func> onMoveNext) - { - _innerReader = innerReader; - _onMoveNext = onMoveNext; - } - - public T Current => _innerReader.Current; - - public Task MoveNext(CancellationToken cancellationToken) - { - return _onMoveNext(); - } - } - private class Dtmgimp { public static ClientInterceptorContext TransInfo2Ctx( @@ -192,6 +104,9 @@ public static ClientInterceptorContext TransInfo2Ctx( ctx.Method, diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs index b698cd0..b33e616 100644 --- a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -13,7 +13,8 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r await foreach (var request in requestStream.ReadAllAsync()) { var tb = _client.TransBaseFromGrpc(context); - _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={JsonSerializer.Serialize(tb)}, req={JsonSerializer.Serialize(request)}"); + string subCallId = context.RequestHeaders.GetValue("sub-call-id"); + _logger.LogInformation($"{nameof(StreamTransOutTcc)} subCallId: {subCallId} gid={tb.Gid} op={tb.Op}, req={JsonSerializer.Serialize(request)}"); switch (request.OperateType) { @@ -21,7 +22,6 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r { if (string.IsNullOrWhiteSpace(request.BusiRequest.TransOutResult) || request.BusiRequest.TransOutResult.Equals("SUCCESS")) { - await Task.CompletedTask; await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Tried, waiting your confirm..." }); } else if (request.BusiRequest.TransOutResult.Equals("FAILURE")) @@ -63,7 +63,8 @@ public override async Task StreamTransInTcc(IAsyncStreamReader re await foreach (var request in requestStream.ReadAllAsync()) { var tb = _client.TransBaseFromGrpc(context); - _logger.LogInformation($"{nameof(StreamTransOutTcc)} tb={JsonSerializer.Serialize(tb)}, req={JsonSerializer.Serialize(request)}"); + string subCallId = context.RequestHeaders.GetValue("sub-call-id"); + _logger.LogInformation($"{nameof(StreamTransOutTcc)} subCallId: {subCallId} tb={JsonSerializer.Serialize(tb)}, req={JsonSerializer.Serialize(request)}"); switch (request.OperateType) { diff --git a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs index f509ef5..64c23e2 100644 --- a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs @@ -24,7 +24,10 @@ public async Task StreamTransOutTcc_Try_Confirm() var provider = ITTestHelper.AddDtmGrpc(); Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); - using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); + var headers = new Metadata(); + headers.Add("sub-call-id", "init id"); // 主调用里放上, 用于跟后续response的配对 + + using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(headers); testOutputHelper.WriteLine("Starting background task to receive messages"); var myGrpcProcesser = new MyGrpcProcesser(call, testOutputHelper); var readTask = myGrpcProcesser.HandleResponse(); diff --git a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs index b89278b..c89ada1 100644 --- a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs +++ b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs @@ -29,6 +29,7 @@ public Task HandleResponse() } else { + // 这个位置被人问了, 会覆盖刚才问的。 progress[response.OperateType] = new TaskCompletionSource(new Status(StatusCode.OK, "")); } } diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs index 7be18fc..2da5c04 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -40,15 +40,13 @@ public async Task Execute_StreamGrpcTccAndDo_TryConfirm() string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; AsyncDuplexStreamingCall call = null; MyGrpcProcesser myGrpcProcesser = null; - + Task readTask = null; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - Busi.BusiClient busiClient = null; - // 1. grpc1 TCC - Workflow wf = workflow.NewBranch() + workflow.NewBranch() .OnCommit(async (barrier) => // confirm { await call.RequestStream.WriteAsync(new StreamRequest() @@ -71,36 +69,39 @@ await call.RequestStream.WriteAsync(new StreamRequest() var result = await myGrpcProcesser.GetResult(OperateType.Cancel); Assert.Equal(StatusCode.OK, result.StatusCode); }); - - busiClient = GetBusiClientWithWf(wf, provider); + Busi.BusiClient busiClient = GetBusiClientWithWf(workflow, provider); call = busiClient.StreamTransOutTcc(); myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); - Task readTask = myGrpcProcesser.HandleResponse(); - - using var call2 = call; - + readTask = myGrpcProcesser.HandleResponse(); // try - await call.RequestStream.WriteAsync(new StreamRequest() + var (_, stepEx) = await workflow.Do(async (barrier) => { - OperateType = OperateType.Try, - BusiRequest = busiRequest, - }); - // wait try - var result = await myGrpcProcesser.GetResult(OperateType.Try); - Assert.Equal(StatusCode.OK, result.StatusCode); + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + var result = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.OK, result.StatusCode); + return (""u8.ToArray(), null); + }); // 正向 + if (stepEx != null) + throw stepEx; // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 - (byte[] doResult, Exception ex) = await workflow.NewBranch() + (_, stepEx) = await workflow.NewBranch() // .OnRollback(async (barrier) => // 反向 rollback // { // _testOutputHelper.WriteLine("1. local rollback"); // }) - .Do(async (barrier) => { return ("my result"u8.ToArray(), null); }); // 正向 - if (ex != null) - throw ex; - - await call.RequestStream.CompleteAsync(); - await readTask; + .Do(async (barrier) => + { + _testOutputHelper.WriteLine("2. local do"); + return ("my result"u8.ToArray(), null); + }); // 正向 + if (stepEx != null) + throw stepEx; return await Task.FromResult("my result"u8.ToArray()); }); @@ -110,13 +111,15 @@ await call.RequestStream.WriteAsync(new StreamRequest() DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + using var call2 = call; // first byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + await call.RequestStream.CompleteAsync(); + await readTask; + Assert.Equal("my result", Encoding.UTF8.GetString(result)); TransGlobal trans = await dtmClient.Query(gid, CancellationToken.None); - - // BranchID Op Status // 01 action succeed // 02 action succeed @@ -134,7 +137,6 @@ await call.RequestStream.WriteAsync(new StreamRequest() result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); trans = await dtmClient.Query(gid, CancellationToken.None); - Assert.Equal("succeed", trans.Transaction.Status); Assert.Equal(3, trans.Branches.Count); Assert.Equal("succeed", trans.Branches[0].Status); @@ -145,119 +147,261 @@ await call.RequestStream.WriteAsync(new StreamRequest() Assert.Equal("commit", trans.Branches[2].Op); } - // [Fact] - // public async Task Execute_StreamGrpcTccAndDo_TryCancel() - // { - // var provider = ITTestHelper.AddDtmGrpc(); - // var workflowFactory = provider.GetRequiredService(); - // var loggerFactory = provider.GetRequiredService(); - // WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - // - // string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryCancel)}-{Guid.NewGuid().ToString("D")[..8]}"; - // AsyncDuplexStreamingCall call = null; - // workflowGlobalTransaction.Register(wfName1, async (workflow, data) => - // { - // BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - // - // Busi.BusiClient busiClient = null; - // - // ConcurrentDictionary progress = new ConcurrentDictionary(); - // - // // 1. grpc1 TCC - // Workflow wf = workflow.NewBranch() - // .OnCommit(async (barrier) => // confirm - // { - // await call.RequestStream.WriteAsync(new StreamRequest() - // { - // OperateType = OperateType.Confirm, - // BusiRequest = busiRequest, - // }); - // // wait Confirm - // while (!progress.ContainsKey(OperateType.Confirm)) - // Thread.Sleep(1000); - // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - // }) - // .OnRollback(async (barrier) => // cancel - // { - // await call.RequestStream.WriteAsync(new StreamRequest() - // { - // OperateType = OperateType.Confirm, - // BusiRequest = busiRequest, - // }); - // // wait Confirm - // while (!progress.ContainsKey(OperateType.Confirm)) - // Thread.Sleep(1000); - // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - // }); - // busiClient = GetBusiClientWithWf(wf, provider); - // call = busiClient.StreamTransOutTcc(); - // using var call2 = call; - // - // // try - // await call.RequestStream.WriteAsync(new StreamRequest() - // { - // OperateType = OperateType.Try, - // BusiRequest = busiRequest, - // }); - // // wait try - // while (!progress.ContainsKey(OperateType.Try)) - // Thread.Sleep(1000); - // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - // - // // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 - // (byte[] doResult, Exception ex) = await workflow.NewBranch() - // // .OnRollback(async (barrier) => // 反向 rollback - // // { - // // _testOutputHelper.WriteLine("1. local rollback"); - // // }) - // .Do(async (barrier) => - // { - // // throw new DtmFailureException("db do failed"); // can't throw - // var ex = new DtmFailureException("db do failed"); - // return ("my result"u8.ToArray(), ex); - // }); // 正向 - // if (ex != null) - // throw ex; - // - // return await Task.FromResult("my result"u8.ToArray()); - // }); - // - // string gid = wfName1 + Guid.NewGuid().ToString()[..8]; - // var req = ITTestHelper.GenBusiReq(false, false); - // - // DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); - // TransGlobal trans; - // - // // first - // byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // await readTask; - // Assert.Null(result); - // trans = await dtmClient.Query(gid, CancellationToken.None); - // // BranchID Op Status - // // 01 action succeed - // // 02 action succeed - // // 01 rollback succeed - // Assert.Equal("failed", trans.Transaction.Status); - // Assert.Equal(3, trans.Branches.Count); - // Assert.Equal("succeed", trans.Branches[0].Status); - // Assert.Equal("action", trans.Branches[0].Op); - // Assert.Equal("succeed", trans.Branches[1].Status); - // Assert.Equal("action", trans.Branches[1].Op); - // Assert.Equal("succeed", trans.Branches[2].Status); - // Assert.Equal("rollback", trans.Branches[2].Op); - // - // // same gid again - // Assert.ThrowsAsync(async () => - // { - // var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // // DtmCommon.DtmFailureException - // // db do failed - // // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs - // // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs - // // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs - // }); - // } - // + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TryCancel() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; + AsyncDuplexStreamingCall call = null; + MyGrpcProcesser myGrpcProcesser = null; + Task readTask = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Confirm); + Assert.Equal(StatusCode.OK, result.StatusCode); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Cancel, + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Cancel); + Assert.Equal(StatusCode.OK, result.StatusCode); + }); + Busi.BusiClient busiClient = GetBusiClientWithWf(workflow, provider); + call = busiClient.StreamTransOutTcc(); + myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); + readTask = myGrpcProcesser.HandleResponse(); + // try + var (_, stepEx) = await workflow.Do(async (barrier) => + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + var result = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.OK, result.StatusCode); + return (""u8.ToArray(), null); + }); // 正向 + if (stepEx != null) + throw stepEx; + + // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + (_, stepEx) = await workflow.NewBranch() + // .OnRollback(async (barrier) => // 反向 rollback + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + _testOutputHelper.WriteLine("2. db do with throw failed"); + // throw new DtmFailureException("db do failed"); // can't throw + var ex = new DtmFailureException("db do failed"); + return ("my result"u8.ToArray(), ex); + }); // 正向 + if (stepEx != null) + throw stepEx; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + + using var call2 = call; + // first + + // same gid again + await Assert.ThrowsAsync(async () => + { + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + + await call.RequestStream.CompleteAsync(); + await readTask; + + TransGlobal trans = await dtmClient.Query(gid, CancellationToken.None); + + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action failed + // 01 rollback succeed + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("failed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("rollback", trans.Branches[2].Op); + + // same gid again + Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + // DtmCommon.DtmFailureException + // db do failed + // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs + // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs + // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs + }); + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + Assert.Equal(3, trans.Branches.Count); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("failed", trans.Branches[1].Status); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[2].Status); + Assert.Equal("rollback", trans.Branches[2].Op); + } + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TryFailed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; + AsyncDuplexStreamingCall call = null; + MyGrpcProcesser myGrpcProcesser = null; + Task readTask = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Confirm); + Assert.Equal(StatusCode.Aborted, result.StatusCode); + Assert.Equal("FAILURE", result.Detail); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Cancel); + Assert.Equal(StatusCode.OK, result.StatusCode); + }); + Busi.BusiClient busiClient = GetBusiClientWithWf(workflow, provider); + call = busiClient.StreamTransOutTcc(); + myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); + readTask = myGrpcProcesser.HandleResponse(); + // try + var (_, stepEx) = await workflow.Do(async (barrier) => + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + BusiRequest = busiRequest, + }); + // wait try + var result = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.Aborted, result.StatusCode); + Assert.Equal("FAILURE", result.Detail); + + return (""u8.ToArray(), new DtmFailureException("Try grpc error")); + }); // 正向 + if (stepEx != null) + throw stepEx; + + // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + (_, stepEx) = await workflow.NewBranch() + // .OnRollback(async (barrier) => // 反向 rollback + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + _testOutputHelper.WriteLine("2. local do"); + return ("my result"u8.ToArray(), null); + }); // 正向 + if (stepEx != null) + throw stepEx; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(true, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + + using var call2 = call; + // first + await Assert.ThrowsAsync(async () => + { + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + } + ); + await call.RequestStream.CompleteAsync(); + await Assert.ThrowsAsync(async () => { await readTask; }); // grpc aborted by server try method + + var trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action failed + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("failed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + + // same gid again + await Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + + await call.RequestStream.CompleteAsync(); + await Assert.ThrowsAsync(async () => { await readTask; }); // grpc aborted by server try method + + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action failed + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("failed", trans.Branches[0].Status); + Assert.Equal("action", trans.Branches[0].Op); + } + // // [Fact] // public async Task Execute_StreamGrpcTccAndDo_TryFailed() @@ -382,6 +526,7 @@ private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider var logger = loggerFactory.CreateLogger(); var interceptor = new WorkflowGrpcInterceptor(wf, logger); // inject client interceptor, and workflow instance var callInvoker = channel.Intercept(interceptor); + // var callInvoker = channel.Intercept(); Busi.BusiClient busiClient = new Busi.BusiClient(callInvoker); return busiClient; } From 1f8d88f09e04612cb746077a7c77535c0a72df3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 6 May 2025 15:11:48 +0800 Subject: [PATCH 17/29] test(Dtmgrpc.IntegrationTests): fix expected DtmFailureException in multiple test cases --- .../WorkflowGrpcTest.cs | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index 04c4da1..fd49c20 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -198,9 +198,12 @@ public async Task Execute_DoAndHttp_Failed() DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - - byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - Assert.Null(result); + + await Assert.ThrowsAsync(async () => + { + byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + // same gid again await Assert.ThrowsAsync( async () => { @@ -345,10 +348,13 @@ public async Task Execute_DoAndGrpcSAGA_Should_Failed() DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - + // first - byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // Assert.Null(result); + await Assert.ThrowsAsync(async () => + { + byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + trans = await dtmClient.Query(gid, CancellationToken.None); Assert.Equal("failed", trans.Transaction.Status); // BranchID Op Status CreateTime UpdateTime Url @@ -368,11 +374,11 @@ public async Task Execute_DoAndGrpcSAGA_Should_Failed() Assert.Equal("succeed", trans.Branches[3].Status); Assert.Equal("rollback", trans.Branches[4].Op); Assert.Equal("succeed", trans.Branches[4].Status); - + // same gid again - await Assert.ThrowsAsync( async () => + await Assert.ThrowsAsync(async () => { - result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); } @@ -505,10 +511,12 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - + // first - byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - Assert.Null(result); + await Assert.ThrowsAsync(async () => + { + byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); trans = await dtmClient.Query(gid, CancellationToken.None); // BranchID Op Status // 01 action failed @@ -520,7 +528,7 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() // same gid again await Assert.ThrowsAsync(async () => { - var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); // DtmCommon.DtmFailureException: Status(StatusCode="Aborted", Detail="FAILURE") // // DtmCommon.DtmFailureException @@ -538,7 +546,6 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() Assert.Equal("action", trans.Branches[0].Op); } - [Fact] public async Task Execute_GrpcTccAndDo_Should_DoFailed() { @@ -593,8 +600,11 @@ public async Task Execute_GrpcTccAndDo_Should_DoFailed() TransGlobal trans; // first - byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - Assert.Null(result); + await Assert.ThrowsAsync(async () => + { + byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + trans = await dtmClient.Query(gid, CancellationToken.None); // BranchID Op Status // 01 action succeed From c8388926be972a88029ed6c7a33968de4f8c50d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 6 May 2025 15:33:17 +0800 Subject: [PATCH 18/29] refactor(Dtmgrpc.IntegrationTests): Fix MyGrpcProcesser multi-threaded concurrency bugs --- .../MyGrpcProcesser.cs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs index c89ada1..6588a4c 100644 --- a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs +++ b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs @@ -23,26 +23,17 @@ public Task HandleResponse() await foreach (var response in asyncEnumerable) { testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - if (progress.TryGetValue(response.OperateType, out var tcs)) - { - tcs.TrySetResult(new Status(StatusCode.OK, "")); - } - else - { - // 这个位置被人问了, 会覆盖刚才问的。 - progress[response.OperateType] = new TaskCompletionSource(new Status(StatusCode.OK, "")); - } + TaskCompletionSource tcs = progress.GetOrAdd(response.OperateType, type => new TaskCompletionSource()); + tcs.SetResult(new Status(StatusCode.OK, "")); } } catch (RpcException ex) { testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - if (progress.TryGetValue(OperateType.Try, out var tcs)) - { - bool _ = tcs.TrySetResult(ex.Status); - } - else - progress[OperateType.Try] = new TaskCompletionSource(ex.Status); // TODO 应答对应的哪个请求 + + // TODO 应答对应的哪个请求 + var tcs = progress.GetOrAdd(OperateType.Try, type => new TaskCompletionSource()); + tcs.SetResult(ex.Status); _callDisposed.SetResult(); throw; @@ -58,11 +49,7 @@ public Task HandleResponse() public async Task GetResult(OperateType operateType) { - if (!progress.TryGetValue(operateType, out var tcs)) - { - tcs = new TaskCompletionSource(); - progress[operateType] = tcs; - } + TaskCompletionSource tcs = progress.GetOrAdd(operateType, type => new TaskCompletionSource()); Task.WaitAny(_callDisposed.Task, tcs.Task); return await tcs.Task; From 387f4ad3ccb83e92bfdc3bb13d31b306513394a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 6 May 2025 16:14:14 +0800 Subject: [PATCH 19/29] feat(test): add DtmBranchTransInfo to StreamRequest and update related services - Add DtmBranchTransInfo message to busi.proto - Update BusiApiService_Stream to use DtmBranchTransInfo - Modify WorkflowGrpcStreamTest to include DtmBranchTransInfo in test cases --- .../Services/BusiApiService_Stream.cs | 29 +++++++++++++++---- .../WorkflowGrpcStreamTest.cs | 22 ++++++++++++++ tests/protos/busi.proto | 12 +++++++- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs index b33e616..887c593 100644 --- a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -1,5 +1,6 @@ using System.Text.Json; using busi; +using DtmCommon; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -12,9 +13,17 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r // stream try -> confirm/cancel await foreach (var request in requestStream.ReadAllAsync()) { - var tb = _client.TransBaseFromGrpc(context); - string subCallId = context.RequestHeaders.GetValue("sub-call-id"); - _logger.LogInformation($"{nameof(StreamTransOutTcc)} subCallId: {subCallId} gid={tb.Gid} op={tb.Op}, req={JsonSerializer.Serialize(request)}"); + BranchBarrier barrier = _barrierFactory.CreateBranchBarrier( + request.DtmBranchTransInfo.TransType, + request.DtmBranchTransInfo.Gid, + request.DtmBranchTransInfo.BranchId, + request.DtmBranchTransInfo.Op, _logger); + _logger.LogInformation( + $"{nameof(StreamTransOutTcc)} gid={barrier.Gid} branch_id={barrier.BranchID} op={barrier.Op}, req={JsonSerializer.Serialize(request)}"); + // barrier.Call(db, transaction => + // { + // + // }); switch (request.OperateType) { @@ -62,9 +71,17 @@ public override async Task StreamTransInTcc(IAsyncStreamReader re // stream try -> confirm/cancel await foreach (var request in requestStream.ReadAllAsync()) { - var tb = _client.TransBaseFromGrpc(context); - string subCallId = context.RequestHeaders.GetValue("sub-call-id"); - _logger.LogInformation($"{nameof(StreamTransOutTcc)} subCallId: {subCallId} tb={JsonSerializer.Serialize(tb)}, req={JsonSerializer.Serialize(request)}"); + BranchBarrier barrier = _barrierFactory.CreateBranchBarrier( + request.DtmBranchTransInfo.TransType, + request.DtmBranchTransInfo.Gid, + request.DtmBranchTransInfo.BranchId, + request.DtmBranchTransInfo.Op, _logger); + _logger.LogInformation( + $"{nameof(StreamTransOutTcc)} trans_type={barrier.TransType} gid={barrier.Gid} branch_id={barrier.BranchID} op={barrier.Op}, req={JsonSerializer.Serialize(request)}"); + // barrier.Call(db, transaction => + // { + // + // }); switch (request.OperateType) { diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs index 2da5c04..3284ff6 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -52,6 +52,7 @@ public async Task Execute_StreamGrpcTccAndDo_TryConfirm() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait Confirm @@ -63,6 +64,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait Confirm @@ -79,6 +81,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Try, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait try @@ -147,6 +150,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() Assert.Equal("commit", trans.Branches[2].Op); } + [Fact] public async Task Execute_StreamGrpcTccAndDo_TryCancel() { @@ -170,6 +174,7 @@ public async Task Execute_StreamGrpcTccAndDo_TryCancel() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait Confirm @@ -181,6 +186,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Cancel, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait Confirm @@ -197,6 +203,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Try, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait try @@ -302,6 +309,7 @@ public async Task Execute_StreamGrpcTccAndDo_TryFailed() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait Confirm @@ -314,6 +322,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait Confirm @@ -330,6 +339,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() await call.RequestStream.WriteAsync(new StreamRequest() { OperateType = OperateType.Try, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); // wait try @@ -530,5 +540,17 @@ private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider Busi.BusiClient busiClient = new Busi.BusiClient(callInvoker); return busiClient; } + + private DtmBranchTransInfo CurrentBranchTransInfo(Workflow wf) + { + return new DtmBranchTransInfo() + { + Gid = wf.TransBase.Gid, + TransType = wf.TransBase.TransType, + BranchId = wf.WorkflowImp.CurrentBranch, + Op = wf.WorkflowImp.CurrentOp, + Dtm = wf.TransBase.Dtm, + }; + } } } \ No newline at end of file diff --git a/tests/protos/busi.proto b/tests/protos/busi.proto index 8b34364..b992a42 100644 --- a/tests/protos/busi.proto +++ b/tests/protos/busi.proto @@ -22,12 +22,22 @@ enum OperateType { Confirm = 1; Cancel = 2; } + +message DtmBranchTransInfo { + string Gid = 1; + string TransType = 2; + string BranchId = 3; + string Op = 4; + string Dtm = 5; +} + enum OperateResult { Success = 0; Fail = 1; } message StreamRequest { OperateType OperateType = 1; + DtmBranchTransInfo DtmBranchTransInfo = 2; BusiReq busiRequest = 3; } message StreamReply { @@ -66,7 +76,7 @@ service Busi { rpc QueryPrepared(BusiReq) returns (BusiReply) {} rpc QueryPreparedMySqlReal(BusiReq) returns (google.protobuf.Empty) {} rpc QueryPreparedRedis(BusiReq) returns (google.protobuf.Empty) {} - + // stream TCC rpc StreamTransInTcc(stream StreamRequest) returns (stream StreamReply) {} rpc StreamTransOutTcc(stream StreamRequest) returns (stream StreamReply) {} From 5379b8935e05f509b761e925dd648e5b77eaeacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 6 May 2025 17:18:57 +0800 Subject: [PATCH 20/29] refactor(BusiGrpcService): Add branchBarrier.Call demo with TransactionScope - commented, because without database --- .../Services/BusiApiService_Stream.cs | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs index 887c593..ec88c28 100644 --- a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -1,4 +1,6 @@ +using System.Text; using System.Text.Json; +using System.Transactions; using busi; using DtmCommon; using Google.Protobuf.WellKnownTypes; @@ -13,53 +15,63 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r // stream try -> confirm/cancel await foreach (var request in requestStream.ReadAllAsync()) { - BranchBarrier barrier = _barrierFactory.CreateBranchBarrier( + BranchBarrier branchBarrier = _barrierFactory.CreateBranchBarrier( request.DtmBranchTransInfo.TransType, request.DtmBranchTransInfo.Gid, request.DtmBranchTransInfo.BranchId, request.DtmBranchTransInfo.Op, _logger); _logger.LogInformation( - $"{nameof(StreamTransOutTcc)} gid={barrier.Gid} branch_id={barrier.BranchID} op={barrier.Op}, req={JsonSerializer.Serialize(request)}"); - // barrier.Call(db, transaction => - // { - // - // }); + $"{nameof(StreamTransOutTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}, req={JsonSerializer.Serialize(request)}"); - switch (request.OperateType) - { - case OperateType.Try: - { - if (string.IsNullOrWhiteSpace(request.BusiRequest.TransOutResult) || request.BusiRequest.TransOutResult.Equals("SUCCESS")) - { - await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Tried, waiting your confirm..." }); - } - else if (request.BusiRequest.TransOutResult.Equals("FAILURE")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); - } - else if (request.BusiRequest.TransOutResult.Equals("ONGOING")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); - } - else + // using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, + // new TransactionOptions() + // { + // IsolationLevel = IsolationLevel.ReadCommitted + // }, + // TransactionScopeAsyncFlowOption.Enabled)) + // { + // //DbConnectionManager.GetConnection("TestDB") + // await branchBarrier.Call(db: null, async () => + // { + switch (request.OperateType) { - throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransOutResult}")); - } + case OperateType.Try: + { + if (string.IsNullOrWhiteSpace(request.BusiRequest.TransOutResult) || request.BusiRequest.TransOutResult.Equals("SUCCESS")) + { + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Tried, waiting your confirm..." }); + } + else if (request.BusiRequest.TransOutResult.Equals("FAILURE")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); + } + else if (request.BusiRequest.TransOutResult.Equals("ONGOING")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); + } + else + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransOutResult}")); + } - break; - } - case OperateType.Confirm: - { - await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Confirmed" }); - break; - } - case OperateType.Cancel: - { - await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Canceled" }); - break; - } - default: - throw new ArgumentOutOfRangeException(); + break; + } + case OperateType.Confirm: + { + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Confirmed" }); + break; + } + case OperateType.Cancel: + { + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Canceled" }); + break; + } + default: + throw new ArgumentOutOfRangeException(); + // } + // }); + // + // scope.Complete(); } } @@ -78,10 +90,6 @@ public override async Task StreamTransInTcc(IAsyncStreamReader re request.DtmBranchTransInfo.Op, _logger); _logger.LogInformation( $"{nameof(StreamTransOutTcc)} trans_type={barrier.TransType} gid={barrier.Gid} branch_id={barrier.BranchID} op={barrier.Op}, req={JsonSerializer.Serialize(request)}"); - // barrier.Call(db, transaction => - // { - // - // }); switch (request.OperateType) { From a2fa337d34dea7dda9974c8b2f642cd8fc3d6d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Tue, 6 May 2025 18:07:34 +0800 Subject: [PATCH 21/29] fix: fix workflow http interceptor missed adding query strings --- src/Dtmworkflow/WorkflowHttpInterceptor.cs | 11 +++++++++++ tests/BusiGrpcService/Program.cs | 20 ++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Dtmworkflow/WorkflowHttpInterceptor.cs b/src/Dtmworkflow/WorkflowHttpInterceptor.cs index 81110d3..86b4829 100644 --- a/src/Dtmworkflow/WorkflowHttpInterceptor.cs +++ b/src/Dtmworkflow/WorkflowHttpInterceptor.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using System.Web; namespace Dtmworkflow; @@ -19,6 +20,16 @@ protected override async Task SendAsync(HttpRequestMessage { Func> origin = async (barrier) => { + var uriBuilder = new UriBuilder(request.RequestUri); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + query["branch_id"] = _wf.WorkflowImp.CurrentBranch; + query["gid"] = _wf.TransBase.Gid; + query["op"] = _wf.WorkflowImp.CurrentOp; + query["trans_type"] = _wf.TransBase.TransType; + query["dtm"] = _wf.TransBase.Dtm; + uriBuilder.Query = query.ToString(); + request.RequestUri = uriBuilder.Uri; + var response = await base.SendAsync(request, cancellationToken); return _wf.StepResultFromHTTP(response, null); }; diff --git a/tests/BusiGrpcService/Program.cs b/tests/BusiGrpcService/Program.cs index 0440ba0..a4742ca 100644 --- a/tests/BusiGrpcService/Program.cs +++ b/tests/BusiGrpcService/Program.cs @@ -29,13 +29,25 @@ app.MapGrpcReflectionService(); // test for workflow http branch -app.MapGet("/test-http-ok1", () => "SUCCESS"); -app.MapGet("/test-http-ok2", () => "SUCCESS"); +app.MapGet("/test-http-ok1", context => +{ + Console.Out.WriteLine($"QueryString: {context.Request.QueryString}"); + context.Response.StatusCode = 200; + return context.Response.WriteAsync("SUCCESS"); // FAILURE +}); + +app.MapGet("/test-http-ok2", context => +{ + Console.Out.WriteLine($"QueryString: {context.Request.QueryString}"); + context.Response.StatusCode = 200; + return context.Response.WriteAsync("SUCCESS"); // FAILURE +}); app.MapGet("/409", context => -{ +{ + Console.Out.WriteLine($"QueryString: {context.Request.QueryString}"); context.Response.StatusCode = 409; return context.Response.WriteAsync("i am body, the http branch is 409"); // FAILURE }); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); -app.Run(); +app.Run(); \ No newline at end of file From 7719c9652f4b9a24f8d41aaa531bfb812bc0f79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 7 May 2025 09:32:49 +0800 Subject: [PATCH 22/29] feat(BusiGrpcService): refactor HTTP tests and integrate Swagger - Refactor HTTP test endpoints into a controller - Add Swagger support for API documentation n --- tests/BusiGrpcService/BusiGrpcService.csproj | 1 + .../Controllers/WorkflowHttpTestController.cs | 30 ++++++++++++++++ tests/BusiGrpcService/Program.cs | 34 ++++++------------- 3 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 tests/BusiGrpcService/Controllers/WorkflowHttpTestController.cs diff --git a/tests/BusiGrpcService/BusiGrpcService.csproj b/tests/BusiGrpcService/BusiGrpcService.csproj index efa0382..0f66750 100644 --- a/tests/BusiGrpcService/BusiGrpcService.csproj +++ b/tests/BusiGrpcService/BusiGrpcService.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/BusiGrpcService/Controllers/WorkflowHttpTestController.cs b/tests/BusiGrpcService/Controllers/WorkflowHttpTestController.cs new file mode 100644 index 0000000..34e0d90 --- /dev/null +++ b/tests/BusiGrpcService/Controllers/WorkflowHttpTestController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; + +namespace BusiGrpcService.Controllers +{ + [ApiController] + public class WorkflowHttpTestController : ControllerBase + { + [HttpGet("test-http-ok1")] + public IActionResult TestHttpOk1() + { + Console.Out.WriteLine($"QueryString: {Request.QueryString}"); + return Content("SUCCESS"); + } + + [HttpGet("test-http-ok2")] + public IActionResult TestHttpOk2() + { + Console.Out.WriteLine($"QueryString: {Request.QueryString}"); + return Content("SUCCESS"); + } + + [HttpGet("409")] + public IActionResult Test409() + { + Console.Out.WriteLine($"QueryString: {Request.QueryString}"); + Response.StatusCode = 409; + return Content("i am body, the http branch is 409"); + } + } +} \ No newline at end of file diff --git a/tests/BusiGrpcService/Program.cs b/tests/BusiGrpcService/Program.cs index a4742ca..9a18d11 100644 --- a/tests/BusiGrpcService/Program.cs +++ b/tests/BusiGrpcService/Program.cs @@ -14,10 +14,10 @@ builder.Services.AddGrpc(); builder.Services.AddGrpcReflection(); -builder.Services.AddDtmGrpc(x => -{ - x.DtmGrpcUrl = "http://localhost:36790"; -}); +builder.Services.AddDtmGrpc(x => { x.DtmGrpcUrl = "http://localhost:36790"; }); +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); var app = builder.Build(); @@ -26,28 +26,16 @@ IWebHostEnvironment env = app.Environment; if (env.IsDevelopment()) +{ app.MapGrpcReflectionService(); + app.UseSwagger(); + app.UseSwaggerUI(); +} -// test for workflow http branch -app.MapGet("/test-http-ok1", context => -{ - Console.Out.WriteLine($"QueryString: {context.Request.QueryString}"); - context.Response.StatusCode = 200; - return context.Response.WriteAsync("SUCCESS"); // FAILURE -}); -app.MapGet("/test-http-ok2", context => -{ - Console.Out.WriteLine($"QueryString: {context.Request.QueryString}"); - context.Response.StatusCode = 200; - return context.Response.WriteAsync("SUCCESS"); // FAILURE -}); -app.MapGet("/409", context => -{ - Console.Out.WriteLine($"QueryString: {context.Request.QueryString}"); - context.Response.StatusCode = 409; - return context.Response.WriteAsync("i am body, the http branch is 409"); // FAILURE -}); +app.MapSwagger(); +app.MapDefaultControllerRoute(); +app.MapControllers(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.Run(); \ No newline at end of file From ea28bcd3329a326b4255af41029c749f0e97bc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 7 May 2025 10:19:22 +0800 Subject: [PATCH 23/29] feature(BusiGrpcService): streamline gRPC service methods BranchBarrier.call demo --- .../Services/BusiApiService_Stream.cs | 225 ++++++++++-------- 1 file changed, 123 insertions(+), 102 deletions(-) diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs index ec88c28..62732d9 100644 --- a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -5,6 +5,7 @@ using DtmCommon; using Google.Protobuf.WellKnownTypes; using Grpc.Core; +using MySqlConnector; namespace BusiGrpcService.Services; @@ -15,132 +16,152 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r // stream try -> confirm/cancel await foreach (var request in requestStream.ReadAllAsync()) { - BranchBarrier branchBarrier = _barrierFactory.CreateBranchBarrier( - request.DtmBranchTransInfo.TransType, - request.DtmBranchTransInfo.Gid, - request.DtmBranchTransInfo.BranchId, - request.DtmBranchTransInfo.Op, _logger); - _logger.LogInformation( - $"{nameof(StreamTransOutTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}, req={JsonSerializer.Serialize(request)}"); - - // using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, - // new TransactionOptions() - // { - // IsolationLevel = IsolationLevel.ReadCommitted - // }, - // TransactionScopeAsyncFlowOption.Enabled)) - // { - // //DbConnectionManager.GetConnection("TestDB") - // await branchBarrier.Call(db: null, async () => - // { - switch (request.OperateType) - { - case OperateType.Try: - { - if (string.IsNullOrWhiteSpace(request.BusiRequest.TransOutResult) || request.BusiRequest.TransOutResult.Equals("SUCCESS")) - { - await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Tried, waiting your confirm..." }); - } - else if (request.BusiRequest.TransOutResult.Equals("FAILURE")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); - } - else if (request.BusiRequest.TransOutResult.Equals("ONGOING")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); - } - else - { - throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransOutResult}")); - } + if (request.DtmBranchTransInfo != null) + { + BranchBarrier branchBarrier = _barrierFactory.CreateBranchBarrier( + request.DtmBranchTransInfo.TransType, + request.DtmBranchTransInfo.Gid, + request.DtmBranchTransInfo.BranchId, + request.DtmBranchTransInfo.Op, _logger); + _logger.LogInformation( + $"{nameof(StreamTransOutTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}, req={JsonSerializer.Serialize(request)}"); - break; - } - case OperateType.Confirm: - { - await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Confirmed" }); - break; - } - case OperateType.Cancel: - { - await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Canceled" }); - break; - } - default: - throw new ArgumentOutOfRangeException(); - // } - // }); - // - // scope.Complete(); + await using MySqlConnection conn = GetBarrierConn(); + await branchBarrier.Call(conn, async () => + { + // business logic + await TransOutFn(responseStream, request); + }); + } + else + { + await TransOutFn(responseStream, request); } } _logger.LogInformation($"{nameof(StreamTransOutTcc)} completed"); } + private static async Task TransOutFn(IServerStreamWriter responseStream, StreamRequest request) + { + switch (request.OperateType) + { + case OperateType.Try: + { + if (string.IsNullOrWhiteSpace(request.BusiRequest.TransOutResult) || request.BusiRequest.TransOutResult.Equals("SUCCESS")) + { + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Tried, waiting your confirm..." }); + } + else if (request.BusiRequest.TransOutResult.Equals("FAILURE")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); + } + else if (request.BusiRequest.TransOutResult.Equals("ONGOING")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); + } + else + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransOutResult}")); + } + + break; + } + case OperateType.Confirm: + { + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Confirmed" }); + break; + } + case OperateType.Cancel: + { + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = "Canceled" }); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + public override async Task StreamTransInTcc(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { // stream try -> confirm/cancel await foreach (var request in requestStream.ReadAllAsync()) { - BranchBarrier barrier = _barrierFactory.CreateBranchBarrier( - request.DtmBranchTransInfo.TransType, - request.DtmBranchTransInfo.Gid, - request.DtmBranchTransInfo.BranchId, - request.DtmBranchTransInfo.Op, _logger); - _logger.LogInformation( - $"{nameof(StreamTransOutTcc)} trans_type={barrier.TransType} gid={barrier.Gid} branch_id={barrier.BranchID} op={barrier.Op}, req={JsonSerializer.Serialize(request)}"); - - switch (request.OperateType) + if (request.DtmBranchTransInfo != null) { - case OperateType.Try: + BranchBarrier branchBarrier = _barrierFactory.CreateBranchBarrier( + request.DtmBranchTransInfo.TransType, + request.DtmBranchTransInfo.Gid, + request.DtmBranchTransInfo.BranchId, + request.DtmBranchTransInfo.Op, _logger); + _logger.LogInformation( + $"{nameof(StreamTransInTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}, req={JsonSerializer.Serialize(request)}"); + + await using MySqlConnection conn = GetBarrierConn(); + await branchBarrier.Call(conn, async () => { - if (string.IsNullOrWhiteSpace(request.BusiRequest.TransInResult) || request.BusiRequest.TransInResult.Equals("SUCCESS")) - { - await responseStream.WriteAsync(new StreamReply - { - OperateType = request.OperateType, - Message = "Tried, waiting your confirm..." - }); - } - else if (request.BusiRequest.TransInResult.Equals("FAILURE")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); - } - else if (request.BusiRequest.TransInResult.Equals("ONGOING")) - { - throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); - } - else - { - throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransInResult}")); - } + // business logic + await TransInFn(responseStream, request); + }); + } + else + { + await TransInFn(responseStream, request); + } + } - break; - } - case OperateType.Confirm: + _logger.LogInformation($"{nameof(StreamTransInTcc)} completed"); + } + + private static async Task TransInFn(IServerStreamWriter responseStream, StreamRequest request) + { + switch (request.OperateType) + { + case OperateType.Try: + { + if (string.IsNullOrWhiteSpace(request.BusiRequest.TransInResult) || request.BusiRequest.TransInResult.Equals("SUCCESS")) { await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, - Message = "Confirmed" + Message = "Tried, waiting your confirm..." }); - break; } - case OperateType.Cancel: + else if (request.BusiRequest.TransInResult.Equals("FAILURE")) { - await responseStream.WriteAsync(new StreamReply - { - OperateType = request.OperateType, - Message = "Canceled" - }); - break; + throw new Grpc.Core.RpcException(new Status(StatusCode.Aborted, "FAILURE")); + } + else if (request.BusiRequest.TransInResult.Equals("ONGOING")) + { + throw new Grpc.Core.RpcException(new Status(StatusCode.FailedPrecondition, "ONGOING")); } - default: - throw new ArgumentOutOfRangeException(); + else + { + throw new Grpc.Core.RpcException(new Status(StatusCode.Internal, $"unknow result {request.BusiRequest.TransInResult}")); + } + + break; } + case OperateType.Confirm: + { + await responseStream.WriteAsync(new StreamReply + { + OperateType = request.OperateType, + Message = "Confirmed" + }); + break; + } + case OperateType.Cancel: + { + await responseStream.WriteAsync(new StreamReply + { + OperateType = request.OperateType, + Message = "Canceled" + }); + break; + } + default: + throw new ArgumentOutOfRangeException(); } - - _logger.LogInformation($"{nameof(StreamTransInTcc)} completed"); } } \ No newline at end of file From dc7f64ec2bcb5cfbe3cefb57a6f15b1de87b3aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 7 May 2025 10:47:35 +0800 Subject: [PATCH 24/29] refactor(tests): Chinese comments to English --- src/Dtmworkflow/Workflow.Imp.cs | 2 +- src/Dtmworkflow/WorkflowGrpcInterceptor.cs | 5 - .../BusiApiServiceTest.cs | 5 +- .../MyGrpcProcesser.cs | 2 +- .../WorkflowGrpcStreamTest.cs | 26 ++-- .../WorkflowGrpcTest.cs | 128 +++++++----------- 6 files changed, 68 insertions(+), 100 deletions(-) diff --git a/src/Dtmworkflow/Workflow.Imp.cs b/src/Dtmworkflow/Workflow.Imp.cs index 41f9f5f..d4e92ed 100644 --- a/src/Dtmworkflow/Workflow.Imp.cs +++ b/src/Dtmworkflow/Workflow.Imp.cs @@ -215,7 +215,7 @@ internal StepResult StepResultFromHTTP(HttpResponseMessage resp, Exception err) if (err == null) { - (sr.Data, sr.Error) = Utils.HTTPResp2DtmError(resp); // TODO go 使用了 this.Options.HTTPResp2DtmError(resp), 方便定制 + (sr.Data, sr.Error) = Utils.HTTPResp2DtmError(resp); // TODO go used this.Options.HTTPResp2DtmError(resp), for custom sr.Status = WfErrorToStatus(sr.Error); } diff --git a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs index 245e53f..3f46660 100644 --- a/src/Dtmworkflow/WorkflowGrpcInterceptor.cs +++ b/src/Dtmworkflow/WorkflowGrpcInterceptor.cs @@ -85,9 +85,7 @@ public static ClientInterceptorContext TransInfo2Ctx TransInfo2Ctx TransInfo2Ctx( ctx.Method, ctx.Host, diff --git a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs index 64c23e2..f509ef5 100644 --- a/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/BusiApiServiceTest.cs @@ -24,10 +24,7 @@ public async Task StreamTransOutTcc_Try_Confirm() var provider = ITTestHelper.AddDtmGrpc(); Busi.BusiClient busiClient = GetBusiClientWithWf(null, provider); - var headers = new Metadata(); - headers.Add("sub-call-id", "init id"); // 主调用里放上, 用于跟后续response的配对 - - using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(headers); + using AsyncDuplexStreamingCall call = busiClient.StreamTransOutTcc(); testOutputHelper.WriteLine("Starting background task to receive messages"); var myGrpcProcesser = new MyGrpcProcesser(call, testOutputHelper); var readTask = myGrpcProcesser.HandleResponse(); diff --git a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs index 6588a4c..40da610 100644 --- a/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs +++ b/tests/Dtmgrpc.IntegrationTests/MyGrpcProcesser.cs @@ -31,7 +31,7 @@ public Task HandleResponse() { testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - // TODO 应答对应的哪个请求 + // TODO which request does the response correspond to? var tcs = progress.GetOrAdd(OperateType.Try, type => new TaskCompletionSource()); tcs.SetResult(ex.Status); diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs index 3284ff6..19677a2 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -88,13 +88,13 @@ await call.RequestStream.WriteAsync(new StreamRequest() var result = await myGrpcProcesser.GetResult(OperateType.Try); Assert.Equal(StatusCode.OK, result.StatusCode); return (""u8.ToArray(), null); - }); // 正向 + }); if (stepEx != null) throw stepEx; - // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. (_, stepEx) = await workflow.NewBranch() - // .OnRollback(async (barrier) => // 反向 rollback + // .OnRollback(async (barrier) => // { // _testOutputHelper.WriteLine("1. local rollback"); // }) @@ -102,7 +102,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() { _testOutputHelper.WriteLine("2. local do"); return ("my result"u8.ToArray(), null); - }); // 正向 + }); if (stepEx != null) throw stepEx; @@ -210,13 +210,13 @@ await call.RequestStream.WriteAsync(new StreamRequest() var result = await myGrpcProcesser.GetResult(OperateType.Try); Assert.Equal(StatusCode.OK, result.StatusCode); return (""u8.ToArray(), null); - }); // 正向 + }); if (stepEx != null) throw stepEx; - // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. (_, stepEx) = await workflow.NewBranch() - // .OnRollback(async (barrier) => // 反向 rollback + // .OnRollback(async (barrier) => // { // _testOutputHelper.WriteLine("1. local rollback"); // }) @@ -226,7 +226,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() // throw new DtmFailureException("db do failed"); // can't throw var ex = new DtmFailureException("db do failed"); return ("my result"u8.ToArray(), ex); - }); // 正向 + }); if (stepEx != null) throw stepEx; @@ -348,13 +348,13 @@ await call.RequestStream.WriteAsync(new StreamRequest() Assert.Equal("FAILURE", result.Detail); return (""u8.ToArray(), new DtmFailureException("Try grpc error")); - }); // 正向 + }); if (stepEx != null) throw stepEx; - // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. (_, stepEx) = await workflow.NewBranch() - // .OnRollback(async (barrier) => // 反向 rollback + // .OnRollback(async (barrier) => // { // _testOutputHelper.WriteLine("1. local rollback"); // }) @@ -362,7 +362,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() { _testOutputHelper.WriteLine("2. local do"); return ("my result"u8.ToArray(), null); - }); // 正向 + }); if (stepEx != null) throw stepEx; @@ -540,7 +540,7 @@ private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider Busi.BusiClient busiClient = new Busi.BusiClient(callInvoker); return busiClient; } - + private DtmBranchTransInfo CurrentBranchTransInfo(Workflow wf) { return new DtmBranchTransInfo() diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs index fd49c20..3f23545 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs @@ -86,20 +86,17 @@ public async Task Execute_DoAndHttp_ShouldSuccess() { _testOutputHelper.WriteLine("1. local rollback"); await Task.CompletedTask; - }).Do(async (barrier) => - { - return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); - }); - + }).Do(async (barrier) => { return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); }); + // 2. http1, SAGA - HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => + HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http1 rollback"); await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); - + // 3. http2, TCC - HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => + HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http2 cancel"); await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); @@ -109,16 +106,16 @@ public async Task Execute_DoAndHttp_ShouldSuccess() // NOT must use workflow.NewRequest() await workflow.NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); - + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - + // BranchID Op Status // 01 action succeed // 02 action succeed @@ -138,7 +135,7 @@ public async Task Execute_DoAndHttp_ShouldSuccess() Assert.Equal("succeed", trans.Branches[2].Status); Assert.Equal("commit", trans.Branches[3].Op); Assert.Equal("succeed", trans.Branches[3].Status); - + // same gid again result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); @@ -154,7 +151,7 @@ public async Task Execute_DoAndHttp_ShouldSuccess() Assert.Equal("commit", trans.Branches[3].Op); Assert.Equal("succeed", trans.Branches[3].Status); } - + [Fact] public async Task Execute_DoAndHttp_Failed() { @@ -162,7 +159,7 @@ public async Task Execute_DoAndHttp_Failed() var workflowFactory = provider.GetRequiredService(); var loggerFactory = provider.GetRequiredService(); WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - + string wfName1 = $"wf-simple-{Guid.NewGuid().ToString("D")[..8]}"; workflowGlobalTransaction.Register(wfName1, async (workflow, data) => { @@ -171,31 +168,28 @@ public async Task Execute_DoAndHttp_Failed() { _testOutputHelper.WriteLine("1. local rollback"); await Task.CompletedTask; - }).Do(async (barrier) => - { - return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); - }); - + }).Do(async (barrier) => { return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); }); + // 2. http1 - HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => + HttpResponseMessage httpResult1 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http1 rollback"); await Task.CompletedTask; }).NewRequest().GetAsync("http://localhost:5006/test-http-ok1"); - + // 3. http2 - HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => + HttpResponseMessage httpResult2 = await workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("4. http2 rollback"); await Task.CompletedTask; }).NewRequest().GetAsync("http://localhost:5006/409"); // 409 - + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; @@ -203,12 +197,9 @@ public async Task Execute_DoAndHttp_Failed() { byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); - + // same gid again - await Assert.ThrowsAsync( async () => - { - await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - }); + await Assert.ThrowsAsync(async () => { await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); trans = await dtmClient.Query(gid, CancellationToken.None); Assert.Equal("failed", trans.Transaction.Status); // BranchID Op Status CreateTime UpdateTime Url @@ -229,7 +220,7 @@ public async Task Execute_DoAndHttp_Failed() Assert.Equal("rollback", trans.Branches[4].Op); Assert.Equal("succeed", trans.Branches[4].Status); } - + [Fact] public async Task Execute_DoAndGrpcSAGA_Should_Success() { @@ -244,13 +235,7 @@ public async Task Execute_DoAndGrpcSAGA_Should_Success() BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); // 1. local - workflow.NewBranch().OnRollback(async (barrier) => - { - _testOutputHelper.WriteLine("1. local rollback"); - }).Do(async (barrier) => - { - return ("my result"u8.ToArray(), null); - }); + workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("1. local rollback"); }).Do(async (barrier) => { return ("my result"u8.ToArray(), null); }); // 2. grpc1 Busi.BusiClient busiClient = null; @@ -269,16 +254,16 @@ public async Task Execute_DoAndGrpcSAGA_Should_Success() _testOutputHelper.WriteLine("3. grpc2 rollback"); }); await busiClient.TransInAsync(request); - + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - + // first byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); @@ -288,7 +273,7 @@ public async Task Execute_DoAndGrpcSAGA_Should_Success() Assert.Equal("succeed", trans.Branches[0].Status); Assert.Equal("succeed", trans.Branches[1].Status); Assert.Equal("succeed", trans.Branches[2].Status); - + // same gid again result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); @@ -299,7 +284,7 @@ public async Task Execute_DoAndGrpcSAGA_Should_Success() Assert.Equal("succeed", trans.Branches[1].Status); Assert.Equal("succeed", trans.Branches[2].Status); } - + [Fact] public async Task Execute_DoAndGrpcSAGA_Should_Failed() { @@ -314,10 +299,7 @@ public async Task Execute_DoAndGrpcSAGA_Should_Failed() BusiReq request = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); // 1. local - workflow.NewBranch().OnRollback(async (barrier) => - { - _testOutputHelper.WriteLine("1. local rollback"); - }).Do(async (barrier) => + workflow.NewBranch().OnRollback(async (barrier) => { _testOutputHelper.WriteLine("1. local rollback"); }).Do(async (barrier) => { return await Task.FromResult<(byte[], Exception)>(("my result"u8.ToArray(), null)); }); @@ -339,13 +321,13 @@ public async Task Execute_DoAndGrpcSAGA_Should_Failed() _testOutputHelper.WriteLine("3. grpc2 rollback"); }); Empty response2 = await busiClient.TransInAsync(request); - + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(outFailed: false, inFailed: true); - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; @@ -354,7 +336,7 @@ public async Task Execute_DoAndGrpcSAGA_Should_Failed() { byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); - + trans = await dtmClient.Query(gid, CancellationToken.None); Assert.Equal("failed", trans.Transaction.Status); // BranchID Op Status CreateTime UpdateTime Url @@ -381,8 +363,8 @@ public async Task Execute_DoAndGrpcSAGA_Should_Failed() byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); } - - + + [Fact] public async Task Execute_GrpcTccAndDo_Should_Success() { @@ -408,30 +390,27 @@ public async Task Execute_GrpcTccAndDo_Should_Success() await busiClient.TransOutRevertAsync(request); _testOutputHelper.WriteLine("1. grpc1 cancel"); }); - busiClient = GetBusiClientWithWf(wf, provider); // busiClient的构建依赖Workflow实例,只能这么写 + busiClient = GetBusiClientWithWf(wf, provider); // The construction of busiClient dependence on the Workflow instance, must ugly code // try await busiClient.TransOutTccAsync(request); - // 2. local, 可以是SAG, 因为排在最后,不必写反向的回滚 + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. workflow.NewBranch() - // .OnRollback(async (barrier) => // 反向 rollback + // .OnRollback(async (barrier) => // { // _testOutputHelper.WriteLine("1. local rollback"); // }) - .Do(async (barrier) => - { - return ("my result"u8.ToArray(), null); - }); // 正向 - + .Do(async (barrier) => { return ("my result"u8.ToArray(), null); }); + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(false, false); - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - + // first byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); @@ -448,7 +427,7 @@ public async Task Execute_GrpcTccAndDo_Should_Success() Assert.Equal("action", trans.Branches[1].Op); Assert.Equal("succeed", trans.Branches[2].Status); Assert.Equal("commit", trans.Branches[2].Op); - + // same gid again result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); Assert.Equal("my result", Encoding.UTF8.GetString(result)); @@ -462,7 +441,7 @@ public async Task Execute_GrpcTccAndDo_Should_Success() Assert.Equal("succeed", trans.Branches[2].Status); Assert.Equal("commit", trans.Branches[2].Op); } - + [Fact] public async Task Execute_GrpcTccAndDo_Should_TryFailed() { @@ -498,17 +477,14 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() // { // _testOutputHelper.WriteLine("1. local rollback"); // }) - .Do(async (barrier) => - { - return ("my result"u8.ToArray(), null); - }); - + .Do(async (barrier) => { return ("my result"u8.ToArray(), null); }); + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(outFailed: true, inFailed: false); // 1. trans out try failed - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; @@ -536,7 +512,7 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs }); - + trans = await dtmClient.Query(gid, CancellationToken.None); // BranchID Op Status // 01 action failed @@ -545,7 +521,7 @@ public async Task Execute_GrpcTccAndDo_Should_TryFailed() Assert.Equal("failed", trans.Branches[0].Status); Assert.Equal("action", trans.Branches[0].Op); } - + [Fact] public async Task Execute_GrpcTccAndDo_Should_DoFailed() { @@ -589,22 +565,22 @@ public async Task Execute_GrpcTccAndDo_Should_DoFailed() }); if (ex != null) throw ex; - + return await Task.FromResult("my result"u8.ToArray()); }); string gid = wfName1 + Guid.NewGuid().ToString()[..8]; var req = ITTestHelper.GenBusiReq(outFailed: false, inFailed: false); - + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); TransGlobal trans; - + // first await Assert.ThrowsAsync(async () => { byte[] _ = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); }); - + trans = await dtmClient.Query(gid, CancellationToken.None); // BranchID Op Status // 01 action succeed @@ -630,7 +606,7 @@ public async Task Execute_GrpcTccAndDo_Should_DoFailed() // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs }); } - + private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) { var loggerFactory = provider.GetRequiredService(); From c0fd208b0b90fc451c28740a0d13ee8ef4bb495e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Wed, 7 May 2025 10:58:30 +0800 Subject: [PATCH 25/29] test: simplify BusiClient initialization --- .../WorkflowGrpcStreamTest.cs | 133 +----------------- 1 file changed, 5 insertions(+), 128 deletions(-) diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs index 19677a2..7458d85 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -71,7 +71,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() var result = await myGrpcProcesser.GetResult(OperateType.Cancel); Assert.Equal(StatusCode.OK, result.StatusCode); }); - Busi.BusiClient busiClient = GetBusiClientWithWf(workflow, provider); + Busi.BusiClient busiClient = GetBusiClient(); call = busiClient.StreamTransOutTcc(); myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); readTask = myGrpcProcesser.HandleResponse(); @@ -193,7 +193,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() var result = await myGrpcProcesser.GetResult(OperateType.Cancel); Assert.Equal(StatusCode.OK, result.StatusCode); }); - Busi.BusiClient busiClient = GetBusiClientWithWf(workflow, provider); + Busi.BusiClient busiClient = GetBusiClient(); call = busiClient.StreamTransOutTcc(); myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); readTask = myGrpcProcesser.HandleResponse(); @@ -329,7 +329,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() var result = await myGrpcProcesser.GetResult(OperateType.Cancel); Assert.Equal(StatusCode.OK, result.StatusCode); }); - Busi.BusiClient busiClient = GetBusiClientWithWf(workflow, provider); + Busi.BusiClient busiClient = GetBusiClient(); call = busiClient.StreamTransOutTcc(); myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); readTask = myGrpcProcesser.HandleResponse(); @@ -412,133 +412,10 @@ await call.RequestStream.WriteAsync(new StreamRequest() Assert.Equal("action", trans.Branches[0].Op); } - // - // [Fact] - // public async Task Execute_StreamGrpcTccAndDo_TryFailed() - // { - // var provider = ITTestHelper.AddDtmGrpc(); - // var workflowFactory = provider.GetRequiredService(); - // var loggerFactory = provider.GetRequiredService(); - // WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); - // - // string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryFailed)}-{Guid.NewGuid().ToString("D")[..8]}"; - // Task readTask = null; - // AsyncDuplexStreamingCall call = null; - // workflowGlobalTransaction.Register(wfName1, async (workflow, data) => - // { - // BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - // - // Busi.BusiClient busiClient = null; - // - // ConcurrentDictionary progress = new ConcurrentDictionary(); - // - // // 1. grpc1 TCC - // Workflow wf = workflow.NewBranch() - // .OnCommit(async (barrier) => // confirm - // { - // await call.RequestStream.WriteAsync(new StreamRequest() - // { - // OperateType = OperateType.Confirm, - // BusiRequest = busiRequest, - // }); - // // wait Confirm - // while (!progress.ContainsKey(OperateType.Confirm)) - // Thread.Sleep(1000); - // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - // }) - // .OnRollback(async (barrier) => // cancel - // { - // await call.RequestStream.WriteAsync(new StreamRequest() - // { - // OperateType = OperateType.Confirm, - // BusiRequest = busiRequest, - // }); - // // wait Confirm - // while (!progress.ContainsKey(OperateType.Confirm)) - // Thread.Sleep(1000); - // Assert.Equal(StatusCode.OK, progress[OperateType.Try].StatusCode); - // }); - // busiClient = GetBusiClientWithWf(wf, provider); - // call = busiClient.StreamTransOutTcc(); - // using var call2 = call; - // readTask = Task.Run(async () => - // { - // try - // { - // await foreach (var response in call.ResponseStream.ReadAllAsync()) - // { - // _testOutputHelper.WriteLine($"{response.OperateType}: {response.Message}"); - // progress[response.OperateType] = new Status(StatusCode.OK, ""); - // } - // } - // catch (RpcException ex) - // { - // _testOutputHelper.WriteLine($"Exception caught: {ex.Status.StatusCode} - {ex.Status.Detail}"); - // progress[OperateType.Try] = ex.Status; // how assess response.OperateType - // } - // catch (Exception ex) - // { - // _testOutputHelper.WriteLine($"Exception caught: {ex}"); - // throw; - // } - // }); - // - // // try failed - // await call.RequestStream.WriteAsync(new StreamRequest() - // { - // OperateType = OperateType.Try, - // BusiRequest = busiRequest, - // }); - // // wait try - // while (!progress.ContainsKey(OperateType.Try)) - // Thread.Sleep(1000); - // Assert.Equal(StatusCode.Aborted, progress[OperateType.Try].StatusCode); - // Assert.Equal("FAILURE", progress[OperateType.Try].Detail); - // throw new DtmFailureException($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); - // // throw new Exception($"sub trans1 try failed(grpc): {progress[OperateType.Try].Detail}"); - // }); - // - // string gid = wfName1 + Guid.NewGuid().ToString()[..8]; - // var req = ITTestHelper.GenBusiReq(outFailed: true, false); - // - // DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); - // TransGlobal trans; - // - // // first - // // await Assert.ThrowsAsync(async () => - // { - // byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // } - // // ); - // await readTask; - // trans = await dtmClient.Query(gid, CancellationToken.None); - // Assert.Equal("failed", trans.Transaction.Status); - // Assert.Equal(0, trans.Branches.Count); - // - // - // // same gid again - // await Assert.ThrowsAsync(async () => - // { - // var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); - // // DtmCommon.DtmFailureException - // // sub trans1 try failed(grpc): FAILURE - // // at Dtmworkflow.Workflow.Process(WfFunc2 handler, Byte[] data) in src/Dtmworkflow/Workflow.Imp.cs - // // at Dtmworkflow.WorkflowGlobalTransaction.Execute(String name, String gid, Byte[] data, Boolean isHttp) in src/Dtmworkflow/WorkflowGlobalTransaction.cs - // // at Dtmgrpc.IntegrationTests.WorkflowGrpcTest.Execute_GrpcTccAndDo_Should_DoFailed() in tests/Dtmgrpc.IntegrationTests/WorkflowGrpcTest.cs - // } - // ); - // } - - private static Busi.BusiClient GetBusiClientWithWf(Workflow wf, ServiceProvider provider) + private static Busi.BusiClient GetBusiClient() { - var loggerFactory = provider.GetRequiredService(); var channel = GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol); - var logger = loggerFactory.CreateLogger(); - var interceptor = new WorkflowGrpcInterceptor(wf, logger); // inject client interceptor, and workflow instance - var callInvoker = channel.Intercept(interceptor); - // var callInvoker = channel.Intercept(); - Busi.BusiClient busiClient = new Busi.BusiClient(callInvoker); - return busiClient; + return new Busi.BusiClient(channel); } private DtmBranchTransInfo CurrentBranchTransInfo(Workflow wf) From 68871fb13ba843504dcfaf7e6995d49fa9449fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Fri, 9 May 2025 15:39:51 +0800 Subject: [PATCH 26/29] refactor: remove NET5_0_OR_GREATER conditional compilation(project is .net7.0) --- src/Dtmworkflow/WorkflowGlobalTransaction.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Dtmworkflow/WorkflowGlobalTransaction.cs b/src/Dtmworkflow/WorkflowGlobalTransaction.cs index ce4bbec..44214db 100644 --- a/src/Dtmworkflow/WorkflowGlobalTransaction.cs +++ b/src/Dtmworkflow/WorkflowGlobalTransaction.cs @@ -52,7 +52,6 @@ public void Register(string name, WfFunc2 handler, params Action[] cus }); } -#if NET5_0_OR_GREATER public async Task ExecuteByQS(Microsoft.AspNetCore.Http.IQueryCollection query, byte[] body) { _ = query.TryGetValue("gid", out var gid); @@ -60,7 +59,6 @@ public async Task ExecuteByQS(Microsoft.AspNetCore.Http.IQueryCollection query, await Execute(op, gid, body, true); } -#endif #if DEBUG // for sample only public bool Exists(string name) From 420f454c60bc4487d105cba0fbf05ae19872b47d Mon Sep 17 00:00:00 2001 From: wooln Date: Fri, 27 Dec 2024 17:50:22 +0800 Subject: [PATCH 27/29] =?UTF-8?q?sample:=20sample=20for=20workflow=20Execu?= =?UTF-8?q?teByQS=EF=BC=88http=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sample: sample for workflow ExecuteByQS - Implement wf-crash endpoint to simulate workflow crash - Implement wf-resume endpoint for workflow http callback --- .../DtmSample/Controllers/WfTestController.cs | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/samples/DtmSample/Controllers/WfTestController.cs b/samples/DtmSample/Controllers/WfTestController.cs index 614e969..c1e7d5a 100644 --- a/samples/DtmSample/Controllers/WfTestController.cs +++ b/samples/DtmSample/Controllers/WfTestController.cs @@ -7,12 +7,10 @@ using Microsoft.Extensions.Options; using System; using System.IO; -using System.Diagnostics; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; -using System.Text.Unicode; using System.Threading; using System.Threading.Tasks; using Exception = System.Exception; @@ -257,5 +255,84 @@ public async Task TccRollBack(CancellationToken cancellationToken return Ok(TransResponse.BuildFailureResponse()); } } + + + private static readonly string wfNameForResume = "wfNameForResume"; + + /// + /// + /// + /// + /// + [HttpPost("wf-crash")] + public async Task Crash(CancellationToken cancellationToken) + { + if (!_globalTransaction.Exists(wfNameForResume)) + { + _globalTransaction.Register(wfNameForResume, async (wf, data) => + { + var content = new ByteArrayContent(data); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var outClient = wf.NewBranch().NewRequest(); + await outClient.PostAsync(_settings.BusiUrl + "/TransOut", content); + + // the first branch succeed, then crashed, the dtm server will call back the flowing wf-call-back + // manual stop application + Environment.Exit(0); + + var inClient = wf.NewBranch().NewRequest(); + await inClient.PostAsync(_settings.BusiUrl + "/TransIn", content); + + return null; + }); + } + + var req = JsonSerializer.Serialize(new TransRequest("1", -30)); + await _globalTransaction.Execute(wfNameForResume, Guid.NewGuid().ToString("N"), Encoding.UTF8.GetBytes(req), true); + + return Ok(TransResponse.BuildSucceedResponse()); + } + + [HttpPost("wf-resume")] + public async Task WfResume(CancellationToken cancellationToken) + { + try + { + if (!_globalTransaction.Exists(wfNameForResume)) + { + // register again after manual crash by Environment.Exit(0); + _globalTransaction.Register(wfNameForResume, async (wf, data) => + { + var content = new ByteArrayContent(data); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + var outClient = wf.NewBranch().NewRequest(); + await outClient.PostAsync(_settings.BusiUrl + "/TransOut", content); + + var inClient = wf.NewBranch().NewRequest(); + await inClient.PostAsync(_settings.BusiUrl + "/TransIn", content); + + return null; + }); + } + + // prepared call ExecuteByQS + using var bodyMemoryStream = new MemoryStream(); + await Request.Body.CopyToAsync(bodyMemoryStream, cancellationToken); + byte[] bytes = bodyMemoryStream.ToArray(); + string body = Encoding.UTF8.GetString(bytes); + _logger.LogDebug($"body: {body}"); + + await _globalTransaction.ExecuteByQS(Request.Query, bodyMemoryStream.ToArray()); + + return Ok(TransResponse.BuildSucceedResponse()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Workflow Error"); + return Ok(TransResponse.BuildFailureResponse()); + } + } } } From 6cef5505e28ba04ef8d71cc16247688251d0293f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 12 May 2025 15:29:24 +0800 Subject: [PATCH 28/29] (DrefactortmCommon): modify BranchBarrier.Call method to return execution status and reason --- src/DtmCommon/Barrier/BranchBarrier.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/DtmCommon/Barrier/BranchBarrier.cs b/src/DtmCommon/Barrier/BranchBarrier.cs index 7453717..d3f9a05 100644 --- a/src/DtmCommon/Barrier/BranchBarrier.cs +++ b/src/DtmCommon/Barrier/BranchBarrier.cs @@ -42,7 +42,7 @@ public BranchBarrier(string transType, string gid, string branchID, string op, D public int BarrierID { get; set; } - public async Task Call(DbConnection db, Func busiCall) + public async Task<(bool done, string reason)> Call(DbConnection db, Func busiCall) { this.BarrierID = this.BarrierID + 1; var bid = this.BarrierID.ToString().PadLeft(2, '0'); @@ -91,7 +91,7 @@ public async Task Call(DbConnection db, Func busiCall) #else await tx.CommitAsync(); #endif - return; + return (false, isNullCompensation ? "isNullCompensation" : "isDuplicateOrPend"); } await busiCall.Invoke(tx); @@ -118,9 +118,10 @@ public async Task Call(DbConnection db, Func busiCall) throw; } + return (true, string.Empty); } - public async Task Call(DbConnection db, Func busiCall, TransactionScopeOption transactionScope = TransactionScopeOption.Required, IsolationLevel isolationLevel = IsolationLevel.Serializable) + public async Task<(bool done, string reason)> Call(DbConnection db, Func busiCall, TransactionScopeOption transactionScope = TransactionScopeOption.Required, IsolationLevel isolationLevel = IsolationLevel.Serializable) { this.BarrierID = this.BarrierID + 1; var bid = this.BarrierID.ToString().PadLeft(2, '0'); @@ -158,7 +159,7 @@ public async Task Call(DbConnection db, Func busiCall, TransactionScopeOpt if (isNullCompensation || isDuplicateOrPend) { Logger?.LogInformation("Will not exec busiCall, isNullCompensation={isNullCompensation}, isDuplicateOrPend={isDuplicateOrPend}", isNullCompensation, isDuplicateOrPend); - return; + return (false, isNullCompensation ? "isNullCompensation" : "isDuplicateOrPend"); } await busiCall.Invoke(); scope.Complete(); @@ -174,7 +175,7 @@ public async Task Call(DbConnection db, Func busiCall, TransactionScopeOpt throw; } } - + return (true, string.Empty); } public async Task QueryPrepared(DbConnection db) From b2fa155af1f360c44601551b465c08baf6cf906b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 12 May 2025 15:35:22 +0800 Subject: [PATCH 29/29] fix(BusiGrpcService): handle null compensation duplicate or pend in streaming services --- .../Services/BusiApiService_Stream.cs | 14 +- .../WorkflowGrpcStreamTest.cs | 415 ++++++++++++++++-- 2 files changed, 394 insertions(+), 35 deletions(-) diff --git a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs index 62732d9..d81ca10 100644 --- a/tests/BusiGrpcService/Services/BusiApiService_Stream.cs +++ b/tests/BusiGrpcService/Services/BusiApiService_Stream.cs @@ -27,11 +27,16 @@ public override async Task StreamTransOutTcc(IAsyncStreamReader r $"{nameof(StreamTransOutTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}, req={JsonSerializer.Serialize(request)}"); await using MySqlConnection conn = GetBarrierConn(); - await branchBarrier.Call(conn, async () => + (bool done, string reason) = await branchBarrier.Call(conn, async () => { // business logic await TransOutFn(responseStream, request); }); + if (!done) + { + _logger.LogInformation($"NOT done, reason:{reason} {nameof(StreamTransOutTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}"); + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = reason }); + } } else { @@ -98,11 +103,16 @@ public override async Task StreamTransInTcc(IAsyncStreamReader re $"{nameof(StreamTransInTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}, req={JsonSerializer.Serialize(request)}"); await using MySqlConnection conn = GetBarrierConn(); - await branchBarrier.Call(conn, async () => + (bool done, string reason) = await branchBarrier.Call(conn, async () => { // business logic await TransInFn(responseStream, request); }); + if (!done) + { + _logger.LogInformation($"NOT done, reason:{reason} {nameof(StreamTransInTcc)} gid={branchBarrier.Gid} branch_id={branchBarrier.BranchID} op={branchBarrier.Op}"); + await responseStream.WriteAsync(new StreamReply { OperateType = request.OperateType, Message = reason }); + } } else { diff --git a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs index 7458d85..a1cf6ef 100644 --- a/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs +++ b/tests/Dtmgrpc.IntegrationTests/WorkflowGrpcStreamTest.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using System; -using System.Collections.Concurrent; using System.Net.Http; using System.Text; using System.Threading; @@ -10,7 +9,6 @@ using DtmCommon; using Dtmworkflow; using Grpc.Core; -using Grpc.Core.Interceptors; using Grpc.Net.Client; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -61,15 +59,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() }) .OnRollback(async (barrier) => // cancel { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), - BusiRequest = busiRequest, - }); - // wait Confirm - var result = await myGrpcProcesser.GetResult(OperateType.Cancel); - Assert.Equal(StatusCode.OK, result.StatusCode); + Assert.Fail("should not run OnRollback"); }); Busi.BusiClient busiClient = GetBusiClient(); call = busiClient.StreamTransOutTcc(); @@ -171,15 +161,7 @@ public async Task Execute_StreamGrpcTccAndDo_TryCancel() workflow.NewBranch() .OnCommit(async (barrier) => // confirm { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), - BusiRequest = busiRequest, - }); - // wait Confirm - var result = await myGrpcProcesser.GetResult(OperateType.Confirm); - Assert.Equal(StatusCode.OK, result.StatusCode); + Assert.Fail("should not run OnCommit"); }) .OnRollback(async (barrier) => // cancel { @@ -287,7 +269,7 @@ await call.RequestStream.WriteAsync(new StreamRequest() } [Fact] - public async Task Execute_StreamGrpcTccAndDo_TryFailed() + public async Task Execute_StreamGrpcTccAndDo_TryServerFailed() { var provider = ITTestHelper.AddDtmGrpc(); var workflowFactory = provider.GetRequiredService(); @@ -306,22 +288,13 @@ public async Task Execute_StreamGrpcTccAndDo_TryFailed() workflow.NewBranch() .OnCommit(async (barrier) => // confirm { - await call.RequestStream.WriteAsync(new StreamRequest() - { - OperateType = OperateType.Confirm, - DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), - BusiRequest = busiRequest, - }); - // wait Confirm - var result = await myGrpcProcesser.GetResult(OperateType.Confirm); - Assert.Equal(StatusCode.Aborted, result.StatusCode); - Assert.Equal("FAILURE", result.Detail); + Assert.Fail("should not run OnCommit"); }) .OnRollback(async (barrier) => // cancel { await call.RequestStream.WriteAsync(new StreamRequest() { - OperateType = OperateType.Confirm, + OperateType = OperateType.Cancel, DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), BusiRequest = busiRequest, }); @@ -381,7 +354,6 @@ await call.RequestStream.WriteAsync(new StreamRequest() byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); } ); - await call.RequestStream.CompleteAsync(); await Assert.ThrowsAsync(async () => { await readTask; }); // grpc aborted by server try method var trans = await dtmClient.Query(gid, CancellationToken.None); @@ -412,6 +384,383 @@ await call.RequestStream.WriteAsync(new StreamRequest() Assert.Equal("action", trans.Branches[0].Op); } + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TryClientThrowFailed() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; + AsyncDuplexStreamingCall call = null; + MyGrpcProcesser myGrpcProcesser = null; + Task readTask = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + Assert.Fail("should not run OnCommit"); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Cancel, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Cancel); + Assert.Equal(StatusCode.OK, result.StatusCode); + }); + Busi.BusiClient busiClient = GetBusiClient(); + call = busiClient.StreamTransOutTcc(); + myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); + readTask = myGrpcProcesser.HandleResponse(); + // try + var (_, stepEx) = await workflow.Do((barrier) => + { + // throw + throw new DtmFailureException("try do manual client error"); + }); + if (stepEx != null) + throw stepEx; + + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. + (_, stepEx) = await workflow.NewBranch() + // .OnRollback(async (barrier) => + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + _testOutputHelper.WriteLine("2. local do"); + return ("my result"u8.ToArray(), null); + }); + if (stepEx != null) + throw stepEx; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(true, false); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + + using var call2 = call; + // first + await Assert.ThrowsAsync(async () => + { + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + } + ); + await call.RequestStream.CompleteAsync(); + await readTask; + + var trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 rollback succeed + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("rollback", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + + // same gid again + await Assert.ThrowsAsync(async () => + { + var result = await workflowGlobalTransaction.Execute(wfName1, gid, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); + }); + + await call.RequestStream.CompleteAsync(); + await readTask; + + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("failed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 rollback succeed + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("rollback", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + } + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TrySucceed_RegisterBranch_ThenCrash_ThenExecuteByQs_Continue() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; + AsyncDuplexStreamingCall call = null; + MyGrpcProcesser myGrpcProcesser = null; + Task readTask = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Confirm); + Assert.Equal(StatusCode.Aborted, result.StatusCode); + Assert.Equal("FAILURE", result.Detail); + }) + .OnRollback(async (barrier) => // cancel + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Cancel, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Cancel); + Assert.Equal(StatusCode.OK, result.StatusCode); + }); + Busi.BusiClient busiClient = GetBusiClient(); + call = busiClient.StreamTransOutTcc(); + myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); + readTask = myGrpcProcesser.HandleResponse(); + // try + var (_, stepEx) = await workflow.Do((barrier) => + { + // Assert should skip + return Task.FromResult<(byte[], Exception)>(([], new DtmFailureException("should skip branch 01 try"))); + }); + if (stepEx != null) + throw stepEx; + + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. + (_, stepEx) = await workflow.NewBranch() + // .OnRollback(async (barrier) => + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + _testOutputHelper.WriteLine("2. local do"); + return ("my result"u8.ToArray(), null); + }); + if (stepEx != null) + throw stepEx; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(true, false); + var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req)); + + // init: try failed, then crash + var tb = TransBase.NewTransBase(gid, "workflow", "i am dtm", "1"); + // 1. Prepare + await dtmClient.PrepareWorkflow(tb, CancellationToken.None); + // 2. branch 1 action + await dtmClient.TransRegisterBranch(tb, new() + { + { "data", Encoding.UTF8.GetString(data) }, + { "branch_id", "01" }, + { "op", "action" }, + { "status", "succeed" }, + }, "registerBranch", CancellationToken.None); + + // Asserts + var trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("prepared", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + Assert.Equal(1, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + + using var call2 = call; + + // dtm callback executeByQs + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, data); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + await call.RequestStream.CompleteAsync(); + await readTask; + + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action succeed + Assert.Equal(2, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("02", trans.Branches[1].BranchId); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + + // same gid again + result = await workflowGlobalTransaction.Execute(wfName1, gid, data); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + await call.RequestStream.CompleteAsync(); + await readTask; + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action succeed + Assert.Equal(2, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("02", trans.Branches[1].BranchId); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + } + + [Fact] + public async Task Execute_StreamGrpcTccAndDo_TrySucceedButMissed_ThenCrash_ThenExecuteByQs_Continue() + { + var provider = ITTestHelper.AddDtmGrpc(); + var workflowFactory = provider.GetRequiredService(); + var loggerFactory = provider.GetRequiredService(); + WorkflowGlobalTransaction workflowGlobalTransaction = new WorkflowGlobalTransaction(workflowFactory, loggerFactory); + + string wfName1 = $"{nameof(this.Execute_StreamGrpcTccAndDo_TryConfirm)}-{Guid.NewGuid().ToString("D")[..8]}"; + AsyncDuplexStreamingCall call = null; + MyGrpcProcesser myGrpcProcesser = null; + Task readTask = null; + workflowGlobalTransaction.Register(wfName1, async (workflow, data) => + { + BusiReq busiRequest = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); + + // 1. grpc1 TCC + workflow.NewBranch() + .OnCommit(async (barrier) => // confirm + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Confirm, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), + BusiRequest = busiRequest, + }); + // wait Confirm + var result = await myGrpcProcesser.GetResult(OperateType.Confirm); + Assert.Equal(StatusCode.Aborted, result.StatusCode); + Assert.Equal("FAILURE", result.Detail); + }) + .OnRollback(async (barrier) => // cancel + { + Assert.Fail("should not run OnRollback"); + }); + Busi.BusiClient busiClient = GetBusiClient(); + call = busiClient.StreamTransOutTcc(); + myGrpcProcesser = new MyGrpcProcesser(call, _testOutputHelper); + readTask = myGrpcProcesser.HandleResponse(); + // try + var (_, stepEx) = await workflow.Do(async (barrier) => + { + await call.RequestStream.WriteAsync(new StreamRequest() + { + OperateType = OperateType.Try, + DtmBranchTransInfo = this.CurrentBranchTransInfo(workflow), + BusiRequest = busiRequest, + }); + // wait try + var result = await myGrpcProcesser.GetResult(OperateType.Try); + Assert.Equal(StatusCode.OK, result.StatusCode); + return (""u8.ToArray(), null); + }); + if (stepEx != null) + throw stepEx; + + // 2. local, maybe SAG, at the end, no need to write the reverse rollback. + (_, stepEx) = await workflow.NewBranch() + // .OnRollback(async (barrier) => + // { + // _testOutputHelper.WriteLine("1. local rollback"); + // }) + .Do(async (barrier) => + { + _testOutputHelper.WriteLine("2. local do"); + return ("my result"u8.ToArray(), null); + }); + if (stepEx != null) + throw stepEx; + + return await Task.FromResult("my result"u8.ToArray()); + }); + + DtmClient dtmClient = new DtmClient(provider.GetRequiredService(), provider.GetRequiredService>()); + string gid = wfName1 + Guid.NewGuid().ToString()[..8]; + var req = ITTestHelper.GenBusiReq(false, false); + var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req)); + + // init: try failed, then crash + var tb = TransBase.NewTransBase(gid, "workflow", "i am dtm", "1"); + // Prepare + await dtmClient.PrepareWorkflow(tb, CancellationToken.None); + + // Asserts + var trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("prepared", trans.Transaction.Status); + Assert.Empty(trans.Branches); + + using var call2 = call; + + // dtm callback executeByQs + byte[] result = await workflowGlobalTransaction.Execute(wfName1, gid, data); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + await call.RequestStream.CompleteAsync(); + await readTask; + + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action succeed + Assert.Equal(2, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("02", trans.Branches[1].BranchId); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + + // same gid again + result = await workflowGlobalTransaction.Execute(wfName1, gid, data); + Assert.Equal("my result", Encoding.UTF8.GetString(result)); + await call.RequestStream.CompleteAsync(); + await readTask; + trans = await dtmClient.Query(gid, CancellationToken.None); + Assert.Equal("succeed", trans.Transaction.Status); + // BranchID Op Status CreateTime UpdateTime Url + // 01 action succeed + // 02 action succeed + Assert.Equal(2, trans.Branches.Count); + Assert.Equal("01", trans.Branches[0].BranchId); + Assert.Equal("action", trans.Branches[0].Op); + Assert.Equal("succeed", trans.Branches[0].Status); + Assert.Equal("02", trans.Branches[1].BranchId); + Assert.Equal("action", trans.Branches[1].Op); + Assert.Equal("succeed", trans.Branches[1].Status); + } + private static Busi.BusiClient GetBusiClient() { var channel = GrpcChannel.ForAddress(ITTestHelper.BuisgRPCUrlWithProtocol);