Skip to content

Commit 0998d17

Browse files
authored
GitHub actions build pipeline (#1)
Enables automatic build validation for all PRs and commits to `main`. Also includes several other misc. improvements: - Self-host sidecar in test process - gRPC channel sharing to make tests faster - Improved logging - Updated protobuf package version
1 parent 68ccf85 commit 0998d17

File tree

10 files changed

+227
-15
lines changed

10 files changed

+227
-15
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Validate Build
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths-ignore: [ '**.md' ]
7+
pull_request:
8+
branches: [ main ]
9+
paths-ignore: [ '**.md' ]
10+
11+
jobs:
12+
build:
13+
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- uses: actions/checkout@v2
18+
- name: Setup .NET
19+
uses: actions/setup-dotnet@v1
20+
with:
21+
dotnet-version: 6.0.x
22+
- name: Enable App Service MyGet feed
23+
run: dotnet nuget add source https://www.myget.org/F/azure-appservice/api/v3/index.json --name appservice-myget
24+
- name: Restore dependencies
25+
run: dotnet restore
26+
- name: Build
27+
run: dotnet build --no-restore -p:FileVersionRevision=$GITHUB_RUN_NUMBER
28+
- name: Test
29+
run: dotnet test --no-build --verbosity normal

CHANGELOG.md

266 Bytes

Updates

  • Enabled class-based orchestrators and activities to derive from intermediate base classes
  • Made the DurableTaskAttribute.Name property optional
  • Enabled Source Link
  • Changed OSS license to MIT
  • Removed submodule & replaced with nuget package (Microsoft.DurableTask.Sidecar.Protobuf)
  • Added support for reusing gRPC channels across clients/workers
  • Enabled functional tests to run in isolation for GitHub Actions

Breaking changes

  • Renamed ITaskActivityContext to TaskActivityContext and made it an abstract class
  • Replaced Context properties in orchestrator and activity classes with method parameters
  • Removed GetInput<T> methods from context objects

DurableTask.Sdk.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{6E392DF7-D
1414
LICENSE = LICENSE
1515
nuget.config = nuget.config
1616
README.md = README.md
17+
.github\workflows\validate-build.yml = .github\workflows\validate-build.yml
1718
EndProjectSection
1819
EndProject
1920
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{F9812EB9-A3CD-4E80-8CEA-243AE2AF925F}"

src/DurableTask/DurableTask.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
3939
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.38.0" />
4040
<PackageReference Include="Microsoft.Azure.DurableTask.Core" Version="2.7.0" />
41-
<PackageReference Include="Microsoft.DurableTask.Sidecar.Protobuf" Version="0.1.0-alpha" />
41+
<PackageReference Include="Microsoft.DurableTask.Sidecar.Protobuf" Version="0.2.0-alpha" />
4242
</ItemGroup>
4343

4444
<ItemGroup>

src/DurableTask/Grpc/DurableTaskGrpcClient.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class DurableTaskGrpcClient : DurableTaskClient
2525
readonly IConfiguration? configuration;
2626
readonly GrpcChannel sidecarGrpcChannel;
2727
readonly TaskHubSidecarServiceClient sidecarClient;
28+
readonly bool ownsChannel;
2829

2930
bool isDisposed;
3031

@@ -35,8 +36,19 @@ public class DurableTaskGrpcClient : DurableTaskClient
3536
this.logger = SdkUtils.GetLogger(builder.loggerFactory ?? this.services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance);
3637
this.configuration = builder.configuration ?? this.services.GetService<IConfiguration>();
3738

38-
string sidecarAddress = builder.address ?? SdkUtils.GetSidecarAddress(this.configuration);
39-
this.sidecarGrpcChannel = GrpcChannel.ForAddress(sidecarAddress);
39+
if (builder.channel != null)
40+
{
41+
// Use the channel from the builder, which was given to us by the app (thus we don't own it and can't dispose it)
42+
this.sidecarGrpcChannel = builder.channel;
43+
this.ownsChannel = false;
44+
}
45+
else
46+
{
47+
// We have to create our own channel and are responsible for disposing it
48+
this.sidecarGrpcChannel = GrpcChannel.ForAddress(builder.address ?? SdkUtils.GetSidecarAddress(this.configuration));
49+
this.ownsChannel = true;
50+
}
51+
4052
this.sidecarClient = new TaskHubSidecarServiceClient(this.sidecarGrpcChannel);
4153
}
4254

@@ -48,8 +60,11 @@ public override async ValueTask DisposeAsync()
4860
{
4961
if (!this.isDisposed)
5062
{
51-
await this.sidecarGrpcChannel.ShutdownAsync();
52-
this.sidecarGrpcChannel.Dispose();
63+
if (this.ownsChannel)
64+
{
65+
await this.sidecarGrpcChannel.ShutdownAsync();
66+
this.sidecarGrpcChannel.Dispose();
67+
}
5368

5469
GC.SuppressFinalize(this);
5570
this.isDisposed = true;
@@ -218,6 +233,7 @@ public sealed class Builder
218233
internal ILoggerFactory? loggerFactory;
219234
internal IDataConverter? dataConverter;
220235
internal IConfiguration? configuration;
236+
internal GrpcChannel? channel;
221237
internal string? address;
222238

223239
public Builder UseLoggerFactory(ILoggerFactory loggerFactory)
@@ -238,6 +254,23 @@ public Builder UseAddress(string address)
238254
return this;
239255
}
240256

257+
/// <summary>
258+
/// Configures a <see cref="GrpcChannel"/> to use for communicating with the sidecar process.
259+
/// </summary>
260+
/// <remarks>
261+
/// This builder method allows you to provide your own gRPC channel for communicating with the Durable Task
262+
/// sidecar service. Channels provided using this method won't be disposed when the client is disposed.
263+
/// Rather, the caller remains responsible for shutting down the channel after disposing the client.
264+
/// </remarks>
265+
/// <param name="channel">The gRPC channel to use.</param>
266+
/// <returns>Returns this <see cref="Builder"/> instance.</returns>
267+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="channel"/> is <c>null</c>.</exception>
268+
public Builder UseGrpcChannel(GrpcChannel channel)
269+
{
270+
this.channel = channel ?? throw new ArgumentNullException(nameof(channel));
271+
return this;
272+
}
273+
241274
public Builder UseDataConverter(IDataConverter dataConverter)
242275
{
243276
this.dataConverter = dataConverter ?? throw new ArgumentNullException(nameof(dataConverter));

src/DurableTask/Grpc/DurableTaskGrpcWorker.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class DurableTaskGrpcWorker : IHostedService, IAsyncDisposable
3737
readonly ILogger logger;
3838
readonly IConfiguration? configuration;
3939
readonly GrpcChannel sidecarGrpcChannel;
40+
readonly bool ownsChannel;
4041
readonly TaskHubSidecarServiceClient sidecarClient;
4142
readonly WorkerContext workerContext;
4243

@@ -62,8 +63,19 @@ public class DurableTaskGrpcWorker : IHostedService, IAsyncDisposable
6263
this.orchestrators = builder.taskProvider.orchestratorsBuilder.ToImmutable();
6364
this.activities = builder.taskProvider.activitiesBuilder.ToImmutable();
6465

65-
string sidecarAddress = builder.address ?? SdkUtils.GetSidecarAddress(this.configuration);
66-
this.sidecarGrpcChannel = GrpcChannel.ForAddress(sidecarAddress);
66+
if (builder.channel != null)
67+
{
68+
// Use the channel from the builder, which was given to us by the app (thus we don't own it and can't dispose it)
69+
this.sidecarGrpcChannel = builder.channel;
70+
this.ownsChannel = false;
71+
}
72+
else
73+
{
74+
// We have to create our own channel and are responsible for disposing it
75+
this.sidecarGrpcChannel = GrpcChannel.ForAddress(builder.address ?? SdkUtils.GetSidecarAddress(this.configuration));
76+
this.ownsChannel = true;
77+
}
78+
6779
this.sidecarClient = new TaskHubSidecarServiceClient(this.sidecarGrpcChannel);
6880
}
6981

@@ -156,6 +168,12 @@ async ValueTask IAsyncDisposable.DisposeAsync()
156168
{
157169
}
158170

171+
if (this.ownsChannel)
172+
{
173+
await this.sidecarGrpcChannel.ShutdownAsync();
174+
this.sidecarGrpcChannel.Dispose();
175+
}
176+
159177
GC.SuppressFinalize(this);
160178
}
161179

@@ -294,10 +312,34 @@ async Task OnRunOrchestratorAsync(P.OrchestratorRequest request)
294312
result.CustomStatus,
295313
result.Actions);
296314

