< Summary

Information
Class: Elsa.Platform.Integration.Services.PlatformRuntimeCommandClient
Assembly: Elsa.Platform.Integration
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Platform.Integration/Services/PlatformRuntimeCommandClient.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 73
Coverable lines: 73
Total lines: 149
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 30
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.cctor()100%210%
PollAsync()0%110100%
ClaimAsync()0%2040%
DownloadArtifactAsync()0%2040%
ReportProgressAsync(...)100%210%
CompleteAsync(...)100%210%
FailAsync(...)100%210%
RejectAsync(...)100%210%
SendMutationAsync()100%210%
CreateRequest(...)0%620%
SendJsonAsync()100%210%
BuildUri(...)0%4260%
CopyBoundedAsync()0%2040%
CreateJsonOptions()100%210%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Platform.Integration/Services/PlatformRuntimeCommandClient.cs

#LineLine coverage
 1using System.Net;
 2using System.Net.Http.Json;
 3using System.Text.Json;
 4using System.Text.Json.Serialization;
 5using Elsa.Platform.Integration.Models;
 6using Elsa.Platform.Integration.Options;
 7using Microsoft.Extensions.Options;
 8
 9namespace Elsa.Platform.Integration.Services;
 10
 011public class PlatformRuntimeCommandClient(HttpClient httpClient, IOptions<ElsaPlatformIntegrationOptions> options) : IPl
 12{
 13    private const string EngineSecretHeaderName = "X-Elsa-Engine-Secret";
 14    private const string LeaseHeaderName = "X-Elsa-Command-Lease";
 15    private const string WorkerHeaderName = "X-Elsa-Worker-Id";
 016    private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
 017    private readonly ElsaPlatformIntegrationOptions _options = options.Value;
 18
 19    public async Task<IReadOnlyList<PlatformRuntimeCommand>> PollAsync(CancellationToken cancellationToken = default)
 20    {
 021        using var request = CreateRequest(HttpMethod.Get, BuildUri($"/deployments/runtime/engines/{_options.EngineId:D}/
 022        using var response = await httpClient.SendAsync(request, cancellationToken);
 023        if (response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
 024            throw new InvalidOperationException("Elsa Platform runtime command poll was not authorized.");
 25
 026        response.EnsureSuccessStatusCode();
 027        var body = await response.Content.ReadFromJsonAsync<PlatformRuntimeCommandListResponse>(JsonOptions, cancellatio
 028        return body?.Commands ?? [];
 029    }
 30
 31    public async Task<PlatformRuntimeCommandClaimResponse?> ClaimAsync(Guid commandId, CancellationToken cancellationTok
 32    {
 033        using var response = await SendJsonAsync(
 034            BuildUri($"/deployments/runtime/commands/{commandId:D}/claim"),
 035            new PlatformRuntimeCommandClaimRequest(_options.EngineId, _options.WorkerId, (int)_options.ClaimLeaseDuratio
 036            cancellationToken);
 37
 038        if (response.StatusCode == HttpStatusCode.Conflict || response.StatusCode == HttpStatusCode.NotFound)
 039            return null;
 40
 041        response.EnsureSuccessStatusCode();
 042        return await response.Content.ReadFromJsonAsync<PlatformRuntimeCommandClaimResponse>(JsonOptions, cancellationTo
 043    }
 44
 45    public async Task<Stream> DownloadArtifactAsync(
 46        PlatformRuntimeCommand command,
 47        PlatformArtifactItem artifact,
 48        string leaseToken,
 49        CancellationToken cancellationToken = default)
 50    {
 051        if (string.IsNullOrWhiteSpace(artifact.DownloadUrl))
 052            throw new InvalidOperationException("Platform runtime command artifact does not include a download URL.");
 53
 054        using var request = CreateRequest(HttpMethod.Get, BuildUri(artifact.DownloadUrl));
 055        request.Headers.Add(LeaseHeaderName, leaseToken);
 056        request.Headers.Add(WorkerHeaderName, _options.WorkerId);
 057        using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationT
 058        response.EnsureSuccessStatusCode();
 059        var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
 060        var buffer = new MemoryStream();
 061        await CopyBoundedAsync(stream, buffer, cancellationToken);
 062        buffer.Position = 0;
 063        return buffer;
 064    }
 65
 66    public Task ReportProgressAsync(
 67        Guid commandId,
 68        string leaseToken,
 69        string status,
 70        int? percentComplete,
 71        string message,
 72        CancellationToken cancellationToken = default) =>
 073        SendMutationAsync(
 074            commandId,
 075            "progress",
 076            new PlatformRuntimeCommandProgressRequest(leaseToken, status, percentComplete, message),
 077            cancellationToken);
 78
 79    public Task CompleteAsync(Guid commandId, PlatformRuntimeCommandCompleteRequest request, CancellationToken cancellat
 080        SendMutationAsync(commandId, "complete", request, cancellationToken);
 81
 82    public Task FailAsync(Guid commandId, PlatformRuntimeCommandFailRequest request, CancellationToken cancellationToken
 083        SendMutationAsync(commandId, "fail", request, cancellationToken);
 84
 85    public Task RejectAsync(Guid commandId, PlatformRuntimeCommandRejectRequest request, CancellationToken cancellationT
 086        SendMutationAsync(commandId, "reject", request, cancellationToken);
 87
 88    private async Task SendMutationAsync<TRequest>(
 89        Guid commandId,
 90        string action,
 91        TRequest body,
 92        CancellationToken cancellationToken)
 93    {
 094        using var response = await SendJsonAsync(BuildUri($"/deployments/runtime/commands/{commandId:D}/{action}"), body
 095        response.EnsureSuccessStatusCode();
 096    }
 97
 98    private HttpRequestMessage CreateRequest(HttpMethod method, Uri uri)
 99    {
 0100        var request = new HttpRequestMessage(method, uri);
 0101        if (!string.IsNullOrWhiteSpace(_options.EngineSecret))
 0102            request.Headers.TryAddWithoutValidation(EngineSecretHeaderName, _options.EngineSecret);
 0103        return request;
 104    }
 105
 106    private async Task<HttpResponseMessage> SendJsonAsync<TRequest>(Uri uri, TRequest body, CancellationToken cancellati
 107    {
 0108        var request = CreateRequest(HttpMethod.Post, uri);
 0109        request.Content = JsonContent.Create(body, options: JsonOptions);
 0110        return await httpClient.SendAsync(request, cancellationToken);
 0111    }
 112
 113    private Uri BuildUri(string path)
 114    {
 0115        var endpoint = _options.PlatformEndpoint ?? throw new InvalidOperationException("Elsa Platform endpoint is requi
 0116        if (Uri.TryCreate(path, UriKind.Absolute, out var absoluteUri))
 0117            return absoluteUri;
 118
 0119        var relative = path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)
 0120            ? path
 0121            : $"/api/workspaces/{_options.WorkspaceId:D}{path}";
 0122        return new Uri($"{endpoint.AbsoluteUri.TrimEnd('/')}{relative}");
 123    }
 124
 125    private async Task CopyBoundedAsync(Stream source, Stream destination, CancellationToken cancellationToken)
 126    {
 0127        var buffer = new byte[81920];
 0128        long total = 0;
 0129        while (true)
 130        {
 0131            var read = await source.ReadAsync(buffer, cancellationToken);
 0132            if (read == 0)
 0133                return;
 134
 0135            total += read;
 0136            if (total > _options.MaxArtifactBytes)
 0137                throw new InvalidOperationException("Elsa Platform artifact exceeds the configured runtime size limit.")
 138
 0139            await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
 140        }
 0141    }
 142
 143    private static JsonSerializerOptions CreateJsonOptions()
 144    {
 0145        var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
 0146        options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
 0147        return options;
 148    }
 149}