Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7b93b58
feat: 2 versions - Need Refactor
jaironalves Nov 21, 2025
b98ac4e
feat: Redis Durable Jobs
jaironalves Nov 27, 2025
bcb6316
Initial analysis of Redis DurableJobs package
Copilot Nov 28, 2025
42f12ed
Make Redis DurableJobs production-ready with proper hosting infrastru…
Copilot Nov 28, 2025
308c6ad
Add Redis DurableJobs tests following Azure test patterns
Copilot Nov 28, 2025
73fb231
Update RedisJobShardOptions to use ConfigurationOptions pattern and r…
Copilot Nov 28, 2025
e98c0ba
Add missing using Orleans.Configuration namespace in RedisDurableJobs…
Copilot Nov 30, 2025
451a6e5
Remove nullable type annotations from test fixture (nullable not enab…
Copilot Nov 30, 2025
15d9134
Fix StreamRange count parameter - remove invalid 0 value
Copilot Nov 30, 2025
833aa43
Remove unnecessary note comment from StreamRange call
Copilot Nov 30, 2025
4316469
Add Encode method to RedisStreamJsonSerializer and use JobOperationJs…
Copilot Nov 30, 2025
c358bdf
Fix UnregisterShardAsync to delete shard when job count is zero like …
Copilot Nov 30, 2025
e4f62f8
Set Metadata property in RedisJobShard constructor to sync with Azure…
Copilot Nov 30, 2025
b616734
Fix CreateShardAsync to save all metadata fields using cjson in Lua s…
Copilot Nov 30, 2025
b730c36
Remove new Metadata property - use base class property instead
Copilot Nov 30, 2025
088b209
feat: Revert lines projects positions
jaironalves Nov 30, 2025
953c954
Add LoggerMessage attributes to RedisJobShardManager
Copilot Dec 1, 2025
c6c3dda
Merge branch 'dotnet:main' into feature/redis-durablejobs
jaironalves Dec 2, 2025
5f7051d
Remove unused log methods with Azure-specific references from Redis D…
Copilot Dec 3, 2025
32e80c3
Fix critical issues in Redis DurableJobs implementation
Copilot Dec 4, 2025
b2b7c2b
Fix metadata version calculation bug in Redis DurableJobs
Copilot Dec 4, 2025
a4e925a
Add Portuguese summary of Redis DurableJobs review
Copilot Dec 4, 2025
61a989e
Remove review markdown files as requested
Copilot Dec 5, 2025
7ee6d6d
Centralize Redis operations into RedisOperationsManager for DurableJo…
Copilot Dec 5, 2025
6c8f389
Fix method signature in TryTakeOwnershipAsync call
Copilot Dec 5, 2025
1fbda00
Add clarifying comments for design decisions
Copilot Dec 5, 2025
e410213
feat: Fix Encode Return Type
jaironalves Dec 6, 2025
5856f47
feat: Review unused var
jaironalves Dec 6, 2025
d7eb627
Apply PR review suggestions: use 'is null/is not null' pattern and nu…
Copilot Dec 6, 2025
2013565
Fix code formatting to follow project guidelines
Copilot Dec 6, 2025
978b9b2
Validate CreateMultiplexer option
jaironalves Dec 6, 2025
c369cc2
feat: Copilot Review
jaironalves Dec 7, 2025
5f49aec
fix: Move redis checker to constructor - Same other redis implementat…
jaironalves Dec 7, 2025
4ecab6b
feat: Extensions remove duplicates
jaironalves Dec 7, 2025
f391104
feat: Copilot Review
jaironalves Dec 7, 2025
671c9a9
feat: Key prefix redis options
jaironalves Dec 7, 2025
841de00
feat: Merge main
jaironalves Mar 28, 2026
e9f0de9
Update test project to match the new project layout
jaironalves Mar 28, 2026
24c23ad
Add internal visible
jaironalves Mar 28, 2026
72ce1f9
Remove properties
jaironalves Mar 28, 2026
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
1 change: 1 addition & 0 deletions Orleans.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
</Folder>
<Folder Name="/src/Extensions/Redis/">
<Project Path="src/Redis/Orleans.Clustering.Redis/Orleans.Clustering.Redis.csproj" />
<Project Path="src/Redis/Orleans.DurableJobs.Redis/Orleans.DurableJobs.Redis.csproj" />
<Project Path="src/Redis/Orleans.GrainDirectory.Redis/Orleans.GrainDirectory.Redis.csproj" />
<Project Path="src/Redis/Orleans.Persistence.Redis/Orleans.Persistence.Redis.csproj" />
<Project Path="src/Redis/Orleans.Reminders.Redis/Orleans.Reminders.Redis.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Orleans.Configuration;
using Orleans.Configuration.Internal;
using Orleans.DurableJobs;
using Orleans.DurableJobs.Redis;

namespace Orleans.Hosting;

/// <summary>
/// Extensions for configuring Redis durable jobs.
/// </summary>
public static class RedisDurableJobsExtensions
{
/// <summary>
/// Adds durable jobs storage backed by Redis.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="configure">The delegate used to configure the durable jobs storage.</param>
/// <returns>The provided <see cref="ISiloBuilder"/>, for chaining.</returns>
public static ISiloBuilder UseRedisDurableJobs(this ISiloBuilder builder, Action<RedisJobShardOptions> configure)
{
builder.ConfigureServices(services => services.UseRedisDurableJobs(configure));
return builder;
}

/// <summary>
/// Adds durable jobs storage backed by Redis.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="configureOptions">The configuration delegate.</param>
/// <returns>The provided <see cref="ISiloBuilder"/>, for chaining.</returns>
public static ISiloBuilder UseRedisDurableJobs(this ISiloBuilder builder, Action<OptionsBuilder<RedisJobShardOptions>> configureOptions)
{
builder.ConfigureServices(services => services.UseRedisDurableJobs(configureOptions));
return builder;
}

/// <summary>
/// Adds durable jobs storage backed by Redis.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configure">The delegate used to configure the durable jobs storage.</param>
/// <returns>The provided <see cref="IServiceCollection"/>, for chaining.</returns>
public static IServiceCollection UseRedisDurableJobs(this IServiceCollection services, Action<RedisJobShardOptions> configure)
{
services.AddDurableJobs();
services.AddSingleton<RedisJobShardManager>();
services.AddFromExisting<JobShardManager, RedisJobShardManager>();
services.Configure(configure);
services.ConfigureFormatter<RedisJobShardOptions>();
return services;
}

/// <summary>
/// Adds durable jobs storage backed by Redis.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configureOptions">The configuration delegate.</param>
/// <returns>The provided <see cref="IServiceCollection"/>, for chaining.</returns>
public static IServiceCollection UseRedisDurableJobs(this IServiceCollection services, Action<OptionsBuilder<RedisJobShardOptions>>? configureOptions)
{
services.AddDurableJobs();
services.AddSingleton<RedisJobShardManager>();
services.AddFromExisting<JobShardManager, RedisJobShardManager>();
configureOptions?.Invoke(services.AddOptions<RedisJobShardOptions>());
services.ConfigureFormatter<RedisJobShardOptions>();
services.AddTransient<IConfigurationValidator>(sp => new RedisJobShardOptionsValidator(sp.GetRequiredService<IOptionsMonitor<RedisJobShardOptions>>().Get(Options.DefaultName), Options.DefaultName));
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using StackExchange.Redis;

namespace Orleans.Hosting;

/// <summary>
/// Options for configuring the Redis durable jobs provider.
/// </summary>
public class RedisJobShardOptions
{
/// <summary>
/// Gets or sets the Redis client configuration.
/// </summary>
[RedactRedisConfigurationOptions]
public ConfigurationOptions? ConfigurationOptions { get; set; }

/// <summary>
/// Gets or sets a delegate to create the Redis connection multiplexer.
/// </summary>
/// <remarks>
/// This delegate is called once during initialization to create the connection.
/// </remarks>
public Func<RedisJobShardOptions, Task<IConnectionMultiplexer>> CreateMultiplexer { get; set; } = DefaultCreateMultiplexer;

/// <summary>
/// Gets or sets the prefix for shard identifiers.
/// </summary>
/// <remarks>
/// This prefix is used to namespace shards in Redis, allowing multiple applications to share the same Redis instance.
/// </remarks>
public string ShardPrefix { get; set; } = "shard";

/// <summary>
/// Gets or sets the maximum number of retries when creating a shard in case of ID collisions.
/// </summary>
public int MaxShardCreationRetries { get; set; } = 5;

/// <summary>
/// Gets or sets the maximum number of job operations to batch together in a single write.
/// Default is 128 operations.
/// </summary>
public int MaxBatchSize { get; set; } = 128;

/// <summary>
/// Gets or sets the minimum number of job operations to batch together before flushing.
/// Default is 1 operation (immediate flush, optimized for latency).
/// </summary>
public int MinBatchSize { get; set; } = 1;

/// <summary>
/// Gets or sets the maximum time to wait for additional operations if the minimum batch size isn't reached
/// before flushing a batch.
/// Default is 100 milliseconds.
/// </summary>
public TimeSpan BatchFlushInterval { get; set; } = TimeSpan.FromMilliseconds(100);

/// <summary>
/// The default multiplexer creation delegate.
/// </summary>
public static async Task<IConnectionMultiplexer> DefaultCreateMultiplexer(RedisJobShardOptions options)
=> await ConnectionMultiplexer.ConnectAsync(options.ConfigurationOptions!);
}

internal class RedactRedisConfigurationOptions : RedactAttribute
{
public override string Redact(object value) => value is ConfigurationOptions cfg ? cfg.ToString(includePassword: false) : base.Redact(value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Orleans.Hosting;

/// <summary>
/// Validates <see cref="RedisJobShardOptions"/>.
/// </summary>
public class RedisJobShardOptionsValidator : IConfigurationValidator
{
private readonly RedisJobShardOptions _options;
private readonly string _name;

/// <summary>
/// Initializes a new instance of the <see cref="RedisJobShardOptionsValidator"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="name">The name.</param>
public RedisJobShardOptionsValidator(RedisJobShardOptions options, string name)
{
_options = options;
_name = name;
}

/// <inheritdoc/>
public void ValidateConfiguration()
{
if (_options.ConfigurationOptions is null)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.ConfigurationOptions)} is required.");
}

if (_options.CreateMultiplexer is null)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.CreateMultiplexer)} is required.");
}
if (string.IsNullOrWhiteSpace(_options.ShardPrefix))
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.ShardPrefix)} is required.");
}

