Add Authority Advisory AI and API Lifecycle Configuration

- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings.
- Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations.
- Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration.
- Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options.
- Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations.
- Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client.
- Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
master
2025-11-02 13:40:38 +02:00
parent 66cb6c4b8a
commit f98cea3bcf
516 changed files with 68157 additions and 24754 deletions

View File

@@ -1,179 +1,492 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.Worker.Options;
using StellaOps.Scanner.Worker.Processing;
using Xunit;
namespace StellaOps.Scanner.Worker.Tests;
public sealed class EntryTraceExecutionServiceTests : IDisposable
{
private readonly string _tempRoot;
public EntryTraceExecutionServiceTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-service-{Guid.NewGuid():n}");
Directory.CreateDirectory(_tempRoot);
}
[Fact]
public async Task ExecuteAsync_Skips_When_ConfigMetadataMissing()
{
var analyzer = new CapturingEntryTraceAnalyzer();
var service = CreateService(analyzer);
var context = CreateContext(new Dictionary<string, string>());
await service.ExecuteAsync(context, CancellationToken.None);
Assert.False(analyzer.Invoked);
Assert.False(context.Analysis.TryGet<EntryTraceGraph>(ScanAnalysisKeys.EntryTraceGraph, out _));
}
[Fact]
public async Task ExecuteAsync_BuildsContext_AndStoresGraph()
{
var configPath = Path.Combine(_tempRoot, "config.json");
File.WriteAllText(configPath, """
{
"config": {
"Env": ["PATH=/bin:/usr/bin"],
"Entrypoint": ["/entrypoint.sh"],
"WorkingDir": "/workspace",
"User": "scanner"
}
}
""");
var layerDirectory = Path.Combine(_tempRoot, "layer-1");
Directory.CreateDirectory(layerDirectory);
File.WriteAllText(Path.Combine(layerDirectory, "entrypoint.sh"), "#!/bin/sh\necho hello\n");
var metadata = new Dictionary<string, string>
{
[ScanMetadataKeys.ImageConfigPath] = configPath,
[ScanMetadataKeys.LayerDirectories] = layerDirectory,
["image.digest"] = "sha256:test-digest"
};
var analyzer = new CapturingEntryTraceAnalyzer();
var service = CreateService(analyzer);
var context = CreateContext(metadata);
await service.ExecuteAsync(context, CancellationToken.None);
Assert.True(analyzer.Invoked);
Assert.NotNull(analyzer.LastEntrypoint);
Assert.Equal("/entrypoint.sh", analyzer.LastEntrypoint!.Entrypoint[0]);
Assert.NotNull(analyzer.LastContext);
Assert.Equal("scanner", analyzer.LastContext!.User);
Assert.Equal("/workspace", analyzer.LastContext.WorkingDirectory);
Assert.Contains("/bin", analyzer.LastContext.Path);
Assert.True(context.Analysis.TryGet(ScanAnalysisKeys.EntryTraceGraph, out EntryTraceGraph stored));
Assert.Same(analyzer.Graph, stored);
}
private EntryTraceExecutionService CreateService(IEntryTraceAnalyzer analyzer)
{
var workerOptions = new ScannerWorkerOptions();
var entryTraceOptions = new EntryTraceAnalyzerOptions();
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Trace));
return new EntryTraceExecutionService(
analyzer,
Options.Create(entryTraceOptions),
Options.Create(workerOptions),
loggerFactory.CreateLogger<EntryTraceExecutionService>(),
loggerFactory);
}
private static ScanJobContext CreateContext(IReadOnlyDictionary<string, string> metadata)
{
var lease = new TestLease(metadata);
return new ScanJobContext(lease, TimeProvider.System, DateTimeOffset.UtcNow, CancellationToken.None);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempRoot))
{
Directory.Delete(_tempRoot, recursive: true);
}
}
catch
{
// ignore cleanup failures
}
}
private sealed class CapturingEntryTraceAnalyzer : IEntryTraceAnalyzer
{
public bool Invoked { get; private set; }
public EntrypointSpecification? LastEntrypoint { get; private set; }
public EntryTraceContext? LastContext { get; private set; }
public EntryTraceGraph Graph { get; } = new(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty);
public ValueTask<EntryTraceGraph> ResolveAsync(EntrypointSpecification entrypoint, EntryTraceContext context, CancellationToken cancellationToken = default)
{
Invoked = true;
LastEntrypoint = entrypoint;
LastContext = context;
return ValueTask.FromResult(Graph);
}
}
private sealed class TestLease : IScanJobLease
{
private readonly IReadOnlyDictionary<string, string> _metadata;
public TestLease(IReadOnlyDictionary<string, string> metadata)
{
_metadata = metadata;
EnqueuedAtUtc = DateTimeOffset.UtcNow;
LeasedAtUtc = EnqueuedAtUtc;
}
public string JobId { get; } = $"job-{Guid.NewGuid():n}";
public string ScanId { get; } = $"scan-{Guid.NewGuid():n}";
public int Attempt => 1;
public DateTimeOffset EnqueuedAtUtc { get; }
public DateTimeOffset LeasedAtUtc { get; }
public TimeSpan LeaseDuration => TimeSpan.FromMinutes(5);
public IReadOnlyDictionary<string, string> Metadata => _metadata;
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.EntryTrace.Runtime;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.FS;
using StellaOps.Scanner.Surface.Secrets;
using StellaOps.Scanner.Surface.Validation;
using StellaOps.Scanner.Worker.Options;
using StellaOps.Scanner.Worker.Processing;
using Xunit;
namespace StellaOps.Scanner.Worker.Tests;
public sealed class EntryTraceExecutionServiceTests : IDisposable
{
private readonly string _tempRoot;
public EntryTraceExecutionServiceTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-service-{Guid.NewGuid():n}");
Directory.CreateDirectory(_tempRoot);
}
[Fact]
public async Task ExecuteAsync_Skips_When_ConfigMetadataMissing()
{
var analyzer = new CapturingEntryTraceAnalyzer();
var store = new CapturingEntryTraceResultStore();
var service = CreateService(analyzer, store);
var context = CreateContext(new Dictionary<string, string>());
await service.ExecuteAsync(context, CancellationToken.None);
Assert.False(analyzer.Invoked);
Assert.False(context.Analysis.TryGet<EntryTraceGraph>(ScanAnalysisKeys.EntryTraceGraph, out _));
Assert.False(store.Stored);
}
[Fact]
public async Task ExecuteAsync_BuildsContext_AndStoresGraph()
{
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
var analyzer = new CapturingEntryTraceAnalyzer();
var store = new CapturingEntryTraceResultStore();
var service = CreateService(analyzer, store);
var context = CreateContext(metadata);
await service.ExecuteAsync(context, CancellationToken.None);
Assert.True(analyzer.Invoked);
Assert.NotNull(analyzer.LastEntrypoint);
Assert.Equal("/entrypoint.sh", analyzer.LastEntrypoint!.Entrypoint[0]);
Assert.NotNull(analyzer.LastContext);
Assert.Equal("scanner", analyzer.LastContext!.User);
Assert.Equal("/workspace", analyzer.LastContext.WorkingDirectory);
Assert.Contains("/bin", analyzer.LastContext.Path);
Assert.True(context.Analysis.TryGet(ScanAnalysisKeys.EntryTraceGraph, out EntryTraceGraph stored));
Assert.Equal(analyzer.Graph.Outcome, stored.Outcome);
Assert.Contains(stored.Diagnostics, diagnostic => diagnostic.Reason == EntryTraceUnknownReason.RuntimeSnapshotUnavailable);
Assert.True(context.Analysis.TryGet(ScanAnalysisKeys.EntryTraceNdjson, out ImmutableArray<string> ndjsonPayload));
Assert.False(ndjsonPayload.IsDefaultOrEmpty);
Assert.True(store.Stored);
Assert.NotNull(store.LastResult);
Assert.Equal(context.ScanId, store.LastResult!.ScanId);
Assert.Equal("sha256:test-digest", store.LastResult.ImageDigest);
Assert.Equal(stored, store.LastResult.Graph);
Assert.Equal(ndjsonPayload, store.LastResult.Ndjson);
}
[Fact]
public async Task ExecuteAsync_UsesCachedGraphWhenAvailable()
{
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
var analyzer = new CapturingEntryTraceAnalyzer();
var store = new CapturingEntryTraceResultStore();
var cache = new InMemorySurfaceCache();
var service = CreateService(analyzer, store, surfaceCache: cache);
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
Assert.True(analyzer.Invoked);
analyzer.Reset();
store.Reset();
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
Assert.False(analyzer.Invoked);
Assert.True(store.Stored);
}
[Fact]
public async Task ExecuteAsync_ReplacesSecretReferencesUsingSurfaceSecrets()
{
var metadata = CreateMetadata("API_KEY=secret://inline/api-key");
var analyzer = new CapturingEntryTraceAnalyzer();
var store = new CapturingEntryTraceResultStore();
var secrets = new StubSurfaceSecretProvider(new Dictionary<(string Type, string Name), byte[]>
{
{("inline", "api-key"), Encoding.UTF8.GetBytes("resolved-value")}
});
var service = CreateService(analyzer, store, surfaceSecrets: secrets);
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
Assert.True(analyzer.Invoked);
Assert.Equal("resolved-value", analyzer.LastContext!.Environment["API_KEY"]);
}
[Fact]
public async Task ExecuteAsync_FallsBackToBase64ForBinarySecrets()
{
var metadata = CreateMetadata("BLOB=secret://inline/blob");
var analyzer = new CapturingEntryTraceAnalyzer();
var store = new CapturingEntryTraceResultStore();
var payload = new byte[] { 0x00, 0xFF, 0x10 };
var secrets = new StubSurfaceSecretProvider(new Dictionary<(string Type, string Name), byte[]>
{
{("inline", "blob"), payload}
});
var service = CreateService(analyzer, store, surfaceSecrets: secrets);
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
Assert.True(analyzer.Invoked);
Assert.Equal(Convert.ToBase64String(payload), analyzer.LastContext!.Environment["BLOB"]);
}
[Fact]
public async Task ExecuteAsync_SkipsWhenSurfaceValidationFails()
{
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
var analyzer = new CapturingEntryTraceAnalyzer();
var store = new CapturingEntryTraceResultStore();
var issues = new[]
{
SurfaceValidationIssue.Error("cache", "unwritable")
};
var validator = new StaticSurfaceValidatorRunner(SurfaceValidationResult.FromIssues(issues));
var service = CreateService(analyzer, store, surfaceValidator: validator);
await service.ExecuteAsync(CreateContext(metadata), CancellationToken.None);
Assert.False(analyzer.Invoked);
Assert.False(store.Stored);
}
private EntryTraceExecutionService CreateService(
IEntryTraceAnalyzer analyzer,
IEntryTraceResultStore store,
ISurfaceCache? surfaceCache = null,
ISurfaceValidatorRunner? surfaceValidator = null,
ISurfaceSecretProvider? surfaceSecrets = null,
ISurfaceEnvironment? surfaceEnvironment = null)
{
var workerOptions = new ScannerWorkerOptions();
var entryTraceOptions = new EntryTraceAnalyzerOptions();
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Trace));
surfaceEnvironment ??= new StubSurfaceEnvironment();
surfaceCache ??= new InMemorySurfaceCache();
surfaceValidator ??= new NoopSurfaceValidatorRunner();
surfaceSecrets ??= new StubSurfaceSecretProvider();
var serviceProvider = new ServiceCollection()
.AddSingleton<ISurfaceEnvironment>(surfaceEnvironment)
.BuildServiceProvider();
return new EntryTraceExecutionService(
analyzer,
Microsoft.Extensions.Options.Options.Create(entryTraceOptions),
Microsoft.Extensions.Options.Options.Create(workerOptions),
loggerFactory.CreateLogger<EntryTraceExecutionService>(),
loggerFactory,
new EntryTraceRuntimeReconciler(),
store,
surfaceValidator,
surfaceEnvironment,
surfaceCache,
surfaceSecrets,
serviceProvider);
}
private static ScanJobContext CreateContext(IReadOnlyDictionary<string, string> metadata)
{
var lease = new TestLease(metadata);
return new ScanJobContext(lease, TimeProvider.System, DateTimeOffset.UtcNow, CancellationToken.None);
}
private Dictionary<string, string> CreateMetadata(params string[] environmentEntries)
{
var configPath = Path.Combine(_tempRoot, $"config-{Guid.NewGuid():n}.json");
var env = environmentEntries.Length == 0
? new[] { "PATH=/bin:/usr/bin" }
: environmentEntries;
var envJson = string.Join(",", env.Select(value => $"\"{value}\""));
File.WriteAllText(configPath,
$$"""
{
"config": {
"Env": [{{envJson}}],
"Entrypoint": ["/entrypoint.sh"],
"WorkingDir": "/workspace",
"User": "scanner"
}
}
""");
var rootDirectory = Path.Combine(_tempRoot, $"root-{Guid.NewGuid():n}");
Directory.CreateDirectory(rootDirectory);
File.WriteAllText(Path.Combine(rootDirectory, "entrypoint.sh"), "#!/bin/sh\necho hello\n");
return new Dictionary<string, string>
{
[ScanMetadataKeys.ImageConfigPath] = configPath,
[ScanMetadataKeys.RootFilesystemPath] = rootDirectory,
[ScanMetadataKeys.LayerDirectories] = rootDirectory,
["image.digest"] = "sha256:test-digest"
};
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempRoot))
{
Directory.Delete(_tempRoot, recursive: true);
}
}
catch
{
// ignore cleanup failures
}
}
private sealed class CapturingEntryTraceAnalyzer : IEntryTraceAnalyzer
{
public bool Invoked { get; private set; }
public EntrypointSpecification? LastEntrypoint { get; private set; }
public EntryTraceContext? LastContext { get; private set; }
public EntryTraceGraph Graph { get; } = new(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray<EntryTracePlan>.Empty,
ImmutableArray<EntryTraceTerminal>.Empty);
public ValueTask<EntryTraceGraph> ResolveAsync(EntrypointSpecification entrypoint, EntryTraceContext context, CancellationToken cancellationToken = default)
{
Invoked = true;
LastEntrypoint = entrypoint;
LastContext = context;
return ValueTask.FromResult(Graph);
}
public void Reset()
{
Invoked = false;
LastEntrypoint = null;
LastContext = null;
}
}
private sealed class CapturingEntryTraceResultStore : IEntryTraceResultStore
{
public bool Stored { get; private set; }
public EntryTraceResult? LastResult { get; private set; }
public Task<EntryTraceResult?> GetAsync(string scanId, CancellationToken cancellationToken)
{
return Task.FromResult<EntryTraceResult?>(null);
}
public Task StoreAsync(EntryTraceResult result, CancellationToken cancellationToken)
{
Stored = true;
LastResult = result;
return Task.CompletedTask;
}
public void Reset()
{
Stored = false;
LastResult = null;
}
}
private sealed class TestLease : IScanJobLease
{
private readonly IReadOnlyDictionary<string, string> _metadata;
public TestLease(IReadOnlyDictionary<string, string> metadata)
{
_metadata = metadata;
EnqueuedAtUtc = DateTimeOffset.UtcNow;
LeasedAtUtc = EnqueuedAtUtc;
}
public string JobId { get; } = $"job-{Guid.NewGuid():n}";
public string ScanId { get; } = $"scan-{Guid.NewGuid():n}";
public int Attempt => 1;
public DateTimeOffset EnqueuedAtUtc { get; }
public DateTimeOffset LeasedAtUtc { get; }
public TimeSpan LeaseDuration => TimeSpan.FromMinutes(5);
public IReadOnlyDictionary<string, string> Metadata => _metadata;
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
private sealed class StubSurfaceEnvironment : ISurfaceEnvironment
{
public StubSurfaceEnvironment()
{
var cacheRoot = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "surface-cache-tests"));
Settings = new SurfaceEnvironmentSettings(
new Uri("https://surface.example"),
"surface-cache",
null,
cacheRoot,
1024,
false,
Array.Empty<string>(),
new SurfaceSecretsConfiguration("inline", "tenant", null, null, null, AllowInline: true),
"tenant",
new SurfaceTlsConfiguration(null, null, null));
RawVariables = new Dictionary<string, string>();
}
public SurfaceEnvironmentSettings Settings { get; }
public IReadOnlyDictionary<string, string> RawVariables { get; }
}
private sealed class NoopSurfaceValidatorRunner : ISurfaceValidatorRunner
{
public ValueTask<SurfaceValidationResult> RunAllAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(SurfaceValidationResult.Success());
}
public ValueTask EnsureAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
{
return ValueTask.CompletedTask;
}
}
private sealed class StaticSurfaceValidatorRunner : ISurfaceValidatorRunner
{
private readonly SurfaceValidationResult _result;
public StaticSurfaceValidatorRunner(SurfaceValidationResult result)
{
_result = result;
}
public ValueTask<SurfaceValidationResult> RunAllAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
{
return ValueTask.FromResult(_result);
}
public ValueTask EnsureAsync(SurfaceValidationContext context, CancellationToken cancellationToken = default)
{
return ValueTask.CompletedTask;
}
}
private sealed class InMemorySurfaceCache : ISurfaceCache
{
private readonly Dictionary<string, byte[]> _store = new();
private readonly object _gate = new();
public async Task<T> GetOrCreateAsync<T>(
SurfaceCacheKey key,
Func<CancellationToken, Task<T>> factory,
Func<T, ReadOnlyMemory<byte>> serializer,
Func<ReadOnlyMemory<byte>, T> deserializer,
CancellationToken cancellationToken = default)
{
if (TryRead(key, deserializer, out var existing))
{
return existing;
}
var created = await factory(cancellationToken).ConfigureAwait(false);
var payload = serializer(created).ToArray();
lock (_gate)
{
_store[key.ToString()] = payload;
}
return created;
}
public Task<T?> TryGetAsync<T>(
SurfaceCacheKey key,
Func<ReadOnlyMemory<byte>, T> deserializer,
CancellationToken cancellationToken = default)
{
return Task.FromResult(TryRead(key, deserializer, out var value) ? value : default);
}
public Task SetAsync(
SurfaceCacheKey key,
ReadOnlyMemory<byte> payload,
CancellationToken cancellationToken = default)
{
lock (_gate)
{
_store[key.ToString()] = payload.ToArray();
}
return Task.CompletedTask;
}
private bool TryRead<T>(SurfaceCacheKey key, Func<ReadOnlyMemory<byte>, T> deserializer, out T value)
{
lock (_gate)
{
if (_store.TryGetValue(key.ToString(), out var bytes))
{
value = deserializer(new ReadOnlyMemory<byte>(bytes));
return true;
}
}
value = default!;
return false;
}
}
private sealed class StubSurfaceSecretProvider : ISurfaceSecretProvider
{
private readonly Dictionary<(string Type, string Name), byte[]> _secrets;
private readonly bool _throwOnMissing;
public StubSurfaceSecretProvider(Dictionary<(string Type, string Name), byte[]>? secrets = null, bool throwOnMissing = false)
{
_secrets = secrets ?? new Dictionary<(string Type, string Name), byte[]>();
_throwOnMissing = throwOnMissing;
}
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
{
var key = (request.SecretType, request.Name ?? string.Empty);
if (_secrets.TryGetValue(key, out var payload))
{
return ValueTask.FromResult(SurfaceSecretHandle.FromBytes(payload));
}
if (_throwOnMissing)
{
throw new SurfaceSecretNotFoundException(request);
}
return ValueTask.FromResult(SurfaceSecretHandle.Empty);
}
}
}

