Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\StorageBackend.cs" Link="StorageBackend.cs" />
<ProjectReference Include="..\DurableJobsJournaling.Silo\DurableJobsJournaling.Silo.csproj" />
<ProjectReference Include="..\DurableJobsJournaling.Web\DurableJobsJournaling.Web.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Azure.Provisioning;
using Azure.Provisioning.Storage;
using DurableJobsJournaling;
using DurableJobsJournaling.AppHost.OpenTelemetryCollector;
using Microsoft.Extensions.Configuration;

Expand All @@ -28,57 +30,75 @@

const string OtelMetricExportIntervalMilliseconds = "5000";

var storageProvider = builder.Configuration.GetValue("Playground:Storage:Provider", "Azurite");
var useAzurite = storageProvider.Equals("Azurite", StringComparison.OrdinalIgnoreCase);
var useAzure = storageProvider.Equals("Azure", StringComparison.OrdinalIgnoreCase);
var backend = StorageBackendConfiguration.Parse(builder.Configuration.GetValue("Playground:Storage:Provider", "Azurite"));

var storage = builder.AddAzureStorage("storage");
if (useAzurite)
if (backend.IsEmulator())
{
storage.RunAsEmulator();
}
else if (useAzure)
else
{
storage.ConfigureInfrastructure(infrastructure =>
{
var storageAccount = infrastructure.GetProvisionableResources().OfType<StorageAccount>().Single();
storageAccount.Kind = StorageKind.BlockBlobStorage;
storageAccount.Sku = new StorageSku { Name = StorageSkuName.PremiumLrs };
storageAccount.AccessTier.ClearValue();
RemoveUnsupportedPremiumBlobStorageOutputs(infrastructure);
storageAccount.Kind = backend == StorageBackend.PremiumBlob ? StorageKind.BlockBlobStorage : StorageKind.StorageV2;
storageAccount.Sku = new StorageSku { Name = backend == StorageBackend.PremiumBlob ? StorageSkuName.PremiumLrs : StorageSkuName.StandardLrs };
if (backend == StorageBackend.PremiumBlob)
{
storageAccount.AccessTier.ClearValue();
RemoveUnsupportedPremiumBlobStorageOutputs(infrastructure);
}
});
}
else
var tableStorage = backend == StorageBackend.PremiumBlob ? builder.AddAzureStorage("clusteringstorage") : storage;
if (backend == StorageBackend.PremiumBlob)
{
throw new InvalidOperationException($"Unknown Playground:Storage:Provider value '{storageProvider}'. Use 'Azurite' or 'Azure'.");
tableStorage.ConfigureInfrastructure(infrastructure =>
{
var storageAccount = infrastructure.GetProvisionableResources().OfType<StorageAccount>().Single();
storageAccount.Sku = new StorageSku { Name = StorageSkuName.StandardLrs };
});
}

var blobs = storage.AddBlobs("blobs");
var tableStorage = useAzurite ? storage : builder.AddAzureStorage("clusteringstorage");
if (useAzure && builder.ExecutionContext.IsPublishMode)
var tables = tableStorage.AddTables("tables");

SetStorageRoles(storage, backend.UsesTableJournal()
? [StorageBuiltInRole.StorageTableDataContributor]
: backend == StorageBackend.PremiumBlob
? [StorageBuiltInRole.StorageBlobDataContributor]
: [StorageBuiltInRole.StorageBlobDataContributor, StorageBuiltInRole.StorageTableDataContributor]);
if (backend == StorageBackend.PremiumBlob)
{
storage.ClearDefaultRoleAssignments();
tableStorage.ClearDefaultRoleAssignments();
SetStorageRoles(tableStorage, [StorageBuiltInRole.StorageTableDataContributor]);
}

var tables = tableStorage.AddTables("tables");

var orleans = builder.AddOrleans("cluster")
.WithClustering(tables);

