Using Sorted Sets Of Redis To Delay Execution In ASP.NET Core

Introduction

 
In a previous article, I showed you how to delay execution via keyspace notifications of Redis in ASP.NET Core, and I will introduce another solution based on Redis.
Sorted Sets, a data structure of Redis, also can help us work it out.
 
We can make a timestamp as score, and the data as value. Sorted Sets provides a command that can return all the elements in the sorted set  with a score between two special scores.
 
Setting 0 as the minimum score and current timestamp as the maximum score, we can get all the values whose timestamp are less than the current timestamp, and they should be executed at once and should be removed from Redis.
 
Taking a sample for more information.
 
Add some values at first.
  1. ZADD task:delay 1583546835 "180"  
  2. ZADD task:delay 1583546864 "181"  
  3. ZADD task:delay 1583546924 "182"   
Suppose the current timestamp is 1583546860, so we can get all values via the following command.
  1. ZRANGEBYSCORE task:delay 0 1583546860 WITHSCORES LIMIT 0 1 
We will get the value 180 from the above sample, and then we can do what we want to do.
 
Now, let's take a look at how to do this in ASP.NET Core.
 

Create Project

 
Create a new ASP.NET Core Web API project and install CSRedisCore.
  1. <ItemGroup>  
  2.     <PackageReference Include="CSRedisCore" Version="3.4.1" />  
  3. </ItemGroup>  
Add an interface named ITaskServices and a class named TaskServices.
  1. public interface ITaskServices  
  2. {  
  3.     Task DoTaskAsync();  
  4.   
  5.     Task SubscribeToDo();  
  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.   
  17.         var cacheKey = "task:delay";  
  18.         int sec = new Random().Next(1, 5);  
  19.         var time = DateTimeOffset.Now.AddSeconds(sec).ToUnixTimeSeconds();  
  20.         var taskId = new Random().Next(1, 10000);  
  21.         await RedisHelper.ZAddAsync(cacheKey, (time, taskId));  
  22.         Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} done {taskId} here - {sec}");  
  23.     }  
  24.   
  25.     public async Task SubscribeToDo()  
  26.     {  
  27.         var cacheKey = "task:delay";  
  28.         while (true)  
  29.         {  
  30.             var vals = RedisHelper.ZRangeByScore(cacheKey, -1, DateTimeOffset.Now.ToUnixTimeSeconds(), 1, 0);  
  31.   
  32.             if (vals != null && vals.Length > 0)  
  33.             {  
  34.                 var val = vals[0];  
  35.   
  36.                 // add a lock here may be more better  
  37.                 var rmCount = RedisHelper.ZRem(cacheKey, vals);  
  38.   
  39.                 if (rmCount > 0)  
  40.                 {  
  41.                     Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} begin to do task {val}");  
  42.                 }  
  43.             }  
  44.             else  
  45.             {  
  46.                 await Task.Delay(500);  
  47.             }  
  48.         }  
  49.     }  
  50. }  
Here we will use DateTimeOffset.Now.AddSeconds(sec).ToUnixTimeSeconds() to generate the timestamp, the sec parameter means that we should execute the task after some seconds.
 
For the delay execution, it will poll the values from Redis to consume the tasks, and make it sleep 500 milliseconds if cannot get some values.
 
When we get a value from Redis, before we execute the delay task, we should remove it from Redis at first.
 
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.         return "done";  
  17.     }  
  18. }  
We will put the subscribe to a BackgroundService
  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 async Task ExecuteAsync(CancellationToken stoppingToken)  
  13.     {  
  14.         stoppingToken.ThrowIfCancellationRequested();  
  15.         await _taskServices.SubscribeToDo();  
  16.     }  
  17. }  
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.     }  

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 sorted sets.

I hope this will help you!