View File

@@ -153,7 +153,7 @@ public sealed class RedisWorkerSmokeTests
public async Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken)
{
var request = new QueueLeaseRequest(_consumerName, 1, _queueOptions.DefaultLeaseDuration);
var leases = await _queue.LeaseAsync(request, cancellationToken).ConfigureAwait(false);
var leases = await _queue.LeaseAsync(request, cancellationToken);
if (leases.Count == 0)
{
return null;
@@ -221,23 +221,23 @@ public sealed class RedisWorkerSmokeTests
public async ValueTask RenewAsync(CancellationToken cancellationToken)
{
await _lease.RenewAsync(_options.DefaultLeaseDuration, cancellationToken).ConfigureAwait(false);
await _lease.RenewAsync(_options.DefaultLeaseDuration, cancellationToken);
}
public async ValueTask CompleteAsync(CancellationToken cancellationToken)
{
await _lease.AcknowledgeAsync(cancellationToken).ConfigureAwait(false);
await _lease.AcknowledgeAsync(cancellationToken);
_deps.JobCompleted.TrySetResult();
}
public async ValueTask AbandonAsync(string reason, CancellationToken cancellationToken)
{
await _lease.ReleaseAsync(QueueReleaseDisposition.Retry, cancellationToken).ConfigureAwait(false);
await _lease.ReleaseAsync(QueueReleaseDisposition.Retry, cancellationToken);
}
public async ValueTask PoisonAsync(string reason, CancellationToken cancellationToken)
{
await _lease.DeadLetterAsync(reason, cancellationToken).ConfigureAwait(false);
await _lease.DeadLetterAsync(reason, cancellationToken);
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;