var storagePrefix = $"run-{DateTimeOffset.UtcNow:yyyyMMdd-HHmmss}";
var runId = Guid.NewGuid().ToString("N");
var silo = builder.AddProject<Projects.DurableJobsJournaling_Silo>("silo")
.WithReference(orleans)
.WithReference(blobs)
.WithReference(tables)
.WaitFor(blobs)
.WaitFor(tables)
.WaitFor(otelCollector)
.WithReplicas(1)
.WithEnvironment("Playground__Storage__Container", "durablejobs-journaling-playground")
.WithEnvironment("Playground__Storage__Prefix", storagePrefix)
.WithEnvironment("Playground__Storage__Provider", backend.ToString())
.WithEnvironment("Playground__Storage__Container", $"durablejobs-{runId}")
.WithEnvironment("Playground__Storage__Table", $"durablejobs{runId}")
.WithEnvironment("OTEL_METRIC_EXPORT_INTERVAL", OtelMetricExportIntervalMilliseconds);

if (backend.UsesTableJournal())
{
var journals = storage.AddTables("journals");
silo.WithReference(journals).WaitFor(journals);
}
else
{
var blobs = storage.AddBlobs("blobs");
silo.WithReference(blobs).WaitFor(blobs);
}

builder.AddProject<Projects.DurableJobsJournaling_Web>("web")
.WithReference(orleans.AsClient())
.WithReference(tables)
Expand All @@ -91,6 +111,14 @@

builder.Build().Run();

static void SetStorageRoles(IResourceBuilder<AzureStorageResource> storage, StorageBuiltInRole[] roles)
{
storage.ClearDefaultRoleAssignments()
.WithAnnotation(new DefaultRoleAssignmentsAnnotation(roles
.Select(role => new RoleDefinition(role.ToString(), StorageBuiltInRole.GetBuiltInRoleName(role)))
.ToHashSet()));
}