if (_options.MaxShardCreationRetries < 1)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.MaxShardCreationRetries)} must be at least 1.");
}

if (_options.MaxBatchSize < 1)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.MaxBatchSize)} must be at least 1.");
}

if (_options.MinBatchSize < 1)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.MinBatchSize)} must be at least 1.");
}

if (_options.MinBatchSize > _options.MaxBatchSize)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.MinBatchSize)} must not exceed {nameof(_options.MaxBatchSize)}.");
}

if (_options.BatchFlushInterval < TimeSpan.Zero)
{
throw new OrleansConfigurationException($"Invalid configuration for {nameof(RedisJobShardOptions)} with name '{_name}'. {nameof(_options.BatchFlushInterval)} must not be negative.");
}
}
}
106 changes: 106 additions & 0 deletions src/Redis/Orleans.DurableJobs.Redis/JobOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Text.Json.Serialization;

namespace Orleans.DurableJobs.Redis;

/// <summary>
/// Represents an operation to be performed on a durable job.
/// </summary>
internal readonly struct JobOperation
{
/// <summary>
/// The type of operation to perform.
/// </summary>
public enum OperationType
{
Add,
Remove,
Retry,
}

/// <summary>
/// Gets or sets the type of operation.
/// </summary>
public OperationType Type { get; init; }

/// <summary>
/// Gets or sets the job identifier.
/// </summary>
public string Id { get; init; }

/// <summary>
/// Gets or sets the job name (only used for Add operations).
/// </summary>
public string? Name { get; init; }

/// <summary>
/// Gets or sets the due time (used for Add and Retry operations).
/// </summary>
public DateTimeOffset? DueTime { get; init; }

/// <summary>
/// Gets or sets the target grain ID (only used for Add operations).
/// </summary>
public GrainId? TargetGrainId { get; init; }

/// <summary>
/// Gets or sets the job metadata (only used for Add operations).
/// </summary>
public IReadOnlyDictionary<string, string>? Metadata { get; init; }

/// <summary>
/// Creates an Add operation for scheduling a new job.
/// </summary>
/// <param name="id">The job identifier.</param>
/// <param name="name">The job name.</param>
/// <param name="dueTime">The job due time.</param>
/// <param name="targetGrainId">The target grain ID.</param>
/// <param name="metadata">The job metadata.</param>
/// <returns>A new JobOperation for adding a job.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="id"/> or <paramref name="name"/> is null or empty.</exception>
public static JobOperation CreateAddOperation(string id, string name, DateTimeOffset dueTime, GrainId targetGrainId, IReadOnlyDictionary<string, string>? metadata)
{
ArgumentException.ThrowIfNullOrEmpty(id);
ArgumentException.ThrowIfNullOrEmpty(name);

return new() { Type = OperationType.Add, Id = id, Name = name, DueTime = dueTime, TargetGrainId = targetGrainId, Metadata = metadata };
}

/// <summary>
/// Creates a Remove operation for canceling a job.
/// </summary>
/// <param name="id">The job identifier.</param>
/// <returns>A new JobOperation for removing a job.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="id"/> is null or empty.</exception>
public static JobOperation CreateRemoveOperation(string id)
{
ArgumentException.ThrowIfNullOrEmpty(id);

return new() { Type = OperationType.Remove, Id = id };
}

/// <summary>
/// Creates a Retry operation for rescheduling a job.
/// </summary>
/// <param name="id">The job identifier.</param>
/// <param name="dueTime">The new due time.</param>
/// <returns>A new JobOperation for retrying a job.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="id"/> is null or empty.</exception>
public static JobOperation CreateRetryOperation(string id, DateTimeOffset dueTime)
{
ArgumentException.ThrowIfNullOrEmpty(id);

return new() { Type = OperationType.Retry, Id = id, DueTime = dueTime };
}
}