297-
this.logger.SendingOrchestratorResponse(name, response.InstanceId, response.Actions.Count);
315+
this.logger.SendingOrchestratorResponse(
316+
name,
317+
response.InstanceId,
318+
response.Actions.Count,
319+
GetActionsListForLogging(response.Actions));
320+
298321
await this.sidecarClient.CompleteOrchestratorTaskAsync(response);
299322
}
300323

324+
static string GetActionsListForLogging(IReadOnlyList<P.OrchestratorAction> actions)
325+
{
326+
if (actions.Count == 0)
327+
{
328+
return string.Empty;
329+
}
330+
else if (actions.Count == 1)
331+
{
332+
return actions[0].OrchestratorActionTypeCase.ToString();
333+
}
334+
else
335+
{
336+
// Returns something like "ScheduleTask x5, CreateTimer x1,..."
337+
return string.Join(", ", actions
338+
.GroupBy(a => a.OrchestratorActionTypeCase)
339+
.Select(group => $"{group.Key} x{group.Count()}"));
340+
}
341+
}
342+
301343
OrchestratorExecutionResult CreateOrchestrationFailedActionResult(Exception e)
302344
{
303345
return this.CreateOrchestrationFailedActionResult(
@@ -398,6 +440,7 @@ public sealed class Builder
398440
internal IServiceProvider? services;
399441
internal IConfiguration? configuration;
400442
internal string? address;
443+
internal GrpcChannel? channel;
401444

402445
internal Builder()
403446
{
@@ -411,6 +454,23 @@ public Builder UseAddress(string address)
411454
return this;
412455
}
413456

457+
/// <summary>
458+
/// Configures a <see cref="GrpcChannel"/> to use for communicating with the sidecar process.
459+
/// </summary>
460+
/// <remarks>
461+
/// This builder method allows you to provide your own gRPC channel for communicating with the Durable Task
462+
/// sidecar service. Channels provided using this method won't be disposed when the worker is disposed.
463+
/// Rather, the caller remains responsible for shutting down the channel after disposing the worker.
464+
/// </remarks>
465+
/// <param name="channel">The gRPC channel to use.</param>
466+
/// <returns>Returns this <see cref="Builder"/> instance.</returns>
467+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="channel"/> is <c>null</c>.</exception>
468+
public Builder UseGrpcChannel(GrpcChannel channel)
469+
{
470+
this.channel = channel ?? throw new ArgumentNullException(nameof(channel));
471+
return this;
472+
}
473+
414474
public Builder UseLoggerFactory(ILoggerFactory loggerFactory)
415475
{
416476
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));

src/DurableTask/Logs.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ static partial class Logs
2424
[LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = "{instanceId}: Received request for '{name}' orchestrator.")]
2525
public static partial void ReceivedOrchestratorRequest(this ILogger logger, string name, string instanceId);
2626

