Using Redis To Delay Execution In ASP.NET Core

Introduction

 
Due to some business requirements, many operations should not begin to execute right away;  they should begin after some seconds, minutes or hours.
 
For example, say there is a task, and it contains two steps. When we finish the first step, the second step should begin after 5 minutes.
 
How can we solve this problem?
 
Thread.Sleep() and Task.Delay() is a very easy solution that we can use. But it may block our applictions.
 
And in this article, I will introduce a solution based on keyspace notifications of Redis.
 
Here we will use expired events to do it, but this solution also has some limitations , because it may have a significant delay. That means that delay of execution may have smoe error, and will not be very accurate!
 
Let's take a look at this solution. 
 

Set Up Redis

 
Keyspace notifications is a feature available since 2.8.0, so the version of Redis should not be less than 2.8.0.
 
We should modify an important configuration so that we can enable this feature.
  1. ############################# Event notification ##############################  
  2.  
  3. # Redis can notify Pub/Sub clients about events happening in the key space.  
  4. # This feature is documented at http://redis.io/topics/notifications  
  5. #  
  6. # .........  
  7. #  
  8. #  By default all notifications are disabled because most users don't need  
  9. #  this feature and the feature has some overhead. Note that if you don't  
  10. #  specify at least one of K or E, no events will be delivered.  
  11. notify-keyspace-events ""  
The default value of notify-keyspace-events is empty, we should modify it to Ex.
  1. notify-keyspace-events "Ex"  
Then we can startup the Redis server.
 
 
 

Create Project

Create a new ASP.NET Core Web API project and install CSRedisCore.
  1. <Project Sdk="Microsoft.NET.Sdk.Web">  
  2.   
  3.   <PropertyGroup>  
  4.     <TargetFramework>netcoreapp3.1</TargetFramework>  
  5.   </PropertyGroup>  
  6.   
  7.   <ItemGroup>  
  8.     <PackageReference Include="CSRedisCore" Version="3.4.1" />  
  9.   </ItemGroup>  
  10.   
  11. </Project>  
Add an interface named ITaskServices and a class named TaskServices.
  1. public interface ITaskServices  
  2. {  
  3.     void SubscribeToDo(string keyPrefix);  
  4.   
  5.     Task DoTaskAsync();  
  6. }  
  7.   
  8. public class TaskServices : ITaskServices  
  9. {  
  10.     public async Task DoTaskAsync()  
  11.     {  
  12.         // do something here  
  13.         // ...  
  14.   
  15.         // this operation should be done after some min or sec  
  16.         var taskId = new Random().Next(1, 10000);  
  17.         int sec = new Random().Next(1, 5);  
  18.   
  19.         await RedisHelper.SetAsync($"task:{taskId}""1", sec);  
  20.         await RedisHelper.SetAsync($"other:{taskId + 10000}""1", sec);  
  21.     }  
  22.   
  23.     public void SubscribeToDo(string keyPrefix)  
  24.     {  
  25.         RedisHelper.Subscribe(  
  26.             ("__keyevent@0__:expired", arg =>  
  27.                 {  
  28.                     var msg = arg.Body;  
  29.                     Console.WriteLine($"recive {msg}");  
  30.                     if (msg.StartsWith(keyPrefix))  
  31.                     {  
  32.                         // read the task id from expired key  
  33.                         var val = msg.Substring(keyPrefix.Length);  
  34.                         Console.WriteLine($"Redis + Subscribe {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} begin to do task {val}");  
  35.                     }  
  36.                 })  
  37.         );  
  38.     }  
  39. }  
As you can see, we set a redis key with expiration. The expiration is the delay time.
 
For the delay execution, we can find it in SubscribeToDo method. It subscribes a channel named __keyevent@0__:expired.
 
When a key is expired, redis server will publish a message to this channel, and subscribers will receive it.
 
After reciving the notification, the client will begin to do the job. 
 
Before the client receives the notification, the delay job will not be executed, so that it can help us to do the job for a delay.
 
Here is the entry of this operation.
  1. [ApiController]  
  2. [Route("api/tasks")]  
  3. public class TaskController : ControllerBase  
  4. {  
  5.     private readonly ITaskServices _svc;  
  6.   
  7.     public TaskController(ITaskServices svc)  
  8.     {  
  9.         _svc = svc;  
  10.     }  
  11.   
  12.     [HttpGet]  
  13.     public async Task<string> Get()  
  14.     {  
  15.         await _svc.DoTaskAsync();  
  16.         System.Console.WriteLine("done here");  
  17.         return "done";  
  18.     }  
  19. }  
Put the subscriber to a BackgroundService, so that it can run in the background.
  1. public class SubscribeTaskBgTask : BackgroundService  
  2. {  
  3.     private readonly ILogger _logger;  
  4.     private readonly ITaskServices _taskServices;  
  5.   
  6.     public SubscribeTaskBgTask(ILoggerFactory loggerFactory, ITaskServices taskServices)  
  7.     {  
  8.         this._logger = loggerFactory.CreateLogger<RefreshCachingBgTask>();  
  9.         this._taskServices = taskServices;  
  10.     }  
  11.   
  12.     protected override Task ExecuteAsync(CancellationToken stoppingToken)  
  13.     {  
  14.         stoppingToken.ThrowIfCancellationRequested();  
  15.   
  16.         _taskServices.SubscribeToDo("task:");  
  17.   
  18.         return Task.CompletedTask;  
  19.     }  
  20. }  
At last, we should register the above services in startup class.
  1. public class Startup  
  2. {  
  3.     // ...  
  4.       
  5.     public void ConfigureServices(IServiceCollection services)  
  6.     {  
  7.         var csredis = new CSRedis.CSRedisClient("127.0.0.1:6379");  
  8.         RedisHelper.Initialization(csredis);  
  9.   
  10.         services.AddSingleton<ITaskServices, TaskServices>();  
  11.         services.AddHostedService<SubscribeTaskBgTask>();  
  12.   
  13.         services.AddControllers();  
  14.     }  
  15. }  
Here is the result after running this application. 
 
 
 
 Here is the source code you can find in my GitHub page.

Summary

 
This article showed you a simple solution of how to delay execution in ASP.NET Core using Redis Keyspace Notifications.
 
I hope this will help you!