static void RemoveUnsupportedPremiumBlobStorageOutputs(AzureResourceInfrastructure infrastructure)
{
foreach (var output in infrastructure.GetProvisionableResources()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\StorageBackend.cs" Link="StorageBackend.cs" />
<ProjectReference Include="..\DurableJobsJournaling.Abstractions\DurableJobsJournaling.Abstractions.csproj" />
<ProjectReference Include="..\DurableJobsJournaling.ServiceDefaults\DurableJobsJournaling.ServiceDefaults.csproj" />
<ProjectReference Include="..\..\..\src\Orleans.Server\Orleans.Server.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Azure.Data.Tables;
using Azure.Storage.Blobs;
using DurableJobsJournaling;
using DurableJobsJournaling.Silo;
using Orleans.Dashboard;
using Orleans.Journaling;
Expand All @@ -7,27 +9,53 @@
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.AddAzureBlobServiceClient("blobs");
builder.AddKeyedAzureTableServiceClient("tables");

var storageContainer = builder.Configuration.GetValue("Playground:Storage:Container", "durablejobs-journaling-playground");
var storagePrefix = builder.Configuration.GetValue("Playground:Storage:Prefix", $"run-{DateTimeOffset.UtcNow:yyyyMMdd-HHmmss}");
var backend = StorageBackendConfiguration.Parse(builder.Configuration.GetValue("Playground:Storage:Provider", "Azurite"));
if (backend.UsesTableJournal())
{
builder.AddAzureTableServiceClient("journals");
builder.Services.AddOptions<AzureTableJournalStorageOptions>()
.Configure<TableServiceClient>((options, client) =>
{
options.TableServiceClient = client;
});
}
else
{
builder.AddAzureBlobServiceClient("blobs");
builder.Services.AddOptions<AzureBlobJournalStorageOptions>()
.Configure<BlobServiceClient>((options, client) =>
{
options.BlobServiceClient = client;
});
}

builder.UseOrleans(siloBuilder =>
{
#pragma warning disable ORLEANSEXP003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
if (backend.UsesTableJournal())
{
siloBuilder.UseAzureTableDurableJobs(options =>
{
options.TableName = builder.Configuration["Playground:Storage:Table"]
?? throw new InvalidOperationException("Set Playground:Storage:Table to the shared run-specific table name.");
});
}
else
{
siloBuilder.UseAzureBlobDurableJobs(options =>
{
options.ContainerName = builder.Configuration["Playground:Storage:Container"]
?? throw new InvalidOperationException("Set Playground:Storage:Container to the shared run-specific container name.");
});
}

siloBuilder
.AddDashboard()
.AddActivityPropagation()
.AddIncomingGrainCallFilter<GrainRequestMetricsFilter>()
.AddDistributedGrainDirectory()
.UseAzureBlobDurableJobs(
options =>
{
options.ContainerName = storageContainer;
options.GetWalBlobName = journalId => $"{storagePrefix}/{journalId.Value}/wal";
options.GetCheckpointBlobName = (journalId, snapshotId) => $"{storagePrefix}/{journalId.Value}/chk.{snapshotId}";
})
.UseJsonJournalFormat(DurableJobsJournalingJsonContext.Default)
.Configure<DurableJobsOptions>(options =>
{
Expand All @@ -46,12 +74,6 @@
#pragma warning restore ORLEANSEXP003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
});

builder.Services.AddOptions<AzureBlobJournalStorageOptions>()
.Configure<BlobServiceClient>((options, blobServiceClient) =>
{
options.BlobServiceClient = blobServiceClient;
});

var app = builder.Build();
app.MapDefaultEndpoints();
app.MapOrleansDashboard();
Expand Down
69 changes: 69 additions & 0 deletions playground/DurableJobsJournaling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Durable Jobs journaling playground

The Aspire app runs durable workflow grains, a web load driver, and
Prometheus/Grafana monitoring. Select journal storage using the shared
`Playground:Storage:Provider` setting:

| Value | Journal storage | Clustering |
| --- | --- | --- |
| `Azurite` (default) | Azurite Blob | Azurite Table |
| `AzuriteTable` | Azurite Table | Azurite Table |
| `StandardBlob` | Standard Azure Blob account | Table service on the standard account |
| `PremiumBlob` | Premium LRS BlockBlobStorage account | Separate standard account's Table service |
| `Table` | Azure Table | Table service on the same standard account |
| `Azure` | Alias for `PremiumBlob` | Separate standard account's Table service |

Both Blob tiers use append-blob WALs and block-blob checkpoints. Premium accounts
expose Blob resources; the AppHost removes unsupported queue/table outputs and
supplies clustering through a separate standard account.

From the repository root, with the .NET SDK and a Docker-compatible container
runtime available:

```powershell
dotnet run --project playground\DurableJobsJournaling\DurableJobsJournaling.AppHost

# To select Table journaling on the emulator:
$env:Playground__Storage__Provider = 'AzuriteTable'
dotnet run --project playground\DurableJobsJournaling\DurableJobsJournaling.AppHost
```

The AppHost passes the normalized backend to the silo and references `blobs` or
`journals` for journal data, plus `tables` for clustering. The web client uses
the clustering reference. Each app run generates a GUID-based container
(`durablejobs-...`) or journal table (`durablejobs...`) shared by its silo
replicas. Blob layout remains `wal/{journalId}` and
`checkpoints/{journalId}/{snapshotId}`, enabling catalog discovery with the
provider's default layout.

The run-specific resources preserve journals for inspection. Record the
container/table name from Aspire's environment configuration and delete that
resource when the run's data is no longer needed. For restart/recovery
experiments, keep the shared resource name and workload identity stable across
the processes being restarted.

Cloud backend selection enables Aspire's Azure resource configuration and can
provision chargeable resources. Use an explicitly approved Azure development
environment and its standard Aspire authentication/deployment procedure.
`PremiumBlob` configures `BlockBlobStorage` with `Premium_LRS`; standard accounts
use `Standard_LRS`.
Storage role defaults match the selected services: Blob and Table Data
Contributor for a shared standard Blob/clustering account, Table Data Contributor
for Table journals, and separate Blob-only journal and Table-only clustering
assignments for premium Blob. Generic manifest publishing retains role modules
with `principalId` and `principalType` inputs for the deployment's intended
identity. An identity-aware Aspire deployment environment can apply these defaults
to its application identities. Configure that environment and identity before
deployment; local Azurite uses its emulator credentials.
Compare backends using the same compute, region, account redundancy, job
distribution, and `Playground:DurableJobs` settings.

Open the web resource from Aspire's dashboard to configure load concurrency/rate,
start and stop load, drain outstanding work, and inspect workflow metrics.
Existing stage-write and end-to-end latency metrics, durable-job retry policy,
slow start, and scheduling tunings apply to every backend. Use
`Playground__DurableJobs__...` settings to adjust those tunings.

For a finite provider-only workload with isolated resource cleanup, operation
percentiles, and JSON/CSV reports, use the
[Azure journal benchmarks](../../test/Benchmarks/Journaling/Azure/README.md).
26 changes: 26 additions & 0 deletions playground/DurableJobsJournaling/StorageBackend.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace DurableJobsJournaling;

internal enum StorageBackend
{
Azurite,
AzuriteTable,
StandardBlob,
PremiumBlob,
Table
}

internal static class StorageBackendConfiguration
{
public static StorageBackend Parse(string value) => value.ToUpperInvariant() switch
{
"AZURITE" => StorageBackend.Azurite,
"AZURITETABLE" => StorageBackend.AzuriteTable,
"STANDARDBLOB" => StorageBackend.StandardBlob,
"PREMIUMBLOB" or "AZURE" => StorageBackend.PremiumBlob,
"TABLE" => StorageBackend.Table,
_ => throw new InvalidOperationException("Playground:Storage:Provider must be Azurite, AzuriteTable, StandardBlob, PremiumBlob, Table, or Azure (PremiumBlob).")
};

public static bool IsEmulator(this StorageBackend backend) => backend is StorageBackend.Azurite or StorageBackend.AzuriteTable;
public static bool UsesTableJournal(this StorageBackend backend) => backend is StorageBackend.Table or StorageBackend.AzuriteTable;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Orleans.Hosting;

/// <summary>
/// Extensions for configuring Azure Blob Storage durable jobs.
/// Extensions for configuring Azure Storage durable jobs.
/// </summary>
public static class AzureStorageDurableJobsExtensions
{
Expand Down Expand Up @@ -65,6 +65,39 @@ public static IServiceCollection UseAzureBlobDurableJobs(this IServiceCollection
return services;
}

/// <summary>
/// Adds durable jobs backed by Azure Table journal storage.
/// </summary>
/// <param name="builder">The silo builder.</param>
/// <param name="configure">The delegate used to configure journal storage.</param>
/// <returns>The silo builder, for chaining.</returns>
public static ISiloBuilder UseAzureTableDurableJobs(this ISiloBuilder builder, Action<AzureTableJournalStorageOptions> configure)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configure);

builder.AddDurableJobs();
builder.AddAzureTableJournalStorage(configure);
builder.Configure<JsonJournalOptions>(options => options.AddTypeInfoResolver(DurableJobsJsonContext.Default));
builder.Services.UseJournaledDurableJobs();
return builder;
}

/// <summary>
/// Adds durable jobs backed by Azure Table journal storage.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configure">The delegate used to configure journal storage.</param>
/// <returns>The service collection, for chaining.</returns>
public static IServiceCollection UseAzureTableDurableJobs(this IServiceCollection services, Action<AzureTableJournalStorageOptions> configure)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);

new ServiceCollectionSiloBuilder(services).UseAzureTableDurableJobs(configure);
return services;
}

private static IServiceCollection UseJournaledDurableJobs(this IServiceCollection services)
{
services.TryAddSingleton<JournaledJobShardManager>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageId>Microsoft.Orleans.DurableJobs.AzureStorage</PackageId>
<Title>Microsoft Orleans Azure Storage Durable Jobs Provider</Title>
<Description>Microsoft Orleans durable jobs provider backed by Azure Blob Storage</Description>
<Description>Microsoft Orleans durable jobs provider backed by Azure Blob or Azure Table journal storage</Description>
<PackageTags>$(PackageTags) Azure Storage</PackageTags>
<TargetFrameworks>$(DefaultTargetFrameworks)</TargetFrameworks>
<AssemblyName>Orleans.DurableJobs.AzureStorage</AssemblyName>
Expand Down
Loading
Loading