27-
[LoggerMessage(EventId = 11, Level = LogLevel.Debug, Message = "{instanceId}: Sending {count} actions for '{name}' orchestrator.")]
28-
public static partial void SendingOrchestratorResponse(this ILogger logger, string name, string instanceId, int count);
27+
[LoggerMessage(EventId = 11, Level = LogLevel.Debug, Message = "{instanceId}: Sending {count} action(s) [{actionsList}] for '{name}' orchestrator.")]
28+
public static partial void SendingOrchestratorResponse(this ILogger logger, string name, string instanceId, int count, string actionsList);
2929

3030
[LoggerMessage(EventId = 12, Level = LogLevel.Warning, Message = "{instanceId}: '{name}' orchestrator failed with an unhandled exception: {details}.")]
3131
public static partial void OrchestratorFailed(this ILogger logger, string name, string instanceId, string details);

test/DurableTask.Sdk.Tests/DurableTask.Sdk.Tests.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
@@ -20,6 +20,10 @@
2020
</PackageReference>
2121
</ItemGroup>
2222

23+
<ItemGroup>
24+
<PackageReference Include="Microsoft.DurableTask.Sidecar" Version="0.2.0-alpha" />
25+
</ItemGroup>
26+
2327
<ItemGroup>
2428
<ProjectReference Include="..\..\src\DurableTask\DurableTask.csproj" />
2529
</ItemGroup>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using DurableTask.Core;
6+
using DurableTask.Grpc;
7+
using DurableTask.Sidecar;
8+
using DurableTask.Sidecar.Grpc;
9+
using Grpc.Net.Client;
10+
using Microsoft.AspNetCore.Builder;
11+
using Microsoft.AspNetCore.Hosting;
12+
using Microsoft.AspNetCore.Server.Kestrel.Core;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Logging;
15+
16+
namespace DurableTask.Sdk.Tests;
17+
18+
public class GrpcSidecarFixture : IDisposable
19+
{
20+
const string ListenAddress = "http://127.0.0.1:4002";
21+
22+
readonly IWebHost host;
23+
readonly GrpcChannel sidecarChannel;
24+
25+
public GrpcSidecarFixture()
26+
{
27+
InMemoryOrchestrationService service = new();
28+
29+
this.host = new WebHostBuilder()
30+
.UseKestrel(options =>
31+
{
32+
// Need to force Http2 in Kestrel in unencrypted scenarios
33+
// https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0
34+
options.ConfigureEndpointDefaults(listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
35+
})
36+
.UseUrls(ListenAddress)
37+
.ConfigureServices(services =>
38+
{
39+
services.AddGrpc();
40+
services.AddSingleton<IOrchestrationService>(service);
41+
services.AddSingleton<IOrchestrationServiceClient>(service);
42+
services.AddSingleton<TaskHubGrpcServer>();
43+
})
44+
.Configure(app =>
45+
{
46+
app.UseRouting();
47+
app.UseEndpoints(endpoints =>
48+
{
49+
endpoints.MapGrpcService<TaskHubGrpcServer>();
50+
});
51+
})
52+
.Build();
53+
54+
this.host.Start();
55+
56+
this.sidecarChannel = GrpcChannel.ForAddress(ListenAddress);
57+
}
58+
59+
public DurableTaskGrpcWorker.Builder GetWorkerBuilder()
60+
{
61+
// The gRPC channel is reused across tests to avoid the overhead of creating new connections (which is very slow)
62+
return DurableTaskGrpcWorker.CreateBuilder().UseGrpcChannel(this.sidecarChannel);
63+
64+
}
65+
66+
public DurableTaskGrpcClient.Builder GetClientBuilder()
67+
{
68+
// The gRPC channel is reused across tests to avoid the overhead of creating new connections (which is very slow)
69+
return DurableTaskGrpcClient.CreateBuilder().UseGrpcChannel(this.sidecarChannel);
70+
}
71+
72+
public void Dispose()
73+
{
74+
this.sidecarChannel.ShutdownAsync().GetAwaiter().GetResult();
75+
this.sidecarChannel.Dispose();
76+
77+
this.host.StopAsync().GetAwaiter().GetResult();
78+
this.host.Dispose();
79+
}
80+
}

test/DurableTask.Sdk.Tests/OrchestrationPatterns.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,31 @@
99
using System.Threading;
1010
using System.Threading.Tasks;
1111
using DurableTask.Grpc;
12+
using DurableTask.Sdk.Tests;
1213
using DurableTask.Sdk.Tests.Logging;
1314
using Microsoft.Extensions.Logging;
1415
using Xunit;
1516
using Xunit.Abstractions;
1617

1718
namespace DurableTask.Tests;
1819

19-
public class OrchestrationPatterns : IDisposable
20+
public class OrchestrationPatterns : IClassFixture<GrpcSidecarFixture>, IDisposable
2021
{
2122
readonly CancellationTokenSource testTimeoutSource = new(Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(10));
2223
readonly ILoggerFactory loggerFactory;
2324

24-
public OrchestrationPatterns(ITestOutputHelper output)
25+
// Documentation on xunit test fixtures: https://xunit.net/docs/shared-context
26+
readonly GrpcSidecarFixture sidecarFixture;
27+
28+
public OrchestrationPatterns(ITestOutputHelper output, GrpcSidecarFixture sidecarFixture)
2529
{
2630
TestLogProvider logProvider = new(output);
2731
this.loggerFactory = LoggerFactory.Create(builder =>
2832
{
2933
builder.AddProvider(logProvider);
3034
builder.SetMinimumLevel(LogLevel.Debug);
3135
});
36+
this.sidecarFixture = sidecarFixture;
3237
}
3338

3439
/// <summary>
@@ -48,15 +53,15 @@ void IDisposable.Dispose()
4853
/// </summary>
4954
DurableTaskGrpcWorker.Builder CreateWorkerBuilder()
5055
{
51-
return DurableTaskGrpcWorker.CreateBuilder().UseLoggerFactory(this.loggerFactory);
56+
return this.sidecarFixture.GetWorkerBuilder().UseLoggerFactory(this.loggerFactory);
5257
}
5358

5459
/// <summary>
5560
/// Creates a <see cref="DurableTaskGrpcClient"/> configured to output logs to xunit logging infrastructure.
5661
/// </summary>
5762
DurableTaskClient CreateDurableTaskClient()
5863
{
59-
return DurableTaskGrpcClient.CreateBuilder().UseLoggerFactory(this.loggerFactory).Build();
64+
return this.sidecarFixture.GetClientBuilder().UseLoggerFactory(this.loggerFactory).Build();
6065
}
6166

6267
[Fact]

0 commit comments

Comments
 (0)