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.
############################# Event notification ##############################
# Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# .........
#
# By default all notifications are disabled because most users don't need
# this feature and the feature has some overhead. Note that if you don't
# specify at least one of K or E, no events will be delivered.
notify-keyspace-events ""
The default value of notify-keyspace-events is empty, we should modify it to Ex.
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.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CSRedisCore" Version="3.4.1" />
</ItemGroup>
</Project>
Add an interface named ITaskServices and a class named TaskServices.
public interface ITaskServices
{
void SubscribeToDo(string keyPrefix);
Task DoTaskAsync();
}
public class TaskServices : ITaskServices
{
public async Task DoTaskAsync()
{
// do something here
// ...
// this operation should be done after some min or sec
var taskId = new Random().Next(1, 10000);
int sec = new Random().Next(1, 5);
await RedisHelper.SetAsync($"task:{taskId}", "1", sec);
await RedisHelper.SetAsync($"other:{taskId + 10000}", "1", sec);
}
public void SubscribeToDo(string keyPrefix)
{
RedisHelper.Subscribe(
("__keyevent@0__:expired", arg =>
{
var msg = arg.Body;
Console.WriteLine($"recive {msg}");
if (msg.StartsWith(keyPrefix))
{
// read the task id from expired key
var val = msg.Substring(keyPrefix.Length);
Console.WriteLine($"Redis + Subscribe {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} begin to do task {val}");
}
})
);
}
}
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.
[ApiController]
[Route("api/tasks")]
public class TaskController : ControllerBase
{
private readonly ITaskServices _svc;
public TaskController(ITaskServices svc)
{
_svc = svc;
}
[HttpGet]
public async Task<string> Get()
{
await _svc.DoTaskAsync();
System.Console.WriteLine("done here");
return "done";
}
}
Put the subscriber to a BackgroundService, so that it can run in the background.
public class SubscribeTaskBgTask : BackgroundService
{
private readonly ILogger _logger;
private readonly ITaskServices _taskServices;
public SubscribeTaskBgTask(ILoggerFactory loggerFactory, ITaskServices taskServices)
{
this._logger = loggerFactory.CreateLogger<RefreshCachingBgTask>();
this._taskServices = taskServices;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
stoppingToken.ThrowIfCancellationRequested();
_taskServices.SubscribeToDo("task:");
return Task.CompletedTask;
}
}
At last, we should register the above services in startup class.
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
var csredis = new CSRedis.CSRedisClient("127.0.0.1:6379");
RedisHelper.Initialization(csredis);
services.AddSingleton<ITaskServices, TaskServices>();
services.AddHostedService<SubscribeTaskBgTask>();
services.AddControllers();
}
}
Here is the result after running this application.
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.