/// <summary>
/// JSON serialization context for JobOperation with compile-time source generation.
/// </summary>
[JsonSerializable(typeof(JobOperation))]
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = false)]
internal partial class JobOperationJsonContext : JsonSerializerContext
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageId>Microsoft.Orleans.DurableJobs.Redis</PackageId>
<Title>Microsoft Orleans Redis Durable Jobs Provider</Title>
<Description>Microsoft Orleans durable jobs provider backed by Redis</Description>
<PackageTags>$(PackageTags) Redis</PackageTags>
<TargetFrameworks>$(DefaultTargetFrameworks)</TargetFrameworks>
<AssemblyName>Orleans.DurableJobs.Redis</AssemblyName>
<RootNamespace>Orleans.DurableJobs.Redis</RootNamespace>
<OrleansBuildTimeCodeGen>true</OrleansBuildTimeCodeGen>
<DefineConstants>$(DefineConstants)</DefineConstants>
<Nullable>enable</Nullable>
<VersionSuffix Condition="$(VersionSuffix) != ''">$(VersionSuffix).alpha.1</VersionSuffix>
<VersionSuffix Condition="$(VersionSuffix) == ''">alpha.1</VersionSuffix>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(SourceRoot)src\Orleans.Runtime\Orleans.Runtime.csproj" />
<ProjectReference Include="$(SourceRoot)src\Orleans.DurableJobs\Orleans.DurableJobs.csproj" />
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Tester.Redis")]
Loading
Loading